Файлы дополнений APK

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

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

Если вы не хотите использовать файлы дополнений, а размер сжатого скачиваемого файла приложения превышает 100 МБ, загрузите приложение с помощью Android App Bundle. Это позволит увеличить размер сжатого скачиваемого файла до 500 МБ. Кроме того, поскольку при использовании наборов App Bundle создание и подписание 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-файлов не требуется. В следующем фрагменте кода показано, как проверить наличие доступа для чтения, прежде чем запрашивать разрешение на доступ к внешнему хранилищу:

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(). Однако по возможности лучше использовать формат файла дополнения, который позволяет читать данные непосредственно из файла, а не распаковывать их. Например, мы предоставляем проект библиотеки под названием 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. Если нет, то приложение должно скачать файлы расширения из Google Play по протоколу HTTP. Приложение должно отправить запрос клиенту Google Play, используя сервис лицензирования приложений Google Play, который возвращает название, размер файла и URL для каждого файла дополнения. Затем вы скачиваете файлы и сохраняете их в нужном хранилище.

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

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

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

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

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

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

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

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

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

    Чтобы значительно сократить объем кода, который вам нужно написать, и обеспечить удобство для пользователей во время скачивания, мы рекомендуем использовать библиотеку 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 файлов расширения от сервиса лицензирования.

О библиотеке Downloader

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

Чтобы реализовать скачивание файлов дополнений с помощью библиотеки Downloader Library, выполните следующие действия:

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

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

Подготовка к использованию библиотеки Downloader Library

Чтобы использовать библиотеку Downloader Library, скачайте два пакета из SDK Manager и добавьте нужные библиотеки в приложение.

Сначала откройте Android SDK Manager (Tools (Инструменты) > SDK Manager) и в разделе Appearance & Behavior (Внешний вид и поведение) > System Settings (Системные настройки) > Android SDK выберите вкладку SDK Tools, чтобы выбрать и скачать:

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

Создайте новый модуль библиотеки для License Verification Library и Downloader Library. Для каждой библиотеки:

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

Примечание. Библиотека Downloader Library зависит от библиотеки License Verification 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
    

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

Примечание. В пакет APK Expansion входит пример приложения, в котором показано, как использовать библиотеку Downloader Library. В этом примере используется третья библиотека, доступная в пакете APK Expansion, которая называется APK Expansion Zip Library. Если вы планируете использовать ZIP-файлы в качестве файлов дополнений, рекомендуем добавить в приложение библиотеку APK Expansion Zip Library. Подробнее о том, как использовать библиотеку APK Expansion Zip Library…

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

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

<manifest ...>
    <!-- Required to access Google Play Licensing -->
    <uses-permission android:name="com.android.vending.CHECK_LICENSE" />

    <!-- Required to download files from Google Play -->
    <uses-permission android:name="android.permission.INTERNET" />

    <!-- Required to keep CPU alive while downloading files
        (NOT to keep screen awake) -->
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <!-- Required to poll the state of the network connection
        and respond to changes -->
    <uses-permission
        android:name="android.permission.ACCESS_NETWORK_STATE" />

    <!-- Required to check whether Wi-Fi is enabled -->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>

    <!-- Required to read and write the expansion files on shared storage -->
    <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

Примечание. По умолчанию для библиотеки загрузчика требуется уровень API 4, а для библиотеки ZIP-файлов расширения APK – уровень API 5.

Реализация сервиса скачивания

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

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

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

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

Вот пример полной реализации DownloaderService:

Kotlin

// You must use the public key belonging to your publisher account
const val BASE64_PUBLIC_KEY = "YourLVLKey"
// You should also modify this salt
val SALT = byteArrayOf(
        1, 42, -12, -1, 54, 98, -100, -12, 43, 2,
        -8, -4, 9, 5, -106, -107, -33, 45, -1, 84
)

class SampleDownloaderService : DownloaderService() {

    override fun getPublicKey(): String = BASE64_PUBLIC_KEY

    override fun getSALT(): ByteArray = SALT

    override fun getAlarmReceiverClassName(): String = SampleAlarmReceiver::class.java.name
}

Java

public class SampleDownloaderService extends DownloaderService {
    // You must use the public key belonging to your publisher account
    public static final String BASE64_PUBLIC_KEY = "YourLVLKey";
    // You should also modify this salt
    public static final byte[] SALT = new byte[] { 1, 42, -12, -1, 54, 98,
            -100, -12, 43, 2, -8, -4, 9, 5, -106, -107, -33, 45, -1, 84
    };

    @Override
    public String getPublicKey() {
        return BASE64_PUBLIC_KEY;
    }

    @Override
    public byte[] getSALT() {
        return SALT;
    }

    @Override
    public String getAlarmReceiverClassName() {
        return SampleAlarmReceiver.class.getName();
    }
}

Примечание. В качестве значения BASE64_PUBLIC_KEY необходимо указать открытый ключ, принадлежащий вашему аккаунту издателя. Ключ можно найти в Developer Console в разделе с информацией профиля. Это необходимо даже при тестировании скачиваний.

Не забудьте объявить службу в файле манифеста:

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

Реализация приемника оповещений

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

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

Пример:

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 есть несколько API в классе Helper, которые помогают в этом процессе:

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

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

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

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

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

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

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

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

Получение статуса скачивания

Чтобы получать информацию о ходе скачивания и взаимодействовать с DownloaderService, необходимо реализовать интерфейс IDownloaderClient библиотеки Downloader Library. Обычно объект activity, который вы используете для запуска скачивания, должен реализовывать этот интерфейс, чтобы показывать ход скачивания и отправлять запросы сервису.

Для IDownloaderClient требуются следующие методы интерфейса:

onServiceConnected(Messenger m)
После создания экземпляра IStub в действии вы получите вызов этого метода, который передает объект Messenger, связанный с вашим экземпляром DownloaderService. Чтобы отправлять запросы сервису, например приостанавливать и возобновлять скачивание, вызовите 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 будет одним из нескольких возможных значений, указанных одной из констант STATE_* класса IDownloaderClient.

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

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

Совет. Примеры обратных вызовов, которые обновляют интерфейс скачивания, можно найти в файле SampleDownloaderActivity в примере приложения, который поставляется вместе с пакетом APK Expansion.

Вот некоторые полезные общедоступные методы для интерфейса IDownloaderService:

requestPauseDownload()
Приостанавливает скачивание.
requestContinueDownload()
Возобновляет приостановленное скачивание.
setDownloadFlags(int flags)
Задает пользовательские настройки для типов сетей, в которых разрешено скачивать файлы. В текущей реализации поддерживается один флаг – FLAGS_DOWNLOAD_OVER_CELLULAR, но вы можете добавить и другие. По умолчанию этот флаг не включен, поэтому для скачивания файлов расширения пользователь должен быть подключен к сети 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, вам все равно нужно будет использовать класс APKExpansionPolicy, который предоставляется в библиотеке проверки лицензий. Класс APKExpansionPolicy практически идентичен классу ServerManagedPolicy (доступен в библиотеке проверки лицензий Google Play), но включает дополнительную обработку дополнительных данных ответа файла дополнений APK.

Примечание. Если вы используете библиотеку Downloader Library, как описано в предыдущем разделе, она выполняет все взаимодействия с APKExpansionPolicy, поэтому вам не нужно использовать этот класс напрямую.

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

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

Дополнительную информацию о том, как использовать APKExpansionPolicy, если вы не используете библиотеку Downloader Library, можно найти в документации по добавлению лицензирования в приложение. В ней объясняется, как реализовать политику лицензирования, подобную этой.

Чтение файла дополнения

Способ чтения файлов дополнений APK зависит от их типа. Как говорилось в обзоре, файлы расширения могут быть любого типа, но они переименовываются в соответствии с определенным форматом и сохраняются в папке <shared-storage>/Android/obb/<package-name>/.

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

Примечание. При запуске приложения всегда проверяйте, доступно ли внешнее хранилище и можно ли считывать с него данные. Для этого вызывайте метод getExternalStorageState(). Этот метод возвращает одну из нескольких возможных строк, представляющих состояние внешнего хранилища. Чтобы приложение могло его прочитать, возвращаемое значение должно быть MEDIA_MOUNTED.

Получение названий файлов

Как описано в обзоре, файлы дополнений APK сохраняются в определенном формате:

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

Чтобы узнать местоположение и названия файлов расширения, используйте методы getExternalStorageDirectory() и getPackageName(). Они позволяют создать путь к файлам.

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

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 вашего приложения и нужную версию файла дополнения.

Номер версии файла дополнения можно определить разными способами. Простой способ – сохранить версию в файле SharedPreferences в начале скачивания, запросив название файла расширения с помощью метода getExpansionFileName(int index) класса APKExpansionPolicy. Затем вы можете получить код версии, прочитав файл 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, представляющее сумму размеров основного файла и файла исправления. То есть если вы укажете и mainVersion, и patchVersion, то получите ZipResourceFile, который предоставляет доступ для чтения ко всем данным, причем данные файла исправления будут объединены с данными основного файла.
ZipResourceFile
Представляет ZIP-файл в общем хранилище и выполняет все действия, необходимые для создания виртуальной файловой системы на основе ZIP-файлов. Вы можете получить экземпляр с помощью APKExpansionSupport.getAPKExpansionZipFile() или ZipResourceFile, передав путь к файлу дополнения. Этот класс включает в себя множество полезных методов, но в большинстве случаев вам не потребуется доступ к ним. Вот несколько важных методов:
getInputStream(String assetPath)
Предоставляет InputStream для чтения файла в ZIP-архиве. В качестве значения assetPath необходимо указать путь к нужному файлу относительно корневого каталога ZIP-архива.
getAssetFileDescriptor(String assetPath)
Предоставляет AssetFileDescriptor для файла в ZIP-архиве. Вместо assetPath нужно указать путь к нужному файлу относительно корневой папки ZIP-архива. Это полезно для некоторых API Android, которым требуется AssetFileDescriptor, например для некоторых API MediaPlayer.
APEZProvider
В большинстве приложений этот класс не используется. Этот класс определяет ContentProvider, который упорядочивает данные из ZIP-файлов через поставщика контента Uri, чтобы обеспечить доступ к файлам для определенных API Android, которым требуется доступ Uri к медиафайлам. Например, это может быть полезно, если вы хотите воспроизвести видео с помощью VideoView.setVideoURI().

Пропуск сжатия ZIP-архива медиафайлов

Если вы используете файлы дополнений для хранения медиафайлов, ZIP-файл по-прежнему позволяет использовать вызовы воспроизведения мультимедиа Android, которые обеспечивают управление смещением и длиной (например, MediaPlayer.setDataSource() и SoundPool.load()). Чтобы это работало, вы не должны выполнять дополнительное сжатие медиафайлов при создании ZIP-пакетов. Например, при использовании инструмента zip вы должны использовать параметр -n, чтобы указать суффиксы файлов, которые не должны быть сжаты:

zip -n .mp4;.ogg main_expansion media_files

Чтение из ZIP-файла

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

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

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

Тестирование файлов дополнений

Перед публикацией приложения необходимо протестировать чтение и скачивание файлов дополнений.

Тестирование чтения файлов

Прежде чем загружать приложение в 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.
    • Название пакета всегда совпадает с названием APK-файла, к которому прикреплен файл в Google Play.
  3. Теперь, когда файлы расширения находятся на устройстве, вы можете установить и запустить приложение, чтобы протестировать их.

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

  • Не удаляйте и не переименовывайте файлы расширения .obb, даже если вы распаковываете данные в другое место. В противном случае Google Play или само приложение будет постоянно скачивать файл дополнения.
  • Не сохраняйте другие данные в каталоге obb/. Если вам нужно распаковать некоторые данные, сохраните их в папке, указанной в переменной getExternalFilesDir().

Тестирование скачивания файлов

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

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

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

Обновление приложения

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

Файл исправления дополнений технически не отличается от основного файла дополнений. Ни система Android, ни Google Play не выполняют исправление между основным файлом дополнений и файлом исправления дополнений. Код приложения должен самостоятельно выполнять все необходимые исправления.

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

Примечание. Даже если вам нужно изменить только файл исправления дополнений, вы все равно должны обновить APK-файл, чтобы Google Play выполнил обновление. Если изменения в коде приложения не требуются, просто обновите versionCode в манифесте.

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

При обновлении файлов расширения следует учитывать следующее:

  • Для одного приложения можно использовать не более двух файлов дополнений. Один основной файл дополнений и один файл исправления дополнений. При обновлении файла Google Play удаляет предыдущую версию. Ваше приложение должно делать то же самое при обновлении вручную.
  • При добавлении файла исправления дополнений система Android не исправляет приложение или основной файл дополнений. Приложение должно поддерживать данные исправлений. Однако в пакет APK Expansion входит библиотека для использования ZIP-файлов в качестве файлов дополнений. Она объединяет данные из файла исправления с основным файлом дополнений, чтобы вы могли легко прочитать все данные из файла дополнений.