「DataBindingのコードを読む」のスライドが面白かったのでDataBindingについて少しまとめました
上のスライドが面白かったため、DataBindingについてスライドを読んでわかったことや追加で調べたことをメモしました。
時間が足りず、書いて動かしたわけじゃないし雑なのですが、自分用のメモとしてはないよりましでしょう*1。
Androidアプリケーション用Gradle Plugin(Gradle 3.0.0)にてData Binding関連で含まれているもの
- Data Bindingライブラリの依存を自動で追加するルール*2
- Data Binding用の処理で使うと思しき
DataBindigInfo
クラスとそのクラスにアノテーションを利用してデータを突っ込むdataBindingExportBuildInfoタスクの追加 - ビルドプロセスの中で、リソースファイルマージのタイミングでレイアウトリソースから関連パラメータを読み取ってDataBinding(のアノテーションプロセッサ?)で利用しやすいように加工したxmlファイルを用意*3
依存ライブラリの追加について(追加で調べたこと)
- build.gradleに対応するプロパティを設定しておくと、DataBindingに関連するライブラリの依存を自動的に追加するルールがAndroid用Gradleに定義されそう
- Androidプロジェクトのビルドなどのタスクを実行する前後*4に実行されるものとして定義されているみたい
BaseComponentModelPlugin.java
というファイルの中にAndroidアプリケーションプロジェクトで使うbuild.gradle
で使うようなタスクやAndroid アプリケーションプラグインに入っている標準的なConfiguration*5やデフォルトタスクやルールが定義されている*6@Mutate
はbuild.gradleで設定されたビルド対象(のなにか)を引数に取り、ビルド対象に変化を与えるためのRule*7っぽい- build.gradleの
android.dataBinding.enabled
を見て実際ルールの中身が動くかどうか決めていそう*8 - 動く順序として、
@Mutate
で設定されたルールはデフォルトルールの後に動く
- build.gradleの
// https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-experimental/src/main/java/com/android/build/gradle/model/BaseComponentModelPlugin.java#619 @Mutate public void addDataBindingDependenciesIfNecessary( TaskManager taskManager, @Path("android.dataBinding") DataBindingOptions dataBindingOptions) { taskManager.addDataBindingDependenciesIfNecessary( new DataBindingOptionsAdapter(dataBindingOptions)); }
// https://developer.android.com/topic/libraries/data-binding/index.html // build.gradle android { .... dataBinding { enabled = true } }
DataBindingに関係しているクラスについて
ViewDataBindingクラス
layout_xxx
から始まるレイアウトリソースから自動生成されるっぽい- ViewModelからの変更通知を受け取って、それをViewBindingAdapterに通知(on executeBindings() , executeBindings()が呼び出されるきっかけはChreographer.postFrameCallback())
- クラスとしてはViewDataBindingのサブクラス。レイアウトリソースと対応して自動生成される
- 他のViewBindingAdapterクラスと関わる部分(イベントリスナやexecuteBindings()の中身?)もViewBindingAdapterクラスのアノテーションから生成っぽい
- 対応するViewModelと自動生成されたIDを(でかい何かに)登録する
ViewBindingAdapterクラス*10
- EventListenerを書いたり、Viewの更新コード*11を書いたり(Viewへの反映を呼び出すのは ViewDataBinding.executeBindings())
- 自分が管理してるViewが持ってる値を取得することもでき*13、それをViewModelに渡したり*14
ViewModelクラス
- BaseObservableのサブクラス。
- ViewDataBindingにセットして使う。ViewDataBindingでどのViewModelを使うか、といったことは開発者側で定義するし、自動生成されないのでアノテーションつけながら自分で書く
- DataBindingで使いたい属性のgetterには@Bindableをつけておくと、その属性のIDがBRクラスに生えるっぽい
- ModelはViewModelを知ってて、Repositoryとかの方からModelに変更通知 => ModelがViewModel持ってたらViewModelに通知の流れ?
DataBindingComponentクラス
- getXxxBindingAdapter()メソッドで対応するViewDataBindingクラスの初期化を定義
- DataBindingComponentのインスタンスを
DataBindingUtil.setContentView()
に渡すことで、たぶん getXxxBindingAdapter()メソッドが呼び出されてViewDataBindingのインスタンスが生成 - mDirtyFlagsで工夫してそうですが、DataBindingの実装見てたら非同期処理がバリバリ入るので、中途半端な状態のViewDataBindingのインスタンスの参照をDataBinding関連の他のコンポーネントに公開したくない。ので、完全にインスタンスの生成が済んだオブジェクトをreturnするメソッドを定義させることを関連クラスの定義としておくことで、その辺のお作法を強制してるように見えましたが、どうなんでしょうね。
*1:ツッコミお待ちしております。。
*2:http://mike-neck.hatenadiary.com/entry/2015/05/18/220834 こちらの記事によると、Gradle2.4からルールという概念が登場したそうですが、4.5.1でもincubatingです
*3:リリース用にビルドをしていく際も公開するapkなどのファイルに設定ファイルは含まれないはずですが、アノテーションを使ってリリース時の最新コードを生成するためにこの設定ファイルが必要だから、このパースはmergeReleaseResourceでも実行されそうな気がしましたが確認してません
*4:前後ってなんだよって感じですが、はい
*5:compileやprovidedなど
*6:https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-experimental/src/main/java/com/android/build/gradle/model/BaseComponentModelPlugin.java#619
*7:Ruleというものを使ってGradleのドメインモデルを使った新しい書き方をやろう、的な話があり、 @Mutate はそのRuleの一種を示すアノテーション。DefaultRuleの後に動くものらしいのですが、GradleのAPI ドキュメントが4.5.1しかなくてバージョン違いすぎでよくわからず。。 https://docs.gradle.org/current/javadoc/org/gradle/model/Mutate.html
*8:https://developer.android.com/topic/libraries/data-binding/index.html
*10:BindingAdapter見て、いつかRecyclerViewのAPIと並べて眺めると面白いのでは、と思ったのでメモ
*11:こういう値の時はViewを変更せず無視する、とか、一緒のViewBindingAdapterで管理してるバリデーションエラーを表示するViewを可視にする、とかそういう感じ?
*12:なんとなく、View本体とコールバックで使う変化した値を受け取るあたりの形がiOSのUIKitの各クラスに対応して定義されているDelegate書くときの形に似てる
*13:更新時のコールバックが呼ばれる前の値?
*14:多分取得はViewModelに渡すときのViewの属性値のgetterのラッパー。ここは単なる取得だけで、取得した値の加工はViewModelでやる。たとえば、SpinnerでRadioボタンを作って選択するとユーザーの入力は0, 1, 2 ...な整数値だけど、これをドメイン層のModelに渡す前にViewModelでDomain層に使うenum値に直すとか? こうするとCustomViewはドメイン層のModelのことを知らなくていいし、ModelもCustomViewの実装を知らなくて良い。取得メソッドは単なるラッパーであり、ラッパーから値を受け取るようにしているだけだが、この一皮のおかげでViewModelも直接Viewの参照を持たなくてよいため、テストが書きやすい? という気がした
マーケティングオートメーションツールとアプリエンジニア ~プッシュ通知・お知らせダイアローグの運用自動化のためのお仕事~という題で発表しました
ふだんやっているお仕事の中から印象的だった事柄をオムニバス方式でお話しさせていただく体の発表をさせていただきました。
技術的にはGradle入門とGCM, FCMを使うとプッシュが届くけど何が起こっているのか、みたいなお話がありますがお仕事紹介の側面が強いです。
反省
- 30分あるからLT5本(マーケティングオートメーションツール5分/Push10分/InApp5分/クロスプラットフォーム5分/テスト5分)いくぞ、という内容出したけど、失敗でした
- 大規模カンファレンスに沿った品質の内容を、と思うとどんどんそれぞれのトピックが膨らんでいって削れないところをどう選ぶか、みたいな感じでして
- また、それぞれを一定以上の質にする、というところで時間がかかってしまい、細かいブラッシュアップが登壇3時間前までかかってしまった
- 自分が1日に生産できる資料の量はLT5分/日っぽいです
- 削られた内容について
- Chromebook > Chromebookはマルチウィンドウ、ウィンドウリサイズが前提の機種だからそのあたり不具合出やすくなるよ、という話の予定でしたがこちらのセッションでそれを専門に扱っていただくようだったためカット
- Appium VS Espresso, XCUITest > こちらも時間が足りず、DroidKaigiの前にテスト自動化カンファレンスの方でLTさせていただいたこともあり泣く泣くカットしました :bow: http://woshidan.hatenablog.com/entry/2017/12/12/083000
- マーケティングオートメーションツールについて > 5分くらい話せるかな、と思ったのですが30秒くらい触れただけですね。。また機会がありましたらぜひー
同時間帯はGraph QLやDropBoxの話の裏番組だったため3人くらい来るかなーという感じだったので小さい方の部屋とはいえほぼ満杯だったときはびっくりしました。
こちらから話を振ったときも挙手してくださる方がいたり実況してくださる方がいたり、部屋を出るとき面白かったーとつぶやいてくださる方がいたり、ありがたかったです。
時間内に収めるためにら行を削るべき、とクロスプラットフォームの章の用語を見直していたら直前に図に誤りが見つかって訂正したのもいい思い出ですが、もし次回以降LTのオムニバス方式で話す機会があるとしてもCFPでは制限時間の半分くらいに収まるようにします。体力的に、死ぬ。
運営スタッフの方々もありがとうございました。
他にも書いた方がいいことがある感じはしますが、書かずじまいになりそうなので書くことがあったら追記します。。
Media Streaming on Androidを見て面白かったので細々調べたメモ(動画のストリーミング配信プロトコル周り)
このメモはセッションで話された内容の書き起こしではなく、上記のスライドの内容が面白かったので自分で勝手に調べたメモです。
調べた結果とセッション中と用語のずれがある場合は、Abema TVのCode IQのプレス記事*1を優先させていただきました。
目次
- Androidで動画を再生するためのクラス
- Streamingで動画をダウンロードする、そのためにそれ用のコーデックで圧縮する
- Apple製のストリーミング動画配信プロトコル HLS(HTTP Live Streaming)とその実装に必要なファイル
- Abemaで採用しているストリーミング動画配信プロトコル MPEG-DASHの説明
Androidで動画を再生するためのクラス
- MediaPlayerとExoPlayerがある
- 前者はAPI Level 1からある、詳細はいじれないが単純なユースケースの場合はこれをポンと置いておけば良い
- 後者はAPI Level 14から利用でパラメータがカスタマイズできる
- 利用するときはGradleで
com:google:android:exoplayer:exoplayer:x.x.x
の依存を追加 - このライブラリは使う機能に合わせて全体やCore, UI, HLS, OKHTTP(通信周りのカスタマイズ)に分かれている
- 利用するときはGradleで
Streamingで動画をダウンロードする、そのためにそれ用のコーデックで圧縮する
- 動画をダウンロードして再生する、という場面では再生用のファイルをすべてDLしてから再生する場合と再生用のデータを都度都度ダウンロードして再生する場合がある
- ストリーミング、の場合は後者
- いわゆるストリーミングにも大きく分けてプログレッシブダウンロード方式(ファイルをHDにダウンロードする)とストリーミング方式(ファイルの内容はその場で取得するだけ)がある
- 動画を都度都度ダウンロードするとして
Apple製のストリーミング動画配信プロトコル HLS(HTTP Live Streaming)とその実装に必要なファイル
- h264で圧縮したファイルでストリーミング配信をしていますが、そのストリーミング配信のプロトコルはHLS(HTTP Live Streaming)がありますよ
- HLSを定義したのはApple
- HLSでストリーミング配信をするには3つのファイルが関係あって
- Master Playlist (拡張子
.m3u8
) - Media Playlist (拡張子
.m3u8
) - ts files (拡張子
.ts
)
- Master Playlist (拡張子
- ストリーミング再生のために関連ファイルを取得していく手順
- ちなみに、tsファイルは「MPEG-2 TS」ファイル
Abemaで採用しているストリーミング動画配信プロトコル MPEG-DASHの説明
- さっきHLSの話をしたんですが、Abema TVで実際利用しているのはMPEG-DASHというプロトコルですよ*7
- MPEG-DASHで主要なファイルは
MPD(.mpd)
,fragmented mp4 file(.m4s)
,Initialization file(.m4s)
*10 - 普通のmp4 vs fragmented mp4 file
- mp4はbox*11という要素が連なった木構造のデータ形式となっている
- Fragmented mp4 fileはわざわざ名前を変えている通り、少々勝手が異なる
.mp4
ファイルを構成する主要な要素について、フラグメントごとに要素を用意している(moof, mdat)- フラグメントのファイル群だけでは動画全体の情報が足りないため、全体を統括するためのファイルが必要でこれがinitialization fileやInitialization fragmentと呼ばれている*14
- Initialization Fragmentの方はftyp(ファイル形式)は
dash
で動画全体のメタデータに当たるmoov
を持っている
- Initialization Fragmentの方はftyp(ファイル形式)は
- 上記動画ファイルとは別にMPD(Media Presentation Description)がある
長くなってきたので一旦ここまでにします。。
*1:https://codeiq.jp/magazine/2017/11/55460/
*2: http://blog.socialcast.jp/05/post-15/ https://dev.classmethod.jp/tool/http-live-streaming/ など
*3:Wikipediaですが https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%87%E3%83%83%E3%82%AF#%E5%8B%95%E7%94%BB%E5%9C%A7%E7%B8%AE%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%87%E3%83%83%E3%82%AF
*4:http://blog.kokoni.jp/2014/02/14/hls%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9/
*5:ファイルごとの再生時間を見て次のファイルをいつ頃取ったらいいとか見るのだろうかと思ったけどこれは想像です
*6:https://k-tai.watch.impress.co.jp/docs/column/keyword/515059.html http://did2memo.net/2017/02/20/http-live-streaming/
*7:Amebaさんのプレス https://codeiq.jp/magazine/2017/11/55460/ 概要としてはこちらもよさそう https://www.ite.or.jp/contents/keywords/1701keyword.pdf
*8:https://codeiq.jp/magazine/2017/11/55460/ より引用
*9:https://www.jstage.jst.go.jp/article/itej/67/2/67_109/_pdf より引用
*10:ファイル拡張子がスライド表記と異なるが https://codeiq.jp/magazine/2017/11/55460/
*11:mp4の前身であるQuickTimeフォーマットに倣いatomとも呼ばれる この辺の参考は http://yebisupress.dac.co.jp/2015/11/04/profile%EF%BC%9Fatom%EF%BC%9Fmp4%E3%81%AE%E3%82%88%E3%81%8F%E3%82%8F%E3%81%8B%E3%82%89%E3%81%AA%E3%81%84%E3%81%82%E3%82%8C%E3%81%93%E3%82%8C%EF%BC%88atom%E7%B7%A8/
*12:ファイルサイズ、再生時間など動画本体以外の付随情報
*13:動画や音声ですね
*14:https://www.jstage.jst.go.jp/article/itej/67/2/67_109/_pdf も参照
adbのCLIでAndroidのエミュレータを起動したりアプリの起動、テストの実行をしたりする
この記事はAndroid Advent Calender その2の15日目の記事です。
今回は
https://developer.android.com/studio/command-line/adb.html https://developer.android.com/studio/run/emulator-commandline.html https://developer.android.com/studio/test/command-line.html
を見て色々素振りしてみようと思います。
シミュレータの起動
/Users/woshidan/Library/Android/sdk/tools/emulator -avd Nexus_5X_API_22
で起動することが可能です。テストを実行させたり、アプリをインストールして起動したりしてみましょう。
アプリのインストール
$ adb install app/build/outputs/apk/app-debug.apk app/build/outputs/apk/app-debug.apk: 1 file pushed. 5.8 MB/s (2910562 bytes in 0.477s) pkg: /data/local/tmp/app-debug.apk Success # 二つ以上シミュレータを起動 or 実機を接続している場合 $ adb devices List of devices attached 162EJP011A71181614 device emulator-5554 device $ adb -s emulator-5554 install app/build/outputs/apk/app-debug.apk app/build/outputs/apk/app-debug.apk: 1 file pushed. 142.6 MB/s (2910562 bytes in 0.019s) pkg: /data/local/tmp/app-debug.apk Success
apkはプロジェクトのbuildディレクトリ以下を探せばあると思います。
アプリのアンインストール
$ adb uninstall package Success
アプリの起動
# アクションがandroid.intent.action.VIEWのIntentに反応するActivityへ暗黙的Intentを送る $ adb shell am start -a android.intent.action.VIEW # 特定のActivityへ明示的Intentを送る $ adb shell am start -n io.test.woshidan/io.test.woshidan.MainActivity
Logcatのログを標準出力に出す
$ adb logcat
操作の様子を録画する
# API19以上の実機かAPI24以上のシミュレータ $ screenrecord /sdcard/Movies/demo.mp4 # adb pull で当該ファイルをPCなどに持ってこれる
テストを実行する
ADBを使う
# テストで実行可能なinstrumantationの一覧 $ adb shell pm list instrumentation instrumentation:com.android.emulator.smoketests/android.support.test.runner.AndroidJUnitRunner (target=com.android.emulator.smoketests) instrumentation:com.android.smoketest.tests/com.android.smoketest.SmokeTestRunner (target=com.android.smoketest) instrumentation:com.example.android.apis/.app.LocalSampleInstrumentation (target=com.example.android.apis) instrumentation:com.example.woshidan.myapplication.test/android.support.test.runner.AndroidJUnitRunner (target=com.example.woshidan.myapplication) instrumentation:com.example.woshidan.newtestapplication.test/android.support.test.runner.AndroidJUnitRunner (target=com.example.woshidan.newtestapplication) # プロジェクト全体のAndroidTest $ adb shell am instrument -w com.example.woshidan.myapplication.test/android.support.test.runner.AndroidJUnitRunner # AndroidTestのうち特定のクラスのテストを行う $ adb shell am instrument -w -e class com.example.woshidan.myapplication.test.ExampleTest com.example.woshidan.myapplication.test/android.support.test.runner.AndroidJUnitRunner
gradleを使う
// プロジェクト用のbuild.gradle buildscript { repositories { google() // gradlewでgoogle()リポジトリに探しにいくためにはここに追加する必要あり jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' } }
// アプリ用のbuild.gradle android { compileSdkVersion 26 defaultConfig { applicationId "com.example.woshidan.newtestapplication" minSdkVersion 15 targetSdkVersion 26 buildToolsVersion '26.0.1' versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary= true } ...
# プロジェクトのAndroidTest全体を行う(JUnitを使うUnitテストは connectedAndroid を取る) $ ./gradlew app:connectedAndroidTest # プロジェクトのAndroidTestのうち特定のメソッドのものを行う $ ./gradlew app:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.woshidan.myapplication.MainActivityTest#mainActivityTest
// 出力される実行結果 // build/generated/outputs/connected/以下にある // build/generated/reports/以下にはHTML形式のレポートも <?xml version='1.0' encoding='UTF-8' ?> <testsuite name="com.example.woshidan.myapplication.MainActivityTest" tests="1" failures="0" errors="0" skipped="0" time="0.665" timestamp="2017-12-20T17:11:10" hostname="localhost"> <properties> <property name="device" value="Nexus_5X_API_22(AVD) - 5.1.1" /> <property name="flavor" value="" /> <property name="project" value="app" /> </properties> <testcase name="mainActivityTest" classname="com.example.woshidan.myapplication.MainActivityTest" time="0.563" /> </testsuite>
まとまってませんが色々試して満足したので現場からは以上です。
Cocos2d-x 2.2.6をMac OS Sierraで動くようにするまでのメモ
仕事でやってて面白かったので許可を取って公開。
環境
- Cocos2d-x 2.2.6
- Eclipse Oxygen.1a Release (4.7.1a)
- Android NDK, Revision 10e
- Android SDK & tools v24.0.3 のもの一式
- Android Studioとは別に用意
- Eclipseの環境設定で下図のように設定
メモ
プロジェクトの作成
python create_project.py -project MyGame -package com.MyCompany.AwesomeGame -language cpp
Eclipse インストーラからJavaのIDEをインストールして環境変数を設定
Help > Install New SoftWare
Work with
の欄にhttps://dl-ssl.google.com/android/eclipse
を入力
cocos2dxのプロジェクトをインポートとプロジェクト間の依存関係の設定
Import > Andorid > Existing Android Code Into Workspace
でcocos2d-x-2.2.6/cocos2dx
のplatform/andorid/java
をインポートCopy projects into workspace
にチェックしておく
Import > Andorid > Existing Android Code Into Workspace
でcocos2d-x-2.2.6/cocos2dx/ProjectName
をインポートCopy projects into workspace
にチェックしないProjectName
のAndroidManifest.xml 中のminSDKVersion
が小さすぎるので14くらいにあげておく
PackageManager
上でProjectName
の上を右クリックしてProperties > Android
を開き、Libraryのところで、libcocos2dxを指すよう設定するisLibrary
のチェックはしない(アプリプロジェクトにisLibraryのチェックをするとapkが生成されない)
アプリ側のプロジェクトのLinked Resourcesの修正
- プロジェクトのところで右クリックして
Properteis > Resources > Linked Resources
のLinked Resources
のタブを確認。Classes
,cocos2dx
,extensions
,scropting
の壊れているパスを修正
古めのNDKを用意してNDK_ROOTの環境変数を設定
- 古めのNDK(Android NDK, Revision 10e)を https://developer.android.com/ndk/downloads/older_releases.html からDL
Eclipse
のPreferences > Android > NDK
の項目の NDK LocationにDLしてきたNDKを配置したパスを書く
Preferences > Android > NDK
に設定したパスが Eclipse に見えてないことがあるのでC/C++ > Build > Environment
にもNDK_ROOT
として追加Bulid Project
した結果、build_native.sh
は走ってくれたが。。
トラブルシューティング
Could not find BuildTest.apk!
...
- https://stackoverflow.com/questions/4778113/android-eclipse-could-not-find-apk
- アプリケーションプロジェクトのプロパティに
isLibrary
のチェックが入ってないか
Unknown error: Unable to build: the file dx.jar was not loaded from the SDK folder!
Failed to load /Users/woshidan/Library/Android/sdk/build-tools/26.0.2/lib/dx.jar
- https://stackoverflow.com/questions/5228453/android-adt-error-dx-jar-was-not-loaded-from-the-sdk-folder
Eclipse起動時に Failed to get the required ADT version number from the SDK
- Eclipseが想定しているADTのライブラリ構成のパスが古いせいで、ビルドツールの特定のライブラリが見つからないって言われてる(その2)
- Andorid SDKと一緒のディレクトリに入っているツールは
build-tools
以外は最新版しか保持できない。buildtools
以外のツールのバージョンが v26以上だと同様のメッセージが出るのでAndorid Studio用のSDKとは別のディレクトリにEclipse用の古いバージョンのSDKを入れ直してそちらを使う
エミュレータを起動しようとしたら[Start]が押せず詳細を確認したら Google Nexus 5X No longer exists as a device
Description Resource Path Location TypeThe container 'Android Dependencies' references non existing library'cocos2d-x-2.2.6/cocos2dx/platform/android/java/bin/libcocos2dx.jar' ...
- http://renkaze.seesaa.net/article/405756568.html
libcocos2xd
のプロジェクトでjar
を吐き出すパスが別のパスに設定されているlibcocos2dx
のプロジェクトで右クリックしてProperties > Java Build Path < Source
の下側のDefault output folder
をlibcocos2dx/bin/classes
に設定
java.lang.IllegalArgumentException: No configs match configSpec
というエラーでエミュレータで起動するとクラッシュする
- http://mapyo.hatenablog.com/entry/2013/08/27/Mac%E3%81%ABCocos2d-x%E3%81%AE%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%82%92%E4%BD%9C%E3%82%8B
- エミュレータ作成時の設定で
Emulation Options
のUse Host GPU
をチェック
Eclipse
でエミュレータを起動しようとしたら PANIC: Missing emulator engine program for 'arm' CPUS...
と出る
- Eclipse の ADT からエミュレータのシステムイメージが見えてないとかそういうことな気がする。
- Android Studioで使っている最新のAndroid SDK & Tools とは別にEclipse用の古いバージョンのSDKを入れ直したら解決した
Google Nexus 5X no longer exists as a device
とでて Nexus 5
のスキンのエミュレータがつけない
- Eclipse の ADT は対応しているバージョンより新しいAndroid SDKのスキンは存在しないものとして扱う、みたいな雰囲気
- Eclipse のデバイスマネージャから選択できるスキンのエミュレータを使う
まとめ
そろそろEclipseでAndroidを開発するのをやめたほうがいいのでは。
現場からは以上です。
インテントフラグとActivityStackのふるまいについていくらか
Android
の Activity
のスタックの振る舞いを設定するものとして、
AndroidManifest.xml
に記述するactivity
要素のlaunchMode
属性Activity
を立ち上げる時に使うIntent
に付与するインテントフラグ
があります。先日、launchModeの話をしたので、今日はインテントフラグの実験を少ししようかと思います。
今回インテントフラグがどんなものか確認する際に取り上げる具体例としては、
あたりにします。今回の実験に使ったコードの全体像はこちらで適宜、/* some flags */
周辺のコードを編集した分を実行結果と一緒に紹介します。
TL;DR
長くなったので。
- FLAG_ACTIVITY_SINGLE_TOP
launchMode
のsingleTop
と同じ- このフラグをつけて起動したActivityと同じActivityがスタックの一番上にあるなら、既存のインスタンスの
onNewIntent()
を呼びだす
- FLAG_ACTIVITY_CLEAR_TOP
- FLAG_ACTIVITY_NO_HISTORY
- このフラグをつけて起動した
Activity
はActivity
のスタックに積まれない - =
onActivityResult
が決して呼び出されない
- このフラグをつけて起動した
FLAG_ACTIVITY_SINGLE_TOP
まず、MainActivity
を起動するときだけ、FLAG_ACTIVITY_SINGLE_TOP
を利用してどのような挙動をするか見てみましょう。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, NextActivity.class); startActivity(intent); } }); }
public class NextActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_next); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, NextActivity.class); startActivity(intent); } }); }
MainActivityが一番上にあるときはその上にActivityが積まれず
NextActivityが一番上にあるときは新しいMainActivityが積まれます。
launchMode
の singleTop
と同じ振る舞いですね。
FLAG_ACTIVITY_CLEAR_TOP
MainActivityを表示しようとしたとき、FLAG_ACTIVITY_CLEAR_TOP
のインテントフラグを付与することにします。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d("MainActivity", "MainActivity " + MainActivity.this.toString()); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, NextActivity.class); startActivity(intent); } }); }
public class NextActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_next); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, NextActivity.class); startActivity(intent); } }); }
MainActivityが一番上にあるとき、すでにあるActivityが破棄されてから別のActivityが作り直されて一番上に積まれ直します。
Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());
でインスタンスが変わっているというログがこちらです。
D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@3a2375cd D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@21a00ad7 D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@ac67751
何枚か NextActivity
をスタックに積んでから MainActivity
を起動し、その後バックキーを押すとアプリが終了します。
この場合のLog.d("MainActivity", "MainActivity " + MainActivity.this.toString());
でインスタンスが変わっているというログがこちらです。
D/MainActivity: MainActivity com.example.woshidan.contexttest.MainActivity@2007c35 D/MainActivity: MainActivity com.example.woshidan.contexttest.MainActivity@25e3619b
もう少し別の条件で検証するとわかりやすいですが、起動しようとした MainActivity
より上のスタックにある NextActivity
と古い MainActivity
を捨て、MainActivity
を作り直して、呼び出していることがわかります。
ドキュメントのフラグ一覧のところの FLAG_ACTIVITY_CLEAR_TOP
の欄には、
If set, and the activity being launched is already running in the current task, then instead of launching a new instance of that activity, all of the other activities on top of it will be closed and this Intent will be delivered to the (now on top) old activity as a new Intent.
とあり、これは上記の挙動と異なるように見えます。
しかし、これは仕様どおりで、さらに FLAG_ACTIVITY_CLEAR_TOP
の単体の項目のドキュメントを読むと、FLAG_ACTIVITY_SINGLE_TOP
との併用か、 Activity
の launchMode
が singleTop
に設定されていなければ、既存のインスタンスへちょっとIntentが送られないことがあることがわかります。
それでは、 FLAG_ACTIVITY_CLEAR_TOP
とFLAG_ACTIVITY_SINGLE_TOP
との併用した場合の挙動を
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d("MainActivity", "MainActivity " + MainActivity.this.toString()); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, NextActivity.class); startActivity(intent); } }); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Log.d("MainActivity", "onNewIntent"); Toast.makeText(this, intent.getStringExtra("KEY_MESSAGE"), Toast.LENGTH_SHORT).show(); }
public class NextActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_next); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, NextActivity.class); startActivity(intent); } }); } }
で試してみましょう。今度は、一覧の部分にあるように既存のMainActivityのインスタンスにIntentが送られるようになったみたいです。
FLAG_ACTIVITY_NO_HISTORY
どのActivityを起動するときも FLAG_ACTIVITY_NO_HISTORY
のインテントフラグを付与してがんがんActivityを起動してみます。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, NextActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); } }); }
public class NextActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_next); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, NextActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); } }); } }
たくさんスタックを積んだはずが、2回バックキーを押すとアプリが終了してしまいました。
最初の一回のバックキーで一番最初に起動した MainActivity
へ戻ってしまったようです。
FLAG_ACTIVITY_NO_HISTORY
のインテントフラグをつけて起動されたActivityは、Activityのスタックに積まれないようです。
インテントフラグはActivityを起動する側が指定するので、使う側の都合によってActivityのスタック上での扱いを決める方法、みたいな感じなんですかね。
現場からは以上です。
参考
AndroidのCamera2 APIを触ってみた
ちょっと面白そうだったので調べました。
概要
https://developer.android.com/reference/android/hardware/camera2/package-summary.html からざっくり読み取った結果、Camera2 APIでカメラ経由で画像を取得したりする流れをかなり大雑把にいうと
CameraManager
を利用し、CameraDevice.StateCallback
経由でCameraDevice
のインスタンスを取得- Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して
CameraDevice
のインスタンスからcreateCaptureSession(List, CameraCaptureSession.StateCallback, Handler)
メソッドによりCameraCaptureSession
を生成 - 1フレーム分の入力を取得するための入力リクエスト(
CaptureRequest
のインスタンス)を作る CaptureSession
にリクエストをセットする- リクエストが処理されて、
TotalCaptureResult
(カメラデバイスの現在の状態など)オブジェクトの生成や、出力先への1フレーム分への画像データの送信が行われる
となるようです。それぞれ見ていきましょう。
なお、ただでさえややこしいので、今回のサンプルコードは意図的にtargetSDKVersionを21にして、パーミッション周りやエラー対応の処理はビルドが通るギリギリくらいまで省いています。。
CameraManager
を利用し、CameraDevice.StateCallback
経由で CameraDevice
のインスタンスを取得
CanmeraManager
を利用してそのデバイスで利用できるカメラのIdを取得- カメラのIdと
CameraDevice.StateCallback
のインスタンスを利用して、CameraManager.openCamera()
メソッドでCameraDevice
をオープン CameraDevice.StateCallback
のonOpened
コールバックでCameraDevice
のインスタンスを取得
利用するカメラの種類を制限したかったりする場合はCameraDevice
をオープンする前に行います。
また、サンプルによるとプレビュー領域のサイズや向きなどを設定したい場合はプレビューを表示する TextureView
に対して行うようです。
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); String selectedCameraId = ""; try { selectedCameraId = manager.getCameraIdList()[0]; // https://github.com/googlesamples/android-Camera2Basic/blob/5dad16c103715b5e7e3c001cc5f6067f8d23f29e/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java#L499 // あたりにあるのですが、顔用カメラを使いたくないなどがあれば、CameraCharacteristicsを経由して確認可能 // CameraCharacteristics characteristics // = manager.getCameraCharacteristics(selectedCameraId); } catch (CameraAccessException e) { e.printStackTrace(); } try { manager.openCamera(selectedCameraId, mStateCallback, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); }
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice cameraDevice) { mCameraDevice = cameraDevice; } @Override public void onDisconnected(CameraDevice cameraDevice) { cameraDevice.close(); mCameraDevice = null; } @Override public void onError(CameraDevice cameraDevice, int error) { cameraDevice.close(); mCameraDevice = null; } };
CameraDevice
のインスタンスから CameraCaptureSession
を生成
Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して CameraDevice
のインスタンスから createCaptureSession(List, CameraCaptureSession.StateCallback, Handler)
メソッドにより CameraCaptureSession
を生成します。
- 出力先として
TextureView
を用意する TextureView
からSurfaceTexture
を取得してSurface
を生成する- 2の
Surface
をListに入れて、CameraCaptureSession.createCaptureSession()
メソッドを呼び出す
2の Surface
は、 ImageReader
や MediaCodec
など他の出力先クラスからも生成/取得が可能です*1。
// Activity.onCreate内 mTextureView = (TextureView) findViewById(R.id.texture); mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { // 先ほどのカメラを開く部分をメソッド化した openCamera(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } });
private CameraCaptureSession mCaptureSession = null; // ... private void createCameraPreviewSession() { SurfaceTexture texture = mTextureView.getSurfaceTexture(); texture.setDefaultBufferSize(320, 240); // 自分の手元のデバイスで決めうちしてます Surface surface = new Surface(texture); try { mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { // カメラがcloseされている場合 if (null == mCameraDevice) { return; } } mCaptureSession = session; @Override public void onConfigureFailed(CameraCaptureSession session) { } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } }
1フレーム分の入力を取得するための入力リクエスト( CaptureRequest
のインスタンス)を作る
CaptureRequest
はカメラが何ができるかなどの情報が必要なため、 CameraDevice
クラスが CaptureRequest.Builder
のファクトリメソッドを持っていますので利用します。
リクエストには色々ありますが、今回のリクエストは Target
に与えた Surface
によくあるプレビューの表示を作るための画像データを送ってくれ、というものです(たぶん)。
private CaptureRequest.Builder mPreviewRequestBuilder; private CaptureRequest mPreviewRequest; ... // createCameraPreviewSession メソッド内で try { mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewRequestBuilder.addTarget(surface); mPreviewRequest = mPreviewRequestBuilder.build(); } catch (CameraAccessException e) { e.printStackTrace(); }
CaptureSession
にリクエストをセットする
先ほど生成したリクエストに加えて、 CameraCaptureSession.CaptureCallback
が必要です。
プレビュー表示のいろいろなエラーハンドリングをする場合は結構書く必要があるみたいですが、今回はその辺は考えないのでnullを与えています。
この時点で TextureView を置いた領域にカメラで撮影した範囲が表示されているのが確認できます。
mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { // カメラがcloseされている場合 if (null == mCameraDevice) { return; } mCaptureSession = session; try { session.setRepeatingRequest(mPreviewRequest, null, null); // `CaptureSession` にリクエストをセットする } catch (CameraAccessException e) { e.printStackTrace(); } }
TotalCaptureResult
へ送られてきた1フレーム分への画像データの処理について
前節で TotalCaptureResult
へデータが送られてくるところまでは確認しましたが、 プレビューだけ延々と表示しても仕方ないので、ここへ飛んできたデータを利用する方法の一例を書いておしまいにします。
下記のコードでは、ボタンを押すとプレビューの表示を止めて、プレビューに表示している画像をファイルへ保存しています。
Button capture = (Button) findViewById(R.id.button_capture); capture.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { mCaptureSession.stopRepeating(); // プレビューの更新を止める if(mTextureView.isAvailable()) { File file = new File(getFilesDir(), "surface_text.jpg"); FileOutputStream fos = new FileOutputStream(file); Bitmap bitmap = mTextureView.getBitmap(); bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos); fos.close(); } } catch (CameraAccessException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } });
真剣にやろうと思ったらActivityやFragmentのライフサイクルに合わせてリソースの解放とか色々あるんですが今日は流れを把握するためにこんな感じで。
プレビューの表示止めてファイルに書き込む部分まで含めて全体のコードは以下となります。一番初めに書きましたが、今回はCamera2 APIの流れだけを把握しようと例外処理を省くため、targetSDKVersion=21
にしているので少々注意してお試しください。
現場からは以上です。