APK 拡張ファイル

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 を使って自動的にファイルをダウンロードできるようにする必要があります。

ダウンロード処理の概要は次のようになります。

  1. ユーザーが Google Play からアプリをインストールすることを選択します。
  2. Google Play が拡張ファイルをダウンロードできる場合(ほとんどのデバイスはこれに該当します)、拡張ファイルは APK と一緒にダウンロードされます。

    Google Play が拡張ファイルをダウンロードできない場合、APK のみがダウンロードされます。

  3. ユーザーがアプリを起動したときに、アプリは拡張ファイルがすでにデバイスに保存されているかどうかを確認する必要があります。
    1. 保存されている場合、アプリは実行に進みます。
    2. 保存されていない場合、アプリは Google Play から HTTP 経由で拡張ファイルをダウンロードする必要があります。アプリは Google Play のアプリ ライセンス サービスを使って Google Play クライアントにリクエストを送信する必要があります。この応答として、各拡張ファイルの名前、ファイルサイズ、URL が返されます。この情報を使ってファイルをダウンロードし、正しい保存先に保存します。

注意: アプリの起動時にファイルがデバイスにない場合に Google Play から拡張ファイルをダウンロードするために必要なコードを追加することが重要です。拡張ファイルのダウンロードに関する次のセクションで説明するように、Google ではこの処理を大幅に簡素化し、最小限のコードでサービスからダウンロードを実行できるライブラリを用意しました。

開発チェックリスト

アプリで拡張ファイルを使用するために必要な作業をまとめると、次のようになります。

  1. まず、アプリの圧縮ダウンロード サイズとして 100 MB を超える容量が必要かどうかを判断します。容量は貴重なので、ダウンロードの合計サイズをできる限り小さくする必要があります。複数の画面密度用に複数のバージョンのグラフィック アセットを提供することで 100 MB を超える場合は、代わりに複数の APK を公開し、対象とする画面に必要なアセットのみを各 APK に含めることを検討してください。最良の結果を得るには、Google Play に公開する際に Android App Bundle をアップロードします。アプリのコンパイル済みコードとリソースはすべて Android App Bundle に含まれますが、APK の生成と署名は Google Play ストアで行います。
  2. APK から切り離すアプリリソースを特定し、メイン拡張ファイルとして使用するファイルにそのリソースをパッケージします。

    通常、メイン拡張ファイルを更新する場合は 2 つ目のパッチ拡張ファイルのみを使用してください。ただし、リソースがメイン拡張ファイルの上限の 2 GB を超える場合は、残りのアセットにパッチファイルを使用できます。

  3. デバイスの共有保存先にある拡張ファイルからリソースを使うようにアプリを開発します。

    拡張ファイルの削除、移動、名前変更は行わないでください。

    アプリの形式に特にこだわらない場合は、拡張ファイル用の ZIP ファイルを作成し、APK Expansion Zip Library を使って読み取ることをおすすめします。

  4. アプリのメイン アクティビティに、起動時に拡張ファイルがデバイスにあるかどうかを確認するロジックを追加します。ファイルがデバイスにない場合は、Google Play のアプリ ライセンス サービスを使って拡張ファイルの URL をリクエストし、拡張ファイルをダウンロードして保存します。

    作成する必要があるコードを大幅に削減し、ダウンロード時のユーザー エクスペリエンスを改善するために、Downloader Library を使ってダウンロード動作を実装することをおすすめします。

    このライブラリを使わず、自分でダウンロード サービスを作成する場合は、拡張ファイルの名前を変更しないよう注意し、拡張ファイルの保存先を正確に指定してください。

アプリを開発し終えたら、ガイドに沿って拡張ファイルをテストします。

ルールと制限

APK 拡張ファイルの追加は、Play Console を使ってアプリをアップロードする場合に利用できる機能です。アプリを初めてアップロードする場合や、拡張ファイルを使うアプリを更新する場合は、次のルールと制限に注意してください。

  1. 各拡張ファイルのサイズの上限は 2 GB とします。
  2. Google Play から拡張ファイルをダウンロードするためには、アプリの入手先が Google Play でなければなりません。他の方法でアプリをインストールした場合、Google Play から拡張ファイルの URL は提供されません。
  3. アプリ内からダウンロードを実行する場合、Google Play が各ファイルに提供する URL はダウンロードごとに割り振られるため、有効期間が短く、アプリに提供された後すぐに切れます。
  4. 新しい APK でアプリを更新する場合や、同じアプリの複数の APK をアップロードする場合は、古い APK 用のアップロード済みの拡張ファイルを選択できます。拡張ファイルの名前は変わらず、ファイルが最初に関連付けられた APK に割り振られたバージョンが残ります。
  5. デバイスごとに異なる拡張ファイルを提供するために拡張ファイルを複数の APK と組み合わせて使用する場合も、versionCode 値を一意にするためにデバイスごとに別々の APK をアップロードし、APK ごとに異なるフィルタを宣言する必要があります。
  6. 拡張ファイルのみを変更してもアプリのアップデートを発行することはできません。アプリを更新するには新しい APK をアップロードする必要があります。拡張ファイル内のアセットのみに関係する変更の場合は、versionCode(および必要に応じて versionName)を変更するだけで APK を更新できます。

  7. obb/ ディレクトリには他のデータを保存しないでください。一部のデータを解凍する必要がある場合は、getExternalFilesDir() で指定された場所に保存してください。
  8. (アップデートを実行する場合を除き).obb 拡張ファイルを削除したり名前を変更したりしないでください。削除したり名前を変更したりすると、Google Play(またはアプリ自体)が拡張ファイルを繰り返しダウンロードします。
  9. 拡張ファイルを手動で更新する場合は、古い拡張ファイルを削除する必要があります。

拡張ファイルのダウンロード

ほとんどの場合、Google Play は APK のインストールや更新と同時に拡張ファイルをダウンロードしてデバイスに保存します。したがって、アプリを初めて起動したときでも拡張ファイルは利用可能です。ただし、アプリ自体が拡張ファイルをダウンロードしなければならないこともあります。この場合は、Google Play のアプリ ライセンス サービスからの応答で提供される URL から拡張ファイルをリクエストします。

拡張ファイルのダウンロードに必要な基本的なロジックは次のとおりです。

  1. アプリが起動したら、共有保存先Android/obb/<package-name>/ ディレクトリ内)で拡張ファイルを探します。
    1. 拡張ファイルがあれば、これで完了です。アプリは次の操作に進むことができます。
    2. 拡張ファイルがない場合は、次のようにします。
      1. Google Play のアプリ ライセンス サービスを使ってアプリの拡張ファイルの名前、サイズ、URL をリクエストして取得します。
      2. Google Play から提供された URL を使って拡張ファイルをダウンロードし保存します。ファイルは共有保存先Android/obb/<package-name>/)に保存し、Google Play の応答で提供された正確なファイル名を使用する必要があります。

        注: Google Play が各拡張ファイルに提供する URL はダウンロードごとに割り振られるため、有効期間が短く、アプリに提供された後すぐに切れます。

アプリが無料(有料アプリ以外)の場合は、アプリ ライセンス サービスを使用していない可能性があります。このサービスの主な目的は、アプリのライセンス ポリシーを適用し、ユーザーにアプリを使用する権限を付与することです(ユーザーは 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 の新しいライブラリ モジュールを作成します。各ライブラリについて:

  1. [File] > [New] > [New Module] を選択します。
  2. [Create New Module] ウィンドウで、[Android Library]、[Next] の順に選択します。
  3. [app/Library name] を指定し(「Google Play License Library」、「Google Play Downloader Library」など)、[Minimum SDK level]、[Finish] の順に選択します。
  4. [File] > [Project Structure] を選択します。
  5. [Properties] タブを選択し、[Library Repository]<sdk>/extras/google/ ディレクトリ(License Verification Library の場合は play_licensing/、Downloader Library の場合は play_apk_expansion/downloader_library/)からライブラリを入力します。
  6. [OK] を選択して新しいモジュールを作成します。

注: Downloader Library は License Verification Library に応じて異なります。License Verification Library を Downloader Library のプロジェクト プロパティに追加します。

または、コマンドラインから、ライブラリを追加するプロジェクトを更新します。

  1. <sdk>/tools/ ディレクトリに移動します。
  2. 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 に複数の権限が必要です。この権限は、アプリのマニフェスト ファイルで宣言する必要があります。その 2 つは次のとおりです。

<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 公開鍵である文字列を返す必要があります。この文字列は、Google Play Console のプロフィール ページから入手できます(ライセンスの設定をご覧ください)。
getSALT()
ライセンスの PolicyObfuscator を作成する際に使うランダムなバイトの配列を返す必要があります。このソルトにより、ライセンス データが保存されている難読化された 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>

アラーム レシーバを実装する

ファイルのダウンロードの進行状況を監視し、必要に応じてダウンロードを再開するために、DownloaderServiceIntent をアプリの 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 を使ってダウンロードを開始する手順は次のとおりです。

  1. ファイルがダウンロード済みかどうかを確認します。

    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 を返した場合、アプリはダウンロードを開始する必要があります。

  2. 静的メソッド 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_REQUIREDDOWNLOAD_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
    }
    
  3. 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() 呼び出しを受け取ります。ここで、IStubconnect() を呼び出してアプリの 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();
    }
    

    IStubconnect() を呼び出すと、アクティビティが 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()
前述と同じメソッドで、両方の拡張ファイルの完全ファイルパスを返します。
getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion)
メインファイルとパッチファイルの合計を表す ZipResourceFile を返します。つまり、mainVersionpatchVersion の両方を指定すると、パッチファイルのデータがメインファイルの上にマージされて、すべてのデータの読み取りアクセス権を提供する 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 にアップロードする前に、アプリが共有ストレージからファイルを読み取れるかどうかをテストします。ここで必要な操作は、ファイルをデバイスの共有ストレージの適切な場所に追加してアプリを起動することだけです。

  1. デバイスで、Google Play がファイルを保存する共有ストレージに適切なディレクトリを作成します。

    たとえば、パッケージ名が com.example.android の場合は、共有ストレージ領域に Android/obb/com.example.android/ というディレクトリを作成する必要があります(テストデバイスをパソコンに接続して共有ストレージをマウントし、このディレクトリを手動で作成します)。

  2. このディレクトリに手動で拡張ファイルを追加します。Google Play が使用するファイル名の形式と一致するようにファイル名を変更します。

    たとえば、ファイル形式にかかわらず、com.example.android アプリのメイン拡張ファイルの名前は main.0300110.com.example.android.obb にします。 バージョン コードの値は自由です。次の点にのみご注意ください。

    • メイン拡張ファイルの先頭は常に main で、パッチ拡張ファイルの先頭は常に patch です。
    • パッケージ名は拡張ファイルの接続先である Google Play 上の APK のパッケージ名と常に一致します。
  3. これで拡張ファイルがデバイスに追加されたので、アプリをインストールして実行し、拡張ファイルをテストできます。

拡張ファイルを扱う場合に注意すべき点は次のとおりです。

  • .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 ファイルを拡張ファイルとして使用するためのライブラリが用意され、このライブラリがパッチファイルのデータをメイン拡張ファイルにマージするので、拡張ファイルのデータはすべて簡単に読み取ることができます。