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
参考
stringリソースの文字列にリンクを設定する - 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">
タグで囲みます
xxx
と yyy
は任意の文字列を設定することが可能で、複数のannotationタグを設定した際にどの文字列かを判断するために使うことができます
リンクの設定
以下の手順でstringリソースからTextViewへセットするためのCharSequence
を生成することができます
- 元となる文字列
R.string.text_template
を取得 text_template
からannotationタグの情報を取り出す<annotation link="document_link">
を表すAnnotationインスタンスを取り出す- 取り出したAnnotationから
ClickableSpan
を作成する - 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タグの属性と値を揃えておくことで、コードの変更が不要になるケースが増えます
参考
Parameterizedテストのテストケース名をわかりやすく表示する
はじめに
JUnitのParameterizedテストでテストケース名は以下の図のように、パラメータごとに test[0]
, test[1]
, ... という表示がされます
この表示では、いくつかのパラメータでテストが失敗した場合に、どのパラメータの時にテストが失敗しているかがわかりづらいという問題があります
今回はこの問題を解決するためにテストケース名を変更する方法を説明します
※ コードは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のユニットテストでクラスをモック可能にするためのフレームワークです。
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クラスが生成されなかった時にした事について
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); } }
ここまでで、 Target
と Request
という2つのクラスが作成されました。また、Request#begin
でリクエストが始まるところまでをみていきました。
次は、リクエストの生成処理(buildRequest
)についてみていきたいと思います。
具体的なRequestインスタンスの生成
Request
はinterfaceのため、具体的な実装は各具象クラスに記述されています。そのため、先ほど無視したRequest
インスタンスの生成処理に戻りたいと思います(RequestBuilder#buildRequest
)。
glide/RequestBuilder.java at v4.11.0 · bumptech/glide
buildRequest
には buildRequestRecursive
を呼び出す処理があり、何やら再帰的にRequest
を生成していそうです。
buildRequestRecursive
では、thumbnail
やerror
の設定を行ったかどうかによって処理が分岐されています。
この時に設定したエラーやサムネイルのRequestBuilder
は、load
で返却されるRequestBuilder
の中で保持されるので再帰的な構造になっています。
thumbnail
や error
の設定を行うコード例
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/Height
やLayoutParams
などからサイズを求めることが可能な場合は計算を行った結果で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の画像リクエストが始まるところから表示するまでの流れをみてきました。
簡単にまとめると、以下のような流れとなっていました。
RequestBuilder
からRequest
とTarget
を生成- 生成した
Request
をRequestManger
で管理/実行する Target
でサイズを計算するEngine
が実際に画像を取得する(リモートやキャッシュから)- 取得した画像を
Target
が表示する
途中のEngine
の処理の詳細については触れませんでしたが、それでもGlideが多くのことを行っていることが分かりました。
Glideの画像取得処理の肝であるEngine
に関しては、また別でみていきたいと思います!