Drowsy Dog's Diary

any note, any thought

2014年8月25日
by kazoo
0 comments

[android] APK Expansion Files(2)

前回の続き。Expansion Files のダウンロードについて。

アプリの起動時、あるべき Expansion files がそのパス(shared storage location の Android/obb/<package-name>/
)になかったらその時点で Google Play から DL してくる必要がある。アプリは Application Licensing service を使って対応するファイル名・サイズ・URL を取得し、ファイルを DL して適切なパスに保存を行わなければいけない。

前記事の通り、このための Downloader Library とそれを使ったサンプルが SDK に用意されている。また、Google Play にライセンスの Verification を行い URL をリクエストする Licensing サービスは以下に用意されている。

<ANDROID_SDK_DIR>/extras/google/play_licensing/

ルールと制限

  • Expansion File の上限はそれぞれ2GB
  • ユーザは Google Play からアプリを DL していなければならない。それ以外の方法でインストールされたアプリは Expansion File を DL できない
  • Google Play が提供する URL は、すべての DL に対してユニークであり、短時間で無効化される
  • Expansion Files の更新には、APK 本体の更新も必要
  • obb/ ディレクトリに他のデータを保存してはいけない
  • .obb ファイルを削除・リネームしてはいけない

Expansion Files のダウンロード

Licensing Verification Library (LVL)での認証処理に加えて、ダウンロードのための HTTP 接続とファイル保存のコードが必要になる。またそのために考慮すべき Issue は、

  • ストレージ容量が足りているかの確認
  • UI をブロックせず、またダウンロード途中でもユーザが離脱できるようにバックグラウンドで動作すること
  • ダウンロード中に起こる各種エラーへの対応
  • ネットワークの切断に対応し、また可能であれば途中からレジュームする
  • バックグラウンドでのダウンロード中、そのステータスと進捗をユーザに知らせること

Google 製の Downloader Library を使うことで、これらのタスクをすべて簡素化できる。開発者は、以下のライブラリをアプリに組込めば利用可能。

  • <ANDROID_SDK_DIR>/extras/google/play_licensing/
  • <ANDROID_SDK_DIR>/extras/google/play_apk_expansion/downloader_library/

Downloader Library が Licensing Library に依存する形になっているので、Eclipse などのプロジェクトに組込んで、Main となるプロジェクトのプロパティで前者のライブラリプロジェクトを追加してやればよいはず。

また、zip フォーマットを Expansion Files として使う場合は、APK Expansion Zip Library の使用も推奨されている。

パーミッション

Expansion Files のダウンロードには、Manifest に以下のパーミッションの宣言が必要。

ダウンローダの実装

Downloader Library は DownloaderService という Service のサブクラスを提供する。開発者はこれを継承して自前のダウンローダを実装する。DownloadService はまた、

  • BroadcastReceiver を登録し、必要なときにダウンロードを停止・再開するためにネットワーク接続状況を監視する
  • サービスが kill されたときのためにリトライ用のタイマーをスケジュールする
  • ダウンロード進捗あるいはエラーを表示するための Notification を持つ
  • マニュアルでダウンロードが pause/resume できる仕組みを与える
  • shared storage がマウントされ利用可能であること、ファイルがすでに存在しないこと、容量が足りることを確認して NG の場合は知らせてくれる

開発者は、DownloadService を継承したクラスを作り、以下の3つのメソッドを Override する。

ここで、BASE64_PUBLIC_KEY は、Developer Console で確認できる各開発者アカウントに対応する鍵に変更する。
また、getSALT() は、SharedPreferences に保存される License のデータを難読化するためのソルト値。これも別のランダムな値に更新しておく。
getAlarmReceiverClassName() は、ダウンロードが(何らかのエラー後に)再開したときのアラームを受け取るアプリ内の BroadcastReceiver を返すようにしておく。

追加したサービスは忘れずに Manifest にも追記しておく。

AlarmReceiver の実装

ダウンロードの進捗をモニターし、必要に応じて再開するために、DownloaderService は RTC_WAKEUP アラームを設定する。開発者は BroadcastReceiver を Downloader Library から呼べるように定義する。

ダウンロードの開始

前回記事に書いた Helper ライブラリを使ってExpansion Files の存在(無いこと)を確認したら、下記の static メソッドを呼んでダウンロードを開始する。

DownloaderClientMarshaller.startDownloadServiceIfRequired(Context c, PendingIntent notificationClient, Class serviceClass).

パラメータの意味はそれぞれ:

  • context: アプリケーションの Context
  • notificationClient: メイン Activity を開始するための PendingIntent。DownloaderService からの Notification で使用され、ダウンロードの進捗を表示する(通常はDL開始したものと同じ)Activity を開始するための PendingIntent を登録する
  • serviceClass: DownloaderService の Class オブジェクト。サービスを開始しダウンロードを始めるために必要

戻り値は以下のようになる。

  • NO_DOWNLOAD_REQUIRED: ダウンロード不要
  • LVL_CHECK_REQUIRED: Expansion Files の URL を得るためライセンスのベリフィケーションが必要
  • DOWNLOAD_REQUIRED: すでに URL が判明しておりダウンロードが開始可能

通常は LVL_CHECK_REQUIRED と DOWNLOAD_REQUIRED の違いを意識しない。Main の Activity から startDownloadServiceIfRequired() を呼び、単純に戻り値が NO_DOWNLOAD_REQUIRED かどうかをチェックすればよい。 NO_DOWNLOAD_REQUIRED 以外の何かが返ったときは、Downloader Library は ダウンロードを開始し、その進捗を表示するための Activity をセットアップする。

サンプルは以下の通り。

startDownloadServiceIfRequired() が NO_DOWNLOAD_REQUIRED 以外を返したときは、DownloaderClientMarshaller.CreateStub(IDownloaderClient client, Class downloaderService) を呼んで IStub インスタンスを生成する。IStub は Activity と DownloaderService をバインドする(進捗の通知などのため)仕組みを提供する。CreateStub() には、IDownloaderClient インターフェイスを実装した DownloadService の実装を渡す。

この IStub の生成は Activity の onCreate() で startDownloadServiceIfRequired() の後に行うことが推奨される。

先ほどのサンプルの続きは以下のようになる。

onCreate() を抜けた後、onResume() の中で IStub の connect() を呼ぶ。そして onStop() で disconnect() を呼んでやるとよい。

進捗・エラーステートの受信

Downloader Library にある、IDownloaderClient インターフェイスを使う。通常は、ダウンロードを開始した Activity にこれを実装する。

onServiceConnected(Messenger m) は、IStub を生成した後 DownloaderService との接続を通知してくれる。これを受けたら、pause/resume が送れるように DownloaderServiceMarshaller.CreateProxy() を呼んで、IDownloaderService のインターフェイスを取得しておく。

また、onDownloadStateChanged(int newState), onDownloadProgress(DownloadProgressInfo progress) で、それぞれ状態や進捗の変化を通知してくれる。

STATE_COMPLETED が返れば ダウンロード完了。必要に応じてダウンロードしたファイルのバリデーションなどを行い、Activity を抜ければよい(これも Downloader Sample にサンプルがある)。また、IDownloadService を取得した後は、それに対して requestPauseDownload()/requestContinueDownload() を呼ぶことで、手動で中断・再開ができる。

Expansion Files のテスト

Google Play に APK と Expansion Files を UP する前に、まず正しいパスから Expansion Files を読み出せるかをテストする。
たとえば、パッケージ名が com.example.android であれば、Shared Storage に Android/obb/com.example.android/ ディレクトリを作成し、$ adb push などでファイルを設置し、Google Play 上で扱われる正しいファイル名に .obb ファイルをリネームする。
ファイル名フォーマットは、main.0300110.com.example.android.obb のようになる。バージョンコードは何でもよいが、アプリがそれを正しく知っていることが必要。LVL は Expansion Files ではなく、あくまでも APK のバージョンコードに基づいて、Google Play 上のリソースを探しにゆく。ここでバージョンコードが正しくないと、”Download failed because the resources could not be found” というエラーになるので注意。

ダウンロードのテストは、Google Play の Beta バージョンとして Expansion Files をアップロードすれば、通常の APK 同様にベータテストユーザがダウンロードしてテストすることができる。

2014年8月22日
by kazoo
1 Comment

[android] APK Expansion Files

Play Store に登録する Android アプリのサイズ上限は 50MB。
これを超えるサイズのアプリを登録したい場合は、APK Expansion Files という仕組みを使って、別ファイルにアプリを分割することで、最大 4GB までのアプリを登録できる。
こないだ超えてしまったのでこれについて調べた。

先に結論言うとかなりめんどいので、できるだけやらない方がよいと思う。

最近のゲームでよくあるように、小さな APK で起動して、自前のサーバと自前のダウンローダで必要なデータを都度取ってくる仕組みが用意できるなら、もちろん必要はない。

拡張ファイルについて

2種類ある。main と patch。それぞれ最大 2GB。
patch 拡張ファイルはオプション的に、main 拡張ファイルに対して小さな更新を担うイメージ。使うのは main だけでもよい。

それぞれ独立して Google Play に登録でき、APK から使用する拡張ファイルのバージョンを指定できるが、「拡張ファイルだけ」を更新することはできない。拡張ファイルを更新するときは、APK(のバージョンコード)の更新も必要。

拡張ファイルのフォーマットは ZIP, PDF, MP4, etc.何でも可能。また、jobb というコマンドツールが用意されており、これを使ってファイルセットを .obb というひとつの暗号化ディスクイメージのファイルにまとめることができる。obb は Opaque Binary Blob の略。

jobb コマンドは下記にあり、

<ANDROID_SDK_DIR>/tools/jobb

という感じで使う。

  • -pn: アプリのパッケージ名
  • -pv: パッケージのバージョン。ここで指定したものより大きなバージョン(android:versionCode)のアプリが、これをマウントできる
  • -d: 入力ディレクトリ名。これで指定したディレクトリ以下のファイルとサブディレクトリがひとつの obb にまとめられる。-d resource としたとき、解凍すると resource ディレクトリそのものは無いので注意
  • -k: obb を暗号化する場合のパスワードを設定
  • -o: 出力ファイル。名前はなんでもよい

Google Play に拡張ファイルをアップロードすると、ファイルタイプに関係無く以下のフォーマットにリネームされる。

[main|patch].<expansion-version>.<package-name>.obb

上記の例だと、main.4.com.hogehoge.pkgname.obb となる。

拡張ファイルのダウンロード先とマウント

以下に、ダウンローダのサンプルとヘルパーライブラリが用意されている。ファイルパスの取得などはヘルパーを使うのが楽。

<ANDROID_SDK_DIR>/extras/google/play_apk_expansion/downloader_sample/
<ANDROID_SDK_DIR>/extras/google/play_apk_expansion/downloader_library/

ダウンロードされた拡張ファイルは、
<shared-storage>/Android/obb/<package-name>/

以下に保存される。
<shared-storage> は、getExternalStorageDirectory() で取得できるパス。一般的には /sdcard になると思う。<package-name> は前述のパッケージ名と同じ。

アプリは自分が必要な拡張ファイルのバージョンを知っているので、まずこの obb ファイルを探しに行き、StorageManager を使ってこれをマウントする。ざっくり書くとこんな感じ。

マウントに成功すると、onObbStateChange(String, int)が、/mnt 以下のそのパスを教えてくれる。

ダウンロードの保証

ほとんどの場合、拡張ファイルはアプリ本体と一緒にダウンロードされる。ユーザからは Google Play 上の(50MB より大きな)ひとつのアプリとしか見えない。

しかし、まれに拡張ファイルのみダウンロードに失敗したり、ユーザが手動でファイルを削除したりする場合がある。このケースについてもアプリは最初に拡張ファイルの存在を確認し、無い場合には自前でダウンロードできるのが望ましい。

…が、実際 50MB をちょっと超えるくらいであれば、そうそうダウンロードに失敗することは無いのではと思われる。ストレージが足りないときに起こるのかもしれないが、手元では一度も確認できなかった。ここは割り切って拡張ファイルが無ければエラー終了でも良いのではとも思われる(公式ドキュメントにはもちろん must be able to と書かれているが)。

で、先述のダウンローダライブラリと、

<ANDROID_SDK_DIR>/extras/google/play_licensing/library/

に用意されているライセンス用ライブラリを import して、Google Play から拡張ファイルのダウンロードを行うことができる。この方法も先のサンプルにほぼ書かれていますが力尽きたので後略。。。気が向けばまた書きます。

参考:
APK Expansion File | Android Developers
APK拡張ファイルの使用 | Android デベロッパー ヘルプ

Expansion Files について | キノコの自省録

2014年7月2日
by kazoo
0 comments

[android]ギャラリーから画像を縮小して読込む

BitmapFactory でそのまま読むとメモリ不足が起きるようなでかい画像の場合、BitmapFactory.Options の inJustDecodeBounds を true にして、メモリに展開せずにパラメータだけをデコードできるので、その後あらためてスケールサイズ(inSampleSize)を指定してデコードしてやればよい。

2014年7月2日
by kazoo
0 comments

[android] KitKat でギャラリーから画像取得

ギャラリーから写真を取得するとき、以前の Intent.ACTION_PICK が返す URI が、

content://media/external/images/media/3951

のように返っていたのに対し、
KitKat(API Level 19) では、MediaStore.Images.Media.DATA で ContentResolver にクエリした場合、

content://com.android.providers.media.documents/document/image:3951

といった URI が返ってくる。

Android Gallery on KitKat returns different Uri for Intent.ACTION_GET_CONTENT – Stack Overflow

KitKat では、Intent に ACTION_OPEN_DOCUMENT を設定し、takePersistableUriPermission(android.net.Uri, int) を呼んでやる必要があるとのこと。

2014年6月29日
by kazoo
0 comments

[android]INSTALL_FAILED_UID_CHANGED

腹立たしいので久しぶりに書く。
NEXUS 5 にて、Eclipse からパッケージをインストールする際、唐突に以下のエラー:

[2014-06-27 17:52:18 – Xxxxxxx] Installation error: INSTALL_FAILED_UID_CHANGED
[2014-06-27 17:52:18 – Xxxxxxx] Please check logcat output for more details.
[2014-06-27 17:52:18 – Xxxxxxx] Launch canceled!

Continue Reading →