- 投稿日:2019-12-09T23:49:55+09:00
Deep Understanding of Seek
はじめに
ExoPlayerでは以下のようにシーク操作を行うことができます。
player.seekTo(positionMs)今回はこれが内部でどのようなことをやっていのかを詳しく解説します。
具体的には
- 渡されたpositionMsはどれだけ正確にシークされるのか。また、なぜそのような正確性になるのか。
- シーク処理にはどのくらい時間がかかるのか。また、なぜそのような時間がかかるのか。
を解説します。
注) ExoPlayer2.10.8で検証を行っています。
注) ExoPlayerでの話なので、他のPlayerではロジックや結果などが異なる可能性があります。シークの正確性
シークの正確性を知るにはまずpts(presentation time stamp)を知る必要があります。
動画は一枚の瞬間を表したframeと呼ばれる集合体で構成されていますが、映像はこのframeがパラパラ漫画のように連続的に描画されることによって映し出されます。そして、この一つ一つのframeはptsというframeが表示されるべき時間を表したタイムスタンプが紐付いており、その時間になったタイミングで、decodeされたframeを描画する必要があります。
つまり、映像が持つ最小の時間単位はこのptsということになります。
(pts以外にもdts(decode time stamp)なども存在します。)この映像が持つ最小単位の時間であるptsを元にシークするのが
frame accurate seeking
で、frame単位でシーク位置に最もふさわしいframeを見つけ出しそのframeから描画を開始します。一方で、
key-frame accurate seeking
と呼ばれるものも存在します。これは、Key-Frame単位でシーク位置を探索するもので、frame accurate seekingよりもシーク後の1frame目が表示されるのが早いですが、シーク位置はframe accurate seekingよりも正確ではありません。つまり、遅いけど正確な
frame accurate seeking
、早いけど曖昧なkey-frame accurate seeking
ということになります。ExoPlayerではどちらが使われているかというとデフォルトで
frame accurate seeking
が使われています。シークの所要時間
ExoPlayerでは、
frame accurate seeking
が使われていますが、どのようにframeを見つけ出しているのでしょうか?
ExoPlayerはSyncPointsという概念を元にシークすべきframeを見つけています。SyncPointsとは、探索可能な特定の区間を区切ることのできるframeらを示しており、この特定のFrameで区切られた区間を元にシークすべきframeを探索しています。
では、探索可能な特定の区間を区切ることのできるframeとはどのframeのことでしょうか?
これは、ストリーミングプロトコルやコンテイナーフォーマットによって変わります。たとえば、progressiveに再生されるmp4であれば
stbl
ボックスからkey-frameの位置が特定できるため、SyncPointsはKey-Frameになります。また、fmp4で再生されるMPEG-DASHであればSyncPointsはfmp4で区切られた区間になり、MPEG2-TSで再生されるHLSであればSyncPointsはTSファイルごとになります。seekするframeが見つかるとExoPlayerはその区間からデータをデコーダーに詰めていき、デコードを開始します。
ここで注意したいのは、seekするframeではなく、区間の初めのframeからデコーダーに積まれていることです。そのため、ExoPlayerは区間の先頭のframeからseekするframe以前のBufferに対して、isDecodeOnlyのフラグを追加します。このフラグを追加することによって区間の先頭のframeからseekするframe以前のframeはデコーダーによってデコードされますが、デコードされたフレームは描画されずただskipされます。
そして、seekするframe以降のframeがDecodeされるとそのフレームはSurface上に描画されます。(なぜseekしたいframeの直前のKey-FrameからDecoderに入れてないのかはわからないです。)
SeekParameters
シーク処理は、シークするframeの探索とシークするframeまでのdecode処理により、ある程度時間がかかってしまいます。
ここで、シーク処理の時間を早めるためにSeekParametersというものがあります。SeekParametersはそれぞれ、EXACT、CLOSEST_SYNC, PREVIOUS_SUNC, NEXT_SYNCの四つの種類があります。
EXACTは今まで説明してきたframe accurate seeking
ですが、それ以外のものは、シークすべき対象のframeをSyncPointsで区切られたFrameにします。こうすることで、無駄なdecode処理が省かれシークされて1framem目が表示されるまでの時間が短くなります。以下では区切られたPointのframeをsync-frameと読んでいますが、このsync-frameは先ほどのSyncPointsの定義から、場合によってはKey-Frameですが、再生種別によってはKey-Frame単位ではない場合もある(複数のkey-frameにまたがっている可能性がある)ので正確には
key-frame accurate seeking
ではないことに注意してください。
SeekParameters 概要 EXACT Defaultのシーク方法。 直後の一番近いframeにシークします。 CLOSEST_SYNC 一番近いsync-frameにシークします。 PREVIOUS_SYNC 直前の一番近いsync-frameにシークします。 NEXT_SYNC 直後の一番近いsync-frameにシークします。 HLSはシークが遅い?
ExoPlayerでHLSのストリームをシークすると、他の再生種別に比べてシークが圧倒的に遅いです。
これは、なぜ遅いかというと通常TSファイルの動画の長さは通常6~10s程度あるので、他の再生種別と比べてSyncPointsの区間が圧倒的に長くなってしまうからです。(SynPointsはProgressiveなmp4であればkey-frame単位、fmp4のDASHではあればfmp4単位)そのため、まずSeekしたいframeが見つかると6~10sものファイルをまず取得する必要があります。この時点でまず時間がかかります。また、そのあとにセグメントの後ろの方にSeekしたいFrameがあった場合には取ってきたほぼ全てのframeをデコードまでした後にそれらをSkipして捨てなければなりません。
実際に、通信環境が良好な環境(60Mbps程度)で、セグメントの前の方と後ろの方でシークして見ると、セグメントの前の方にシークした場合には上部のsb(skipped buffer count)が9になっておりシークまでの時間もある程度早いですが、セグメントの後ろの方にシークした場合には、sbが198になっており、シークまでの時間も先ほどと比べて時間がかかっています。
少しわかりにくいですが、「セグメントの最後の方にシーク」の方は映像が少し止まっているのがわかります。
実際には、seekTo()を読んでから1frame目が描画されるまで、「セグメントの最初の方にシーク」は85ms、「セグメントの最後の方にシーク」は757msかかっています。
セグメントの最初の方にシーク セグメントの最後の方にシーク これで、さらに通信速度がそこまでよくない環境だった場合には、セグメントファイル(6~10s)を取得する時間も他の再生種別と比べて大きく影響してきます。
では、SeekParemetersを使うのはどうでしょうか?
実はExoPlayerはHLSにおいてはSeekParametersをサポートしていません。
理由はSyncPointsがあまりにもシーク位置と異なってしまうからです。そのため、HLSでは毎回frame accurate seekingが行われます。https://github.com/google/ExoPlayer/issues/2882
まとめ
- ExoPlayerはデフォルトでframe accurate seekingを行うが、SeekParametersを使って挙動を変えることができる。(HLS以外)
- HLSはSyncPointsの区間長さが他の再生種別よりも圧倒的に大きくなるためシークが遅くなる。
- 投稿日:2019-12-09T23:43:25+09:00
Android10でもクリップボードを使いたいっ
はじめに
この記事は、Android Advent Calendar 2019の9日目です!
Androidでクリップボードを扱うには、以下のようにClipboardManagerを使うのが一般的です。
クリップボードを画面起動時に取得するにはonResumeなどでClipboardManagerを呼び出して取得していました。MainActivity.ktclass MainActivity : AppCompatActivity() { // 中略 override fun onResume() { super.onResume() Snackbar.make(root, getClipboard(), Snackbar.LENGTH_SHORT).show() } private fun Context.getClipboard(): String { val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? return clipboard?.primaryClip?.getItemAt(0)?.text?.toString().orEmpty() } }しかしAndroid10では、上記の方法でクリップボードの中身を取得しようとするとnullが帰ってきて取得することが出来なくなってしまいました。
Android10 それ以下の端末 これの現象はAndroid10で、ユーザーのプライバシーを守るためにクリップボードの取得に制限がかけられたことに起因しています。
Limited access to clipboard data
Unless your app is the default input method editor (IME) or is the app that currently has focus, your app cannot access clipboard data on Android 10 or higher.
https://developer.android.com/about/versions/10/privacy/changesこの記事で書かれているように、Android10ではクリップボードのデータにアクセスするためには入力システム(IME)か現在フォーカスを持っているアプリである必要があります。
そのためAndroid10では、いくつかのクリップボードアプリが使えなくなっているといわれています。
そこでこの記事では、どのように実装すれば正しく取得することができるかについて書いていきたいと思います。3行まとめ
- クリップボードを取得するためにはフォーカスが必要になった。
- Androidでフォーカスが来たことを検知するのは
Activity#onWindowFocusChanged
。Activity#onWindowFocusChanged
でクリップボードは取得するようにする。原因
まず、原因と解決方法を詳しく調査するためにgooglesourceでClipboardManagerの実装を見ました。
原因となった修正のコミットとdiffはこちらのようです。変更されたコードを見ていくと
clipboardAccessAllowed
というメソッドが今回の修正で大きく変更されていることがわかります。- private boolean clipboardAccessAllowed(int op, String callingPackage, int callingUid) { + private boolean clipboardAccessAllowed(int op, String callingPackage, int uid, + @UserIdInt int userId) { // Check the AppOp. - if (mAppOps.noteOp(op, callingUid, callingPackage) != AppOpsManager.MODE_ALLOWED) { + if (mAppOps.noteOp(op, uid, callingPackage) != AppOpsManager.MODE_ALLOWED) { return false; } // Shell can access the clipboard for testing purposes. @@ -641,7 +750,6 @@ return true; } // The default IME is always allowed to access the clipboard. - int userId = UserHandle.getUserId(callingUid); String defaultIme = Settings.Secure.getStringForUser(getContext().getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD, userId); if (!TextUtils.isEmpty(defaultIme)) { @@ -654,16 +762,31 @@ switch (op) { case AppOpsManager.OP_READ_CLIPBOARD: // Clipboard can only be read by applications with focus.. - boolean allowed = mWm.isUidFocused(callingUid); + // or the application have the INTERNAL_SYSTEM_WINDOW and INTERACT_ACROSS_USERS_FULL + // at the same time. e.x. SystemUI. It needs to check the window focus of + // Binder.getCallingUid(). Without checking, the user X can't copy any thing from + // INTERNAL_SYSTEM_WINDOW to the other applications. + boolean allowed = mWm.isUidFocused(uid) + || isInternalSysWindowAppWithWindowFocus(callingPackage); if (!allowed && mContentCaptureInternal != null) { // ...or the Content Capture Service - allowed = mContentCaptureInternal.isContentCaptureServiceForUser(callingUid, - userId); + // The uid parameter of mContentCaptureInternal.isContentCaptureServiceForUser + // is used to check if the uid has the permission BIND_CONTENT_CAPTURE_SERVICE. + // if the application has the permission, let it to access user's clipboard. + // To passed synthesized uid user#10_app#systemui may not tell the real uid. + // userId must pass intending userId. i.e. user#10. + allowed = mContentCaptureInternal.isContentCaptureServiceForUser( + Binder.getCallingUid(), userId); } if (!allowed && mAutofillInternal != null) { // ...or the Augmented Autofill Service - allowed = mAutofillInternal.isAugmentedAutofillServiceForUser(callingUid, - userId); + // The uid parameter of mAutofillInternal.isAugmentedAutofillServiceForUser + // is used to check if the uid has the permission BIND_AUTOFILL_SERVICE. + // if the application has the permission, let it to access user's clipboard. + // To passed synthesized uid user#10_app#systemui may not tell the real uid. + // userId must pass intending userId. i.e. user#10. + allowed = mAutofillInternal.isAugmentedAutofillServiceForUser( + Binder.getCallingUid(), userId); } if (!allowed) { Slog.e(TAG, "Denying clipboard access to " + callingPackageこのメソッドの中では、
isInternalSysWindowAppWithWindowFocus
が現在のアカウントでアプリにフォーカスがあたっているかを判定しています。isInternalSysWindowAppWithWindowFocusは、WindowManagerがフォーカスがあたっているかをチェックして、フォーカスがあたっている場合にクリップボードを使うことが出来るように制御しています。- boolean allowed = mWm.isUidFocused(callingUid); + boolean allowed = mWm.isUidFocused(uid) || isInternalSysWindowAppWithWindowFocus(callingPackage);+ /** + * To check if the application has granted the INTERNAL_SYSTEM_WINDOW permission and window + * focus. + * <p> + * All of applications granted INTERNAL_SYSTEM_WINDOW has the risk to leak clip information to + * the other user because INTERNAL_SYSTEM_WINDOW is signature level. i.e. platform key. Because + * some of applications have both of INTERNAL_SYSTEM_WINDOW and INTERACT_ACROSS_USERS_FULL at + * the same time, that means they show the same window to all of users. + * </p><p> + * Unfortunately, all of applications with INTERNAL_SYSTEM_WINDOW starts very early and then + * the real window show is belong to user 0 rather user X. The result of + * WindowManager.isUidFocused checking user X window is false. + * </p> + * @return true if the app granted INTERNAL_SYSTEM_WINDOW permission. + */ + private boolean isInternalSysWindowAppWithWindowFocus(String callingPackage) { + // Shell can access the clipboard for testing purposes. + if (mPm.checkPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW, + callingPackage) == PackageManager.PERMISSION_GRANTED) { + if (mWm.isUidFocused(Binder.getCallingUid())) { + return true; + } + } + + return false; + }
そのためこの修正の入ったAndroid10では、IMEもしくはフォーカスがあたったアプリでしかクリップボードは取得できなくなっています。
解決策
解決策としてはフォーカスが当たったタイミングが取得できればクリップボードも取得できるはずです。
Androidのライフサイクルは以下の順で呼ばれます。
onResumeのタイミングではまだ画面が生成され終わっていないためフォーカスを取得するにはonWindowFocusChangedで取得する必要があります。
ライフサイクル タイミング onCreate Activity起動時 onStart Activity表示時 onResume Activityが前面になる時 onWindowFocusChanged フォーカスが変わった時 onWindowFocusChangedは、フォーカスが変化した時に呼ばれ、hasFocusでFocusの値を取得することができます。hasFocusがtrueになったときはアプリにフォーカスが来ているので、以下のように書くことで画面起動時にクリップボードの値を正しく取得することが可能になります。
MainActivity.ktclass MainActivity : AppCompatActivity() { // 中略 override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus) { Snackbar.make(root, getClipboard(), Snackbar.LENGTH_SHORT).show() } } private fun Context.getClipboard(): String { val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? return clipboard?.primaryClip?.getItemAt(0)?.text?.toString().orEmpty() } }まとめ
これまで、Androidではクリップボードを何も考えずに使うことができていました。しかしセキュリティの観点からもAndroid10では難しくなってしまいました。
今後アプリ起動時にクリップボードからデータを取得する際にはonWindowFocusChanged
の中で取得しましょう。
- 投稿日:2019-12-09T23:17:08+09:00
[Android]デバッグ中のアプリのSQLiteデータベースのファイルを取り出して中身を見たい
環境
Android Studio 3.5.3
方法
Android Studioの右下にある
Device File Explorer
を開く。
すると以下のような画面が出てくるので、
/data/deta/(バンドルID)/databases/
とたどる。
するとSQLiteのデータベースファイルの一覧が現れるので、右クリックして「Save As...」などする。
あとはDB Browser for SQLiteなどで開いて見るとよい。
参考
- 投稿日:2019-12-09T22:23:51+09:00
RxFirebaseでRxJava + Firebaseを手軽に始める
はじめに
RxJavaを簡単な例(OkHttpとか)で触っているうちは、OkHttpやその補助ライブラリで容易にRx化できるかと思います。
ただし、いざ、自分で他のライブラリと組み合わせて書いていこうとすると、辛い部分が多く存在します。特に、Firebaseと組み合わせようとすると、RxJavaに慣れていないうちは
どう書けばいいかがわからないこと
記事が圧倒的に少ない
ためにつらい思いをすると思います。
(たとえばこちらの記事にのっています)
そこで
RxFirebaseというライブラリを使うことで
簡単にRxJava+Firebaseを始めることができます。実際に使ってみる
ここからは実際のプロジェクトでの使用方法を説明していきます。
また、Firebaseなどの呼び出しはモジュールとして切り出している例を紹介します。インストール方法
RxFirebaseのREADMEに書いている通りに導入していきます。
build.gradledependencies { compile 'com.github.FrangSierra:RxFirebase:1.5.6' }build.gradleallprojects { repositories { ... maven { url "https://jitpack.io" } } }FirebaseAuthの場合
今回は単純なemailでの認証を考えます。
FirebaseAuthService.ktobject FirebaseAuthService: AuthService { val auth: FirebaseAuth init { auth = FirebaseAuth.getInstance() } override fun signIn(email: String, password: String): Maybe<AuthResult?> { return RxFirebaseAuth.signInWithEmailAndPassword(auth, email, password) } }例えば、emailでのシンプルな認証の場合、RxFirebaseが用意しているメソッド
signInWithEmailAndPassword
に
Firebase
のインスタンスを噛ませるだけでできます。これをもし、自分でやる場合は、自分でSingleやObservableを作ったりと、なれていないとわかりづらいところが多々あります。
あとは、Observerを加えるだけですが、MainActivityで呼び出したりするとき
MainActivity.ktFirebaseAuthService.signIn(email, password) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : MaybeObserver<AuthResult?> { override fun onSuccess(t: AuthResult) { // うまくいったときー } override fun onComplete() { // 完了したときー } override fun onSubscribe(d: Disposable) { // こうどくしはじめたときー } override fun onError(e: Throwable) { // エラーがでちゃったとき } })というふうに呼び出してやるだけで簡単に使えます。
最後に
最近のAndroid開発ではRxJavaがよく出てきますがとっつきづらい部分が多々あると思います。
今回のように、RxFirebaseを用いることで、初心者にとって実装が難しい部分をライブラリに任せるながら
FirebaseとRxJavaを利用することができるため、「ちょっとRxJavaをさわってみようかな」といった方には
ベターな選択肢だと思います。また、今回のようにサービス層側の実装をきれいに書いてあげることで、
RxJavaに慣れてからRxFirebaseに依存しないものに置き換えることも簡単であり、
RxFirebaseを利用するもの、しないものの共存もできるので、非常に便利です。それでは、RxFirebaseでよきRxJava Lifeを!!
- 投稿日:2019-12-09T21:09:16+09:00
AndroidでKinesis Video Stream のWebRTC機能を試す
(雑ですが備忘録です)
ビルド&実行
以下のGithubに沿ってCognitoの設定を行い、サンプルコードをビルドする。
https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-androidMaster起動後に、別のAndroid端末でViewerを起動すると、ビデオ通話ができました。
また、Kinesis Video Streams のSignaling Channelsから、CloudWatchのログ等が確認できました。
コード上のメモ
チャネルARNの取得
mChannelArn = describeSignalingChannelResult.getChannelInfo().getChannelARN();Channel ARN is arn:aws:kinesisvideo:ap-northeast-1:123456789:channel/demo-channel/1575874258965ARNの末尾の
1575874258965
はUnix Time StampぽいですEndpoint 取得
上記のARNやロール(MasterかViwerか)を設定し、リソースエンドポイントを取得しています。
GetSignalingChannelEndpointResult getSignalingChannelEndpointResult = awsKinesisVideoClient.getSignalingChannelEndpoint( new GetSignalingChannelEndpointRequest() .withChannelARN(mChannelArn) .withSingleMasterChannelEndpointConfiguration( new SingleMasterChannelEndpointConfiguration() .withProtocols("WSS", "HTTPS") .withRole(role)));{ResourceEndpointList: [{Protocol: WSS,ResourceEndpoint: wss://<id>.kinesisvideo.ap-northeast-1.amazonaws.com}, {Protocol: HTTPS,ResourceEndpoint: https://<id>.kinesisvideo.ap-northeast-1.amazonaws.com}]}こちらで取得したHTTPSのエンドポイントを使って、
AWSKinesisVideoSignalingClient
のインスタンスを作成し、ICEサーバーの情報を取得しています。ICE Server list
getIceServerConfigResult.getIceServerList()手元で出力されたログをみたところ、2つのIPをturnを使って取得しているようでした。
{Uris: [turn:12.34.56.78:443],Username: xxx,Password: XXX,Ttl: 300} {Uris: [turn:23.45.67.89:443],Username: yyy,Password: YYY,Ttl: 300}WebRtcActivity.javaPeerConnection.IceServer stun = PeerConnection .IceServer .builder(String.format("stun:stun.kinesisvideo.%s.amazonaws.com:443", mRegion)) .createIceServer(); peerIceServers.add(stun);Stun サーバーについてはは、リージョンごとに固定のエンドポイントを指定しています。
上記の情報等を用いて、PeerConnectionFactory.createPeerConnection()
でピア接続を行います。
- 投稿日:2019-12-09T21:02:07+09:00
Android StudioのBreakpoints再入門
はじめに
みなさん、Androidアプリのデバッグ時にアプリケーションコードにむやみに
Log.d()
を埋めて、再ビルドを何回も繰り返しながらデバッグしていませんか?
もしくはブレークポイントでsuspendさせまくって、Resume Program
(suspend状態から再開するやつ)を連打していませんか?でもアプリの規模が大きくなっていくと、再ビルドに掛かる時間も無視できなくなったり、
Resume Program
の連打し過ぎで指が腱鞘炎になったりします。そんなときはAndroid StudioのBreakpointsを上手く設定して、何度も「再ビルド→再インストール」や
Resume Program
連打から脱却しましょう。目標
- デバッグのために何度も「
Log.d()
を埋め込んでアプリを再ビルド→再インストール」をやめるResume Program
連打をやめる環境
Android Studio3.5
Android 9(なんとなく)「ブレークポイント」でログを出力する
実行時に通ったパスを知りたいけど、
Log.d()
を埋めて再ビルドを待つのは嫌。でも、ブレークポイントを張りまくって毎回処理がsuspendされるのは鬱陶しい…
そんな時はブレークポイントを上手に設定して、suspendさせること無くログを出力しましょう。手順
- いつも通り、対象の行にブレークポイントを張る
- Breakpoints(デフォルトではShift + Command + F8) > 左の一覧から該当のブレークポイントを選択 >
Suspend
のチェックを外す"Breakpoint hit" message
にチェックを付ける (お好みでStack traceも出力できます)DONE
をクリック(チェックボックスをクリックしただけだと反映されない)ブレークポイントを設定したら、いつも通りデバッガをアタッチして実際に該当箇所の処理を実行するだけ。
該当する行が実行されると以下のようにログが出力されます。
※LogcatではなくDebuggerのConsoleタブに出力されるので注意が必要ですアプリを再ビルドすることなく
Breakpoint reached at net.fdash.adventcalendartest.MainActivity.onCreate(MainActivity.kt:22)
というログを出力させることが出来ました!ついでに変数の中身、関数の戻り値を出力したい
例えば、RecyclerViewのポジションやタップされた座標のようにどんどん変わっていく値を見るために、毎回ブレークポイントでsuspendさせて値を確認するのは効率が悪いですよね。
そんなときに↓のようなログを埋めてアプリを再ビルドしたことはありませんか?override fun onBindViewHolder(holder: ViewHolder, position: Int) { Log.d(TAG, "${position}番目のセルにViewHolderがbindされたよ")実はこの場合も、アプリを再ビルドすることなく実現できます。
手順
- 先程と同様にsuspendさせないブレークポイントを設定する
Evaluate and log:
にチェックを入れテキストボックスに表示させたい文字列を入れる(コードの評価をしてくれるので、変数や関数呼び出しを含めることも可能)あとは、同じようにデバッガをアタッチして該当するコードを実行するだけです。
アプリを再ビルドすることも、ViewHolderがbindされる度にsuspendされることもなく、何番目のセルにViewHolderがbindされたか知ることが出来ます。ブレークポイントの条件いろいろ
ちなみに、ブレークポイントには細かく条件を設定することができます。
項目 説明 Remove once hit
初回到達時にブレークポイントが外れる Instance filters:
指定したインスタンスID ( MainActivity@10792
の10792
の部分)のインスタンスでだけブレークポイントが有効になるClass filters:
指定したクラスの場合だけブレークポイントが有効になる
継承関係がある場合に、特定のサブクラスだけブレークポイントを有効(もしくは無効)などが可能Pass count:
該当する行に指定した回数到達したときにブレークポイントが有効になる
(3
を指定すると3, 6, 9, ...
回目に到達した場合のみブレークポイントが有効)Caller filters:
特定のクラスやメソッドから呼ばれた場合のみブレークポイントが有効(もしくは無効)になる
指定の仕方に癖があるので注意(例えば*foo*
と指定すれば、foo
を含むクラスやメソッドからの呼び出し時のみ有効/無効になる
詳しくはjetbrainsのヘルプページを参照Disable until breakpoint is hit:
:ブレークポイントに依存関係を設定出来る。依存先のブレークポイントに到達するまでは自身のブレークポイントは無効だが、依存先のブレークポイントに到達したあとは自身のブレークポイントが有効になる。 何回処理が実行されたか知りたい
当然、ブレークポイントでsuspendもログ出力もさせないことが出来ます。
ただ、suspendもログ出力もしないブレークポイントに何の意味があるのでしょうか。実は、DebuggerのConsoleタブ > Overheadの部分には、ブレークポイントを通過した回数とデバッグに掛かったオーバーヘッドが表示されています。(実際の処理に何ms掛かったかを表示するわけではないので注意)
つまり、ログ出力やsuspendしないブレークポイントも、「どこか分からないけど、何かの処理が沢山呼ばれてる...」時の調査に威力を発揮します。
アプリ起動時の処理をデバッグする
アプリ起動時にクラッシュする場合、原因を突き止めるためにブレークポイントを張って止めたいけど「起動後にデバッガをアタッチしたら間に合わないよ!」とか「毎回
Debug
やりなおすの面倒!」ってなりますよね。
もしくは「バックグラウンドでプロセスがKILLされた後の復帰時にデバッガをアタッチしておきたい」という場合。そんなときは、デバッガをアタッチするまでアプリが起動しないようにしてしまいましょう。
手順
デバッグ対象のアプリを起動(普通にランチャーから起動するだけ)
こうすることで、
MainActivity#onCreate
も確実にブレークポイントで止める事ができます。まとめ
- ブレークポイントはただ処理を中断するだけではない
- ブレークポイントを上手く使えば、デバッグのための再ビルド回数が削減出来る(開発が早くなる!)
- ブレークポイントを上手く使えば、
Resume Program
の連打が不要になる(腱鞘炎にならない!)現場からは以上です。
明日は @ysat さんの【ActiveRecordを使いつつDBのデータをメモ化して扱ってみる】です。
お楽しみに!!
- 投稿日:2019-12-09T19:44:59+09:00
Androidエンジニアの面接で聞きがちな質問集
概要
どうもAndroidエンジニアの渡邉です。自分は仕事で採用や面談に関わることが多いのですが、入社後に活躍していただけるだろうかというインタビューと、Androidの知識に関するインタビューをします。今回は勉強会の懇親会などで採用に関わっている人とかにも「どんな質問します?」と聞いたことのある、Androidエンジニアインタビューあるあるな質問と回答をまとめたいと思います。自分の場合JetPackやアーキテクチャデザインパターンについては面接で必ずと言っていいほど聞きますが、その他の質問は自分だったら聞かれたら嬉しいなという感じで、技術を習得する姿勢を証明できれば、あとは入社してから覚えていければいいんじゃないというスタンスです。
環境や前提条件
Androidの中途採用の面接やSESでの面談で、テックブログやGithub、職務経歴書から読み取れないAndroidの知識に関するインタビューをするときを考えます。 答えが一つじゃないものに関しては解答例を上げたいと思います。
QA
Q. Android OSで知っているバージョンと名前を教えてください
note : ほぼアイスブレイク。Kitkatのとき辛かったよね〜みたいな話をするみたいです。
A. あ、はい。
version name note Android 10 - https://developer.android.com/about/versions/10 Android 9 Pie https://developer.android.com/about/versions/pie Android 8.0 Oreo https://developer.android.com/about/versions/oreo Android 7.0~7.1 Nougat https://developer.android.com/about/versions/nougat Android 6.0 Marshmallow https://developer.android.com/about/versions/marshmallow Android 5.0~5.1 Lollipop https://developer.android.com/about/versions/lollipop Android 4.4 Kitkat https://developer.android.com/about/versions/kitkat 参考:https://developer.android.com/about/versions/10
Q. Android SDKとはなんでしょうか?
A. あ、はい。Androidアプリを開発するときに必要とされるツールセットのことでSoftware Development Kitの略です。Debuggerやエミュレータ、AndroidのAPIがそれになります。Googleが新しいAndroidのバージョンをリリースするたびに、対応するSDKがリリースされるので、開発者は最新の機能を実装するために、SDKをダウンロードしてAndroidStudioにインストールする必要があります。
参考:https://www.techopedia.com/definition/4220/android-sdk
Q. Applicationとはなんのことでしょうか?
A. あ、はい。Applicationクラスは、アクティビティなどすべてのコンポーネントを含むAndroidアプリ内の基本クラス(神クラス)です。 AndroidManifest.xmlのタグの「android:name」属性として指定することにより、サブクラスを実装することででき、アプリケーション/パッケージのプロセスが作成されるときに、他のクラスの前にインスタンス化されます。
Q. Contextとはなんのことでしょうか?
A. あ、はい。Contextというとまぁ2つにわかれて, ApplicationContext, ActivityContextになります。Context自体は、アプリ環境に関するグローバルな情報へのインターフェースで、画像やレイアウト、stringsなどののリソースやDBやPreferenceへのアクセスや、インテントのブロードキャストの受信などアプリケーションレベルの操作や呼び出しをする時に使う抽象クラスですね。
ApplicationContextは、Activityのライフサイクルをまたいでコンテキストを渡す場合に使います。画像を読み込むライブラリのGlideに渡すコンテキストとかで使ったりしますかね。
ActivityContextはアクティビティで使用することができるもので、ライフサイクルに関連付けられているものになります。ActivityのスコープでContextを渡す場合や、現在のContextにライフサイクルが関連付けられているContextが必要な場合は、これをを使用する必要があります。画面遷移のIntentに使用するとかですかね。
参考:
- https://developer.android.com/reference/android/content/Context
- https://stackoverflow.com/questions/31964737/glide-image-loading-with-application-contextQ. Activityとはなんでしょうか?
A. あ、はい。 Androidアプリを構成する重要なコンポーネントで、アプリがUIを描画するウィンドウって感じですね。アプリと別アプリのエントリーポイントとして機能します。AndroidManifestのタグ内でタグを宣言してあげる必要があります。アーキテクチャデザインパターンなどではFragmentと並んで、Viewとして位置づけされたりしますかね。Androidの場合は
main()
で起動するWebアプリとかのプログラミングとは異なってライフサイクルのコールバックバックメソッドを呼び出すことで、Activityインスタンス内のコードが開始されるものになってます。参考: https://developer.android.com/guide/components/activities/intro-activities
Q. Activityのライフサイクルとは何で、何があるかと、それぞれどんなタイミングで呼ばれるのでしょうか?
A. あ、はい。Activityにはアプリ内を遷移したり、バックグラウンドにしたり、電話が来たりっていうイベントなどで変化する状態を認識できるようにライフサイクルのコールバックっていうのが用意されていて、それぞれ、
- onCreate : システムが初めてアクティビティを作成するときに発生するコールバックで、アクティビティの存続期間にわたり一度だけ発生する必要がある基本的なアプリケーション起動ロジックを実行しますね。例えばViewのバインドとかMVVMでいうところのViewModelのの関連付け。savedInstanceStateなど以前に保存された状態の復元などですね。
- onStart : Activityが開始状態になると発生するコールバックで、アプリでActivityがフォアグラウンドに移動し、インタラクティブにできる状態ですね。
- onResume : Activityが再開状態になると、Activityはフォアグラウンドに移動し、システムが onResume() コールバックを呼び出しますね。カメラのプレビュー開始などのフォアグラウンドにある状態で実行する必要が機能を有効にする処理なんかをかく感じですね
- onPause : ユーザーが他のActivity起動したり、バックグラウンドにするときなど、Activityから離れる時にシステムが呼ぶコールバックですね。GPSのセンサーとか、電池寿命に関係しそうなリソースを解放する時とかに呼ぶ感じですね。でもマルチウィンドウモードとか考慮すると、解放するタイミング検討する必要があるんですよね〜。
- onStop : ユーザーに対して表示されなくなったActivityって停止状態になるんで、その時システムは onStop() コールバックしますね。Roomとかでデータを永続化する処理なんかでタイミングどうするってなったときとかはここに書いたりしますね。onStopされたActivityが画面復帰するとonRestart()をよびますね。
- onDestory : アクティビティが破棄される前に呼び出されますね。注意したいのは画面が回転した時とかシステムのフォントとか変えたりとかすると実行される感じですね。
Activityのライフサイクルは基礎って感じなのでFragmentとかViewModelのライフサイクルも並行して覚えるのが重要って感じですね。
参考: https://developer.android.com/guide/components/activities/activity-lifecycle
Q. GradleのBuildTypeとは何で、何をするために使うのでしょうか?
A. あ、はい。アプリをビルドする時とかにGradleが使用するプロパティの定義するときに使うんですけど、ProGuardつかって難読化するかとか、flavourでビルドにつかうリソースとかも定義しますし、buildTypeとflavourで組み合わせて依存関係と署名設定を適切に管理するためのbuild variantをつくりますね。
参考:
- https://developer.android.com/studio/build/build-variants?hl=jaQ. Fragmentとはなんでしょうか?
A. あ、はい。UIの挙動とか部位を表すもので、最近だとSingleActivityのアプリを作ったりするので1つのActivityで複数のFragmentを組み合わせたりとか、まぁViewPagerとかBottomNavigationとか組み合わせたりですね。別々のActivityでFragmentを再利用したりすることもできて、Activityのモジュラーセクション的なものですね。
参考:https://developer.android.com/guide/components/fragments
Q. 画面を回転するとActivityはどうなりますか?
A. あ、はい。画面を回転するとonDestoryが呼ばれます。つまりActivityのインスタンスが破棄されちゃって新しいインスタンスが回転した新しい向きでできます。他のLifecycleメソッドは最初にActivityが作成されたときと同じフローで呼ばれますね。
Q. 画面が回転してもデータがリセットされたり、リロードされないようにするにはどうすればいいでしょうか?
A. あ、はい。まぁ基本的にはViewModelとonSaveInstanceStateの組み合わせですかね。ViewModelはLifeCycle-Aware、つまり回転しても破棄されることがないのでViewModelにデータを格納してまたバインドしたり、EditTextとかのUIデータはonSaveInstanceState使いますかね。
参考:
- https://developer.android.com/topic/libraries/architecture/lifecycle
- https://developer.android.com/topic/libraries/architecture/viewmodelQ. Android Studioの機能について便利だと思うところ教えてください
A. あ、はい。例えば、プロファイリングツールが便利ですね。パフォーマンスチューニングする時にCPU、メモリ、グラフィック、ネットワーク、デバイス バッテリーなどのリソースをアプリが効率的に使用できていない場所を発見するのに使います。
他にはpluginは便利そうなのは入れたりしてます。AndroidStudioはIntelliJ IDEAがベースになってるのでpluginが使えるのはありがたいですね。
Kotlin Fill Classとか
ADB IdeaとかADBコマンド実行する時にショートカットすることで作業が円滑になりますね。
参考:
- https://developer.android.com/studio/intro
- https://plugins.jetbrains.com/plugin/10942-kotlin-fill-class
- https://plugins.jetbrains.com/plugin/7380-adb-idea/
- https://medium.com/@eyal_katz/top-17-plugins-for-android-studio-b53daca83977Q. Android JetPackについて知っていることを教えてください
A. あ、はい。Android JetPackは2018年のGoogleIOで発表された開発を簡単に作成するためのライブラリやツール、ガイダンスのことですね。ボイラープレートコードの手間を省いてくれるのでドメインに手中することができます。あぁこれもJetPackだったのかみたいな項目も多いんですけど、KTX, DataBinding, LiveData, Navigation, Room, ViewModelとかは今まで作ったアプリ的に重視して深掘りしてきました。けっこう勉強会(GDG Tokyo)でも取り上げられる内容なので、特に気になるライブラリはキャッチアップしてました。https://gdg-tokyo.connpass.com/
参考:
https://developer.android.com/jetpack?hl=jaQ. Androidのアーキテクチャデザインパターンについて知っていることや、MVC,MVP,MVVMと違い、問題点など教えてください
A. あ、はい。最近のモバイルアプリの要件が高まっていることからAndroidのアーキテクチャデザインパターンは関心が集まっていると感じます。2008年にAndroidが登場してから現在に至るまでに、Androidエンジニアは、FatActivity問題といわれる、Activityで複数の機能を実装したり、内部で機能が密結合したり、コールバックをActivity自身で受け取ったりといったコールバック地獄に悩まされていました。その課題を解決するためには機能を適切なモジュールに分離する必要があったのです。それがアーキテクチャデザインパターンを採用するモチベーションとなっていて、各パターンの一般的な原則は「関心の分離」です。ActivityやFragmentといったAndroidのOSとアプリを繋げる結合クラスへの依存を最小限にすることが「アプリのアーキテクチャガイド」でも推奨されていて、MVVMがデファクトスタンダート言ったところではないでしょうか。
MVCは、Model-View-Controllerの頭文字で、モデルはモデルデータクラスのことを指していて、ビューはxmlファイルを参照し、コントローラーはビジネスロジックです。コントローラーがAndroidAPIに密に結合しているため、単体テストがかけないのが問題といったところではないでしょうか。
MVPは、Model-View-Presenterの頭文字です。ビューには、xmlおよびactivityやfragmentクラスが含まれます。そのため、アクティビティはビューインターフェイスを実装するのが理想的だと思います。
MVVMは、Model-View-ViewModelの頭文字です。 Modelは、オンライン上のRemoteデータソース、ローカルのSQLiteやPreference、ビジネスロジックで構成されます。ViewModelはデータをラップし、ビューのデータをLiveDataなどで保持して、準備する役割を果たします。 また、ビューからモデルにイベントを仲介する役割を担います。
参考:
- https://developer.android.com/jetpack/docs/guide?hl=ja
- https://peaks.cc/books/architecture_patterns
- https://github.com/android/architecture-samples
- https://www.techyourchance.com/mvp-mvc-android-1/まとめ
以上、Androidエンジニア面接で聞きがちな質問をまとめてみました。面接などで役に立てばと思います。あとRxやCoroutineなど聞かれることも多いみたいなので、勉強しておくとよいでしょう。あとだいたい簡単なサンプルアプリの提出などがあるので、細かいところはコードレビューでスキルのチェックをするみたいです。
もちろんこのリストは決して完璧ではありませんので、自分はこういうこと聞いてるよ〜というのがあればTwitter(@nabetaro_jp)に教えてください!
読んでいただいてありがとうございました!
- 投稿日:2019-12-09T19:09:10+09:00
Androidアプリ内でGoogle Maps Platformを使う
Androidアプリにて、Google Maps Platformを動かすようにするまでで少し詰まったので、まとめておきます。
はじめに
- マップ
- ルート
- プレイス
サービスがあって、それぞれ
- Directions API
- Distance Matrix API
- Elevation API
- Geocoding API
- Maps Static API
- Places API
- Roads API
- Time Zone API
のAPIの種類があります。このAPIはWebのURLを通して叩くと、JSONファイルとして取得できます。例えば公式を参考に、緯度・経度が35.684064, 139.774517の場所を知りたければ、
API_KEY
が登録されているキーだとして、WebブラウザのURLにhttps://maps.googleapis.com/maps/api/geocode/json?latlng=35.684064,139.774517&key=YOUR_API_KEYとアクセスすると
{ "plus_code" : { "compound_code" : "MQMF+JR 日本、東京都東京", "global_code" : "8Q7XMQMF+JR" }, "results" : [ { "address_components" : [ { "long_name" : "日本橋", "short_name" : "日本橋", "types" : [ "establishment", "point_of_interest" ] }, { "long_name" : "1 国道1号", "short_name" : "1 国道1号", "types" : [ "premise" ] }, { "long_name" : "8", "short_name" : "8", "types" : [ "political", "sublocality", "sublocality_level_4" ] }, { "long_name" : "1丁目", "short_name" : "1丁目", "types" : [ "political", "sublocality", "sublocality_level_3" ] }, { "long_name" : "日本橋室町", "short_name" : "日本橋室町", "types" : [ "political", "sublocality", "sublocality_level_2" ] }, { "long_name" : "中央区", "short_name" : "中央区", "types" : [ "locality", "political" ] }, { "long_name" : "東京都", "short_name" : "東京都", "types" : [ "administrative_area_level_1", "political" ] }, { "long_name" : "日本", "short_name" : "JP", "types" : [ "country", "political" ] }, { "long_name" : "103-0022", "short_name" : "103-0022", "types" : [ "postal_code" ] } ], ...と、先程の緯度経度に対応する日本橋の住所や情報が出力されます。
このAPI、このようにWebページから叩けるのですが、NativeなJava, Python, Go, Node.js用にクライアントライブラリが公式で用意されていますGoogle Maps (GitHub)。
今回は、その中でのJava用のツールを用いて、Androidアプリ内で使用出来るまでの設定を行います。
前提
- 既にGoogle Cloud PlatformにてGoogle MapsのAPI Keyは取得しています
- Android studio 3.5.3
- Android Gradle Plugin 3.5.3
- Gradle Version 5.4.1
実装
Activityを持ったProjectを作る
クライアントライブラリをインポートする
Google Maps Platform Javaクライアントライブラリを, アプリの方の (プロジェクトの方ではありません)
build.gladle
に以下をスコープの外に追記します。build.gradle(app)repositories { mavenCentral() } dependencies { implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.google.maps:google-maps-services:0.10.0' implementation 'org.slf4j:slf4j-simple:1.7.26' implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1' implementation 'com.squareup.okhttp3:okhttp:3.4.1' }google-maps-servicesのバージョンは最新版が0.10.1ですが、ここを0.10.1にすると動きませんでしたので、バージョンを落として0.10.0にしています。また、JSONを取得するのに
gson
, HTTP通信を行うのに必要なモジュール類も入れています。
書き終わりましたら、右上にある
Sync Now
をクリックします。Maps APIを準備する
strings.xml
に書いても良いかもしれませんが、今回はapp/res/values/google_maps_api.xml
ファイルを作って、そちらに書きます。app/res/values/google_maps_api.xml<resources> <string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">API_KEY</string> </resources>MainActivityのレイアウトを作る
出力形式をJSONにするので、
ScrollView
でTextView
をくくります。activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="TextView" app:layout_constraintTop_toTopOf="parent" tools:layout_editor_absoluteX="179dp" /> </ScrollView> </androidx.constraintlayout.widget.ConstraintLayout>MainActivityを書く
Activityの中身を書いていきます。今回は勉強中のKotlinで書いてみました。
MainActivity.ktpackage com.example.googlemaps_test_kt import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import com.google.gson.GsonBuilder import com.google.maps.GeoApiContext import com.google.maps.GeocodingApi import com.google.maps.model.LatLng import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val context = GeoApiContext.Builder() .apiKey(getString(R.string.google_maps_key)) //API KEYを取得 .build() val latLng = LatLng(35.684064, 139.774517) val results = GeocodingApi.reverseGeocode(context, latLng).language("ja").awaitIgnoreError() val gson = GsonBuilder().setPrettyPrinting().create() textView.apply { text = gson.toJson(results[0].addressComponents) } } }(例外処理などはとりあえずなしで書いてしまいました)
Kotlinでも何一つ迷わずにJavaのライブラリを利用出来ました。インターネット接続の権限をマニフェストに追加する
これを忘れていてずっと悩んでいた…
HTTPでAPIを叩くので、必要な権限です。AndroidManifest.xml<uses-permission android:name="android.permission.INTERNET" />を追加
結果
URLからAPIを叩いたのと同じ結果になりました!
あとは、緯度経度をGPSで取得したり、他のAPIを使ったり、その結果をGSONで煮たり焼いたりすれば、色々と楽しいことができそうです!参考
google maps platform java client library(READMEにサンプルコードが載っています)
https://github.com/googlemaps/google-maps-services-java
Gradleについて(TIPS # 2とか)
http://gihyo.jp/dev/serial/01/android_studio/0006?page=1
Android studio 3からcompile
->implementation
に変わった
http://tech.hikware.com/article/20180330a.html
Buildのエラー対処
https://stackoverflow.com/questions/39545629/noclassdeffounderror-failed-resolution-of-lokhttp3-internal-platform
https://stackoverflow.com/questions/17360924/securityexception-permission-denied-missing-internet-permission
- 投稿日:2019-12-09T18:31:51+09:00
(ネタがなかったので)2日ほどかけて社内向け端末管理アプリを作ってみた
この記事はフラー Advent Calendar 2019の 9 日目の記事です。
この会社に入って一年半、Androidエンジニアとして走り続けました。
しかしながら、土日に入って「何を書こうかな」と考えた所、なんだかどれも記事にしづらかったり、記事にしていいのかわからない内容だったりしました。
ので、新しく記事にできそうなものを作ろうと思い立ちました。何故、端末管理アプリを作ったか
起稿の1週間ほど前にちょうど弊本部長から、「端末管理を
フラーに合った形
で、ちゃんと運用できるようにしてほしい」との依頼が入り、どうするかちょうど考え中でした。現在弊事業部では、端末の貸し出し管理を紙で運用しています。
貸し出しを行う時には、紙に「貸出端末」「貸出者」「借りた日」を記載し、返却時に「返却済み」にチェックを入れます。
……辛いですね。
何が面倒かというと
1.フラー社内の端末管理対象が増え続けていて、どんどん書き込まれていくため見辛い
2.定期的な棚卸しがしづらい(されてない)
3.紙運用で、誰が持っているかとかを確認しづらい(そもそも面倒なので書かない)
この辺でしょうか。スタートアップの初期段階では紙運用でも問題なく回っていたのだと思いますが、フラーも従業員&端末が爆増中。紙運用が辛くなってきた次第です。
GoogleCalendarや、Slack、Jiraでの運用も考えましたが、やっぱりそれ用に作られたものじゃないのでつらいなぁという感じ。
あと、みんなの前職で端末管理どうやってたか聞くと、やっぱりそれ用のソフトウェアを使っていたりしたので、どうせそこに行き着くんだろうなという結論に至り作り始めました。構成
フラーは柏の葉と新潟の2拠点あり、それぞれで端末を借りることができます。
この特性上、sharedpreferences
などを使ってローカル管理するわけにもいかないので、サーバーらしきものを立てないといけません。
しかしながら私はしがないAndroidエンジニア、サーバーを立てたりし始めると時間がかかってしまいます。
そこで長岡花火アプリでもお世話になった、FireStoreを使うことにしました。
構成は単純で、FireStore上に更新されそうなデータ達「端末データ」「貸出者データ」「事業部データ」「支社データ」を格納して、それをReadWriteするだけです。実際に作ったもの
RecyclerViewのアイテムをタップすると詳細が見れて、貸出する場合はユーザーと返却予定日を入力させます。
FireStoreQueryの動的生成
動的生成にちょっとだけ詰まったので。
FireStoreへの問い合わせQueryは、普通にするなら以下の感じでいけます。
collection .whereEqualTo("company_id", companyId) .get() .addOnSuccessListener {しかし今回、検索機能をつけているので、EditTextなどの条件を元に、FireStoreに問い合わせるQueryを変更しなければなりません。
最初は以下の感じで実装すれば、collectionに条件をどんどん追加してくれるんだと思っていたのですが、これをやると検索条件が追加されていなかった。
var collection = db.collection(getString(R.string.firestore_devices_collection_name)) val companyId = binding.editCompanyId.text.toString() collection.whereEqualTo("company_id", companyId) collection .get() .addOnSuccessListener { // ... }一旦collectionのQueryをとって、
collection = collection.whereEqualTo
をやってやらないといけないんですね。var collection = db.collection(getString(R.string.firestore_devices_collection_name)) as Query val companyId = binding.editCompanyId.text.toString() collection = collection.whereEqualTo("company_id", companyId) collection .get() .addOnSuccessListener { ...ちょっと分かりづらかった。
終わりに
デザイナーがいないので見た目がちょっと雑になってしまいましたが、一旦これを端末保管場所の隣に置いて運用してみようかと思います。
だいたい2日でProject作成から一旦完成まで持っていくことができました。
今後は返却日超過者などをSlackに通知できるようにしたりして、棚卸しへの対応などもできるようにしたいなと思っております。
FireStore、便利だなぁ…![]()
- 投稿日:2019-12-09T18:31:28+09:00
?ダンジョンを可視化するアプリ?
?2018年アドベントカレンダー?
?RPGメイキングツール?
去年のクソアプリアドベントカレンダーでRPGメイキングツールを作成したがっちょです。
今回は、今までに作ったダンジョン・迷路・地形の自動生成をまとめて可視化できるアプリを創ったので紹介します。
?2019年アドベントカレンダー?
?TerrainView?
DungeonTemplateLibraryを視覚的に体験できるアプリをリリースしました。
— がっちょ( ¨̮ ) (@wanotaitei) October 9, 2019
ダンジョン/地形の生成がタップ/クリックで出来ます。
ぜひインストールしてみて下さい。
【Android版】https://t.co/IWOklgQ7UC
【Windows版(x64)】https://t.co/S9Jt0qkCUr
【Windows版(x86)】https://t.co/ZL9musF7Kn pic.twitter.com/bJrYDFO721今回は "TerrainView" という地形の自動生成を可視化して楽しめるアプリを創りました。
今のところAndroidとWindows版のみがあります。?ダウンロード?
>> Android
>> Windows(x86)
>> Windows(x64)?プレイ動画?
Dungeon Template Libraryの機能をGUI環境で可視化。 pic.twitter.com/UglDtYqttH
— がっちょ( ¨̮ ) (@wanotaitei) December 9, 2019タッチするごとに自動で新しい地形が生成されます。
?機能紹介?
約30種類のダンジョン・迷路・地形の自動生成を試すことが出来ます。
左画面は生成結果を出力する画面。右画面は生成を選択する画面です。?操作方法?
入力 説明 上矢印キー 1つ上の機能を選択する。 下矢印キー 1つ下の機能を選択する。 Wキー 1つ上の機能を選択する。 Sキー 1つ下の機能を選択する。 スペースキー 自動的に地形を生成する。 エンターキー 自動的に地形を生成する。 左矢印キー 自動的に地形を生成する。 右矢印キー 自動的に地形を生成する。 Aキー 自動的に地形を生成する。 Dキー 自動的に地形を生成する。 左画面タップ 自動的に地形を生成する。 右画面タップ 機能を選択する。 左画面マウス左クリック 自動的に地形を生成する。 右画面マウス左クリック 機能を選択する。 キーボード入力・マウスクリック・画面タッチのいずれかで遊ぶことが出来ます。
?写真?
?ソースコード?
"Dungeon Template Library"として地形生成がまとめられています。
各生成のソースコードは以上のように記述されています。
?感想?
ライブラリを可視化するツール(アプリ)を作るとどのような機能が提供されているのかひと目でわかる。
タッチ1回で自動生成される楽しさ。教育用のアプリとして何か作れる可能性が高い(子どもに受けそう)。1年後までにまた別の始点で新しいモノを創ってみたいと思います。
最後までお読みいただきありがとうございました!
- 投稿日:2019-12-09T16:34:28+09:00
[ Android ] Applicationを継承したクラスを使う
以前、セッターを使って異なるクラス間で値をやり取りする方法を紹介した。
https://qiita.com/QiitaD/items/50fb0b6b7709e66a5041今回はApplicationクラスを継承したクラスを作り、そのクラスを介して異なるクラス間で値をやり取りする方法を紹介する。
クラスの作成と登録
以下のようにApplicationクラスを継承したクラスを作る。
public class MyApplication extends Application{ public static String test = "test"; }しかしこれだけでは使用できない。以下のようにしてAndroidManifestに登録する必要がある。
<application> android:name=".MyApplication" </application>使用方法
MyApplicationクラスは以下のように使用する。
//アプリケーションを取得し、MyaApplication型変数に代入する MyApplication myApplication = (MyApplication) this.getApplication(); //上記の変数からMyApplicationクラスのメンバ変数を呼び出す textView.setText(myApplication.test);
- 投稿日:2019-12-09T16:29:45+09:00
Android Emulatorへのコピペがダルい問題
Android Emulatorへのコピペが不便
久しぶりにAndroid Emulatorを触ってて、ホストマシンでコピーしたテキストを
command + V
でコピペ出来ず何かと不便だったので調べてみた。
環境
macOS 10.14.6
Android Studio 3.5.2adb経由でペースト
ググってよく出てくるのがadbコマンドを通したペースト方法。
$ adb shell input text 'hogehoge'
ペーストするのにいちいちコマンド打つのダルい。
EditText長押しでペースト
さすがにもっと良い方法あるやろ、と探して見つけたのがこちら。
Emulator上でEditText長押しで「Paste」メニューが出る。
これで簡単にペーストできる。
今日でペチペチ手打ち職人も卒業だ。やったぜ。
- 投稿日:2019-12-09T15:06:21+09:00
Firebase MLKit
Introduction
ML Kit for Firebase is a machine learning toolkit made by Google for Android and iOS. (Well still in Beta version)
With this kit you can use on-device pre-trained APIs :You can also use cloud APIs for more accurate answer :
- Text recognition
- Face detection
- Barcode scanning
- Image labeling
- Object detection & tracking
- Language identification
- Translation
- Smart reply generator (only in english)
You can use your custom pre-trained models and you can train your own classification model. (for images labeling only)
- Text recognition
- Image labeling
- Landmark recognition
Android dependency
In app/build.gradle you should add :android { //... aaptOptions { noCompress "tflite" } } dependencies { //... // ml-vision general implementation 'com.google.firebase:firebase-ml-vision:24.0.1' // Face Detection (contours) implementation 'com.google.firebase:firebase-ml-vision-face-model:19.0.0' // Barcode Scanning implementation 'com.google.firebase:firebase-ml-vision-barcode-model:16.0.1' // Image labeling implementation 'com.google.firebase:firebase-ml-vision-image-label-model:19.0.0' // Object detection implementation 'com.google.firebase:firebase-ml-vision-object-detection-model:19.0.3' // ml-natural general implementation 'com.google.firebase:firebase-ml-natural-language:22.0.0' // Langauge identification implementation 'com.google.firebase:firebase-ml-natural-language-language-id-model:20.0.7' // Translation implementation 'com.google.firebase:firebase-ml-natural-language-translate-model:20.0.7' // Smart Replies implementation 'com.google.firebase:firebase-ml-natural-language-smart-reply-model:20.0.7' } apply plugin: 'com.google.gms.google-services'If you want to use a custom pre-trained model(AutoML-trined model) to load your own model you will need to add :
implementation 'com.google.firebase:firebase-ml-vision-automl:18.0.3'
Text recognition
// Create FirebaseVisionImage Object (here from an url) FirebaseVisionImage image = FirebaseVisionImage.fromFilePath(context, uri); // Create an instance of FirebaseVisionTextRecognizer with on-device model FirebaseVisionTextRecognizer detector = FirebaseVision.getInstance().getOnDeviceTextRecognizer(); // Or with cloud model // FirebaseVisionTextRecognizer detector = FirebaseVision.getInstance().getCloudTextRecognizer(); // Process the image Task<FirebaseVisionText> result = detector.processImage(image) .addOnSuccessListener(new OnSuccessListener<FirebaseVisionText>() { @Override public void onSuccess(FirebaseVisionText firebaseVisionText) { // Task completed successfully // ... } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception // ... } });FirebaseVisionText will contain bounding box, text, language recognized, paragraph, confidence score.
Face detection
![]()
Face detection is done on device only and you can get facial contours too (optional).// Create FirebaseVisionImage Object (here from an url) FirebaseVisionImage image = FirebaseVisionImage.fromFilePath(context, uri); // Set Options FirebaseVisionFaceDetectorOptions options = new FirebaseVisionFaceDetectorOptions.Builder() .setPerformanceMode(FirebaseVisionFaceDetectorOptions.ACCURATE) .setClassificationMode(FirebaseVisionFaceDetectorOptions.ALL_CLASSIFICATIONS) .setLandmarkMode(FirebaseVisionFaceDetectorOptions.ALL_LANDMARKS) .setContourMode(FirebaseVisionFaceDetectorOptions.ALL_CONTOURS) .build(); // Create an instance of FirebaseVisionFaceDetector FirebaseVisionFaceDetector detector = FirebaseVision.getInstance().getVisionFaceDetector(options); // Process the image Task<List<FirebaseVisionFace>> result = detector.detectInImage(image) .addOnSuccessListener( new OnSuccessListener<List<FirebaseVisionFace>>() { @Override public void onSuccess(List<FirebaseVisionFace> faces) { // Task completed successfully // ... } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception // ... } });As a result FirebaseVisionFace contains : Face bounds, head rotation, eyes ears, mouth nose coordinate, classification probability (smiling, eyes opened, happy ...)
You can also get a tracking Id in case of video streaming.Barcode scanning
Many different formats are supported :
Code 128, Code 39, Code 93, Codabar, EAN-13, EAN-8, ITF, UPC-A, UPC-E, QR Code, PDF417, Aztec, Data Matrix.// Create FirebaseVisionImage Object (here from an url) FirebaseVisionImage image = FirebaseVisionImage.fromFilePath(context, uri); // Set Options FirebaseVisionBarcodeDetectorOptions options = new FirebaseVisionBarcodeDetectorOptions.Builder() .setBarcodeFormats( FirebaseVisionBarcode.FORMAT_QR_CODE, FirebaseVisionBarcode.FORMAT_AZTEC) .build(); // Create an instance of FirebaseVisionBarcodeDetector FirebaseVisionBarcodeDetector detector = FirebaseVision.getInstance().getVisionBarcodeDetector(); // Process the image Task<List<FirebaseVisionBarcode>> result = detector.detectInImage(image) .addOnSuccessListener(new OnSuccessListener<List<FirebaseVisionBarcode>>() { @Override public void onSuccess(List<FirebaseVisionBarcode> barcodes) { // Task completed successfully // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception // ... } });The results will depend on the barcode type
Image labeling
Image labeling can be used on-device with ~400 labels or on-cloud with ~10000 labels// Create FirebaseVisionImage Object (here from an url) FirebaseVisionImage image = FirebaseVisionImage.fromFilePath(context, uri); // Create an instance of FirebaseVisionImageLabeler with on-device model FirebaseVisionImageLabeler labeler = FirebaseVision.getInstance().getOnDeviceImageLabeler(); // Or with cloud model // FirebaseVisionCloudImageLabelerOptions options = new FirebaseVisionCloudImageLabelerOptions.Builder().setConfidenceThreshold(0.7f).build(); // FirebaseVisionImageLabeler labeler = FirebaseVision.getInstance().getOnDeviceImageLabeler(options); // Process the image labeler.processImage(image) .addOnSuccessListener(new OnSuccessListener<List<FirebaseVisionImageLabel>>() { @Override public void onSuccess(List<FirebaseVisionImageLabel> labels) { // Task completed successfully // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception // ... } });Return is just a list of FirebaseVisionImageLabel who contains : label and confidence score.
You can use AutoML Vision Edge to use your own model of classification.
Object detection & tracking
With this you can identify main object and track it (when streaming)// Create FirebaseVisionImage Object (here from an url) FirebaseVisionImage image = FirebaseVisionImage.fromFilePath(context, uri); // Multiple object detection in static images FirebaseVisionObjectDetectorOptions options = new FirebaseVisionObjectDetectorOptions.Builder() .setDetectorMode(FirebaseVisionObjectDetectorOptions.SINGLE_IMAGE_MODE) .enableMultipleObjects() .enableClassification() // Optional .build(); // Create an instance of FirebaseVisionObjectDetector FirebaseVisionObjectDetector objectDetector = FirebaseVision.getInstance().getOnDeviceObjectDetector(options); // Process the image objectDetector.processImage(image) .addOnSuccessListener( new OnSuccessListener<List<FirebaseVisionObject>>() { @Override public void onSuccess(List<FirebaseVisionObject> detectedObjects) { // Task completed successfully // ... } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception // ... } });Result is a list of FirebaseVisionObject who contains: tracking Id, bounds, category, confidence score.
Landmark recognition
You can recognize well-known landmarks in an image. This api can only be use on-cloud.// Create FirebaseVisionImage Object (here from an url) FirebaseVisionImage image = FirebaseVisionImage.fromFilePath(context, uri); // Set the options FirebaseVisionCloudDetectorOptions options = new FirebaseVisionCloudDetectorOptions.Builder() .setModelType(FirebaseVisionCloudDetectorOptions.LATEST_MODEL) .setMaxResults(15) .build(); // Create an instance of FirebaseVisionCloudLandmarkDetector FirebaseVisionCloudLandmarkDetector detector = FirebaseVision.getInstance().getVisionCloudLandmarkDetector(options); // Process the image Task<List<FirebaseVisionCloudLandmark>> result = detector.detectInImage(image) .addOnSuccessListener(new OnSuccessListener<List<FirebaseVisionCloudLandmark>>() { @Override public void onSuccess(List<FirebaseVisionCloudLandmark> firebaseVisionCloudLandmarks) { // Task completed successfully // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception // ... } });Result is a list of FirebaseVisionCloudLandmark who contains: Name, bounds, latitude, longitude, confidence score.
Language identification
This api doesn't use image but String.FirebaseLanguageIdentification languageIdentifier = FirebaseNaturalLanguage.getInstance().getLanguageIdentification(); languageIdentifier.identifyAllLanguages(text) .addOnSuccessListener( new OnSuccessListener<String>() { @Override public void onSuccess(List<IdentifiedLanguage> identifiedLanguages) { for (IdentifiedLanguage identifiedLanguage : identifiedLanguages) { String language = identifiedLanguage.getLanguageCode(); float confidence = identifiedLanguage.getConfidence(); Log.i(TAG, language + " (" + confidence + ")"); } } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Model couldn’t be loaded or other internal error. // ... } });Result is a list of IdentifiedLanguage who contains: Language Code and confidence score.
Translation
Translation can be done with a on-device api but it can be use only for casual and simple translation over 59 languages (Japanese is supported).
Model is trained to translate to and from English; so if you choose to translate between non-English languages, English will be used as an intermediate translation, which can affect quality.// Create an English-Japanese translator: FirebaseTranslatorOptions options = new FirebaseTranslatorOptions.Builder() .setSourceLanguage(FirebaseTranslateLanguage.EN) .setTargetLanguage(FirebaseTranslateLanguage.JP) .build(); final FirebaseTranslator englishJapaneseTranslator = FirebaseNaturalLanguage.getInstance().getTranslator(options); final String text = "Merry Christmas"; // We need to download the model first // Each model is around 30MB and are stored locally to be reused FirebaseModelDownloadConditions conditions = new FirebaseModelDownloadConditions.Builder() .requireWifi() .build(); englishJapaneseTranslator.downloadModelIfNeeded(conditions) .addOnSuccessListener( new OnSuccessListener<Void>() { @Override public void onSuccess(Void v) { // Model downloaded successfully. We can start translation englishJapaneseTranslator.translate(text) .addOnSuccessListener( new OnSuccessListener<String>() { @Override public void onSuccess(@NonNull String translatedText) { // Translation successful. // translatedText <- "メリークリスマス" } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Error during the translation. } }); } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Model couldn’t be downloaded or other internal error. } });Smart reply generator (only in english)
The model work with the 10 most recent messages and provides a maximum of 3 suggested responses.
// Define a conversation history // Local User speaks to Remote User // Smart reply is about what Local User may answer. List<FirebaseTextMessage> conversation = new ArrayList<>(); conversation.add(FirebaseTextMessage.createForRemoteUser("It's Christmas time", System.currentTimeMillis(),"userId1")); conversation.add(FirebaseTextMessage.createForLocalUser("Kids are happy", System.currentTimeMillis())); conversation.add(FirebaseTextMessage.createForRemoteUser("Will Santa Claus come tonight ?", System.currentTimeMillis(),"userId1")); FirebaseSmartReply smartReply = FirebaseNaturalLanguage.getInstance().getSmartReply(); smartReply.suggestReplies(conversation) .addOnSuccessListener(new OnSuccessListener<SmartReplySuggestionResult>() { @Override public void onSuccess(SmartReplySuggestionResult result) { if (result.getStatus() == SmartReplySuggestionResult.STATUS_NOT_SUPPORTED_LANGUAGE) { // The conversation's language isn't supported, so the // the result doesn't contain any suggestions. } else if (result.getStatus() == SmartReplySuggestionResult.STATUS_SUCCESS) { // Task completed successfully for (SmartReplySuggestion suggestion : result.getSuggestions()) { String replyText = suggestion.getText(); } } } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception } });As result a list of SmartReplySuggestion Object. Each one will contain only a text.
- 投稿日:2019-12-09T11:11:38+09:00
Androidで新機能を実装するときに考慮すること
この記事は、ドワンゴ Advent Calendar 2019の10日目の記事です。
ドワンゴではN予備校の開発をしています。
今年に入ってReactでのWebからKotlin言語でのAndroid開発に配属が変わり、N予備校の新機能を実装する中でWebでは意識してこなかった「Android特有の課題」にいくつかぶつかりました。
今後の開発で同じ課題にぶつからないためにも、ここでAndroidで新機能を実装するときに考慮することをまとめます。
(コードを書く前の話が中心なので、多くの部分はiOSにも共通していると思います。)
1. API取得失敗時のエラー処理
Webであれば、APIの通信エラーが発生したらブラウザのエラー表示が出て、ブラウザのリロードを実行すれば解決しますが、Androidではそれらを自前で用意しなければいけません。
しかも、Androidの通信環境は不安定になることが多いので、なおさら注意が必要です。
具体的には以下のポイントを考慮します。
エラー時には何を表示するか
- エラーになったことをToastで一部表示する
- エラーになったことをDialogで表示する
- エラー画面を表示する
N予備校Androidでは例えば以下のように対策しています。
Toast表示 Dialog表示 エラー画面表示 プロフィール設定画面 教材選択画面 授業一覧画面 大きく分類すると、画面遷移する前のエラーに対してはToast(一部Snackbarに移行)でエラー内容を表示、画面遷移した後のエラーに対してはDialogやエラー画面でエラー内容を表示しています。
どちらのパターンに対しても、何らかのルールを作って共通化できると簡潔に実装できると思うので、まずはルールを整備するところから。
(N予備校Androidはまだルールが整備されていないので、画面によって対応がまちまちだったりします。。。)
エラー時にどうやってリロード/リトライするか
- 同じ処理をもう一度実行してもらう
- リロードボタンを設置する
- 画面を下に引っ張ってリロードする
上記の画像そのままの順番で、このように対策しています。
上記の画像のように、「保存」ボタンをもう一度押してもらったり、Dialogの「リトライ」ボタンを押してもらうという流れはわかりやすいかと思うのですが、下に引っ張ってもらう方法は何らかの画面表示が必要かもしれません。
新機能を作成するときには、これらのエラー系処理が抜けてしまって、エラーが発生したときに画面が真っ白になってしまうことがあるので、しっかり事前に考慮することが必要です。
2. ローディング時の待ち合わせ処理
これはWebとも共通ですが、ローディング中の処理をどうするかを考慮する必要があります。
何度も言いますが、Androidの通信環境は不安定になりやすいために、ローディングが長くなりがちです。
そのローディング画面が実装されていなくて真っ白な画面が続くとユーザが離れてしまうので、何らかの表示が必要になります。
具体的には以下のポイントを考慮します。
ローディング表示の種類
- 画面全体にローディングを表示する
- 画面の一部にローディングを表示する
N予備校Androidでは、例えば以下のように実装しています。
画面全体 画面一部 教材画面 授業詳細モーダル ユーザから見ると、画面全体のローディングが続くことは恐らく苦痛なので、取得するデータを分割するなどして、なるべく画面一部のローディングで済ませたいです。
いかに素早くユーザに必要なデータを表示できるか、という視点からAPIの設計・分割を行うことが理想でしょう。
より使いやすいアプリにするために、事前のローディング画面の考慮は重要です。
3. Activityが破棄されたときの処理
Androidでは、アプリがバックグラウンドに回されると、端末のリソースを確保するためにActivityが削除されることがあります。
Activityが破棄されると、アプリに戻ってきたときに画面の再描画が必要となり、それに伴って入力されていた値が消えてしまうので、必要な情報はActivityが破棄されても残る場所(SavedInstanceStateやDB)に入れる必要があります。
ただし、SavedInstanceStateの容量は1MBまでの制限があるので全てのデータを持つことはできません。
バックグラウンドから復旧するときに必要なデータと、APIで取得しなおせば良いデータをあらかじめ考慮しておくと、思わぬ問題を防ぐことができます。
Activity破棄の詳しい説明や動作確認の方法についてはいつもこの記事を参考にしています。
【Android】savedInstanceStateの意味と開発者オプション【初心者向け】
4. バックキーで画面を戻す処理
Androidにはバックキーがデフォルトで動作するようになっているので、その制御も考慮する必要があります。
画面の戻り先を制御する
例えば、ログイン画面からホーム画面に遷移したときに、何も考えずに実装しているとホーム画面でバックキーを押したときにログイン画面に戻ることになってしまいます。
戻ってほしくない画面にバックキーで戻らないようにするために、Activity/Fragmentによる画面の履歴を管理したり、バックキーが押されたことを検知して処理を上書きする必要があります。
機能を実装していくときには画面遷移図を作成して、各画面におけるバックキーの遷移先を考慮しておくと安心です。
Toolbarのバックキーによる画面の戻り先も制御する
バックキーだけでなく、Toolbarにも
←
などを置いて画面を戻ることができるようにするでしょう。
←
のような明らかにバックキーと同じ挙動をするボタンであれば処理もバックキーと同じにすれば良いですが、それ以外の場合にはバックキーと区別して考えます。例えば、以下の画面のように左上に
x
ボタンをつけるのであれば、そのボタンでどこまで画面を戻す(削除する)のかをバックキーとは別に考慮します。(「物理ベーシック 第0回 はじめに」(無料回)より抜粋)
Toolbarの戻るボタンもバックキーと同じだろうと思っていると後から挙動がおかしくなったりするので、こちらも同じく画面遷移図を作って事前に仕様を決めておきましょう。
5. 直リンクでアプリを開く処理
同一アプリ内、またはブラウザなどの他のアプリからURLをタップしたときに、アプリの任意のページに遷移させる必要があるかを考えます。
直リンクの場合は通常の画面遷移と異なるので、必要なAPIが取得できているか、ログインしていない場合に弾くようになっているかも考慮しなければなりません。
一方で、直リンクはユーザのアクセスを容易にする上で必要になることもあるので、機能を実装する前に忘れずに検討したいところです。
新機能を実装するときに、これらの要素の考慮漏れがあると後から大変なことになりかねないので、チェックリストとして使っていきたいと思います。
また新たな要素が出てきたら随時更新します。
- 投稿日:2019-12-09T01:03:04+09:00
URLにパスやクエリパラメータを追加する
初投稿です。Android で URL にパスやクエリパラメータを追加したいときは
Uri.buildUpon()
が便利ですね。val uri = Uri.parse(uriString) .buildUpon() .appendPath("path") .appendQueryParameter("foo", "bar") .build()
- 投稿日:2019-12-09T01:03:04+09:00
URIに値を追加する
初投稿です。Android で URI にパスやクエリパラメータなどを追加したいときは
Uri.buildUpon()
が便利ですね。val uri = Uri.parse(uriString) .buildUpon() .appendPath("path") .appendQueryParameter("foo", "bar") .build()
- 投稿日:2019-12-09T00:54:31+09:00
AndroidでFirebaseStorageを使ってみよう
Android初心者アドベントカレンダー9日目を担当しますカーキです.
普段はLife is Tech!というところで大学生メンターをしたり、名古屋のスタートアップのスタメンという会社でモバイルアプリエンジニアインターン生として働いています.FirebaseStorageとは
FirebaseStorageはGoogleが提供するFirebaseの機能の一つです.
FirebaseStorageではその名の通り、ファイルの送受信がいとも簡単に行うことができます.
そしてAndroidでは後述しますが画像を受け取る際にGlideという画像ローダーライブラリを使用することができます!(やったね!)準備
プロジェクト側
build.gradle
にfirebase-storageを追加します.
またGlideを使ってファイルをダウンロードする際に必要になるfirebase-uiも同時に記述しておくとなお良いです.build.gradleimplementation 'com.google.firebase:firebase-storage:19.1.0' implementation 'com.firebaseui:firebase-ui-storage:4.3.1'現在(2019年12月9日時点)の最新のバージョンを入力していますが、より新しいのもがあればそちらを使用してください.
Firebaseコンソール側
FirebaseStorageのページの『始める』から進めていけばFirebaseStorageのバケットを作成することができます.
また初期設定ではFirebaseCloudStoreと同様にセキュリティルールが誰でもアクセスできるという甘めの設定になっているので、リリースする前にはしっかり見直す必要があります.ファイルをアップロードする
ファイルをアップロードするために端末内の画像のURIパスからファイルを指定してアップロードすることができます.
Firesorageのパスを作成する
画像をアップロードする際にファイルをどのようなフォルダ構成で格納するのかを示すパスを作成する必要があります.
今回は例としてUsers
フォルダの配下に画像をアップロードすることとします.
フルパスが必要になるため、ファイル名は画像選択の際に取得するか任意に決めておく必要があります.MainActivity.ktval storage = FirebaseStorage.getInstance() val userImageRef = storage.reference.child("Users/user001.jpg")これでFirestorageのパスを作成することができました.
アップロードの処理
端末のギャラリーなどから選択した画像のURIを
imageUri
としています.
流れとしてはUri→パス→ファイル→インプットストリーム
に変換してfirebasestorageに渡していきますMainActivity.ktval path = imageUri.path?: return val stream = FileInputStream(File(path)) val uploadTask = userImageRef.putStream(stream) uploadTask.addOnSuccessListener { // アップロード成功時 } .addOnFailureListener { error -> // エラー発生時 }成功時と失敗時のリスナーにより、それぞれでの処理をハンドリングしていくことができます.
Glideを使ってファイルを受け取る
この場合もアップロードする場合と同様にして取得したいファイルのFirestorage上でのパスを作成する必要があります.
MainActivity.ktval storage = FirebaseStorage.getInstance() val userImageRef = storage.reference.child("Users/user001.jpg")続いてGlideを使用してFirebaseStorageのパスから画像を取得していきます.
Glideは指定した画像URLから画像をImageView
に表示させるという元々の機能がありますが、
それをFirebaseStorageでもそのまま使用できることになります.MainActivity.ktGlide.with(this) .load(userImageRef) .into(imageView)Glideではプレースホルダー用画像を指定することでファイルのローディング中にデフォルト画像を表示することができます.
MainActivity.ktGlide.with(this) .load(userImageRef) .placeholder(R.drawable.ic_default_user) .into(imageView)まとめ
どうだったでしょうか、
画像をアップロードする際はUriからインプットストリームを作成する必要があり、少しややこしいですがファイルのダウンロードの場合はGlideを使用することでかなり楽になりますよね.ただGldieはもちろん画像ローダーであるため、画像以外の場合はGlideを使用してダウンロードすることはできません.
その場合はぜひ公式のドキュメントを参考にしてみてください
Android でファイルをダウンロードする
- 投稿日:2019-12-09T00:17:59+09:00
Firebaseと連携してAndroid Studioに?を追加する方法
この記事はAndroid #2 Advent Calendar 2019の12/9の記事です。
前日は @tak-wisteria さんの「Android × TDD」でした。はじめに
Android Studioを普段使用している方で上記のアイコンがあるのをご存知でしょうか。
上記の?の画像はゲーム画面などではなくAndroid Studioの一部を拡大したものです。?は若干不気味ですがAndroid開発をとても便利にしてくれます。
Android Studioでの?のセットアップ方法と?の使い方を説明します。本記事ではMac OS 10.15, Android Studio 3.5を使用しています。
前編: ?を追加する
1. 新規にプロジェクトを作成する
File > New Project からプロジェクトを作成します。
サンプルではEmpty Activityを選択しました。作成するとアプリ本体のほか、androidTestやtestが実行できます。
以下はandroidTestを実行したところです。?はまだありません。2. Firebaseプロジェクトを作成する
FirebaseのドキュメントAndroid プロジェクトに Firebase を追加するを参考にFirebaseプロジェクトを作成してください。完了すると以下のようにTest Labを含む各機能が使用できるようになります。
3. Firebase Test Labと連携する
AndroidプロジェクトとFirebase Test Labを連携します。
Run > Edit Configuration
を選択します。- 左のペインからAndroid Instrumented Testsの子項目を選択します。
- Targetを
Use the Device..
からFirebase Test Lab Device Matrix
に変更します。- Cloud Project で作成したFirebaseプロジェクトを選択します。
Matrix Configuration はいったん数字が少ない方を選択してください。
Firebase有料プラン契約中の場合は課金額が増加する場合があります。Configurationsが設定できたらandroidTestを実行します。
すると..Test Resultsの上部にまぶたのようなアイコンが見えます..チェックマークを選択すると
なんと目が開きました!
しかし?を選択するとScreenshot are not available..
と注意文が表示されます。
現在はまだ未設定の状態です。後編: ?を使用する
前編で気づいたと思いますが?はFirebase Test Labのスクリーンショット関連の機能です。
前編完了時では機能の使用ができていないので?をきちんと使える状態にします。4. ScreenShotterをセットアップする
Firebase Test Lab インストゥルメンテーション テストのスクリーンショットを作成する を参考に以下を行います。
- cloudtestingscreenshotter_lib.aarのファイル追加
- aarを有効化 (projectの
build.gradle
とappのbuild.gradle
変更)AndroidManifest.xml
のパーミッション追加。製品アプリのパーミッションを汚したくない場合はBuildVariantを分けdebug/AndroidManifest.xml
を別に作成するなどしてください。AndroidManifest.xml<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.INTERNET"/>5. Activityのテストができる状態にする
app/build.gradle
のJUnit, Runner, Tules, cloudtestingscreenshotter_lib の設定を確認します。build.gradledependencies { : testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestCompile (name:'cloudtestingscreenshotter_lib', ext:'aar') }ActivityTestでスクリーンショットを取得できるようにします。
ExampleInstrumentedTest.kt
を以下のように変更してください。ExampleInstrumentedTest.kt@RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { // テスト開始時にMainActivityを起動するRuleです @get:Rule val activityTestRule = ActivityTestRule( MainActivity::class.java, true, true) // テスト開始時に指定のパーミッションを許可するRuleです @get:Rule var grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( "android.permission.WRITE_EXTERNAL_STORAGE" ) @Test fun takeScreenshot() { // スクリーンショットを"main_screen_1"という名前で取得します ScreenShotter.takeScreenshot("main_screen_1", activityTestRule.activity) } }コードを変更したら再度androidTestを実行してください!
6. ?をクリックする
数分経つとandroidTestがオールグリーンで完了します。
チェックマークがついた行を選択し、?をクリックしましょう。すると以下のようにScreenshot Viewer画面になります。
テストでScreenShotter.takeScreenshot
を行った箇所のスクリーンショットが確認できます。
また、Compareをクリックすると端末やOSでの実行結果の違いが確認できます。くわしいつかいかたはスクリーンショットを表示するで確認できます。
また、以下のようにFirebase Test Labコンソール上でもスクリーンショットを確認可能です。
まとめ
?の正体はFirebase Test LabのScreenshot Viewerの機能でした。
というわけで
Android Studioで?のアイコンで表示されるScreenshot Viewer機能の追加方法・使用方法を紹介しました。注意点その1として、本機能はとても便利ですが、クラウド上でテストを実行するためFirebaseの無料プランだと1日のテスト回数に制限があります。
注意点その2として、この機能は
ActivityScenarioRule
より旧式のActivityTestRule
を使用したほうが良いです。
スクリーンショットのファイル名にクラス名・メソッド名を使用しているのですがActivityScenarioRule
だとクロージャを使用するためメソッド名がUnknown扱いになるためです。さいごに
https://droidkaigi.jp/2020/accepted
DroidKaigi 2020にでることになりました!
この記事のようなテストとスクリーンショットの話を40分くらいします。
Qiita 10回分以上の量の資料をせっせと作成中です。リアルイベントは数年ぶりなので興味のある方はぜひ..!
あしたのAndroid #2 Advent Calendar 2019は
@yamacraft さんの「今年痛い目にあった学びを何か書きます」です!