Google Play では、ユーザーがダウンロードする圧縮 APK を 100 MB 以下にする必要があります。ほとんどのアプリでは、これだけの容量があれば、アプリのすべてのコードとアセットを含めることができます。 しかし、高忠実度グラフィック、メディア ファイルや、その他の大容量アセット用にもっと多くの容量を必要とするアプリもあります。 これまでは、アプリの圧縮ダウンロード サイズが 100 MB を超えた場合には、各自で追加リソースをホストし、ユーザーがアプリを開いたときにそのリソースをダウンロードする必要がありました。追加ファイルのホストと配信はコストがかかる可能性があり、多くの場合、ユーザー エクスペリエンスも理想的とは言えません。Google Play では、この手続きをデベロッパーにとってはより簡単で、ユーザーにとってはより操作性に優れたものにするために、APK を補足する大容量の拡張ファイルを 2 つ添付することを認めています。
Google Play はアプリの拡張ファイルのホストとデバイスへの配信を無償で行っています。拡張ファイルはデバイスの共有ストレージ(「外部」ストレージとも呼ばれる、SD カードや着脱可能 USB パーティション)に保存され、アプリはそのストレージで該当ファイルにアクセスできます。ほとんどのデバイスで、Google Play は APK のダウンロードと同時に拡張ファイルをダウンロードするので、ユーザーがアプリを初めて開いた時点でアプリには必要なデータがすべて揃うことになります。ただし、アプリの起動時にアプリが Google Play からファイルをダウンロードしなければならないこともあります。
拡張ファイルは使用したくないものの、アプリの圧縮ダウンロード サイズが 100 MB を超える場合は、最大 200 MB の圧縮ダウンロード サイズを許容する Android App Bundle を使用してアプリをアップロードする必要があります。さらに、App Bundle を使用する場合、APK の生成と署名が Google Play で行われるため、ユーザーはアプリを実行するのに必要なコードとリソースのみを含む最適化された APK をダウンロードすることになります。デベロッパー側では複数の APK や拡張ファイルを生成、署名、管理する必要がなくなります。ユーザー側にとっては、最適化によりダウンロード サイズが小さくなります。
概要
Google Play Console を使用して APK をアップロードするたびに、1 つまたは 2 つの拡張ファイルを APK に追加できます。各ファイルの上限は 2 GB で、ファイル形式は自由ですが、ダウンロード時の帯域幅を抑えるために圧縮ファイルを使うことをおすすめします。概念的に、拡張ファイルの役割は次のようにそれぞれ異なります。
- メイン拡張ファイルはアプリで必要となる追加リソースの主要な拡張ファイルです。
- パッチ拡張ファイルは省略可能で、メイン拡張ファイルの小規模アップデート用です。
この 2 つの拡張ファイルの使い方は自由ですが、メイン拡張ファイルはメインアセットを提供して、ほとんど更新する必要がないようにし、パッチ拡張ファイルはメインより小さく、「パッチ提供用」として使用し、各メジャー リリースと一緒にまたは必要に応じて更新することをおすすめします。
ただし、アプリをアップデートする場合は、新しいパッチ拡張ファイルしか必要なくても、更新した versionCode
をマニフェストで指定して新しい APK もアップロードする必要があります(Play Console を使って既存の APK に拡張ファイルをアップロードすることはできません)。
注: パッチ拡張ファイルは意味的にはメイン拡張ファイルと同じで、各ファイルは自由に使うことができます。
ファイル名の形式
アップロードする拡張ファイルの形式は自由です(ZIP、PDF、MP4 など)。JOBB ツールを使うと、リソース ファイル セットやそのセットの後続のパッチをカプセル化して暗号化することもできます。ファイル形式にかかわらず、Google Play は拡張ファイルを OBB(Opaque Binary Blob)と見なし、次のスキームに沿ってファイル名を変更します。
[main|patch].<expansion-version>.<package-name>.obb
このスキームは次の 3 つのコンポーネントで構成されます。
main
またはpatch
- 拡張ファイルがメインかパッチかを指定します。APK ごとに 1 つのメインファイルと 1 つのパッチファイルのみを作成できます。
<expansion-version>
- 拡張ファイルが「最初に」関連付けられる APK のバージョン コードと一致する整数です(アプリの
android:versionCode
値と一致します)。「最初に」を強調しているのは、Play Console では更新した拡張ファイルを新しい APK で再利用できますが、拡張ファイルの名前は変わらないためです。つまり、ファイルを最初にアップロードしたときに適用されたバージョンが残ります。
<package-name>
- アプリの Java 形式のパッケージ名です。
たとえば、APK バージョンが 314159 で、パッケージ名が com.example.app の場合、メイン拡張ファイルをアップロードすると、ファイル名は次のように変更されます。
main.314159.com.example.app.obb
保存先
Google Play が拡張ファイルをデバイスにダウンロードすると、ファイルはシステムの共有ストレージに保存されます。正しい動作が確実に行われるように、拡張ファイルの削除、移動、名前変更は行わないでください。アプリ自体が Google Play からダウンロードしなければならない場合は、拡張ファイルもまったく同じ場所に保存してください。
getObbDir()
メソッドは、拡張ファイルの具体的な場所を次の形式で返します。
<shared-storage>/Android/obb/<package-name>/
<shared-storage>
はgetExternalStorageDirectory()
から利用可能な、共有ストレージ領域のパスです。<package-name>
はgetPackageName()
から利用可能な、アプリの Java 形式のパッケージ名です。
このディレクトリ内の拡張ファイルはアプリごとに最大 2 つです。
1 つはメイン拡張ファイルで、もう 1 つはパッチ拡張ファイル(省略可)です。新しい拡張ファイルでアプリを更新すると、前のバージョンは上書きされます。Android 4.4(API レベル 19)以降では、アプリは外部ストレージへのアクセス権限なしで OBB
拡張ファイルを読み取ることができます。ただし、Android 6.0(API レベル 23)以降の一部の実装ではこれまでどおり権限が必要なため、アプリ マニフェストで READ_EXTERNAL_STORAGE
権限を宣言して、次のように実行時に権限をリクエストする必要があります。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Android バージョン 6 以降では、外部ストレージへのアクセス権限を実行時にリクエストする必要があります。 ただし、Android の一部の実装では、OBB ファイルの読み取り権限は必要ありません。次のコード スニペットは、外部ストレージへのアクセス権限をリクエストする前に読み取りアクセス権を確認する方法を示しています。
Kotlin
val obb = File(obb_filename) var open_failed = false try { BufferedReader(FileReader(obb)).also { br -> ReadObbFile(br) } } catch (e: IOException) { open_failed = true } if (open_failed) { // request READ_EXTERNAL_STORAGE permission before reading OBB file ReadObbFileWithPermission() }
Java
File obb = new File(obb_filename); boolean open_failed = false; try { BufferedReader br = new BufferedReader(new FileReader(obb)); open_failed = false; ReadObbFile(br); } catch (IOException e) { open_failed = true; } if (open_failed) { // request READ_EXTERNAL_STORAGE permission before reading OBB file ReadObbFileWithPermission(); }
拡張ファイルを解凍する必要がある場合は、後で OBB
拡張ファイルを削除しないでください。また、同じディレクトリに解凍後のデータを保存しないでください。解凍後のファイルは getExternalFilesDir()
で指定されたディレクトリに保存してください。ただし、可能な限り、データを解凍するのではなく、ファイルから直接読み取れる拡張ファイル形式を使うことをおすすめします。たとえば、Google では ZIP ファイルからデータを直接読み取る APK Expansion Zip Library というライブラリ プロジェクトを提供しています。
注意: APK ファイルとは異なり、共有ストレージに保存されているファイルはすべて、ユーザーや他のアプリで読み取られる可能性があります。
おすすめの方法: メディア ファイルを ZIP にパッケージする場合は、ZIP を解凍せずにオフセットと長さを制御するメディア再生呼び出し(MediaPlayer.setDataSource()
、SoundPool.load()
など)をファイルで使用できます。この方法を利用するには、ZIP パッケージを作成する際にメディア ファイルを圧縮しないでください。たとえば、zip
ツールを使う場合は、次のように -n
オプションを使用して、圧縮対象外のファイルの拡張子を指定します。
zip -n .mp4;.ogg main_expansion media_files
ダウンロード処理
通常、Google Play はデバイスへの APK のダウンロードと同時に拡張ファイルをダウンロードして保存します。ただし、Google Play が拡張ファイルをダウンロードできない場合や、ユーザーが以前ダウンロードした拡張ファイルを削除してファイルがない場合もあります。こうした問題に対応するため、メイン アクティビティが開始したときにアプリが Google Play から提供された URL を使って自動的にファイルをダウンロードできるようにする必要があります。
ダウンロード処理の概要は次のようになります。
- ユーザーが Google Play からアプリをインストールすることを選択します。
- Google Play が拡張ファイルをダウンロードできる場合(ほとんどのデバイスはこれに該当します)、拡張ファイルは APK と一緒にダウンロードされます。
Google Play が拡張ファイルをダウンロードできない場合、APK のみがダウンロードされます。
- ユーザーがアプリを起動したときに、拡張ファイルがすでにデバイスに保存されているかどうかをアプリで確認する必要があります。
注意: アプリの起動時にファイルがデバイスにない場合に Google Play から拡張ファイルをダウンロードするために必要なコードを追加することが重要です。拡張ファイルのダウンロードに関する次のセクションで説明するように、Google ではこの処理を大幅に簡素化し、最小限のコードでサービスからダウンロードを実行できるライブラリを用意しました。
開発チェックリスト
アプリで拡張ファイルを使用するために必要な作業をまとめると、次のようになります。
- まず、アプリの圧縮ダウンロード サイズとして 100 MB を超える容量が必要かどうかを判断します。容量は貴重なので、ダウンロードの合計サイズをできる限り小さくする必要があります。複数の画面密度用に複数のバージョンのグラフィック アセットを提供することで 100 MB を超える場合は、代わりに複数の APK を公開し、対象とする画面に必要なアセットのみを各 APK に含めることを検討してください。最良の結果を得るには、Google Play に公開する際に Android App Bundle をアップロードします。アプリのコンパイル済みコードとリソースはすべて Android App Bundle に含まれますが、APK の生成と署名は Google Play ストアで行います。
- APK から切り離すアプリリソースを特定し、メイン拡張ファイルとして使用するファイルにそのリソースをパッケージします。
通常、メイン拡張ファイルを更新する場合は 2 つ目のパッチ拡張ファイルのみを使用してください。ただし、リソースがメイン拡張ファイルの上限の 2 GB を超える場合は、残りのアセットにパッチファイルを使用できます。
- デバイスの共有保存先にある拡張ファイルからリソースを使うようにアプリを開発します。
拡張ファイルの削除、移動、名前変更は行わないでください。
アプリの形式に特にこだわらない場合は、拡張ファイル用の ZIP ファイルを作成し、APK Expansion Zip Library を使って読み取ることをおすすめします。
- アプリのメイン アクティビティに、起動時に拡張ファイルがデバイスにあるかどうかを確認するロジックを追加します。ファイルがデバイスにない場合は、Google Play のアプリ ライセンス サービスを使って拡張ファイルの URL をリクエストし、拡張ファイルをダウンロードして保存します。
作成する必要があるコードを大幅に削減し、ダウンロード時のユーザー エクスペリエンスを改善するために、Downloader Library を使ってダウンロード動作を実装することをおすすめします。
このライブラリを使わず、自分でダウンロード サービスを作成する場合は、拡張ファイルの名前を変更しないよう注意し、拡張ファイルの保存先を正確に指定してください。
アプリを開発し終えたら、ガイドに沿って拡張ファイルをテストします。
ルールと制限
APK 拡張ファイルの追加は、Play Console を使ってアプリをアップロードする場合に利用できる機能です。アプリを初めてアップロードする場合や、拡張ファイルを使うアプリを更新する場合は、次のルールと制限に注意してください。
- 各拡張ファイルのサイズの上限は 2 GB とします。
- Google Play から拡張ファイルをダウンロードするためには、アプリの入手先が Google Play でなければなりません。他の方法でアプリをインストールした場合、Google Play から拡張ファイルの URL は提供されません。
- アプリ内からダウンロードを実行する場合、Google Play が各ファイルに提供する URL はダウンロードごとに割り振られるため、有効期間が短く、アプリに提供された後すぐに切れます。
- 新しい APK でアプリを更新する場合や、同じアプリの複数の APK をアップロードする場合は、古い APK 用のアップロード済みの拡張ファイルを選択できます。拡張ファイルの名前は変わらず、ファイルが最初に関連付けられた APK に割り振られたバージョンが残ります。
- デバイスごとに異なる拡張ファイルを提供するために拡張ファイルを複数の APK と組み合わせて使用する場合も、
versionCode
値を一意にするためにデバイスごとに別々の APK をアップロードし、APK ごとに異なるフィルタを宣言する必要があります。 - 拡張ファイルのみを変更してもアプリのアップデートを発行することはできません。アプリを更新するには新しい APK をアップロードする必要があります。拡張ファイル内のアセットのみに関係する変更の場合は、
versionCode
(および必要に応じてversionName
)を変更するだけで APK を更新できます。 obb/
ディレクトリには他のデータを保存しないでください。一部のデータを解凍する必要がある場合は、getExternalFilesDir()
で指定された場所に保存してください。- (アップデートを実行する場合を除き)
.obb
拡張ファイルを削除したり名前を変更したりしないでください。削除したり名前を変更したりすると、Google Play(またはアプリ自体)が拡張ファイルを繰り返しダウンロードします。 - 拡張ファイルを手動で更新する場合は、古い拡張ファイルを削除する必要があります。
拡張ファイルのダウンロード
ほとんどの場合、Google Play は APK のインストールや更新と同時に拡張ファイルをダウンロードしてデバイスに保存します。したがって、アプリを初めて起動したときでも拡張ファイルは利用可能です。ただし、アプリ自体が拡張ファイルをダウンロードしなければならないこともあります。この場合は、Google Play のアプリ ライセンス サービスからの応答で提供される URL から拡張ファイルをリクエストします。
拡張ファイルのダウンロードに必要な基本的なロジックは次のとおりです。
- アプリが起動したら、共有保存先(
Android/obb/<package-name>/
ディレクトリ内)で拡張ファイルを探します。- 拡張ファイルがあれば、これで完了です。アプリは次の操作に進むことができます。
- 拡張ファイルがない場合は、次のようにします。
アプリが無料(有料アプリ以外)の場合は、アプリ ライセンス サービスを使用していない可能性があります。このサービスの主な目的は、アプリのライセンス ポリシーを適用し、ユーザーにアプリを使用する権限を付与することです(ユーザーは Google Play でアプリの正規料金を支払い済みです)。拡張ファイルの機能を推進するために、ライセンス サービスが強化され、アプリへの応答に、Google Play でホストされているアプリの拡張ファイルの URL が追加されるようになりました。そのため、アプリが無料であっても、APK 拡張ファイルを使用するには License Verification Library(LVL)を組み込む必要があります。もちろん、アプリが無料の場合は、ライセンス検証を実施する必要はありません。単に拡張ファイルの URL を返すリクエストを実行するライブラリが必要になっただけです。
注: アプリの無料、有料にかかわらず、Google Play が拡張ファイルの URL を提供するのは、アプリの入手先が Google Play の場合に限ります。
LVL の他に、HTTP 経由で拡張ファイルをダウンロードしてデバイスの共有ストレージ上の正しい場所に保存するコードも必要です。この手続きをアプリに組み込む場合は、次の問題を考慮してください。
- デバイスに拡張ファイル分の容量が不足している可能性があるため、ダウンロードの開始前に確認し、容量が不足している場合はユーザーに警告メッセージを発行してください。
- ファイルのダウンロードはバックグラウンド サービスで行ってください。ユーザーの操作を妨げず、ダウンロードを実行している間、ユーザーがアプリ以外の操作を実行できるようにする必要があります。
- リクエストやダウンロードの際にさまざまなエラーが発生する可能性があります。こうしたエラーには適切に対応してください。
- ダウンロード中にネットワーク接続が変更されることがあります。このような変更に対処し、ダウンロードが中断された場合は、可能な限り再開してください。
- バックグラウンドでダウンロードしている間、ダウンロードの進行状況を示す通知を提供し、ダウンロードが完了したらユーザーに知らせ、選択に応じてアプリが再開されるようにしてください。
この作業を簡素化するため、Google では Downloader Library を作成しました。これを使えば、ライセンス サービスを介して拡張ファイルの URL をリクエストし、拡張ファイルをダウンロードして、上記の作業をすべて実行できるだけでなく、アクティビティでダウンロードを一時停止したり再開したりすることもできます。Downloader Library とわずかなコードフックをアプリに追加するだけで、拡張ファイルのダウンロードに関するほぼすべての作業が自動的にコード化されます。このように、デベロッパーの負担を最小限に抑えながら最高のユーザー エクスペリエンスを実現するために、Google では Downloader Library を使って拡張ファイルをダウンロードすることをおすすめしています。次に、このライブラリをアプリに統合する方法について説明します。
Google Play から提供される URL を使って拡張ファイルをダウンロードする独自のソリューションを作成したい場合は、アプリ ライセンスの説明に沿ってライセンス リクエストを実行し、拡張ファイルの名前、サイズ、URL を応答エクストラから取得する必要があります。APKExpansionPolicy
クラス(LVK 内)をライセンス ポリシーとして使用してライセンス サービスから拡張ファイルの名前、サイズ、URL を取得してください。
Downloader Library について
アプリと一緒に APK 拡張ファイルを使う場合にデベロッパーの負担を最小限に抑えながら最高のユーザー エクスペリエンスを実現するには、Google Play APK Expansion Library パッケージにある Downloader Library を使うことをおすすめします。このライブラリはバックグラウンド サービスで拡張ファイルをダウンロードし、ダウンロード状況を示すユーザー通知を表示します。また、ネットワーク接続が切断された場合は、その問題に対処し、可能な限りダウンロードを再開します。
Downloader Library を使って拡張ファイルのダウンロードを実装する手順は次のとおりです。
- 専用の
Service
サブクラスとBroadcastReceiver
サブクラスを拡張します。各サブクラスに数行のコードを追加する必要があります。 - 拡張ファイルがダウンロード済みかどうかを確認し、ダウンロードされていない場合はダウンロード処理を呼び出して進行状況の UI を表示するロジックをメイン アクティビティに追加します。
- ダウンロードの進行状況に関する更新情報を受け取るコールバック インターフェースといくつかのメソッドをメイン アクティビティに実装します。
次に、Downloader Library を使ってアプリをセットアップする方法について説明します。
Downloader Library を使用する前に
Downloader Library を使用するには、SDK Manager から 2 つのパッケージをダウンロードして適切なライブラリをアプリに追加する必要があります。
まず、Android SDK Manager を開き([Tools] > [SDK Manager])、[Appearance & Behavior] > [System Settings] > [Android SDK] で [SDK Tools] タブを選択し、以下を選択してダウンロードします。
- Google Play Licensing Library パッケージ
- Google Play APK Expansion Library パッケージ
License Verification Library と Downloader Library の新しいライブラリ モジュールを作成します。各ライブラリについて:
- [File] > [New] > [New Module] を選択します。
- [Create New Module] ウィンドウで、[Android Library]、[Next] の順に選択します。
- [app/Library name] を指定し(「Google Play License Library」、「Google Play Downloader Library」など)、[Minimum SDK level]、[Finish] の順に選択します。
- [File] > [Project Structure] を選択します。
- [Properties] タブを選択し、[Library Repository] で
<sdk>/extras/google/
ディレクトリ(License Verification Library の場合はplay_licensing/
、Downloader Library の場合はplay_apk_expansion/downloader_library/
)からライブラリを入力します。 - [OK] を選択して新しいモジュールを作成します。
注: Downloader Library は License Verification Library に応じて異なります。License Verification Library を Downloader Library のプロジェクト プロパティに追加します。
または、コマンドラインから、ライブラリを追加するプロジェクトを更新します。
<sdk>/tools/
ディレクトリに移動します。android update project
を実行します。その際、--library
オプションを指定して LVL と Downloader Library の両方をプロジェクトに追加します。次に例を示します。android update project --path ~/Android/MyApp \ --library ~/android_sdk/extras/google/market_licensing \ --library ~/android_sdk/extras/google/market_apk_expansion/downloader_library
LVL と Downloader Library の両方をアプリに追加すれば、Google Play から拡張ファイルをダウンロードする機能を簡単に統合できます。拡張ファイルに選択する形式と共有ストレージから拡張ファイルを読み取る方法は別個の実装であり、アプリのニーズに応じて検討してください。
ヒント: APK Expansion パッケージに、アプリで Downloader Library を使用する方法を示すサンプルアプリが含まれています。このサンプルでは APK Expansion パッケージ内の APK Expansion Zip Library という 3 つ目のライブラリを使用しています。拡張ファイルに ZIP ファイルを使う場合は、APK Expansion Zip Library もアプリに追加することをおすすめします。詳しくは、APK Expansion Zip Library を使用するをご覧ください。
ユーザー権限を宣言する
拡張ファイルをダウンロードするには、Downloader Library にいくつかの権限が必要です。この権限はアプリのマニフェスト ファイルで宣言します。必要な権限は次のとおりです。
<manifest ...> <!-- Required to access Google Play Licensing --> <uses-permission android:name="com.android.vending.CHECK_LICENSE" /> <!-- Required to download files from Google Play --> <uses-permission android:name="android.permission.INTERNET" /> <!-- Required to keep CPU alive while downloading files (NOT to keep screen awake) --> <uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- Required to poll the state of the network connection and respond to changes --> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- Required to check whether Wi-Fi is enabled --> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <!-- Required to read and write the expansion files on shared storage --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ... </manifest>
注: デフォルトでは、Downloader Library には API レベル 4 が必要ですが、APK Expansion Zip Library には API レベル 5 が必要です。
ダウンローダ サービスを実装する
バックグラウンドでダウンロードを実行するために、Downloader Library は DownloaderService
という独自の Service
サブクラスを提供するので、このサブクラスを拡張する必要があります。DownloaderService
は拡張ファイルをダウンロードするだけでなく、次の処理も行います。
- デバイスのネットワーク接続(
CONNECTIVITY_ACTION
ブロードキャスト)の変化をリッスンするBroadcastReceiver
を登録し、必要に応じて(接続が切断された場合など)ダウンロードを一時停止し、ダウンロード可能になったら(接続が確立されたら)ダウンロードを再開できるようにします。 - サービスが強制終了された場合にダウンロードを再試行するように
RTC_WAKEUP
アラームのスケジュールを設定します。 - ダウンロードの進行状況とエラーや状態の変化を表示するカスタムの
Notification
を作成します。 - アプリが手動でダウンロードを一時停止したり再開したりすることを許可します。
- 共有ストレージがマウントされ利用可能であること、ファイルがまだないこと、十分な容量があることをすべて、拡張ファイルのダウンロード前に確認します。このうちのいずれかが当てはまらない場合はユーザーに通知します。
必要な作業は、アプリに DownloaderService
クラスの拡張クラスを作成し、具体的なアプリの情報を提供するように次の 3 つのメソッドをオーバーライドすることだけです。
getPublicKey()
- パブリッシャー アカウントの Base64 エンコード RSA 公開鍵である文字列を返す必要があります。この文字列は Play Console のプロフィール ページで入手できます(ライセンスの設定についての記事をご覧ください)。
getSALT()
- ライセンスの
Policy
がObfuscator
を作成する際に使うランダムなバイトの配列を返す必要があります。このソルトにより、ライセンス データが保存されている難読化されたSharedPreferences
ファイルが一意になり、検出不能になります。 getAlarmReceiverClassName()
- ダウンロードを再開する必要があることを示すアラームを受信するアプリの
BroadcastReceiver
のクラス名を返す必要があります(これはダウンローダ サービスが予期せず停止した場合に発生することがあります)。
たとえば、DownloaderService
の完全な実装は次のようになります。
Kotlin
// You must use the public key belonging to your publisher account const val BASE64_PUBLIC_KEY = "YourLVLKey" // You should also modify this salt val SALT = byteArrayOf( 1, 42, -12, -1, 54, 98, -100, -12, 43, 2, -8, -4, 9, 5, -106, -107, -33, 45, -1, 84 ) class SampleDownloaderService : DownloaderService() { override fun getPublicKey(): String = BASE64_PUBLIC_KEY override fun getSALT(): ByteArray = SALT override fun getAlarmReceiverClassName(): String = SampleAlarmReceiver::class.java.name }
Java
public class SampleDownloaderService extends DownloaderService { // You must use the public key belonging to your publisher account public static final String BASE64_PUBLIC_KEY = "YourLVLKey"; // You should also modify this salt public static final byte[] SALT = new byte[] { 1, 42, -12, -1, 54, 98, -100, -12, 43, 2, -8, -4, 9, 5, -106, -107, -33, 45, -1, 84 }; @Override public String getPublicKey() { return BASE64_PUBLIC_KEY; } @Override public byte[] getSALT() { return SALT; } @Override public String getAlarmReceiverClassName() { return SampleAlarmReceiver.class.getName(); } }
注意: BASE64_PUBLIC_KEY
値を、パブリッシャー アカウントに属する公開鍵になるように更新する必要があります。この公開鍵は、Play Console のプロフィール情報の下にあります。ダウンロードをテストする場合もこの公開鍵が必要になります。
マニフェスト ファイルでサービスを宣言します。
<app ...> <service android:name=".SampleDownloaderService" /> ... </app>
アラーム レシーバを実装する
ファイルのダウンロードの進行状況を監視し、必要に応じてダウンロードを再開するために、DownloaderService
は Intent
をアプリの BroadcastReceiver
に送信する RTC_WAKEUP
アラームのスケジュールを設定します。ダウンロードの状況を確認し、必要に応じてダウンロードを再開する API を Downloader Library から呼び出すように BroadcastReceiver
を定義する必要があります。
必要な作業は、DownloaderClientMarshaller.startDownloadServiceIfRequired()
を呼び出すように onReceive()
メソッドをオーバーライドすることだけです。
例を次に示します。
Kotlin
class SampleAlarmReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { try { DownloaderClientMarshaller.startDownloadServiceIfRequired( context, intent, SampleDownloaderService::class.java ) } catch (e: PackageManager.NameNotFoundException) { e.printStackTrace() } } }
Java
public class SampleAlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { try { DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent, SampleDownloaderService.class); } catch (NameNotFoundException e) { e.printStackTrace(); } } }
このクラスの名前をサービスの getAlarmReceiverClassName()
メソッドで返す必要があります(前のセクションをご覧ください)。
レシーバはマニフェスト ファイルで宣言します。
<app ...> <receiver android:name=".SampleAlarmReceiver" /> ... </app>
ダウンロードを開始する
アプリのメイン アクティビティ(ランチャー アイコンで開始されたアクティビティ)は、拡張ファイルがデバイスにあるかどうかを確認し、拡張ファイルがなければダウンロードを開始します。
Downloader Library を使ってダウンロードを開始する手順は次のとおりです。
- ファイルがダウンロード済みかどうかを確認します。
Downloader Library の
Helper
クラスにこの処理に役立つ API が含まれています。getExpansionAPKFileName(Context, c, boolean mainFile, int versionCode)
doesFileExist(Context c, String fileName, long fileSize)
たとえば、APK Expansion パッケージで提供されるサンプルアプリは、アクティビティの
onCreate()
メソッドで次のメソッドを呼び出して、拡張ファイルがデバイスにあるかどうかを確認します。Kotlin
fun expansionFilesDelivered(): Boolean { xAPKS.forEach { xf -> Helpers.getExpansionAPKFileName(this, xf.isBase, xf.fileVersion).also { fileName -> if (!Helpers.doesFileExist(this, fileName, xf.fileSize, false)) return false } } return true }
Java
boolean expansionFilesDelivered() { for (XAPKFile xf : xAPKS) { String fileName = Helpers.getExpansionAPKFileName(this, xf.isBase, xf.fileVersion); if (!Helpers.doesFileExist(this, fileName, xf.fileSize, false)) return false; } return true; }
この場合は、
XAPKFile
オブジェクトが、既知の拡張ファイルのバージョン番号、ファイルサイズと、その拡張ファイルがメインかどうかに関するブール値を保持します(詳しくは、サンプルアプリのSampleDownloaderActivity
クラスをご覧ください)。このメソッドが false を返した場合、アプリはダウンロードを開始する必要があります。
- 静的メソッド
DownloaderClientMarshaller.startDownloadServiceIfRequired(Context c, PendingIntent notificationClient, Class<?> serviceClass)
を呼び出してダウンロードを開始します。このメソッドが取るパラメータは次のとおりです。
context
: アプリのContext
。notificationClient
: メイン アクティビティを開始するためのPendingIntent
。これは、DownloaderService
がダウンロードの進行状況を示すために作成するNotification
で使用されます。ユーザーが通知を選択すると、ここで指定したPendingIntent
が呼び出され、ダウンロードの進行状況を示すアクティビティが開始されます(通常はダウンロードを開始したアクティビティと同じです)。serviceClass
:DownloaderService
の実装用のClass
オブジェクト。必要に応じてサービスを起動しダウンロードを開始するために必要です。
このメソッドはダウンロードが必要かどうかを示す整数を返します。値は次のいずれかです。
NO_DOWNLOAD_REQUIRED
: ファイルがすでにある、またはダウンロードがすでに実行中の場合に返されます。LVL_CHECK_REQUIRED
: 拡張ファイルの URL を取得するためにライセンス検証が必要な場合に返されます。DOWNLOAD_REQUIRED
: 拡張ファイルの URL がわかっているのにそのファイルがダウンロードされていない場合に返されます。
LVL_CHECK_REQUIRED
とDOWNLOAD_REQUIRED
の動作は本質的に同じで、通常はこの値を気にする必要はありません。startDownloadServiceIfRequired()
を呼び出すメイン アクティビティで、応答がNO_DOWNLOAD_REQUIRED
かどうかを簡単に確認できます。応答がNO_DOWNLOAD_REQUIRED
以外の場合、Downloader Library はダウンロードを開始するので、アクティビティの UI を更新してダウンロードの進行状況を表示する必要があります(次の手順をご覧ください)。応答がNO_DOWNLOAD_REQUIRED
の場合は、ファイルが利用可能なのでアプリを起動できます。例を次に示します。
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Check if expansion files are available before going any further if (!expansionFilesDelivered()) { val pendingIntent = // Build an Intent to start this activity from the Notification Intent(this, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP }.let { notifierIntent -> PendingIntent.getActivity( this, 0, notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT ) } // Start the download service (if required) val startResult: Int = DownloaderClientMarshaller.startDownloadServiceIfRequired( this, pendingIntent, SampleDownloaderService::class.java ) // If download has started, initialize this activity to show // download progress if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { // This is where you do set up to display the download // progress (next step) ... return } // If the download wasn't necessary, fall through to start the app } startApp() // Expansion files are available, start the app }
Java
@Override public void onCreate(Bundle savedInstanceState) { // Check if expansion files are available before going any further if (!expansionFilesDelivered()) { // Build an Intent to start this activity from the Notification Intent notifierIntent = new Intent(this, MainActivity.getClass()); notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); ... PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT); // Start the download service (if required) int startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(this, pendingIntent, SampleDownloaderService.class); // If download has started, initialize this activity to show // download progress if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { // This is where you do set up to display the download // progress (next step) ... return; } // If the download wasn't necessary, fall through to start the app } startApp(); // Expansion files are available, start the app }
startDownloadServiceIfRequired()
メソッドがNO_DOWNLOAD_REQUIRED
以外を返した場合は、DownloaderClientMarshaller.CreateStub(IDownloaderClient client, Class<?> downloaderService)
を呼び出してIStub
のインスタンスを作成します。IStub
はアクティビティをダウンローダ サービスにバインドし、アクティビティがダウンロードの進行状況に関するコールバックを受け取るようにします。CreateStub()
を呼び出してIStub
のインスタンスを作成するには、そのメソッドにIDownloaderClient
インターフェースの実装とDownloaderService
の実装を渡す必要があります。IDownloaderClient
インターフェースについては、次のダウンロードの進行状況を受け取るで説明します。通常、このインターフェースはActivity
クラスに実装し、ダウンロードの状態が変化したときにアクティビティの UI を更新できるようにします。startDownloadServiceIfRequired()
がダウンロードを開始したら、アクティビティのonCreate()
メソッドの実行中にCreateStub()
を呼び出してIStub
のインスタンスを作成することをおすすめします。たとえば、
onCreate()
の前述のコード例では、startDownloadServiceIfRequired()
の結果に次のように応答できます。Kotlin
// Start the download service (if required) val startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired( this@MainActivity, pendingIntent, SampleDownloaderService::class.java ) // If download has started, initialize activity to show progress if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { // Instantiate a member instance of IStub downloaderClientStub = DownloaderClientMarshaller.CreateStub(this, SampleDownloaderService::class.java) // Inflate layout that shows download progress setContentView(R.layout.downloader_ui) return }
Java
// Start the download service (if required) int startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(this, pendingIntent, SampleDownloaderService.class); // If download has started, initialize activity to show progress if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { // Instantiate a member instance of IStub downloaderClientStub = DownloaderClientMarshaller.CreateStub(this, SampleDownloaderService.class); // Inflate layout that shows download progress setContentView(R.layout.downloader_ui); return; }
onCreate()
メソッドが戻ると、アクティビティはonResume()
呼び出しを受け取ります。ここで、IStub
のconnect()
を呼び出してアプリのContext
を渡す必要があります。反対に、アクティビティのonStop()
コールバックではdisconnect()
を呼び出します。Kotlin
override fun onResume() { downloaderClientStub?.connect(this) super.onResume() } override fun onStop() { downloaderClientStub?.disconnect(this) super.onStop() }
Java
@Override protected void onResume() { if (null != downloaderClientStub) { downloaderClientStub.connect(this); } super.onResume(); } @Override protected void onStop() { if (null != downloaderClientStub) { downloaderClientStub.disconnect(this); } super.onStop(); }
IStub
でconnect()
を呼び出すと、アクティビティがDownloaderService
にバインドされて、ダウンロード状態の変化に関するコールバックをIDownloaderClient
インターフェースから受け取るようになります。
ダウンロードの進行状況を受け取る
ダウンロードの進行状況に関する最新情報を受け取ったり DownloaderService
を操作したりするには、Downloader Library の IDownloaderClient
インターフェースを実装する必要があります。
通常、ダウンロードを開始するために使うアクティビティでこのインターフェースを実装し、ダウンロードの進行状況を表示したり、リクエストをサービスに送信したりできるようにします。
IDownloaderClient
に必要なインターフェース メソッドは次のとおりです。
onServiceConnected(Messenger m)
- アクティビティで
IStub
のインスタンスを作成すると、このメソッド呼び出しを受け取ります。このメソッドがDownloaderService
のインスタンスに接続するMessenger
オブジェクトを渡します。ダウンロードの一時停止や再開など、サービスにリクエストを送信するには、DownloaderServiceMarshaller.CreateProxy()
を呼び出して、サービスに接続されたIDownloaderService
インターフェースを受け取る必要があります。おすすめの実装は次のとおりです。
Kotlin
private var remoteService: IDownloaderService? = null ... override fun onServiceConnected(m: Messenger) { remoteService = DownloaderServiceMarshaller.CreateProxy(m).apply { downloaderClientStub?.messenger?.also { messenger -> onClientUpdated(messenger) } } }
Java
private IDownloaderService remoteService; ... @Override public void onServiceConnected(Messenger m) { remoteService = DownloaderServiceMarshaller.CreateProxy(m); remoteService.onClientUpdated(downloaderClientStub.getMessenger()); }
IDownloaderService
オブジェクトが初期化されたら、ダウンローダ サービスにダウンロードの一時停止、再開(requestPauseDownload()
、requestContinueDownload()
)などのコマンドを送信できます。 onDownloadStateChanged(int newState)
- ダウンロードの開始や完了など、ダウンロードの状態が変化するとダウンロード サービスがこのコマンドを呼び出します。
newState
にはIDownloaderClient
クラスのSTATE_*
定数のいずれかで指定される有効な値のいずれかが割り当てられます。ユーザーにとって有用なメッセージを提供するために、
Helpers.getDownloaderStringResourceIDFromState()
を呼び出すことで各状態に対応する文字列をリクエストできます。このメソッドは、Downloader Library にバンドルされている文字列のいずれかに対応するリソース ID を返します。たとえば、文字列「ローミングしているためダウンロードが一時停止されました」はSTATE_PAUSED_ROAMING
に対応します。 onDownloadProgress(DownloadProgressInfo progress)
- ダウンロード サービスが
DownloadProgressInfo
オブジェクトを提供する場合に呼び出します。このオブジェクトは、ダウンロードの残り時間(推定)、現在の速度、全体的な進行状況、合計など、ダウンロードの進行状況に関する各種情報を表すため、ダウンロードの進行状況の UI を更新できます。
おすすめの方法: ダウンロードの進行状況の UI を更新するコールバックの例については、APK Expansion パッケージ内のサンプルアプリにある SampleDownloaderActivity
をご覧ください。
IDownloaderService
インターフェースの次の公開メソッドは便利なメソッドです。
requestPauseDownload()
- ダウンロードを一時停止します。
requestContinueDownload()
- 一時停止したダウンロードを再開します。
setDownloadFlags(int flags)
- ユーザー設定で、ファイルのダウンロードを許可するネットワーク タイプを指定します。現在の実装でサポートされているフラグは
FLAGS_DOWNLOAD_OVER_CELLULAR
の 1 つですが、他のフラグを追加することもできます。デフォルトではこのフラグは無効であるため、ユーザーは Wi-Fi で拡張ファイルをダウンロードする必要があります。モバイル ネットワーク経由でダウンロードできるようにユーザー設定を指定することもできます。その場合は、次のメソッドを呼び出します。Kotlin
remoteService = DownloaderServiceMarshaller.CreateProxy(m).apply { ... setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR) }
Java
remoteService .setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR);
APKExpansionPolicy の使用
Google Play の Downloader Library を使う代わりに独自のダウンローダ サービスを作成する場合も、License Verification Library に用意されている APKExpansionPolicy
を使用する必要があります。APKExpansionPolicy
クラスは ServerManagedPolicy
(Google Play の License Verification Library にあります)とほぼ同じですが、APK 拡張ファイルの応答エクストラに対する追加処理が含まれています。
注: 前述のように Downloader Library を使用すれば、ライブラリが APKExpansionPolicy
の操作をすべて行ってくれるので、このクラスを直接使用する必要はありません。
このクラスには、利用可能な拡張ファイルに関する必要な情報の入手に役立つ次のメソッドが含まれています。
getExpansionURLCount()
getExpansionURL(int index)
getExpansionFileName(int index)
getExpansionFileSize(int index)
Downloader Library を使わない場合の APKExpansionPolicy
の使用法について詳しくは、アプリへのライセンスの追加に関するドキュメントをご覧ください。このようなライセンス ポリシーの実装方法が説明されています。
拡張ファイルの読み取り
APK 拡張ファイルをデバイスに保存した後のファイルの読み取り方法は、使用しているファイルの形式によって異なります。概要で説明したとおり、拡張ファイルの形式は自由ですが、名前は特定のファイル名の形式に沿って変更され、ファイルは <shared-storage>/Android/obb/<package-name>/
に保存されます。
ファイルの読み取り方法にかかわらず、まず外部ストレージを読み取りに使用できることを確認する必要があります。ユーザーは USB 経由でストレージをパソコンにマウントしていたり、実際には SD カードを取り外していたりする可能性があります。
注: アプリが起動したら、getExternalStorageState()
を呼び出して、外部ストレージが利用でき読み取り可能かどうかを必ず確認してください。外部ストレージの状態を表す有効な文字列のいずれかが返されます。外部ストレージがアプリで読み取り可能であるためには、戻り値が MEDIA_MOUNTED
である必要があります。
ファイル名を取得する
概要で説明したとおり、APK 拡張ファイルを保存する際は特定のファイル名の形式が使われます。
[main|patch].<expansion-version>.<package-name>.obb
拡張ファイルの保存先と名前を取得するには、getExternalStorageDirectory()
メソッドと getPackageName()
メソッドを使ってファイルのパスを作成する必要があります。
アプリで 2 つの拡張ファイルの絶対パスを含む配列を取得するために使用できるメソッドは次のとおりです。
Kotlin
fun getAPKExpansionFiles(ctx: Context, mainVersion: Int, patchVersion: Int): Array<String> { val packageName = ctx.packageName val ret = mutableListOf<String>() if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { // Build the full path to the app's expansion files val root = Environment.getExternalStorageDirectory() val expPath = File(root.toString() + EXP_PATH + packageName) // Check that expansion file path exists if (expPath.exists()) { if (mainVersion > 0) { val strMainPath = "$expPath${File.separator}main.$mainVersion.$packageName.obb" val main = File(strMainPath) if (main.isFile) { ret += strMainPath } } if (patchVersion > 0) { val strPatchPath = "$expPath${File.separator}patch.$mainVersion.$packageName.obb" val main = File(strPatchPath) if (main.isFile) { ret += strPatchPath } } } } return ret.toTypedArray() }
Java
// The shared path to all app expansion files private final static String EXP_PATH = "/Android/obb/"; static String[] getAPKExpansionFiles(Context ctx, int mainVersion, int patchVersion) { String packageName = ctx.getPackageName(); Vector<String> ret = new Vector<String>(); if (Environment.getExternalStorageState() .equals(Environment.MEDIA_MOUNTED)) { // Build the full path to the app's expansion files File root = Environment.getExternalStorageDirectory(); File expPath = new File(root.toString() + EXP_PATH + packageName); // Check that expansion file path exists if (expPath.exists()) { if ( mainVersion > 0 ) { String strMainPath = expPath + File.separator + "main." + mainVersion + "." + packageName + ".obb"; File main = new File(strMainPath); if ( main.isFile() ) { ret.add(strMainPath); } } if ( patchVersion > 0 ) { String strPatchPath = expPath + File.separator + "patch." + mainVersion + "." + packageName + ".obb"; File main = new File(strPatchPath); if ( main.isFile() ) { ret.add(strPatchPath); } } } } String[] retArray = new String[ret.size()]; ret.toArray(retArray); return retArray; }
このメソッドを呼び出すには、アプリの Context
と必要な拡張ファイルのバージョンを渡します。
拡張ファイルのバージョン番号はさまざまな方法で指定できます。簡単な方法の 1 つとして、ダウンロードの開始時に SharedPreferences
ファイルにバージョンを保存する方法があります。この場合、APKExpansionPolicy
クラスの getExpansionFileName(int index)
メソッドを使って拡張ファイルの名前を照会します。拡張ファイルにアクセスする必要がある場合は、SharedPreferences
ファイルを読み取ることでバージョン コードを取得できます。
共有ストレージからの読み取りについて詳しくは、データ ストレージについての説明をご覧ください。
APK Expansion Zip Library を使用する
Google Market APK Expansion パッケージには、APK Expansion Zip Library というライブラリが含まれています(<sdk>/extras/google/google_market_apk_expansion/zip_file/
にあります)。これはオプションのライブラリで、拡張ファイルが ZIP ファイルとして保存されている場合の読み取りに利用できます。このライブラリを使えば、仮想ファイル システムとして ZIP 拡張ファイルからリソースを簡単に読み取れます。
APK Expansion Zip Library に含まれているクラスと API は次のとおりです。
APKExpansionSupport
- 拡張ファイル名と ZIP ファイルにアクセスするメソッドが含まれています。
getAPKExpansionFiles()
- 前述と同じメソッドで、2 つの拡張ファイルの絶対ファイルパスを返します。
getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion)
- メインファイルとパッチファイルの合計を表す
ZipResourceFile
を返します。つまり、mainVersion
とpatchVersion
の両方を指定すると、パッチファイルのデータがメインファイルの上にマージされて、すべてのデータの読み取りアクセス権を提供するZipResourceFile
が返されます。
ZipResourceFile
- 共有ストレージにある ZIP ファイルを表し、その ZIP ファイルに基づいて仮想ファイル システムを提供するためのすべての処理を実行します。インスタンスを取得するには、
APKExpansionSupport.getAPKExpansionZipFile()
を使用するか、ZipResourceFile
に拡張ファイルのパスを渡します。このクラスにはさまざまな役立つメソッドが含まれていますが、通常、そのほとんどにアクセスする必要はありません。重要なメソッドは次の 2 つです。getInputStream(String assetPath)
- ZIP ファイル内のファイルを読み取るための
InputStream
を提供します。assetPath
には、希望するファイルの相対パスを指定します(ZIP ファイルの内容のルートを基準とします)。 getAssetFileDescriptor(String assetPath)
- ZIP ファイル内のファイルの
AssetFileDescriptor
を提供します。assetPath
には、希望するファイルの相対パスを指定します(ZIP ファイルの内容のルートを基準とします)。このメソッドはAssetFileDescriptor
を必要とする一部の Android API(MediaPlayer
API など)に役立ちます。
APEZProvider
- ほとんどのアプリではこのクラスを使用する必要がありません。このクラスで定義する
ContentProvider
は、メディア ファイルへのUri
アクセス権を必要とする一部の Android API にファイル アクセス権を付与するためにコンテンツ プロバイダのUri
で ZIP ファイルのデータをマーシャリングします。たとえば、VideoView.setVideoURI()
で動画を再生する場合に役立ちます。
メディア ファイルの ZIP 圧縮をスキップする
拡張ファイルを使ってメディア ファイルを保存する場合は、オフセットや長さを制御する Android メディア再生呼び出し(MediaPlayer.setDataSource()
、SoundPool.load()
など)を ZIP ファイルで利用できます。この方法を利用するためには、ZIP パッケージを作成する際にメディア ファイルを圧縮しないでください。たとえば、zip
ツールを使う場合は、次のように -n
オプションを使用して、圧縮対象外のファイルの拡張子を指定します。
zip -n .mp4;.ogg main_expansion media_files
ZIP ファイルから読み取る
APK Expansion Zip Library を使用しているときに ZIP からファイルを読み取るには、通常、次のようなコードが必要になります。
Kotlin
// Get a ZipResourceFile representing a merger of both the main and patch files val expansionFile = APKExpansionSupport.getAPKExpansionZipFile(appContext, mainVersion, patchVersion) // Get an input stream for a known file inside the expansion file ZIPs expansionFile.getInputStream(pathToFileInsideZip).use { ... }
Java
// Get a ZipResourceFile representing a merger of both the main and patch files ZipResourceFile expansionFile = APKExpansionSupport.getAPKExpansionZipFile(appContext, mainVersion, patchVersion); // Get an input stream for a known file inside the expansion file ZIPs InputStream fileStream = expansionFile.getInputStream(pathToFileInsideZip);
上記のコードを実行すると、メイン拡張ファイルとパッチ拡張ファイルにあるすべてのファイルをマージしたマップから読み取ることで、その 2 つのファイルのいずれかにある任意のファイルのアクセス権が提供されます。getAPKExpansionFile()
メソッドを提供するために必要なものは、アプリの android.content.Context
と、メイン拡張ファイルとパッチ拡張ファイルの両方のバージョン番号のみです。
具体的な拡張ファイルから読み取る場合は、ZipResourceFile
コンストラクタで希望する拡張ファイルのパスを指定します。
Kotlin
// Get a ZipResourceFile representing a specific expansion file val expansionFile = ZipResourceFile(filePathToMyZip) // Get an input stream for a known file inside the expansion file ZIPs expansionFile.getInputStream(pathToFileInsideZip).use { ... }
Java
// Get a ZipResourceFile representing a specific expansion file ZipResourceFile expansionFile = new ZipResourceFile(filePathToMyZip); // Get an input stream for a known file inside the expansion file ZIPs InputStream fileStream = expansionFile.getInputStream(pathToFileInsideZip);
拡張ファイルにこのライブラリを使う方法について詳しくは、サンプルアプリの SampleDownloaderActivity
クラスをご覧ください。CRC を使ってダウンロード ファイルを検証するための追加コードが含まれています。このサンプルを実装のベースとして使用する場合は、xAPKS
配列で拡張ファイルのバイトサイズを宣言する必要があります。
拡張ファイルのテスト
アプリを公開する前に行わなければならないテストが 2 つあります。拡張ファイルの読み取りとファイルのダウンロードです。
ファイルの読み取りをテストする
アプリを Google Play にアップロードする前に、アプリが共有ストレージからファイルを読み取れるかどうかをテストします。ここで必要な操作は、ファイルをデバイスの共有ストレージの適切な場所に追加してアプリを起動することだけです。
- デバイスで、Google Play がファイルを保存する共有ストレージに適切なディレクトリを作成します。
たとえば、パッケージ名が
com.example.android
の場合は、共有ストレージ領域にAndroid/obb/com.example.android/
というディレクトリを作成する必要があります(テストデバイスをパソコンに接続して共有ストレージをマウントし、このディレクトリを手動で作成します)。 - このディレクトリに手動で拡張ファイルを追加します。Google Play が使用するファイル名の形式と一致するようにファイル名を変更します。
たとえば、ファイル形式にかかわらず、
com.example.android
アプリのメイン拡張ファイルの名前はmain.0300110.com.example.android.obb
にします。 バージョン コードの値は自由です。次の点にのみご注意ください。- メイン拡張ファイルの先頭は常に
main
で、パッチ拡張ファイルの先頭は常にpatch
です。 - パッケージ名は拡張ファイルの接続先である Google Play 上の APK のパッケージ名と常に一致します。
- メイン拡張ファイルの先頭は常に
- これで拡張ファイルがデバイスに追加されたので、アプリをインストールして実行し、拡張ファイルをテストできます。
拡張ファイルを扱う場合に注意すべき点は次のとおりです。
.obb
拡張ファイルを削除したり名前を変更したりしないでください(データを別の場所に解凍する場合も同様です)。削除したり名前を変更したりすると、Google Play(またはアプリ自体)が拡張ファイルを繰り返しダウンロードします。obb/
ディレクトリには他のデータを保存しないでください。一部のデータを解凍する必要がある場合は、getExternalFilesDir()
で指定された場所に保存してください。
ファイルのダウンロードをテストする
アプリは初めて起動した時点で手動で拡張ファイルをダウンロードしなければならないことがあります。そのため、この処理をテストすることで確実にアプリが URL を照会してファイルをダウンロードし、デバイスに保存できるようにすることが重要です。
手動ダウンロード処理のアプリの実装をテストするには、アプリを内部テスト版トラックに公開し、権限があるテスト担当者のみがアプリを利用できるようにします。すべて設定どおりに動作すれば、アプリはメイン アクティビティが起動するとすぐに拡張ファイルのダウンロードを開始します。
注: 以前は、公開前のドラフト版をアップロードしてアプリをテストできました。この機能は現在サポートされていません。代わりに、内部テスト版トラック、クローズド テストトラック、オープン テストトラックのいずれかに公開する必要があります。詳細については、ドラフト版アプリのサポート終了をご覧ください。
アプリの更新
Google Play で拡張ファイルを使用する大きなメリットの 1 つは、元のアセットすべてを再ダウンロードせずにアプリを更新できる点です。Google Play では APK ごとに 2 つの拡張ファイルを認めているため、2 つ目のファイルを「パッチ」として使用することでアップデートと新しいアセットを提供できます。これにより、容量が大きいためにユーザーの負担になる可能性があるメイン拡張ファイルを再ダウンロードする必要をなくすことができます。
パッチ拡張ファイルは厳密にはメイン拡張ファイルと同じです。Android システムも Google Play もメイン拡張ファイルとパッチ拡張ファイル間で実際にパッチを適用することはありません。必要なパッチはアプリのコード自体で行う必要があります。
ZIP ファイルを拡張ファイルとして使用する場合、APK Expansion パッケージ内の APK Expansion Zip Library に、メイン拡張ファイルにパッチ拡張ファイルをマージする機能があります。
注: 変更が必要なのがパッチ拡張ファイルのみであっても、Google Play がアップデートを実行できるよう APK も更新する必要があります。アプリのコード変更が必要ない場合は、マニフェストの versionCode
のみを更新するだけで済みます。
Play Console で APK に関連付けられているメイン拡張ファイルを変更しない限り、アプリをすでにインストールしたユーザーにメイン拡張ファイルはダウンロードされません。既存のユーザーに渡されるのは、更新された APK と新しいパッチ拡張ファイルのみです(既存のメイン拡張ファイルは残ります)。
次に、拡張ファイルの更新に関して注意すべき問題をいくつか紹介します。
- アプリに作成できる拡張ファイルは一度に 2 つのみ、つまり、メイン拡張ファイル 1 つとパッチ拡張ファイル 1 つのみです。ファイルのアップデート時に Google Play は前のバージョンを削除します(そのため、手動アップデートの場合はアプリが前のバージョンを削除する必要があります)。
- パッチ拡張ファイルを追加しても、実際に Android システムがアプリやメイン拡張ファイルにパッチを適用するわけではありません。このパッチデータをサポートするようにアプリを作成する必要があります。 ただし、APK Expansion パッケージには ZIP ファイルを拡張ファイルとして使用するためのライブラリが用意され、このライブラリがパッチファイルのデータをメイン拡張ファイルにマージするので、拡張ファイルのデータはすべて簡単に読み取ることができます。