woshidan's blog

あいとゆうきとITと、とっておきの話。

「DataBindingのコードを読む」のスライドが面白かったのでDataBindingについて少しまとめました

speakerdeck.com

上のスライドが面白かったため、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 で設定されたルールはデフォルトルールの後に動く
// 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を(でかい何かに)登録する
    • DataBindingの(でかい何かに)更新しろ~と呼び出されるにはIDが必要で、BR.javaで管理されている?*9

ViewBindingAdapterクラス*10

  • EventListenerを書いたり、Viewの更新コード*11を書いたり(Viewへの反映を呼び出すのは ViewDataBinding.executeBindings())
    • EventListenerに近いが、Viewの特定の属性の変化の監視、といったことができる
      • @BindingAdapter("android:text") のように監視したい属性を書き、監視した属性が変化した時のコールバックをstaticメソッドで定義*12
    • なお、EventListenerの定義はViewBindingAdapterにアノテーションで書くとアノテーションプロセッサによりViewDataBindingに生える?
  • 自分が管理してる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

*9:Data Binding版R.javaみたいですね

*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の参照を持たなくてよいため、テストが書きやすい? という気がした

マーケティングオートメーションツールとアプリエンジニア ~プッシュ通知・お知らせダイアローグの運用自動化のためのお仕事~という題で発表しました

ふだんやっているお仕事の中から印象的だった事柄をオムニバス方式でお話しさせていただく体の発表をさせていただきました。

speakerdeck.com

技術的には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を見て面白かったので細々調べたメモ(動画のストリーミング配信プロトコル周り)

speakerdeck.com

このメモはセッションで話された内容の書き起こしではなく、上記のスライドの内容が面白かったので自分で勝手に調べたメモです。

調べた結果とセッション中と用語のずれがある場合は、Abema TVのCode IQのプレス記事*1を優先させていただきました。

目次

  • Androidで動画を再生するためのクラス
  • Streamingで動画をダウンロードする、そのためにそれ用のコーデックで圧縮する
  • Apple製のストリーミング動画配信プロトコル HLS(HTTP Live Streaming)とその実装に必要なファイル
  • Abemaで採用しているストリーミング動画配信プロトコル MPEG-DASHの説明

Androidで動画を再生するためのクラス

  • MediaPlayerExoPlayerがある
  • 前者はAPI Level 1からある、詳細はいじれないが単純なユースケースの場合はこれをポンと置いておけば良い
  • 後者はAPI Level 14から利用でパラメータがカスタマイズできる
    • 利用するときはGradleで com:google:android:exoplayer:exoplayer:x.x.x の依存を追加
    • このライブラリは使う機能に合わせて全体やCore, UI, HLS, OKHTTP(通信周りのカスタマイズ)に分かれている

Streamingで動画をダウンロードする、そのためにそれ用のコーデックで圧縮する

  • 動画をダウンロードして再生する、という場面では再生用のファイルをすべてDLしてから再生する場合と再生用のデータを都度都度ダウンロードして再生する場合がある
  • ストリーミング、の場合は後者
    • いわゆるストリーミングにも大きく分けてプログレッシブダウンロード方式(ファイルをHDにダウンロードする)とストリーミング方式(ファイルの内容はその場で取得するだけ)がある
      • プログレッシブダウンロード方式は動画の容量が多いとあれなんですが、シークバーを少し前に戻してもふたたびDLせずにその場で再生可能(DLしたファイルがまだ端末内に残っているので)だったり、さきもってDLするので一気に動画の後半へ飛んだりが可能で、一時期のYoutubeなどで採用されていた*2
      • ストリーミング方式はシークバーを動かしてから動画のDLをするまでしばらくかかることはあるが、都度都度クライアント側の状況に合わせた動画の取得が可能だったり、ネットワークの切り替え(Wifi <=> 4Gなど)が起こりがちな状況(モバイルなど)に対して有利
        • クライアント側にコンテンツが残らないのでセキュア
  • 動画を都度都度ダウンロードするとして
    • 動画は音声 + 画像の集まり
    • 画像分だけでもまじめに考えればとてもじゃないが4Gの通信帯域の幅を越してしまうよね
      • 1pixel = 24bits, 1frame = 49 Mbits, 1second = 1492M bits, 1492M bits(1秒あたりの動画容量) > 60Mbps (4G LTEの通信帯域幅(理想))
    • このままだと配信できるわけがないので圧縮して通信しましょう
    • 動画の圧縮方式をコーデック*3といい、h264という圧縮方式が用いられることが多いです
    • 圧縮すると1/100くらいになるので配信できそう

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 を取得
    • Master Playlistに帯域幅ごとetc. の動画ファイル用Media PlaylistのURLが書いてあるのでMedia Playlistを取得
    • Media Playlistに動画ファイルのURLとそのファイルの再生時間(EXTINF *4 )が書いてあるのでそれを見てtsファイルの取得をする*5
  • ちなみに、tsファイルは「MPEG-2 TS」ファイル
    • もともと地上デジタル放送などで用いられる映像・音声データの形式「MPTG-2 TS」をファイル化する際に数秒単位の「MPEG-2 TS」として細切れにし、暗号化したもの*6

Abemaで採用しているストリーミング動画配信プロトコル MPEG-DASHの説明

  • さっきHLSの話をしたんですが、Abema TVで実際利用しているのはMPEG-DASHというプロトコルですよ*7
    • MPEG-DASHの“DASH”はDynamic Adaptive Streaming over HTTPの略で、国際標準化機関ISO/IECが定義したHTTPストリーミングを行うための規格 *8
      • 動画や音声ファイルを管理するメタデータを記述したMPD(Media Presentation Description)と呼ぶマニフェストファイル仕様を定めた規格と、実際に動画コンテンツを伝送するためのsegment formatと呼ぶファイルフォーマットの運用規格 *9
  • MPEG-DASHで主要なファイルは MPD(.mpd), fragmented mp4 file(.m4s), Initialization file(.m4s) *10
  • 普通のmp4 vs fragmented mp4 file
    • mp4はbox*11という要素が連なった木構造データ形式となっている
      • 代表的な要素(box/atom)として
        • ファイルタイプが記述された ftyp
        • メタデータ*12が書いてある moov
        • メディアデータ*13本体の mdat
      • これらのbox/atomの構成の様子はスライド中の mp4dump コマンドや専用のツールで確認出来る
    • Fragmented mp4 fileはわざわざ名前を変えている通り、少々勝手が異なる
    • .mp4 ファイルを構成する主要な要素について、フラグメントごとに要素を用意している(moof, mdat)
      • ファイルタイプが記述された ftyp (これは同じ)
      • sidx: index of moof + mdat
        • 細分化する前のファイルにおいてこのmp4はどこに位置するか
      • moof: メタデータが書いてあるmoovのフラグメント版。全体に対するメタデータではなく分割されたファイルに対するメタデータ
      • mdat: メディアデータ本体 (これは同じ)
    • フラグメントのファイル群だけでは動画全体の情報が足りないため、全体を統括するためのファイルが必要でこれがinitialization fileやInitialization fragmentと呼ばれている*14
      • Initialization Fragmentの方はftyp(ファイル形式)は dash で動画全体のメタデータに当たる moov を持っている
  • 上記動画ファイルとは別にMPD(Media Presentation Description)がある
    • MPDは動画や音声ファイルを管理するメタデータを記述したマニフェストファイル
    • 要素には
      • MPD: Live配信用につどつど動画データを取りに行く vs 一気にダウンロード, バッファするデータの最小値 etcを書く
      • Representation: ビットレートごとの動画データについて記述 など

長くなってきたので一旦ここまでにします。。

*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で動くようにするまでのメモ

仕事でやってて面白かったので許可を取って公開。

環境

スクリーンショット 2017-10-19 13.30.52.png (44.2 kB)

メモ

プロジェクトの作成

python create_project.py -project MyGame -package com.MyCompany.AwesomeGame -language cpp

Eclipse インストーラからJavaIDEをインストールして環境変数を設定

  • Help > Install New SoftWare
    • Work with の欄に https://dl-ssl.google.com/android/eclipse を入力
      • Developer Tools をダウンロード
      • Eclipseを再起動
      • DLしたらどこのAndroid SDK を使うか聞かれるので Andorid Studioで使っているパス /Users/woshidan/Library/Android/sdk あたりでも答えておく (設定した値は Preferences > Android からでも確認可能. Android SDKのバージョンが v26 以上なら後の方に書くようにEclipse用に別途古いAndroid SDKを用意した方が良い)

cocos2dxのプロジェクトをインポートとプロジェクト間の依存関係の設定

  • Import > Andorid > Existing Android Code Into Workspacecocos2d-x-2.2.6/cocos2dxplatform/andorid/java をインポート
    • Copy projects into workspace にチェックしておく
  • Import > Andorid > Existing Android Code Into Workspacecocos2d-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 ResourcesLinked Resources のタブを確認。Classes, cocos2dx, extensions, scropting の壊れているパスを修正

古めのNDKを用意してNDK_ROOTの環境変数を設定

  • 古めのNDK(Android NDK, Revision 10e)を https://developer.android.com/ndk/downloads/older_releases.html からDL
    • EclipsePreferences > 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! ...

Unknown error: Unable to build: the file dx.jar was not loaded from the SDK folder!

Eclipse起動時に Failed to get the required ADT version number from the 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 folderlibcocos2dx/bin/classes に設定

java.lang.IllegalArgumentException: No configs match configSpec というエラーでエミュレータで起動するとクラッシュする

Eclipseエミュレータを起動しようとしたら PANIC: Missing emulator engine program for 'arm' CPUS... と出る

Google Nexus 5X no longer exists as a device とでて Nexus 5 のスキンのエミュレータがつけない

まとめ

そろそろEclipseAndroidを開発するのをやめたほうがいいのでは。

現場からは以上です。

インテントフラグとActivityStackのふるまいについていくらか

AndroidActivity のスタックの振る舞いを設定するものとして、

  • AndroidManifest.xml に記述する activity 要素の launchMode 属性
  • Activity を立ち上げる時に使う Intent に付与するインテントフラグ

があります。先日、launchModeの話をしたので、今日はインテントフラグの実験を少ししようかと思います。

今回インテントフラグがどんなものか確認する際に取り上げる具体例としては、

  • FLAG_ACTIVITY_SINGLE_TOP
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_NO_HISTORY

あたりにします。今回の実験に使ったコードの全体像はこちらで適宜、/* some flags */ 周辺のコードを編集した分を実行結果と一緒に紹介します。

TL;DR

長くなったので。

  • FLAG_ACTIVITY_SINGLE_TOP
    • launchModesingleTop と同じ
    • このフラグをつけて起動したActivityと同じActivityがスタックの一番上にあるなら、既存のインスタンスonNewIntent()を呼びだす
  • FLAG_ACTIVITY_CLEAR_TOP
    • このフラグをつけて起動した Activity よりスタックの上にある Activity を破棄して、フラグをつけた Activity を呼び出す
    • FLAG_ACTIVITY_SINGLE_TOP との併用している、または、起動した ActivitylaunchModesingleTop の場合は既存のインスタンスが再利用される
    • そうでない場合は、既存の Activityインスタンスを破棄してから新しい Activityインスタンスを生成して起動する
  • FLAG_ACTIVITY_NO_HISTORY
    • このフラグをつけて起動した ActivityActivity のスタックに積まれない
    • = 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が積まれます。

launchModesingleTop と同じ振る舞いですね。

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 との併用か、 ActivitylaunchModesingleTop に設定されていなければ、既存のインスタンスへちょっとIntentが送られないことがあることがわかります。

それでは、 FLAG_ACTIVITY_CLEAR_TOPFLAG_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のスタック上での扱いを決める方法、みたいな感じなんですかね。

現場からは以上です。

参考

Intent | Android Developers

AndroidのCamera2 APIを触ってみた

ちょっと面白そうだったので調べました。

概要

https://developer.android.com/reference/android/hardware/camera2/package-summary.html からざっくり読み取った結果、Camera2 APIでカメラ経由で画像を取得したりする流れをかなり大雑把にいうと

  1. CameraManager を利用し、CameraDevice.StateCallback 経由で CameraDeviceインスタンスを取得
  2. Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して CameraDeviceインスタンスから createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) メソッドにより CameraCaptureSession を生成
  3. 1フレーム分の入力を取得するための入力リクエスト( CaptureRequestインスタンス)を作る
  4. CaptureSession にリクエストをセットする
  5. リクエストが処理されて、 TotalCaptureResult (カメラデバイスの現在の状態など)オブジェクトの生成や、出力先への1フレーム分への画像データの送信が行われる

となるようです。それぞれ見ていきましょう。

なお、ただでさえややこしいので、今回のサンプルコードは意図的にtargetSDKVersionを21にして、パーミッション周りやエラー対応の処理はビルドが通るギリギリくらいまで省いています。。

CameraManager を利用し、CameraDevice.StateCallback 経由で CameraDeviceインスタンスを取得

  1. CanmeraManager を利用してそのデバイスで利用できるカメラのIdを取得
  2. カメラのIdと CameraDevice.StateCallbackインスタンスを利用して、CameraManager.openCamera()メソッドで CameraDevice をオープン
  3. CameraDevice.StateCallbackonOpened コールバックで 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 を生成します。

  1. 出力先として TextureView を用意する
  2. TextureView から SurfaceTexture を取得して Surface を生成する
  3. 2のSurface をListに入れて、CameraCaptureSession.createCaptureSession() メソッドを呼び出す

2の Surface は、 ImageReaderMediaCodec など他の出力先クラスからも生成/取得が可能です*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にしているので少々注意してお試しください。

Camera2_API_Sample · GitHub

現場からは以上です。

参考