diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt index 86fe2408a..75cbe6d09 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt @@ -77,8 +77,8 @@ class HttpClient { copied += rc if (rc > 0) { downloadTo.write(buffer, 0, rc) + emitProgress(copied) } - emitProgress(copied) } while (rc > 0) } finally { ByteArrayPool.recycle(buffer) diff --git a/vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt b/vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt new file mode 100644 index 000000000..8302158a4 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt @@ -0,0 +1,105 @@ +package org.microg.vending.ui + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager.NameNotFoundException +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.android.vending.R +import org.microg.gms.ui.TAG +import org.microg.vending.enterprise.CommitingSession +import org.microg.vending.enterprise.Downloading +import org.microg.vending.enterprise.InstallComplete +import org.microg.vending.enterprise.InstallError +import org.microg.vending.enterprise.InstallProgress + +private const val INSTALL_NOTIFICATION_CHANNEL_ID = "packageInstall" + +internal fun Context.notifySplitInstallProgress(packageName: String, sessionId: Int, progress: InstallProgress) { + + val label = try { + packageManager.getPackageInfo(packageName, 0).applicationInfo + .loadLabel(packageManager) + } catch (e: NameNotFoundException) { + Log.e(TAG, "Couldn't load label for $packageName (${e.message}). Is it not installed?") + return + } + + createNotificationChannel() + + val notificationManager = NotificationManagerCompat.from(this) + + when (progress) { + is Downloading -> getDownloadNotificationBuilder().apply { + setContentTitle(getString(R.string.installer_notification_progress_splitinstall_downloading, label)) + setProgress(progress.bytesDownloaded.toInt(), progress.bytesTotal.toInt(), false) + } + CommitingSession -> getDownloadNotificationBuilder().apply { + setContentTitle(getString(R.string.installer_notification_progress_splitinstall_commiting, label)) + setProgress(0, 1, true) + } + else -> null.also { notificationManager.cancel(sessionId) } + }?.apply { + setOngoing(true) + + notificationManager.notify(sessionId, this.build()) + } + +} + +internal fun Context.notifyInstallProgress(displayName: String, sessionId: Int, progress: InstallProgress) { + + createNotificationChannel() + getDownloadNotificationBuilder().apply { + when (progress) { + is Downloading -> { + setContentTitle(getString(R.string.installer_notification_progress_downloading, displayName)) + setProgress(progress.bytesTotal.toInt(), progress.bytesDownloaded.toInt(), false) + setOngoing(true) + } + CommitingSession -> { + setContentTitle(getString(R.string.installer_notification_progress_commiting, displayName)) + setProgress(0, 0, true) + setOngoing(true) + } + InstallComplete -> { + setContentTitle(getString(R.string.installer_notification_progress_complete, displayName)) + setSmallIcon(android.R.drawable.stat_sys_download_done) + } + is InstallError -> { + setContentTitle(getString(R.string.installer_notification_progress_failed, displayName)) + setSmallIcon(android.R.drawable.stat_notify_error) + } + } + + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(sessionId, this.build()) + } + +} + +private fun Context.getDownloadNotificationBuilder() = + NotificationCompat.Builder(this, INSTALL_NOTIFICATION_CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setLocalOnly(true) + +private fun Context.createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel( + NotificationChannel( + INSTALL_NOTIFICATION_CHANNEL_ID, + getString(R.string.installer_notification_channel_description), + NotificationManager.IMPORTANCE_LOW + ).apply { + enableVibration(false) + enableLights(false) + setShowBadge(false) + } + ) + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 7b0062d48..68ae397c0 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -111,12 +111,22 @@ class VendingActivity : ComponentActivity() { } runCatching { + + var lastNotification = 0L installPackagesFromNetwork( packageName = app.packageName, components = downloadUrls.getOrThrow(), httpClient = client, isUpdate = isUpdate - ) { progress -> + ) { session, progress -> + + + // Android rate limits notification updates by some vague rule of "not too many in less than one second" + if (progress !is Downloading || lastNotification + 250 < System.currentTimeMillis()) { + notifyInstallProgress(app.displayName, session, progress) + lastNotification = System.currentTimeMillis() + } + if (progress is Downloading) apps[app] = progress else if (progress is CommitingSession) apps[app] = Pending } diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt index 6b270df1b..27446ff9a 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt @@ -32,7 +32,7 @@ internal suspend fun Context.installPackages( packageName = packageName, componentNames = componentFiles.map { it.name }, isUpdate = isUpdate -) { fileName, to -> +) { session, fileName, to -> val component = componentFiles.find { it.name == fileName }!! FileInputStream(component).use { it.copyTo(to) } component.delete() @@ -44,7 +44,7 @@ internal suspend fun Context.installPackagesFromNetwork( components: List, httpClient: HttpClient = HttpClient(), isUpdate: Boolean = false, - emitProgress: (InstallProgress) -> Unit = {} + emitProgress: (session: Int, InstallProgress) -> Unit = { _, _ -> } ) { val downloadProgress = mutableMapOf() @@ -54,12 +54,12 @@ internal suspend fun Context.installPackagesFromNetwork( componentNames = components.map { it.componentName }, isUpdate = isUpdate, emitProgress = emitProgress, - ) { fileName, to -> + ) { session, fileName, to -> val component = components.find { it.componentName == fileName }!! Log.v(TAG, "installing $fileName for $packageName from network") httpClient.download(component.url, to) { progress -> downloadProgress[component] = progress - emitProgress(Downloading( + emitProgress(session, Downloading( bytesDownloaded = downloadProgress.values.sum(), bytesTotal = components.sumOf { it.size } )) @@ -72,8 +72,8 @@ private suspend fun Context.installPackagesInternal( packageName: String, componentNames: List, isUpdate: Boolean = false, - emitProgress: (InstallProgress) -> Unit = {}, - writeComponent: suspend (componentName: String, to: OutputStream) -> Unit + emitProgress: (session: Int, InstallProgress) -> Unit = { _, _ -> }, + writeComponent: suspend (session: Int, componentName: String, to: OutputStream) -> Unit ) { Log.v(TAG, "installPackages start") @@ -87,7 +87,7 @@ private suspend fun Context.installPackagesInternal( else SessionParams.MODE_INHERIT_EXISTING ) params.setAppPackageName(packageName) - params.setAppLabel(packageName + "Subcontracting") + params.setAppLabel(packageName) params.setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) try { @SuppressLint("PrivateApi") val method = SessionParams::class.java.getDeclaredMethod( @@ -104,7 +104,7 @@ private suspend fun Context.installPackagesInternal( session = packageInstaller.openSession(sessionId) for (component in componentNames) { session.openWrite(component, 0, -1).use { outputStream -> - writeComponent(component, outputStream) + writeComponent(sessionId, component, outputStream) session!!.fsync(outputStream) } } @@ -113,11 +113,11 @@ private suspend fun Context.installPackagesInternal( SessionResultReceiver.pendingSessions[sessionId] = SessionResultReceiver.OnResult( onSuccess = { deferred.complete(Unit) - emitProgress(InstallComplete) + emitProgress(sessionId, InstallComplete) }, onFailure = { message -> deferred.completeExceptionally(RuntimeException(message)) - emitProgress(InstallError(message ?: "UNKNOWN")) + emitProgress(sessionId, InstallError(message ?: "UNKNOWN")) } ) @@ -125,7 +125,7 @@ private suspend fun Context.installPackagesInternal( val pendingIntent = PendingIntent.getBroadcast(this, sessionId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) - emitProgress(CommitingSession) + emitProgress(sessionId, CommitingSession) session.commit(pendingIntent.intentSender) // don't abandon if `finally` step is reached after this point session = null @@ -136,8 +136,10 @@ private suspend fun Context.installPackagesInternal( Log.w(TAG, "Error installing packages", e) throw e } finally { - Log.d(TAG, "Discarding session after error") // discard downloaded data - session?.abandon() + session?.let { + Log.d(TAG, "Discarding session after error") + it.abandon() + } } } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt index f939a0222..ea8b724c4 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt @@ -4,19 +4,13 @@ */ package com.google.android.finsky.splitinstallservice -import android.app.NotificationChannel -import android.app.NotificationManager import android.content.Context import android.content.Intent -import android.content.pm.PackageManager.NameNotFoundException import android.os.Build import android.os.Bundle import android.util.Log import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat import androidx.core.content.pm.PackageInfoCompat -import com.android.vending.R import com.android.vending.installer.KEY_BYTES_DOWNLOADED import com.android.vending.installer.installPackagesFromNetwork import kotlinx.coroutines.CompletableDeferred @@ -25,12 +19,10 @@ import kotlinx.coroutines.withContext import org.microg.vending.billing.AuthManager import org.microg.vending.billing.core.HttpClient import org.microg.vending.delivery.requestDownloadUrls +import org.microg.vending.enterprise.Downloading import org.microg.vending.splitinstall.SPLIT_LANGUAGE_TAG +import org.microg.vending.ui.notifySplitInstallProgress -private const val SPLIT_INSTALL_NOTIFY_ID = 111 - -private const val NOTIFY_CHANNEL_ID = "splitInstall" -private const val NOTIFY_CHANNEL_NAME = "Split Install" private const val KEY_LANGUAGE = "language" private const val KEY_LANGUAGES = "languages" private const val KEY_MODULE_NAME = "module_name" @@ -69,7 +61,6 @@ class SplitInstallManager(val context: Context) { if (authData?.authToken.isNullOrEmpty()) return false authData!! - notify(callingPackage) val components = runCatching { httpClient.requestDownloadUrls( @@ -83,7 +74,6 @@ class SplitInstallManager(val context: Context) { }.getOrNull() Log.v(TAG, "splitInstallFlow requestDownloadUrls returned these components: $components") if (components.isNullOrEmpty()) { - NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) return false } @@ -92,17 +82,24 @@ class SplitInstallManager(val context: Context) { } val success = runCatching { + + var lastNotification = 0L context.installPackagesFromNetwork( packageName = callingPackage, components = components, httpClient = httpClient, isUpdate = false - ) + ) { session, progress -> + // Android rate limits notification updates by some vague rule of "not too many in less than one second" + if (progress !is Downloading || lastNotification + 250 < System.currentTimeMillis()) { + context.notifySplitInstallProgress(callingPackage, session, progress) + lastNotification = System.currentTimeMillis() + } + } }.isSuccess - NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) return if (success) { - sendCompleteBroad(context, callingPackage, components.sumOf { it.size.toLong() }) + sendCompleteBroad(context, callingPackage, components.sumOf { it.size }) components.forEach { splitInstallRecord[it] = DownloadStatus.COMPLETE } true } else { @@ -123,35 +120,6 @@ class SplitInstallManager(val context: Context) { } ?: true } - /** - * Tell user about the ongoing download. - * TODO: make persistent - */ - internal fun notify(installForPackage: String) { - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - notificationManager.createNotificationChannel( - NotificationChannel(NOTIFY_CHANNEL_ID, NOTIFY_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) - ) - } - - val label = try { - context.packageManager.getPackageInfo(installForPackage, 0).applicationInfo - .loadLabel(context.packageManager) - } catch (e: NameNotFoundException) { - Log.e(TAG, "Couldn't load label for $installForPackage (${e.message}). Is it not installed?") - return - } - - NotificationCompat.Builder(context, NOTIFY_CHANNEL_ID).setSmallIcon(android.R.drawable.stat_sys_download) - .setContentTitle(context.getString(R.string.split_install, label)).setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setDefaults( - NotificationCompat.DEFAULT_ALL - ).build().also { - notificationManager.notify(SPLIT_INSTALL_NOTIFY_ID, it) - } - } - private fun sendCompleteBroad(context: Context, packageName: String, bytes: Long) { Log.d(TAG, "sendCompleteBroadcast: $bytes bytes") val extra = Bundle().apply { diff --git a/vending-app/src/main/res/values-zh-rCN/strings.xml b/vending-app/src/main/res/values-zh-rCN/strings.xml index f0554d99e..01c631e37 100644 --- a/vending-app/src/main/res/values-zh-rCN/strings.xml +++ b/vending-app/src/main/res/values-zh-rCN/strings.xml @@ -18,5 +18,5 @@ 如果应用出现异常,请登录您购买该应用所使用的 Google 帐号。 登录 忽略 - 正在下载 %s 所需的组件 + 正在下载 %s 所需的组件 \ No newline at end of file diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index 0e617924d..a255639d2 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -42,5 +42,13 @@ Forget password? Learn more Verify - Downloading required components for %s + + Shows app and component installation progress. + Downloading \"%s\" + Installing \"%s\" + Installed \"%s\" + Failed to install \"%s\" + + Downloading required components for %s + Installing required components for %s