Как с легкостью создать установщик пакетов Android
https://t.me/ai_machinelearning_big_dataИногда требуется установить приложение на устройство не как пользователю, а как разработчику другого приложения. Возможно, вашему приложению, будь то магазин приложений или файловый менеджер, требуется самообновление, а вы его не опубликовали на Play Store. В любом случае вы обратитесь к стандартизированным интерфейсам (API) Android SDK, обеспечивающим установку APK (Android Package Kit). Но, как известно, Android-интерфейсы часто оказываются довольно трудоемкими в использовании.
Возьмем, к примеру, установку APK. Если вы вынуждены поддерживать версии Android ниже 5.0, то для разных версий Android придется использовать разные API: PackageInstaller для версий от 5.0 или какой-нибудь Intent с действием установки.
Способ Intent.ACTION_INSTALL_PACKAGE
Intent довольно прост в использовании. Достаточно создать его, запустить Activity для получения результата и обработать возвращенный код. Вот как обрабатывается установочный intent с помощью API AndroidX Activity Result:
// регистрация лаунчера в Activity или Fragment val installLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val isInstallSuccessful = result.resultCode == RESULT_OK // затем проводятся действия в зависимости от полученного результата } // запуск intent, например, при нажатии на кнопку val intent = Intent().apply { action = Intent.ACTION_INSTALL_PACKAGE setDataAndType(apkUri, "application/vnd.android.package-archive") flags = Intent.FLAG_GRANT_READ_URI_PERMISSION putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) putExtra(Intent.EXTRA_RETURN_RESULT, true) } installLauncher.launch(intent)
Не забудьте объявить разрешение на установку в AndroidManifest:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
Все просто, но и ограничений достаточно (нет поддержки разделенных APK и указания причины неудачной установки), не говоря уже о том, что это действие устарело в Android Q и было заменено в пользу PackageInstaller. Кроме того, не обеспечивается поддержка content: URI на версиях Android ниже 7.0, а также нельзя использовать file: URI на версиях от 7.0 (иначе наступит аварийное завершение FileUriExposedException). Таким образом, для корректной работы на всех версиях необходимо преобразовывать URI и, возможно, даже создавать временную копию файла в зависимости от версии Android.
Способ PackageInstaller
В Android 5.0 компания Google представила PackageInstaller. Это API, который упрощает процесс установки и добавляет возможность установки разделенных APK.
PackageInstaller гораздо более надежен и позволяет создать полноценный магазин приложений или менеджер пакетов. Однако вместе с надежностью приходит и сложность.
Как выполнить установку с помощью PackageInstaller? Сначала необходимо создать сессию:
val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) val packageInstaller = context.packageManager.packageInstaller val sessionId = packageInstaller.createSession(sessionParams) val session = packageInstaller.openSession(sessionId)
Затем необходимо записать в нее используемые APK:
apkUris.forEachIndexed { index, apkUri -> context.contentResolver.openInputStream(apkUri).use { apkStream -> requireNotNull(apkStream) { "$apkUri: InputStream was null" } val sessionStream = session.openWrite("$index.apk", 0, -1) sessionStream.buffered().use { bufferedSessionStream -> apkStream.copyTo(bufferedSessionStream) bufferedSessionStream.flush() session.fsync(sessionStream) } } }
После этого нужно подтвердить изменения в сессии:
val receiverIntent = Intent(context, PackageInstallerStatusReceiver::class.java) val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT } else { PendingIntent.FLAG_UPDATE_CURRENT } val receiverPendingIntent = PendingIntent.getBroadcast(context, 0, receiverIntent, flags) session.commit(receiverPendingIntent.intentSender) session.close()
Что такое PackageInstallerStatusReceiver? Это BroadcastReceiver, который реагирует на события установки. Нужно не забыть зарегистрировать его в AndroidManifest, а также объявить разрешение на установку:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <receiver android:name=".PackageInstallerStatusReceiver" android:exported="false" />
А вот пример реализации PackageInstallerStatusReceiver:
class PackageInstallerStatusReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) when (status) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { // здесь мы обрабатываем подтверждение установки пользователем val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT) if (confirmationIntent != null) { context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } } PackageInstaller.STATUS_SUCCESS -> { // произвольные шаги после успешного выполнения операции } else -> { val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) println("PackageInstallerStatusReceiver: status=$status, message=$message") } } }
Это довольно сложный способ установки приложения.
Третий вариант
Существует еще один способ запуска сессии установки — это Intent.ACTION_VIEW. Но здесь мы его рассматривать не будем, поскольку он не дает результата установки и не имеет прямого отношения к установке пакетов.
Мы рассмотрели различные способы установки приложений. Но это только вершина айсберга. А как быть, если нужно:
- обработать завершение процесса, инициированное системой;
- узнать точную причину неудачи установки;
- получать обновления о ходе установки во время активной сессии установки;
- отложить подтверждение установки пользователем с помощью уведомления?
Есть ли более простой способ сделать все это, не задумываясь обо всех деталях и не написав много кода? Да, и это библиотека Ackpine.
Ackpine — это библиотека, предоставляющая согласованные API для установки и удаления приложений на Android-устройствах. Она проста в использовании, надежна и обеспечивает все то, о чем говорилось выше.
Она поддерживает как Java, так и идиоматический Kotlin с интеграцией корутин “из коробки”.
В качестве источника APK-файлов Ackpine использует Uri, что позволяет подключить практически любые APK-источники через ContentProviders и сделать их устойчивыми. Библиотека использует эту фичу для обеспечения возможности установки разделенных APK, заархивированных в формате zip, без их извлечения.
Посмотрите простой пример установки приложения на Kotlin с помощью Ackpine:
try { when (val result = PackageInstaller.getInstance(context).createSession(apkUri).await()) { is SessionResult.Success -> println("Success") is SessionResult.Error -> println(result.cause.message) } } catch (_: CancellationException) { println("Cancelled") } catch (exception: Exception) { println(exception) }
Разумеется, это “голый” пример. Нам необходимо учесть завершение процесса, а также настроить сессию. Последнее очень легко сделать с помощью DSL-средств Kotlin:
val session = PackageInstaller.getInstance(context).createSession(baseApkUri) { apks += apkSplitsUris confirmation = Confirmation.DEFERRED installerType = InstallerType.SESSION_BASED name = fileName requireUserAction = false notification { title = NotificationString.resource(R.string.install_message_title) contentText = NotificationString.resource(R.string.install_message, fileName) icon = R.drawable.ic_install } }
А для обработки завершения процесса можно написать что-то вроде этого:
savedStateHandle[SESSION_ID_KEY] = session.id // после рестарта процесса val id: UUID? = savedStateHandle[SESSION_ID_KEY] if (id != null) { val result = packageInstaller.getSession(id)?.await() // или какие-либо другие действия, относящиеся к сессии }
Кроме того, Ackpine предоставляет утилиты, позволяющие легко работать с разделенными APK, заархивированными в формате zip (такими как APKS, APKM и XAPK):
val splits = ZippedApkSplits.getApksForUri(zippedFileUri, context) // чтение APK из zip-файла .filterCompatible(context) // фильтрация по наиболее подходящим разделенным вариантам .throwOnInvalidSplitPackage() val splitsList = try { splits.toList() } catch (exception: SplitPackageException) { println(exception) emptyList() }
Получать обновления о ходе установки очень просто:
session.progress .onEach { progress -> println("Got session's progress: $progress") } .launchIn(someCoroutineScope)
Репозиторий библиотеки можно найти на GitHub здесь. Он содержит примеры проектов как на Java, так и на Kotlin. Cайт проекта с документацией находится здесь.