Android端末のDRM情報を取得する

特定のDRMがサポートされているか

MediaCrypto.isCryptoSchemeSupported(uuid)

uuid には以下の値を指定する

val WIDEVIDE_UUID = UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed")
val PLAYREADY_UUID = UUID.fromString("9a04f079-9840-4286-ab92-e65be0885f95")
val CLEAR_KEY_UUID = UUID.fromString("e2719d58-a985-b3c9-781a-b030af78d30e")

DRMのバージョンやセキュリティレベルを取得

val mediaDrm = MediaDrm(WIDEVIDE_UUID)
mediaDrm.getPropertyString(propertyname)

propertyName には MediaDrm.PROPERTY_XXX を指定する

コード例

コード

val propertyNames = listOf(
    MediaDrm.PROPERTY_ALGORITHMS,
    MediaDrm.PROPERTY_DESCRIPTION,
    MediaDrm.PROPERTY_VENDOR,
    MediaDrm.PROPERTY_VERSION,
    MediaDrm.PROPERTY_DEVICE_UNIQUE_ID,
    "securityLevel"
)

val WIDEVIDE_UUID = UUID(0xEDEF8BA979D64ACEu.toLong(), 0xA3C827DCD51D21EDu.toLong())
val isWidevineSupported = MediaCrypto.isCryptoSchemeSupported(WIDEVIDE_UUID)
if (isWidevineSupported) {
    val widevineMediaDrm = MediaDrm(WIDEVIDE_UUID)
    propertyNames
        .map { name ->
            name to widevineMediaDrm.getPropertyStringOrNull(name)
        }
        .forEach { (name, property) ->
            Log.d("DrmInfo", "$name: $property")
        }
}

fun MediaDrm.getPropertyStringOrNull(propertyName: String): String? {
    return try {
        getPropertyString(propertyName)
    } catch (e: Exception) {
        null
    }
}

出力

D/DrmInfo: algorithms: AES/CBC/NoPadding,HmacSHA256
D/DrmInfo: description: Widevine CDM
D/DrmInfo: vendor: Google
D/DrmInfo: version: 1.0
D/DrmInfo: deviceUniqueId: null
D/DrmInfo: securityLevel: L1

参考

MediaDrm  |  Android Developers

ExoPlayer/FrameworkMediaDrm.java at c75f3f77ffc9d5cf8c2973fa02d1c43071ede76e · google/ExoPlayer · GitHub

stringリソースの文字列にリンクを設定する - annotationを添えて

この記事の要約

  • AndroidのTextViewで文字列の一部にリンクを設定する方法について
  • annotationタグを使って、リンク設定をする文字列の範囲を指定する
  • 言語化対応やリンク変更の負担が少ない

やりたいこと

  • 図のようにTextViewの一部( こちら )にリンクを設定する
  • 文字列の範囲を指定する際にマジックナンバーを使用しない
  • タップ時はIntentを投げてブラウザなどに飛ばす

実装

stringリソースの定義

<string name="text_template">
  annotationタグのドキュメントは <annotation link="document_link">こちら</annotation> から飛べます
</string>
<string name="annotation_document">
  https://developer.android.com/guide/topics/resources/string-resource
</string>

リンクを設定したい文字列を <annotation xxx="yyy"> タグで囲みます
xxxyyy は任意の文字列を設定することが可能で、複数のannotationタグを設定した際にどの文字列かを判断するために使うことができます

リンクの設定

以下の手順でstringリソースからTextViewへセットするためのCharSequence を生成することができます

  1. 元となる文字列 R.string.text_template を取得
  2. text_template からannotationタグの情報を取り出す
  3. <annotation link="document_link"> を表すAnnotationインスタンスを取り出す
  4. 取り出したAnnotationから ClickableSpan を作成する
  5. Annotationに含まれている文字列のポジションを使って ClickableSpan を適用する範囲を指定する

コード例

private fun getText(): CharSequence {
  val template = getText(R.string.text_template)
  if (template !is SpannedString) return template

  // Annotationの使い方 https://developer.android.com/guide/topics/resources/string-resource#StylingWithAnnotations
  val annotations = template.getSpans(0, template.length, Annotation::class.java)
  val spannableString = SpannableString(template)

  annotations
    .filter { annotation -> annotation.key == "link" }
    .filter { linkAnnotation -> linkAnnotation.value == "document_link" }
    .forEach { annotation ->
      val linkSpan = object : ClickableSpan() {
        override fun onClick(widget: View) {
          val url = getString(R.string.annotation_document)
          intent = viewIntent(url)
          startActivity(intent)
        }
      }
      spannableString.setSpan(
        linkSpan,
        template.getSpanStart(annotation),
        template.getSpanEnd(annotation),
        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
      )
    }
  return spannableString
}

fun viewIntent(url: String): Intent {
  return Intent(Intent.ACTION_VIEW).apply {
    data = Uri.parse(url)
  }
}

メリット

annotationタグを使用すると文字列の範囲を Annotation インスタンスから取得できるため、文字列の変更を行ってもstringリソースのみの変更で済みます
また、多言語対応する場合にもannotationタグの属性と値を揃えておくことで、コードの変更が不要になるケースが増えます

参考

String resources  |  Android Developers

Parameterizedテストのテストケース名をわかりやすく表示する

はじめに

JUnitのParameterizedテストでテストケース名は以下の図のように、パラメータごとに test[0] , test[1], ... という表示がされます
f:id:scache:20201228203120p:plain:w300

この表示では、いくつかのパラメータでテストが失敗した場合に、どのパラメータの時にテストが失敗しているかがわかりづらいという問題があります
今回はこの問題を解決するためにテストケース名を変更する方法を説明します
※ コードはKotlinを使っていますがJavaでも同様の方法でできます

Parametersの引数を使ってテストケース名を変更する

Parameterizedテストのパラメータを指定する時に @Parameterized.Parameters アノテーションを使用しますが、このアノテーションは引数を受け取ることが可能です
この引数を変更することでテストケース名の表示を変えることができます

例えば、 @Parameterized.Parameters(name = "xxx") と指定すると

test[xxx]
test[xxx]
...
test[xxx]

という表示がされます

固定の文字列を表示しても何のメリットもないので、各パラメータの情報を表示する方法も提供されていて、 テストクラスで受け取るパラメータのindexを指定して表示することができます

例えば以下のように3つの引数がある場合に name = "{0} + {1} = {2}" を指定すると、

@RunWith(Parameterized::class)
class ParameterizedDoubleTest(
    private val v1: Double,
    private val v2: Double,
    private val expected: Double,
) {
        ...
        @Parameterized.Parameters(name = "{0} + {1} = {2}")
        fun params(): List<Array<Any>> {
            return listOf(
                arrayOf(1.5, 2.5, 4.0),
                arrayOf(2.5, 1.5, 4.0),
                arrayOf(1.5, 3.5, 4.0),
                arrayOf(2.5, 2.5, 5.0),
            )
        }
        ...
}

以下のように ${v1} + ${v2} = ${expected} のフォーマットで表示されます

test[1.5 + 2.5 = 4]
test[1.5 + 3.5 = 4]
test[2.5 + 1.5 = 4]
test[2.5 + 2.5 = 5]

それぞれのパラメータは toString() で返ってくる文字列に変換されるため、パラメータを1つのクラスにまとめて表現している場合は toString() をオーバーライドすると良いでしょう

@RunWith(Parameterized::class)
class ParameterizedDescriptionTest(p: Param) {
    data class Param(
        val v1: Double,
        val v2: Double,
        val expected: Double,
    ){
        override fun toString(): String {
            return "$v1 + $v2 = $expected"
        }
    }
        
    companion object{
        ....
        @Parameterized.Parameters(name = "{0}")
        fun params(): List<Param> {
            return listOf(
                Param(1.5, 2.5, 4.0),
                ...
            )
        }
        ...
    }
}

おまけ

Parameters アノテーションの引数は MessageFormat (Java Platform SE 8) のフォーマットが使えます
例えば、以下の指定をするとDoubleの値の少数第2位まで表示することができます

@Parameterized.Parameters(name = "{0, number, #.00} + {1, number, #.00} = {2, number, #.00}")

Mockitoでfinal classをモックする

mockitoとは

mockitoはJavaユニットテストでクラスをモック可能にするためのフレームワークです。

site.mockito.org

gradleを使っている場合は以下のように mockito-core を依存に追加することで使用できます。

dependencies { testCompile "org.mockito:mockito-core:2.+" }

final classをモックする

mockitoは、デフォルトの状態では final なクラスやメソッドをモックすることができなくなっています。
しかし、設定を追加することで final なクラスやメソッドがモック可能になります。今回はその方法を2つ紹介します。

設定ファイルを追加する方法

1つ目はmockitoのドキュメントに書いてある方法で、以下の値を書いた /mockito-extensions/org.mockito.plugins.MockMaker ファイルを追加するというものです。 Mockito - mockito-core 3.4.6 javadoc

mock-maker-inline

テストコードが test ディレクトリに配置されている場合は test/resources/mockito-extensions/org.mockito.plugins.MockMaker に追加すると良いでしょう。

mockito-inline ライブラリを追加する方法

2つ目は mockito-inline ライブラリを追加する方法です。

dependencies{ testCompile "org.mockito:mockito-inline:2.+" }

このライブラリで行っていることは1つ目の方法とほぼ同じで、クラスパスに /mockito-extensions/org.mockito.plugins.MockMaker を追加します。

ライブラリのソースは https://github.com/mockito/mockito/tree/release/2.x/subprojects/inline に上がっており、テストコードやgradleファイルを除けば main/resources/mockito-extensions/org.mockito.plugins.MockMaker のファイルが1つあるだけとなっています。

最後に

final なクラスやメソッドをモックする必要がある場合には、設定ファイルを追加する必要があります。今回、2つの方法を紹介しましたがどちらも同じことを行っているためお好きな方法を選ぶと良いでしょう。

注意点として、ドキュメントにあるように一部のメソッドは上記の方法ではモックすることができません。

Some methods cannot be mocked
Package-visible methods of java.*
native methods

greenrobot EventBusでindexクラスが生成されない時の対応

EventBusライブラリを導入した際にkaptで生成されるはずのindexクラスが生成されなかった時にした事について

github.com

build.gradleの設定

まずはドキュメントに書いてある通りに build.gradle にkaptプラグインの導入やライブラリの設定を行います。

apply plugin: 'kotlin-kapt' // ensure kapt plugin is applied

dependencies {
    def eventbus_version = '3.2.0'
    implementation "org.greenrobot:eventbus:$eventbus_version"
    kapt "org.greenrobot:eventbus-annotation-processor:$eventbus_version"
}

kapt {
    arguments {
        arg('eventBusIndex', 'com.example.myapp.MyEventBusIndex')
    }
}

Subscriber Index - Open Source by greenrobot

KotlinのコードでEventBusを使うには上記で指定した MyEventBusIndex を使います。

しかし、この段階でビルドを行っても MyEventBusIndex が生成されないためクラスを使うことができないという問題が発生しました。

indexクラスを生成するには

indexクラスが生成されるようにするためには、適当なクラスに以下のようなイベントをサブスクライブするためのメソッドを追加する必要があります。

@Subscribe(threadMode = ThreadMode.MAIN)
  fun onMessageEvent(event: Object) {
}

イベントを受け取るメソッドがない時はindexクラスは不要(=生成する必要がない)ためにこのような処理になっていそうですね

おわりに

EventBusを使うのが初めてではなかったので最初からindexクラスを生成する方法でライブラリを導入しようとしたために問題にぶつかってしまいましたが、 indexクラスを使用しない get started を最初に試みていたらはまることもなくライブラリの導入が行えそうでした。

How to get started with EventBus in 3 steps - Open Source by greenrobot

JavaのString.replaceとString.replaceAllの違い

2014年01月23日 ごろに書いた記事です(消そうと思いましたが懐かしいので残しておく)

replaceでは引数に指定した文字をそのまま置き換えるが、 replaceAllでは引数に指定した文字を正規表現と見なして置き換える。

例えば、以下のようなコードでは、

String str = "abc.def.";
System.out.println("Result: " + str.replace(".", "1"));
System.out.println("Result: " + str.replaceAll(".", "1"));

出力は

Result: abc1def1
Result: 11111111

となる。正規表現で"."は任意の文字にマッチする意味を持つので、 正規表現を使うreplaceAllではすべての文字が変換されている。

replace,replaceAllを使う時はこの点に注意しておこう。

ちなみに、replaceAllを使いたい時に"."にマッチさせたい場合には"\."を使えばよい

Glideの画像リクエストが始まるところから表示するまでの流れをおいかける

今回みる処理

以下のようにload(String).into(ImageView) を実行してから、画像が表示されるまでの処理の流れをみていきます。

Glide
    .with(imageView)
    .load(imageUrl)
    .into(imageView);

intoでリクエストを開始するまで

into(ImageView) の中では、ロード処理の開始や成功などのイベント通知を受け取る Target を作成します。 今回の場合は、ロード成功時にViewに画像をセットする ImageViewTarget が作成されます。

return into(
    glideContext.buildImageViewTarget(view, transcodeClass),
    /*targetListener=*/ null,
    requestOptions,
    Executors.mainThreadExecutor());

最終的に以下の into メソッドの呼び出しが行われ、Request インスタンスの生成が行われています。名前的にこのクラスが画像読み込みに大きく関係してきそうです。どのようなインスタンスが作成されるかについては一旦置いておき、全体の流れをみたいと思います。
ここで登場する requestManager は、ActivityやFragmentなどのライフサイクルに合わせてリクエスト処理の開始/停止を管理するクラスです。
requestManager の生成については、Glide.withに渡す引数による処理の違いについて - scache’s blogで書いているので興味がある方はそちらを参考にしてみてください。

private <Y extends Target<TranscodeType>> Y into(
    @NonNull Y target,
    @Nullable RequestListener<TranscodeType> targetListener,
    BaseRequestOptions<?> options,
    Executor callbackExecutor) {
    ...
    Request request = buildRequest(target, targetListener, options, callbackExecutor);

    Request previous = target.getRequest();
    ...  // TargetにセットされているRequestが今回作ったリクエストと同じだったら、セットされているRequestをそのまま使い、targetを返す

    requestManager.clear(target);
    target.setRequest(request);
    requestManager.track(target, request);
    return target;
}

最後の方にある requestManager.track(target, request); の中では runRequest(request) が実行されていて、ここからリクエストが開始しそうなことが分かります。

synchronized void track(@NonNull Target<?> target, @NonNull Request request) {
    targetTracker.track(target);
    requestTracker.runRequest(request);
}

実際に、 RequestTracker#runRequest の中で Request#begin が呼び出されてリクエストが開始されます。(RequestTracker がpausedの場合はresumeになった時に begin が呼び出されます)

public void runRequest(@NonNull Request request) {
    requests.add(request);
    if (!isPaused) {
        request.begin();
    } else {
        request.clear();
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Paused, delaying request");
        }
        pendingRequests.add(request);
    }
}

ここまでで、 TargetRequest という2つのクラスが作成されました。また、Request#beginでリクエストが始まるところまでをみていきました。

次は、リクエストの生成処理(buildRequest)についてみていきたいと思います。

具体的なRequestインスタンスの生成

Request はinterfaceのため、具体的な実装は各具象クラスに記述されています。そのため、先ほど無視したRequestインスタンスの生成処理に戻りたいと思います(RequestBuilder#buildRequest)。
glide/RequestBuilder.java at v4.11.0 · bumptech/glide

buildRequest には buildRequestRecursive を呼び出す処理があり、何やら再帰的にRequestを生成していそうです。

buildRequestRecursive では、thumbnailerrorの設定を行ったかどうかによって処理が分岐されています。 この時に設定したエラーやサムネイルのRequestBuilderは、load で返却されるRequestBuilderの中で保持されるので再帰的な構造になっています。

thumbnailerror の設定を行うコード例

Glide
    .with(imageView)
    .load(imageUrl)
    .thumbnail(thumbRequestBuilder)
    .error(errorRequestBuilder)
    .into(imageView);

まず、buildRequestRecursive ではerrorが設定されているかどうかで以下のように処理が分岐します。

errorBuilderが
    ない -> buildThumbnailRequestRecursiveで生成したRequestを返す
    ある -> 下の2つのRequestを設定したErrorRequestCoordinatorを返す
            - buildThumbnailRequestRecursiveと
            - errorで指定したRequestBuilderのbuildRequestRecursive

どちらのパターンでも呼び出される buildThumbnailRequestRecursive が行っていることはbuildRequestRecursiveと似ており、thumbが設定されている場合は、loadで指定した画像を読み込むRequestとサムネイルを読み込むたむRequestの2つが設定されたThumbnailRequestCoordinatorを返します。

一度整理すると、3つのリクエストの概念があります。 - fullRequest : loadで指定した画像のリクエスト - thumbRequest: thumbで指定した画像のリクエスト - errorRequest: errorで指定した画像のリクエス

再帰的な構造になっているため複雑になってきましたが、1番シンプルなエラーやサムネイルを設定しなかった時の処理をみていきましょう。
この時は、fullRequestのみが作成されます。具体的にインスタンスを生成する処理はSingleRequest.obtainとなっています(buildRequestでこのSingleRequestが返却される)。
glide/RequestBuilder.java at v4.11.0 · bumptech/glide

エラーやサムネイルの設定がある時についてはスルーしていますが、Requestの具体的な型がSingleRequestになることが分かりました。(詳しくは説明を行いませんが、再帰的に処理が行われるためエラーやサムネイルのCoordinatorに設定するリクエストも辿っていくと最終的にSingleRequestになります)

SingleRequestを開始する

Request を生成する処理が長かったですが、これでようやくリクエストを開始する処理をみていくことができます。

リクエストは Request#begin で始まるので Single#begin の処理をみていきます。 ログやコメントなどを除いた begin の処理は以下のようになっています。

@Override
public void begin() {
  synchronized (requestLock) {
    if (model == null) {
      if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        width = overrideWidth;
        height = overrideHeight;
      }
      int logLevel = ...
      onLoadFailed(new GlideException("Received null model"), logLevel);
      return;
    }

    if (status == Status.RUNNING) {
      throw new IllegalArgumentException("Cannot restart a running request");
    }

    if (status == Status.COMPLETE) {
      onResourceReady(resource, DataSource.MEMORY_CACHE);
      return;
    }

    status = Status.WAITING_FOR_SIZE;
    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
      onSizeReady(overrideWidth, overrideHeight);
    } else {
      target.getSize(this);
    }

    if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
        && canNotifyStatusChanged()) {
      target.onLoadStarted(getPlaceholderDrawable());
    }
  }
}

ステータスは、インスタンス生成時はPENDINGになっており、overrideWidthなどを設定するかによって多少かわりますが PENDING -> WAITING_FOR_SIZE -> RUNNING -> COMPLETE のように遷移をします。

model(画像URLなど)が設定されていなかったり、すでにリクエストが動いている時はエラーになります。また、すでに画像読み込み済みの場合は、onResourceReadyが呼び出されます。

WAITING_FOR_SIZE -> RUNNING

まずはWAITING_FOR_SIZEからRUNNINGになるまでをみていきます。

status = Status.WAITING_FOR_SIZE;
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
    onSizeReady(overrideWidth, overrideHeight);
} else {
    target.getSize(this);
}

有効なoverrideWidth, overrideHeightが設定されている場合はすぐにonSizeReadyが呼び出されるのでこれ以上説明することはありません。
設定されていない場合は、Target#getSize(SizeReadyCallback)の呼び出しが行われてTargetにサイズを要求しています。サイズが確定したらコールバックが呼ばれ、その時にonSizeReadyの呼び出しが行われます。

ImageViewに画像を設定する際は ImageViewTarget が使われるため、ImageViewTarget#getSize の処理をみたいと思います(実際の処理は継承元のViewTargetにあります)。

getSize ではViewのgetWidth/HeightLayoutParamsなどからサイズを求めることが可能な場合は計算を行った結果でonSizeReadyの呼び出しを行います。
しかし、Viewがレイアウトリクエスト中(View#isLayoutRequested)など、サイズを求めることができなかった場合は、OnPreDrawListenerをセットしてサイズが確定するのを待ち、onPreDraw内でonSizeReadyの呼び出しを行います。

ViewTreeObserver observer = view.getViewTreeObserver();
layoutListener = new SizeDeterminerLayoutListener(this);
observer.addOnPreDrawListener(layoutListener);

このようにViewのサイズ計算を行うことでサイズを決定し、RUNNING状態に遷移します。

RUNNING -> COMPLETE

サイズを決定してonSizeReadyの呼び出しを行うところまでをみたため、ここからはonSizeReadyで行っている処理についてみていきます。
glide/SingleRequest.java at v4.11.0 · bumptech/glide

onSizeReady は状態のチェックを除けば Engine#load を呼び出しているのみになります。 Engine#load は引数がかなり多く(19個、読むのをやめたくなりますが、少しずつみていきましょう 😓
glide/Engine.java at v4.11.0 · bumptech/glide

はじめにメモリキャッシュにデータが存在するかチェックを行い、存在する場合はResourceCallback#onResourceReady(Resource<?>, DataSource)が実行されます。最終的に画像読み込み後はonResourceReadyの呼び出しが行われそうということが分かりました。

少しずつみていくと言いましたがEngine#loadの処理はとても長くなりそうなので、別の記事で書くことにしてこの記事の本来の目的である画像が表示されるまでの流れに戻るためにonResourceReadyの呼び出しで何が起こるかをみていきたいと思います。

Engine に渡したResourceCallbackインスタンスSingleRequestインスタンスのため、SingleRequest#onResourceReadyの処理にいきます。

SingleRequest には2つのonResourceReadyメソッドがあり、①がEngineから呼び出されます。①ではResource#getで取得できるインスタンスの型が、RequestBuilderの型パラメータ(今回はDrawable)と一致するかをチェックして、一致する場合は②が呼び出されます(もし一致しなかった場合はエラーになります)。

① onResourceReady(Resource<?>, DataSource)
② onResourceReady(Resource<R>, R, DataSource)

ここで呼び出されたメソッドでステータスがCOMPLETEにかわります。また、Target#onResourceReadyが呼び出されます。

SingleRequest#onResourceReady(Resource<R>, R, DataSource)
glide/SingleRequest.java at v4.11.0 · bumptech/glide

ImageViewTargetに画像読み込み完了のコールバックが呼ばれてから

ここまで長かったですが、この時点で画像は読み込まれているので後はImageViewに画像をセットするだけとなりました。

onResourceReadyとその中で呼び出されるsetResourceInternalは以下のようになっていて、最後はsetResourceが実行されます。

public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
    if (transition == null || !transition.transition(resource, this)) {
        setResourceInternal(resource);
    } else {
        maybeUpdateAnimatable(resource);
    }
}

private void setResourceInternal(@Nullable Z resource) {
  setResource(resource);
  maybeUpdateAnimatable(resource);
}

今回のintoで実際に生成されるTargetインスタンスの型はDrawableImageViewTargetになり、そのsetResourceでようやくImageViewに画像がセットされます。

protected void setResource(@Nullable Drawable resource) {
  view.setImageDrawable(resource);
}

まとめ

今回はGlideの画像リクエストが始まるところから表示するまでの流れをみてきました。
簡単にまとめると、以下のような流れとなっていました。

  1. RequestBuilderからRequestTargetを生成
  2. 生成したRequestRequestMangerで管理/実行する
  3. Targetでサイズを計算する
  4. Engineが実際に画像を取得する(リモートやキャッシュから)
  5. 取得した画像をTargetが表示する

途中のEngineの処理の詳細については触れませんでしたが、それでもGlideが多くのことを行っていることが分かりました。
Glideの画像取得処理の肝であるEngineに関しては、また別でみていきたいと思います!