Файлы расширения APK

Google Play требует, чтобы размер загружаемого пользователями сжатого APK-файла не превышал 100 МБ. Для большинства приложений этого достаточно для всего кода и ресурсов приложения. Однако некоторым приложениям требуется больше места для высококачественной графики, медиафайлов или других больших ресурсов. Ранее, если размер загружаемого сжатого файла вашего приложения превышал 100 МБ, вам приходилось самостоятельно размещать и загружать дополнительные ресурсы при открытии приложения пользователем. Размещение и предоставление дополнительных файлов может быть дорогостоящим, а пользовательский опыт часто оставляет желать лучшего. Чтобы упростить этот процесс для вас и сделать его более приятным для пользователей, Google Play позволяет вам прикрепить два больших файла расширения, которые дополняют ваш APK-файл.

Google Play размещает файлы расширения для вашего приложения и предоставляет их устройству бесплатно. Файлы расширения сохраняются в общем хранилище устройства (SD-карта или раздел, монтируемый на USB-накопитель; также известный как «внешнее» хранилище), где ваше приложение может получить к ним доступ. На большинстве устройств Google Play загружает файлы расширения одновременно с загрузкой APK-файла, поэтому ваше приложение будет иметь все необходимое, когда пользователь откроет его в первый раз. Однако в некоторых случаях вашему приложению необходимо загрузить файлы из Google Play при запуске.

Если вы хотите избежать использования файлов расширения, и размер сжатого файла загрузки вашего приложения превышает 100 МБ, вам следует загрузить приложение с помощью Android App Bundles , который позволяет загружать сжатые файлы размером до 500 МБ. Кроме того, поскольку использование пакетов приложений откладывает генерацию и подпись APK-файлов в Google Play, пользователи загружают оптимизированные APK-файлы, содержащие только необходимый код и ресурсы для запуска вашего приложения. Вам не нужно создавать, подписывать и управлять несколькими APK-файлами или файлами расширения, а пользователи получают более компактные и оптимизированные файлы для загрузки.

Обзор

При каждой загрузке APK-файла через Google Play Console у вас есть возможность добавить один или два дополнительных файла. Каждый файл может иметь размер до 2 ГБ и любой формат на ваш выбор, но мы рекомендуем использовать сжатый файл для экономии трафика во время загрузки. Концептуально каждый дополнительный файл выполняет свою роль:

  • Основной файл расширения — это главный файл расширения для дополнительных ресурсов, необходимых вашему приложению.
  • Дополнительный файл патча является необязательным и предназначен для небольших обновлений основного файла расширения.

Хотя вы можете использовать два дополнительных файла по своему усмотрению, мы рекомендуем, чтобы основной дополнительный файл содержал основные ресурсы и обновлялся редко, если вообще обновляется; дополнительный файл с патчами должен быть меньше по размеру и служить «носителем патчей», обновляясь с каждым крупным релизом или по мере необходимости.

Однако, даже если для обновления вашего приложения требуется только новый файл расширения, вам все равно необходимо загрузить новый APK-файл с обновленным versionCode в манифесте. (Play Console не позволяет загружать файл расширения в существующий APK-файл.)

Примечание: файл расширения патча семантически идентичен основному файлу расширения — вы можете использовать каждый файл по своему усмотрению.

формат имени файла

Каждый загружаемый вами файл расширения может быть в любом выбранном вами формате (ZIP, PDF, MP4 и т. д.). Вы также можете использовать инструмент JOBB для инкапсуляции и шифрования набора файлов ресурсов и последующих патчей для этого набора. Независимо от типа файла, Google Play рассматривает их как непрозрачные двоичные блоки и переименовывает файлы, используя следующую схему:

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

Данная схема состоит из трех компонентов:

main или patch
Указывает, является ли файл основным или файлом расширения для патчей. Для каждого APK-файла может быть только один основной файл и один файл патчей.
<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> — это имя пакета вашего приложения в стиле Java, доступное из getPackageName() .

Для каждого приложения в этом каталоге никогда не бывает более двух файлов расширения. Один — это основной файл расширения, а другой — файл расширения для исправлений (если необходимо). Предыдущие версии перезаписываются при обновлении приложения новыми файлами расширения. Начиная с 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-файлов. Следующий фрагмент кода показывает, как проверить наличие доступа на чтение перед запросом разрешения на использование внешнего хранилища:

Котлин

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() . Однако, по возможности, лучше использовать формат файлов расширения, позволяющий читать данные непосредственно из файла, а не распаковывать их. Например, мы предоставили библиотечный проект под названием APK Expansion Zip Library , который считывает данные непосредственно из ZIP-файла.

Внимание: В отличие от APK-файлов, любые файлы, сохраненные в общем хранилище, могут быть прочитаны пользователем и другими приложениями.

Совет: Если вы упаковываете медиафайлы в ZIP-архив, вы можете использовать вызовы воспроизведения медиафайлов с элементами управления смещением и длиной (например, MediaPlayer.setDataSource() и SoundPool.load() ) без необходимости распаковывать ZIP-архив. Для этого необходимо избегать дополнительного сжатия медиафайлов при создании ZIP-архивов. Например, при использовании инструмента zip следует использовать параметр -n для указания суффиксов файлов, которые не следует сжимать:
zip -n .mp4;.ogg main_expansion media_files

Процесс загрузки

В большинстве случаев Google Play загружает и сохраняет файлы расширения одновременно с загрузкой APK-файла на устройство. Однако в некоторых случаях Google Play не может загрузить файлы расширения, или пользователь мог удалить ранее загруженные файлы расширения. Для обработки таких ситуаций ваше приложение должно иметь возможность самостоятельно загружать файлы при запуске основной активности, используя URL-адрес, предоставленный Google Play.

Процесс загрузки на высоком уровне выглядит следующим образом:

  1. Пользователь выбирает установить ваше приложение из Google Play.
  2. Если Google Play может загрузить файлы расширения (что происходит на большинстве устройств), он загружает их вместе с APK-файлом.

    Если Google Play не может загрузить файлы расширения, он загружает только APK-файл.

  3. Когда пользователь запускает ваше приложение, оно должно проверить, сохранены ли уже файлы расширения на устройстве.
    1. Если да, то ваше приложение готово к запуску.
    2. В противном случае, ваше приложение должно загрузить файлы расширения по протоколу HTTP из Google Play. Ваше приложение должно отправить запрос клиенту Google Play, используя службу лицензирования приложений Google Play, которая ответит именем, размером файла и URL-адресом каждого файла расширения. Имея эту информацию, вы затем загружаете файлы и сохраняете их в соответствующем месте хранения .

Внимание: крайне важно включить необходимый код для загрузки файлов расширения из Google Play на случай, если эти файлы еще не установлены на устройстве при запуске приложения. Как обсуждалось в следующем разделе о загрузке файлов расширения , мы предоставили вам библиотеку, которая значительно упрощает этот процесс и выполняет загрузку из сервиса с минимальным количеством кода с вашей стороны.

Контрольный список разработки

Вот краткое описание задач, которые необходимо выполнить для использования файлов расширения в вашем приложении:

  1. Сначала определите, нужно ли вашему приложению загружать сжатый файл размером более 100 МБ. Место на диске ценно, поэтому общий размер загружаемого файла должен быть как можно меньше. Если ваше приложение использует более 100 МБ для предоставления нескольких версий графических ресурсов для разных плотностей экрана, рассмотрите возможность публикации нескольких APK-файлов, каждый из которых содержит только ресурсы, необходимые для целевых экранов. Для достижения наилучших результатов при публикации в Google Play загрузите Android App Bundle , который включает весь скомпилированный код и ресурсы вашего приложения, но откладывает генерацию и подпись APK-файла в Google Play.
  2. Определите, какие ресурсы приложения следует отделить от APK-файла, и упакуйте их в файл, который будет использоваться в качестве основного файла расширения.

    Обычно при обновлении основного файла расширения следует использовать только второй файл расширения. Однако, если ваши ресурсы превышают лимит в 2 ГБ для основного файла расширения, вы можете использовать файл расширения для остальных ресурсов.

  3. Разработайте приложение таким образом, чтобы оно использовало ресурсы из файлов расширения, расположенных в общей памяти устройства.

    Помните, что удалять, перемещать или переименовывать файлы расширения категорически запрещено.

    Если ваше приложение не требует определенного формата, мы рекомендуем создать ZIP-архивы для файлов расширения, а затем прочитать их с помощью библиотеки APK Expansion Zip Library .

  4. Добавьте в основное окно приложения логику, которая проверяет наличие файлов расширения на устройстве при запуске. Если файлов нет на устройстве, используйте службу лицензирования приложений Google Play, чтобы запросить URL-адреса файлов расширения, а затем загрузите и сохраните их.

    Чтобы значительно сократить объем необходимого кода и обеспечить удобство использования во время загрузки, мы рекомендуем использовать библиотеку Downloader для реализации процесса загрузки.

    Если вы создадите собственный сервис загрузки вместо использования библиотеки, имейте в виду, что вы не должны изменять имена файлов расширения и должны сохранять их в соответствующем месте хранения .

После завершения разработки приложения следуйте инструкциям по тестированию файлов расширения .

Правила и ограничения

Добавление файлов расширения APK — это функция, доступная при загрузке приложения через Play Console. При первой загрузке приложения или обновлении приложения, использующего файлы расширения, необходимо учитывать следующие правила и ограничения:

  1. Размер каждого файла расширения не должен превышать 2 ГБ.
  2. Для загрузки файлов расширения из Google Play пользователь должен был получить ваше приложение из Google Play . Google Play не предоставит URL-адреса файлов расширения, если приложение было установлено другим способом.
  3. При загрузке файлов из вашего приложения URL-адрес, предоставляемый Google Play для каждого файла, является уникальным для каждой загрузки, и срок действия каждого из них истекает вскоре после его предоставления вашему приложению.
  4. Если вы обновляете приложение с помощью нового APK-файла или загружаете несколько APK-файлов для одного и того же приложения, вы можете выбрать файлы расширения, которые вы загружали для предыдущего APK-файла. Имя файла расширения не меняется — оно сохраняет версию, полученную APK-файлом, с которым этот файл был первоначально связан.
  5. Если вы используете файлы расширения в сочетании с несколькими APK-файлами для предоставления разных файлов расширения для разных устройств, вам все равно необходимо загружать отдельные APK-файлы для каждого устройства, чтобы указать уникальное значение versionCode и объявить разные фильтры для каждого APK-файла.
  6. Вы не можете обновить приложение, изменив только файлы расширения — для обновления приложения необходимо загрузить новый APK-файл . Если ваши изменения касаются только ресурсов в файлах расширения, вы можете обновить APK-файл, просто изменив versionCode (и, возможно, также versionName ).
  7. Не сохраняйте другие данные в каталог obb/ . Если вам необходимо распаковать какие-либо данные, сохраните их в место, указанное функцией getExternalFilesDir() .
  8. Не удаляйте и не переименовывайте файл расширения .obb (если только вы не выполняете обновление). Это приведет к тому, что Google Play (или само ваше приложение) будет повторно загружать файл расширения.
  9. При ручном обновлении файла расширения необходимо удалить предыдущий файл расширения.

Загрузка файлов расширения

В большинстве случаев Google Play загружает и сохраняет файлы расширения на устройство одновременно с установкой или обновлением APK-файла. Таким образом, файлы расширения доступны при первом запуске вашего приложения. Однако в некоторых случаях вашему приложению необходимо самостоятельно загрузить файлы расширения, запросив их по URL-адресу, предоставленному вам в ответе от службы лицензирования приложений Google Play.

Для загрузки файлов расширения вам потребуется следующая основная логика:

  1. При запуске приложения найдите файлы расширения в общем хранилище (в каталоге Android/obb/<package-name>/ ).
    1. Если файлы расширения присутствуют, значит, все в порядке, и ваше приложение может продолжать работу.
    2. Если файлы расширения отсутствуют :
      1. Отправьте запрос через раздел лицензирования приложений Google Play, чтобы получить имена, размеры и URL-адреса файлов расширения вашего приложения.
      2. Используйте URL-адреса, предоставленные Google Play, для загрузки и сохранения файлов расширения. Файлы необходимо сохранить в общую папку ( Android/obb/<package-name>/ ) и использовать точное имя файла, указанное в ответе Google Play.

        Примечание: URL-адрес, предоставляемый Google Play для файлов расширения, уникален для каждой загрузки, и срок действия каждого из них истекает вскоре после его предоставления вашему приложению.

Если ваше приложение бесплатное (не платное), то вы, вероятно, не использовали сервис лицензирования приложений . Он предназначен в первую очередь для обеспечения соблюдения лицензионных правил вашего приложения и гарантии того, что пользователь имеет право использовать ваше приложение (он законно заплатил за него в Google Play). Для упрощения работы с файлами расширения сервис лицензирования был усовершенствован и теперь предоставляет вашему приложению ответ, включающий URL-адрес файлов расширения вашего приложения, размещенных в Google Play. Таким образом, даже если ваше приложение бесплатное для пользователей, вам необходимо включить библиотеку проверки лицензий (LVL) для использования файлов расширения APK. Конечно, если ваше приложение бесплатное, вам не нужно принудительно проверять лицензии — вам просто нужно, чтобы библиотека выполнила запрос, возвращающий URL-адрес ваших файлов расширения.

Примечание: независимо от того, является ли ваше приложение бесплатным или нет, Google Play возвращает URL-адреса файлов расширения только в том случае, если пользователь загрузил ваше приложение из Google Play.

Помимо LVL-файла, вам потребуется набор кода, который загружает файлы расширения по HTTP-соединению и сохраняет их в нужном месте на общей памяти устройства. При внедрении этой процедуры в ваше приложение следует учитывать несколько моментов:

  • На устройстве может не хватать места для файлов расширения, поэтому перед началом загрузки следует проверить это и предупредить пользователя, если места недостаточно.
  • Загрузка файлов должна происходить в фоновом режиме, чтобы избежать блокировки взаимодействия с пользователем и позволить пользователю покинуть приложение, пока загрузка завершается.
  • В процессе запроса и загрузки могут возникать различные ошибки, которые необходимо корректно обрабатывать.
  • В процессе загрузки может происходить сбой в работе сети, поэтому следует оперативно реагировать на такие изменения и, в случае прерывания, возобновлять загрузку при первой возможности.
  • Пока загрузка происходит в фоновом режиме, следует отображать уведомление, которое показывает ход загрузки, сообщает пользователю о завершении и возвращает пользователя в ваше приложение при выборе.

Чтобы упростить вам эту работу, мы разработали библиотеку Downloader Library , которая запрашивает URL-адреса файлов расширения через службу лицензирования, загружает файлы расширения, выполняет все перечисленные выше задачи и даже позволяет вашему приложению приостанавливать и возобновлять загрузку. Добавив библиотеку Downloader Library и несколько хуков кода в ваше приложение, вы практически полностью реализуете всю работу по загрузке файлов расширения. Таким образом, чтобы обеспечить наилучший пользовательский опыт с минимальными усилиями с вашей стороны, мы рекомендуем использовать библиотеку Downloader Library для загрузки файлов расширения. Информация в следующих разделах объясняет, как интегрировать библиотеку в ваше приложение.

Если вы предпочитаете разработать собственное решение для загрузки файлов расширения с использованием URL-адресов Google Play, вам необходимо следовать документации по лицензированию приложения , чтобы выполнить запрос на получение лицензии, а затем получить имена, размеры и URL-адреса файлов расширения из дополнительных данных ответа. В качестве политики лицензирования следует использовать класс APKExpansionPolicy (входящий в библиотеку проверки лицензий), который получает имена, размеры и URL-адреса файлов расширения от службы лицензирования.

О библиотеке загрузчиков

Чтобы использовать файлы расширения APK в вашем приложении и обеспечить наилучший пользовательский опыт с минимальными усилиями с вашей стороны, мы рекомендуем использовать библиотеку Downloader, которая входит в пакет Google Play APK Expansion Library. Эта библиотека загружает ваши файлы расширения в фоновом режиме, отображает пользователю уведомление о статусе загрузки, обрабатывает потерю сетевого соединения, возобновляет загрузку, если это возможно, и многое другое.

Для реализации загрузки дополнительных файлов с помощью библиотеки Downloader Library вам потребуется всего лишь:

  • Создайте специальный подкласс Service и подкласс BroadcastReceiver , для каждого из которых вам потребуется всего несколько строк кода.
  • Добавьте в основное приложение логику, которая проверяет, были ли уже загружены файлы расширения, и если нет, запускает процесс загрузки и отображает индикатор выполнения.
  • В основной активности реализуйте интерфейс обратного вызова с несколькими методами, который будет получать обновления о ходе загрузки.

В следующих разделах объясняется, как настроить ваше приложение с помощью библиотеки Downloader.

Подготовка к использованию библиотеки загрузчиков.

Для использования библиотеки Downloader необходимо загрузить два пакета из SDK Manager и добавить соответствующие библиотеки в ваше приложение.

Сначала откройте Менеджер SDK Android ( Инструменты > Менеджер SDK ), затем в разделе Внешний вид и поведение > Системные настройки > Android SDK выберите вкладку «Инструменты SDK» , чтобы выбрать и загрузить:

  • Пакет лицензионной библиотеки Google Play
  • Пакет расширения библиотеки APK Google Play

Создайте новый библиотечный модуль для библиотеки проверки лицензий и библиотеки загрузчиков. Для каждой библиотеки:

  1. Выберите Файл > Создать > Новый модуль .
  2. В окне «Создать новый модуль» выберите «Библиотека Android» , а затем нажмите «Далее» .
  3. Укажите название приложения/библиотеки, например, "Google Play License Library" или "Google Play Downloader Library", выберите минимальный уровень SDK , а затем нажмите "Готово ".
  4. Выберите Файл > Структура проекта .
  5. Выберите вкладку «Свойства» , а в поле «Репозиторий библиотеки» укажите библиотеку из каталога <sdk>/extras/google/ ( play_licensing/ для библиотеки проверки лицензий или play_apk_expansion/downloader_library/ для библиотеки загрузчика).
  6. Нажмите ОК , чтобы создать новый модуль.

Примечание: Библиотека загрузчика зависит от библиотеки проверки лицензий. Обязательно добавьте библиотеку проверки лицензий в свойства проекта библиотеки загрузчика.

Или же, из командной строки, обновите свой проект, чтобы включить необходимые библиотеки:

  1. Перейдите в каталог <sdk>/tools/ .
  2. Выполните команду android update project с опцией --library , чтобы добавить в проект библиотеки LVL и Downloader. Например:
    android update project --path ~/Android/MyApp \
    --library ~/android_sdk/extras/google/market_licensing \
    --library ~/android_sdk/extras/google/market_apk_expansion/downloader_library
    

Добавив в приложение библиотеку проверки лицензий и библиотеку загрузки, вы сможете быстро интегрировать возможность загрузки файлов расширения из Google Play. Формат файлов расширения и способ их чтения из общего хранилища — это отдельная задача, которую следует рассмотреть в зависимости от потребностей вашего приложения.

Совет: В пакет расширения APK входит пример приложения, демонстрирующий использование библиотеки загрузчика в приложении. В примере используется третья библиотека, доступная в пакете расширения APK, называемая библиотекой расширения APK в формате ZIP. Если вы планируете использовать ZIP-файлы для файлов расширения, мы рекомендуем также добавить библиотеку расширения APK в формате ZIP в ваше приложение. Для получения дополнительной информации см. раздел ниже об использовании библиотеки расширения APK в формате ZIP .

Объявление прав пользователя

Для загрузки файлов расширения библиотека Downloader требует предоставления нескольких разрешений, которые необходимо указать в файле манифеста вашего приложения. К ним относятся:

<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 требует API уровня 4, а библиотека APK Expansion Zip — API уровня 5.

Реализация службы загрузки

Для выполнения загрузок в фоновом режиме библиотека Downloader предоставляет собственный подкласс Service под названием DownloaderService , который вам следует расширить. Помимо загрузки файлов расширения, DownloaderService также:

  • Регистрирует объект BroadcastReceiver , который отслеживает изменения сетевого подключения устройства (широковещательное сообщение CONNECTIVITY_ACTION ), чтобы при необходимости приостанавливать загрузку (например, из-за потери связи) и возобновлять ее, когда это возможно (подключение установлено).
  • Настраивает срабатывание сигнала RTC_WAKEUP для повторной попытки загрузки в случае завершения работы службы.
  • Создаёт пользовательское Notification , отображающее ход загрузки, а также любые ошибки или изменения состояния.
  • Позволяет вашему приложению вручную приостанавливать и возобновлять загрузку.
  • Перед загрузкой файлов расширения проверяется, смонтировано ли и доступно ли общее хранилище, не существуют ли файлы уже и достаточно ли места. Затем пользователь получает уведомление, если какое-либо из этих условий не выполняется.

Всё, что вам нужно сделать, это создать в своём приложении класс, который расширяет класс DownloaderService , и переопределить три метода для предоставления конкретных сведений о приложении:

getPublicKey()
Эта функция должна возвращать строку, представляющую собой закодированный в Base64 открытый ключ RSA для вашей учетной записи издателя, доступный на странице профиля в Play Console (см. раздел «Настройка лицензирования »).
getSALT()
Эта функция должна возвращать массив случайных байтов, который Policy лицензирования использует для создания Obfuscator . Соль гарантирует, что ваш обфусцированный файл SharedPreferences в котором хранятся данные лицензирования, будет уникальным и недоступным для обнаружения.
getAlarmReceiverClassName()
Эта функция должна возвращать имя класса BroadcastReceiver в вашем приложении, который должен получать оповещение о необходимости перезапуска загрузки (что может произойти, если служба загрузки неожиданно остановится).

Например, вот полная реализация DownloaderService :

Котлин

// 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 , указав в нем открытый ключ, принадлежащий вашей учетной записи издателя. Ключ можно найти в консоли разработчика в разделе информации о вашем профиле. Это необходимо даже при тестировании загрузок.

Не забудьте указать сервис в файле манифеста:

<app ...>
    <service android:name=".SampleDownloaderService" />
    ...
</app>

Внедрение приемника сигналов тревоги

Для отслеживания хода загрузки файлов и перезапуска загрузки при необходимости, DownloaderService планирует отправку сигнала RTC_WAKEUP , который передает Intent в BroadcastReceiver вашего приложения. Необходимо определить BroadcastReceiver таким образом, чтобы он вызывал API из библиотеки Downloader, проверяющий статус загрузки и перезапускающий ее при необходимости.

Вам достаточно переопределить метод onReceive() , чтобы вызвать DownloaderClientMarshaller.startDownloadServiceIfRequired() .

Например:

Котлин

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 необходимо выполнить следующие действия:

  1. Проверьте, были ли загружены файлы.

    Библиотека Downloader включает в себя несколько API в классе Helper , которые помогают в этом процессе:

    • getExpansionAPKFileName(Context, c, boolean mainFile, int versionCode)
    • doesFileExist(Context c, String fileName, long fileSize)

    Например, в примере приложения, входящем в пакет Apk Expansion, в методе onCreate() активности вызывается следующий метод для проверки наличия файлов расширения на устройстве:

    Котлин

    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 для запуска основной активности. Он используется в Notification , которое создает DownloaderService для отображения хода загрузки. Когда пользователь выбирает уведомление, система вызывает указанный здесь объект PendingIntent и должна открыть активность, отображающую ход загрузки (обычно ту же активность, которая запустила загрузку).
    • serviceClass : Объект Class для вашей реализации DownloaderService , необходимый для запуска службы и начала загрузки при необходимости.

    Метод возвращает целое число, указывающее, требуется ли загрузка. Возможные значения:

    • NO_DOWNLOAD_REQUIRED : Возвращается, если файлы уже существуют или загрузка уже идёт.
    • LVL_CHECK_REQUIRED : Возвращается, если для получения URL-адресов файлов расширения требуется проверка лицензии.
    • DOWNLOAD_REQUIRED : Возвращается, если URL-адреса файлов расширения уже известны, но еще не были загружены.

    Поведение переменных LVL_CHECK_REQUIRED и DOWNLOAD_REQUIRED по сути одинаково, и обычно вам не нужно о них беспокоиться. В вашей основной активности, которая вызывает startDownloadServiceIfRequired() , вы можете просто проверить, является ли ответ NO_DOWNLOAD_REQUIRED . Если ответ отличается от NO_DOWNLOAD_REQUIRED , библиотека Downloader начинает загрузку, и вам следует обновить пользовательский интерфейс вашей активности, чтобы отображать ход загрузки (см. следующий шаг). Если ответ NO_DOWNLOAD_REQUIRED , то файлы доступны NO_DOWNLOAD_REQUIRED и ваше приложение может запуститься.

    Например:

    Котлин

    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 , создайте экземпляр IStub , вызвав DownloaderClientMarshaller.CreateStub(IDownloaderClient client, Class<?> downloaderService) . IStub обеспечивает связь между вашей активностью и службой загрузки, так что ваша активность получает обратные вызовы о ходе загрузки.

    Для создания экземпляра IStub с помощью вызова CreateStub() необходимо передать ему реализацию интерфейса IDownloaderClient и реализацию вашего DownloaderService . В следующем разделе, посвященном получению информации о ходе загрузки, рассматривается интерфейс IDownloaderClient , который обычно следует реализовывать в классе Activity , чтобы можно было обновлять пользовательский интерфейс Activity при изменении состояния загрузки.

    Мы рекомендуем вызывать метод CreateStub() для создания экземпляра IStub в методе onCreate() вашего действия, после того как startDownloadServiceIfRequired() начнет загрузку.

    Например, в предыдущем примере кода для onCreate() вы можете отреагировать на результат метода startDownloadServiceIfRequired() следующим образом:

    Котлин

            // 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() , где вам следует вызвать connect() для IStub , передав ему Context вашего приложения. И наоборот, вам следует вызвать disconnect() в коллбэке onStop() вашей активности.

    Котлин

    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();
    }

    Вызов метода connect() у IStub привязывает вашу активность к DownloaderService , так что ваша активность получает обратные вызовы, касающиеся изменений состояния загрузки, через интерфейс IDownloaderClient .

Получение информации о ходе загрузки

Для получения уведомлений о ходе загрузки и взаимодействия с DownloaderService необходимо реализовать интерфейс IDownloaderClient из библиотеки Downloader. Как правило, активность, используемая для запуска загрузки, должна реализовывать этот интерфейс, чтобы отображать ход загрузки и отправлять запросы в службу.

Необходимые методы интерфейса для IDownloaderClient :

onServiceConnected(Messenger m)
После создания экземпляра IStub в вашей активности вы получите вызов этого метода, который передаст объект Messenger , связанный с вашим экземпляром DownloaderService . Чтобы отправлять запросы к сервису, например, для приостановки и возобновления загрузок, необходимо вызвать DownloaderServiceMarshaller.CreateProxy() , чтобы получить интерфейс IDownloaderService подключенный к сервису.

Рекомендуемый вариант реализации выглядит следующим образом:

Котлин

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 будет одним из нескольких возможных значений, указанных в одной из констант STATE_* класса IDownloaderClient .

Чтобы предоставить пользователям полезное сообщение, вы можете запросить соответствующую строку для каждого состояния, вызвав метод Helpers.getDownloaderStringResourceIDFromState() . Это вернет идентификатор ресурса для одной из строк, входящих в состав библиотеки Downloader. Например, строка "Загрузка приостановлена, потому что вы находитесь в роуминге" соответствует состоянию STATE_PAUSED_ROAMING .

onDownloadProgress(DownloadProgressInfo progress)
Служба загрузки обращается к этому объекту для передачи информации DownloadProgressInfo , которая содержит различные сведения о процессе загрузки, включая предполагаемое оставшееся время, текущую скорость, общий прогресс и итоговые значения, чтобы вы могли обновить пользовательский интерфейс отслеживания хода загрузки.

Совет: Примеры таких функций обратного вызова, обновляющих интерфейс отображения хода загрузки, можно найти в классе SampleDownloaderActivity в демонстрационном приложении, входящем в состав пакета Apk Expansion.

Вот некоторые общедоступные методы интерфейса IDownloaderService , которые могут вам пригодиться:

requestPauseDownload()
Приостанавливает загрузку.
requestContinueDownload()
Возобновляет приостановленную загрузку.
setDownloadFlags(int flags)
Устанавливает пользовательские настройки для типов сетей, в которых разрешена загрузка файлов. Текущая реализация поддерживает один флаг, FLAGS_DOWNLOAD_OVER_CELLULAR , но вы можете добавить и другие. По умолчанию этот флаг отключен , поэтому пользователь должен находиться в сети Wi-Fi для загрузки файлов расширения. Возможно, вам потребуется указать пользовательские настройки для разрешения загрузки через сотовую сеть. В этом случае вы можете вызвать:

Котлин

remoteService = DownloaderServiceMarshaller.CreateProxy(m).apply {
    ...
    setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR)
}

Java

remoteService
    .setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR);

Использование APKExpansionPolicy

If you decide to build your own downloader service instead of using the Google Play Downloader Library , you should still use the APKExpansionPolicy that's provided in the License Verification Library. The APKExpansionPolicy class is nearly identical to ServerManagedPolicy (available in the Google Play License Verification Library) but includes additional handling for the APK expansion file response extras.

Note: If you do use the Downloader Library as discussed in the previous section, the library performs all interaction with the APKExpansionPolicy so you don't have to use this class directly.

The class includes methods to help you get the necessary information about the available expansion files:

  • getExpansionURLCount()
  • getExpansionURL(int index)
  • getExpansionFileName(int index)
  • getExpansionFileSize(int index)

For more information about how to use the APKExpansionPolicy when you're not using the Downloader Library , see the documentation for Adding Licensing to Your App , which explains how to implement a license policy such as this one.

Reading the Expansion File

Once your APK expansion files are saved on the device, how you read your files depends on the type of file you've used. As discussed in the overview , your expansion files can be any kind of file you want, but are renamed using a particular file name format and are saved to <shared-storage>/Android/obb/<package-name>/ .

Regardless of how you read your files, you should always first check that the external storage is available for reading. There's a chance that the user has the storage mounted to a computer over USB or has actually removed the SD card.

Note: When your app starts, you should always check whether the external storage space is available and readable by calling getExternalStorageState() . This returns one of several possible strings that represent the state of the external storage. In order for it to be readable by your app, the return value must be MEDIA_MOUNTED .

Getting the file names

As described in the overview , your APK expansion files are saved using a specific file name format:

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

To get the location and names of your expansion files, you should use the getExternalStorageDirectory() and getPackageName() methods to construct the path to your files.

Here's a method you can use in your app to get an array containing the complete path to both your expansion files:

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;
}

You can call this method by passing it your app Context and the desired expansion file's version.

There are many ways you could determine the expansion file version number. One simple way is to save the version in a SharedPreferences file when the download begins, by querying the expansion file name with the APKExpansionPolicy class's getExpansionFileName(int index) method. You can then get the version code by reading the SharedPreferences file when you want to access the expansion file.

For more information about reading from the shared storage, see the Data Storage documentation.

Using the APK Expansion Zip Library

The Google Market Apk Expansion package includes a library called the APK Expansion Zip Library (located in <sdk>/extras/google/google_market_apk_expansion/zip_file/ ). This is an optional library that helps you read your expansion files when they're saved as ZIP files. Using this library allows you to easily read resources from your ZIP expansion files as a virtual file system.

The APK Expansion Zip Library includes the following classes and APIs:

APKExpansionSupport
Provides some methods to access expansion file names and ZIP files:
getAPKExpansionFiles()
The same method shown above that returns the complete file path to both expansion files.
getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion)
Returns a ZipResourceFile representing the sum of both the main file and patch file. That is, if you specify both the mainVersion and the patchVersion , this returns a ZipResourceFile that provides read access to all the data, with the patch file's data merged on top of the main file.
ZipResourceFile
Represents a ZIP file on the shared storage and performs all the work to provide a virtual file system based on your ZIP files. You can get an instance using APKExpansionSupport.getAPKExpansionZipFile() or with the ZipResourceFile by passing it the path to your expansion file. This class includes a variety of useful methods, but you generally don't need to access most of them. A couple of important methods are:
getInputStream(String assetPath)
Provides an InputStream to read a file within the ZIP file. The assetPath must be the path to the desired file, relative to the root of the ZIP file contents.
getAssetFileDescriptor(String assetPath)
Provides an AssetFileDescriptor for a file within the ZIP file. The assetPath must be the path to the desired file, relative to the root of the ZIP file contents. This is useful for certain Android APIs that require an AssetFileDescriptor , such as some MediaPlayer APIs.
APEZProvider
Most apps don't need to use this class. This class defines a ContentProvider that marshals the data from the ZIP files through a content provider Uri in order to provide file access for certain Android APIs that expect Uri access to media files. For example, this is useful if you want to play a video with VideoView.setVideoURI() .

Skipping ZIP compression of media files

If you're using your expansion files to store media files, a ZIP file still allows you to use Android media playback calls that provide offset and length controls (such as MediaPlayer.setDataSource() and SoundPool.load() ). In order for this to work, you must not perform additional compression on the media files when creating the ZIP packages. For example, when using the zip tool, you should use the -n option to specify the file suffixes that should not be compressed:

zip -n .mp4;.ogg main_expansion media_files

Reading from a ZIP file

When using the APK Expansion Zip Library, reading a file from your ZIP usually requires the following:

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);

The above code provides access to any file that exists in either your main expansion file or patch expansion file, by reading from a merged map of all the files from both files. All you need to provide the getAPKExpansionFile() method is your app android.content.Context and the version number for both the main expansion file and patch expansion file.

If you'd rather read from a specific expansion file, you can use the ZipResourceFile constructor with the path to the desired expansion file:

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);

For more information about using this library for your expansion files, look at the sample app's SampleDownloaderActivity class, which includes additional code to verify the downloaded files using CRC. Beware that if you use this sample as the basis for your own implementation, it requires that you declare the byte size of your expansion files in the xAPKS array.

Testing Your Expansion Files

Before publishing your app, there are two things you should test: Reading the expansion files and downloading the files.

Testing file reads

Before you upload your app to Google Play, you should test your app's ability to read the files from the shared storage. All you need to do is add the files to the appropriate location on the device shared storage and launch your app:

  1. On your device, create the appropriate directory on the shared storage where Google Play will save your files.

    For example, if your package name is com.example.android , you need to create the directory Android/obb/com.example.android/ on the shared storage space. (Plug in your test device to your computer to mount the shared storage and manually create this directory.)

  2. Manually add the expansion files to that directory. Be sure that you rename your files to match the file name format that Google Play will use.

    For example, regardless of the file type, the main expansion file for the com.example.android app should be main.0300110.com.example.android.obb . The version code can be whatever value you want. Just remember:

    • The main expansion file always starts with main and the patch file starts with patch .
    • The package name always matches that of the APK to which the file is attached on Google Play.
  3. Now that the expansion file(s) are on the device, you can install and run your app to test your expansion file(s).

Here are some reminders about handling the expansion files:

  • Do not delete or rename the .obb expansion files (even if you unpack the data to a different location). Doing so will cause Google Play (or your app itself) to repeatedly download the expansion file.
  • Do not save other data into your obb/ directory . If you must unpack some data, save it into the location specified by getExternalFilesDir() .

Testing file downloads

Because your app must sometimes manually download the expansion files when it first opens, it's important that you test this process to be sure your app can successfully query for the URLs, download the files, and save them to the device.

To test your app's implementation of the manual download procedure, you can publish it to the internal test track, so it's only available to authorized testers. If everything works as expected, your app should begin downloading the expansion files as soon as the main activity starts.

Note: Previously you could test an app by uploading an unpublished "draft" version. This functionality is no longer supported. Instead, you must publish it to an internal, closed, or open testing track. For more information, see Draft Apps are No Longer Supported .

Updating Your app

One of the great benefits to using expansion files on Google Play is the ability to update your app without re-downloading all of the original assets. Because Google Play allows you to provide two expansion files with each APK, you can use the second file as a "patch" that provides updates and new assets. Doing so avoids the need to re-download the main expansion file which could be large and expensive for users.

The patch expansion file is technically the same as the main expansion file and neither the Android system nor Google Play perform actual patching between your main and patch expansion files. Your app code must perform any necessary patches itself.

If you use ZIP files as your expansion files, the APK Expansion Zip Library that's included with the Apk Expansion package includes the ability to merge your patch file with the main expansion file.

Note: Even if you only need to make changes to the patch expansion file, you must still update the APK in order for Google Play to perform an update. If you don't require code changes in the app, you should simply update the versionCode in the manifest.

As long as you don't change the main expansion file that's associated with the APK in the Play Console, users who previously installed your app will not download the main expansion file. Existing users receive only the updated APK and the new patch expansion file (retaining the previous main expansion file).

Here are a few issues to keep in mind regarding updates to expansion files:

  • There can be only two expansion files for your app at a time. One main expansion file and one patch expansion file. During an update to a file, Google Play deletes the previous version (and so must your app when performing manual updates).
  • When adding a patch expansion file, the Android system does not actually patch your app or main expansion file. You must design your app to support the patch data. However, the Apk Expansion package includes a library for using ZIP files as expansion files, which merges the data from the patch file into the main expansion file so you can easily read all the expansion file data.