diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ae7513c89..3a7800276 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -145,8 +145,8 @@ android:name=".service.DownloadService" android:foregroundServiceType="dataSync" /> - () { id = Constants.NOTIFICATION_CHANNEL_DOWNLOADING, name = getString(stringRes.downloading), ) + createNotificationChannel( + id = NOTIFICATION_CHANNEL_INSTALL, + name = getString(R.string.install) + ) lifecycleScope.launch { _downloadState @@ -267,20 +278,16 @@ class DownloadService : ConnectionService() { .putExtra(MainActivity.EXTRA_CACHE_FILE_NAME, task.release.cacheFileName) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) .toPendingIntent(this) - notificationManager?.notify( - task.notificationTag, - Constants.NOTIFICATION_ID_INSTALL, - NotificationCompat - .Builder(this, Constants.NOTIFICATION_CHANNEL_INSTALL) - .setAutoCancel(true) - .setOngoing(false) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setColor(Color.GREEN) - .setOnlyAlertOnce(true) - .setContentIntent(intent) - .setContentTitle(getString(stringRes.downloaded_FORMAT, task.name)) - .setContentText(getString(stringRes.tap_to_install_DESC)) - .build() + val notification = createInstallNotification( + appName = task.name, + state = InstallState.Pending, + autoCancel = true, + ) { + setContentIntent(intent) + } + notificationManager?.installNotification( + packageName = task.packageName, + notification = notification, ) } diff --git a/core/common/src/main/res/values/strings.xml b/core/common/src/main/res/values/strings.xml index 3e8ec94ce..a6bb7b5ac 100644 --- a/core/common/src/main/res/values/strings.xml +++ b/core/common/src/main/res/values/strings.xml @@ -106,6 +106,10 @@ Shizuku is not installed Installed Installing + Installation Failed + Failed to install %s + Uninstalled + %s has been uninstalled Could not check integrity. Invalid address Invalid fingerprint format diff --git a/installer/src/main/java/com/looker/installer/InstallManager.kt b/installer/src/main/java/com/looker/installer/InstallManager.kt index e24653f5c..ac3b0d853 100644 --- a/installer/src/main/java/com/looker/installer/InstallManager.kt +++ b/installer/src/main/java/com/looker/installer/InstallManager.kt @@ -1,11 +1,9 @@ package com.looker.installer import android.content.Context -import com.looker.core.common.Constants import com.looker.core.common.PackageName import com.looker.core.common.extension.addAndCompute import com.looker.core.common.extension.filter -import com.looker.core.common.extension.notificationManager import com.looker.core.common.extension.updateAsMutable import com.looker.core.datastore.SettingsRepository import com.looker.core.datastore.get @@ -28,7 +26,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -// TODO: Fix the stuck state, and other installer class InstallManager( private val context: Context, settingsRepository: SettingsRepository @@ -92,10 +89,6 @@ class InstallManager( it.install(item) } updateState { put(item.packageName, success) } - context.notificationManager?.cancel( - "download-${item.packageName.name}", - Constants.NOTIFICATION_ID_DOWNLOADING - ) currentQueue.remove(item.packageName.name) } } diff --git a/installer/src/main/java/com/looker/installer/installers/session/SessionInstaller.kt b/installer/src/main/java/com/looker/installer/installers/session/SessionInstaller.kt index 44d7abe2a..fb0dba33b 100644 --- a/installer/src/main/java/com/looker/installer/installers/session/SessionInstaller.kt +++ b/installer/src/main/java/com/looker/installer/installers/session/SessionInstaller.kt @@ -23,7 +23,7 @@ import kotlin.coroutines.resume internal class SessionInstaller(private val context: Context) : Installer { private val installer = context.packageManager.packageInstaller - private val intent = Intent(context, SessionInstallerService::class.java) + private val intent = Intent(context, SessionInstallerReceiver::class.java) companion object { private var installerCallbacks: PackageInstaller.SessionCallback? = null @@ -73,7 +73,7 @@ internal class SessionInstaller(private val context: Context) : Installer { } } - val pendingIntent = PendingIntent.getService(context, id, intent, flags) + val pendingIntent = PendingIntent.getBroadcast(context, id, intent, flags) if (cont.isActive) activeSession.commit(pendingIntent.intentSender) } @@ -90,8 +90,8 @@ internal class SessionInstaller(private val context: Context) : Installer { @SuppressLint("MissingPermission") override suspend fun uninstall(packageName: PackageName) = suspendCancellableCoroutine { cont -> - intent.putExtra(SessionInstallerService.ACTION_UNINSTALL, true) - val pendingIntent = PendingIntent.getService(context, -1, intent, flags) + intent.putExtra(SessionInstallerReceiver.ACTION_UNINSTALL, true) + val pendingIntent = PendingIntent.getBroadcast(context, -1, intent, flags) installer.uninstall(packageName.name, pendingIntent.intentSender) cont.resume(Unit) diff --git a/installer/src/main/java/com/looker/installer/installers/session/SessionInstallerReceiver.kt b/installer/src/main/java/com/looker/installer/installers/session/SessionInstallerReceiver.kt index e48194195..b037a7bd3 100644 --- a/installer/src/main/java/com/looker/installer/installers/session/SessionInstallerReceiver.kt +++ b/installer/src/main/java/com/looker/installer/installers/session/SessionInstallerReceiver.kt @@ -4,17 +4,27 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.PackageInstaller -import android.graphics.Color -import androidx.core.app.NotificationCompat import com.looker.core.common.Constants.NOTIFICATION_CHANNEL_INSTALL -import com.looker.core.common.Constants.NOTIFICATION_ID_DOWNLOADING -import com.looker.core.common.Constants.NOTIFICATION_ID_INSTALL import com.looker.core.common.R import com.looker.core.common.createNotificationChannel import com.looker.core.common.extension.getPackageName import com.looker.core.common.extension.notificationManager +import com.looker.core.common.toPackageName +import com.looker.installer.InstallManager +import com.looker.installer.model.InstallState +import com.looker.installer.notification.createInstallNotification +import com.looker.installer.notification.installNotification +import com.looker.installer.notification.removeInstallNotification +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +@AndroidEntryPoint class SessionInstallerReceiver : BroadcastReceiver() { + + // This is a cyclic dependency injection, I know but this is the best option for now + @Inject + lateinit var installManager: InstallManager + override fun onReceive(context: Context, intent: Intent) { val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) @@ -50,55 +60,47 @@ class SessionInstallerReceiver : BroadcastReceiver() { val appName = packageManager.getPackageName(packageName) - val notificationTag = "download-$packageName" - - val builder = NotificationCompat - .Builder(context, NOTIFICATION_CHANNEL_INSTALL) - .setAutoCancel(true) - - when(status) { - PackageInstaller.STATUS_SUCCESS -> { - if (isUninstall) { - // remove any notification for this app - notificationManager?.cancel(notificationTag, NOTIFICATION_ID_INSTALL) - } else { - val notification = builder - .setSmallIcon(R.drawable.ic_check) - .setColor(Color.GREEN) - .setContentTitle("Installed") - .setTimeoutAfter(5_000) - .setContentText(appName) - .build() - notificationManager?.notify( - notificationTag, - NOTIFICATION_ID_INSTALL, - notification + if (packageName != null) { + when (status) { + PackageInstaller.STATUS_SUCCESS -> { + notificationManager?.removeInstallNotification(packageName) + val notification = context.createInstallNotification( + appName = (appName ?: packageName.substringAfterLast('.')).toString(), + state = InstallState.Installed, + isUninstall = isUninstall, + ) { + setTimeoutAfter(SUCCESS_TIMEOUT) + } + notificationManager?.installNotification( + packageName = packageName.toString(), + notification = notification, ) } - } - PackageInstaller.STATUS_FAILURE_ABORTED -> { - // do nothing if user cancels - } + PackageInstaller.STATUS_FAILURE_ABORTED -> { + installManager.remove(packageName.toPackageName()) + } - else -> { - // problem occurred when installing/uninstalling package - val notification = builder - .setSmallIcon(android.R.drawable.stat_notify_error) - .setColor(Color.GREEN) - .setContentTitle("Unknown Error") - .setContentText(message) - .build() - notificationManager?.notify( - notificationTag, - NOTIFICATION_ID_DOWNLOADING, - notification - ) + else -> { + installManager.remove(packageName.toPackageName()) + val notification = context.createInstallNotification( + appName = appName.toString(), + state = InstallState.Failed, + ) { + setContentText(message) + } + notificationManager?.installNotification( + packageName = packageName, + notification = notification + ) + } } } } companion object { - private const val ACTION_UNINSTALL = "action_uninstall" + const val ACTION_UNINSTALL = "action_uninstall" + + private const val SUCCESS_TIMEOUT = 5_000L } } diff --git a/installer/src/main/java/com/looker/installer/installers/session/SessionInstallerService.kt b/installer/src/main/java/com/looker/installer/installers/session/SessionInstallerService.kt deleted file mode 100644 index 95a48e2ac..000000000 --- a/installer/src/main/java/com/looker/installer/installers/session/SessionInstallerService.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.looker.installer.installers.session - -import android.app.Service -import android.content.Intent -import android.content.pm.PackageInstaller -import android.content.pm.PackageManager -import android.graphics.Color -import android.os.IBinder -import android.view.ContextThemeWrapper -import androidx.core.app.NotificationCompat -import com.looker.core.common.Constants.NOTIFICATION_CHANNEL_DOWNLOADING -import com.looker.core.common.Constants.NOTIFICATION_ID_DOWNLOADING -import com.looker.core.common.R as CommonR -import com.looker.core.common.extension.notificationManager - -class SessionInstallerService : Service() { - companion object { - const val ACTION_UNINSTALL = "action_uninstall" - } - - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) - - if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { - // prompts user to enable unknown source - val promptIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT) - - promptIntent?.let { - it.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) - it.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, "com.android.vending") - it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - startActivity(it) - } - } else { - notifyStatus(intent) - } - - stopSelf() - return START_NOT_STICKY - } - - override fun onBind(intent: Intent?): IBinder? = null - - /** - * Notifies user of installer outcome. - */ - private fun notifyStatus(intent: Intent) { - // unpack from intent - val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) - val name = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) - val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - val isUninstall = intent.getBooleanExtra(ACTION_UNINSTALL, false) - - // get application name for notifications - val appLabel = try { - if (name != null) { - packageManager.getApplicationLabel( - packageManager.getApplicationInfo( - name, - PackageManager.GET_META_DATA - ) - ) - } else { - null - } - } catch (_: Exception) { - null - } - - val notificationTag = "download-$name" - - // start building - val builder = NotificationCompat - .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING) - .setAutoCancel(true) - - when (status) { - PackageInstaller.STATUS_SUCCESS -> { - if (isUninstall) { - // remove any notification for this app - notificationManager?.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING) - } else { - val notification = builder - .setSmallIcon(CommonR.drawable.ic_check) - .setColor(Color.GREEN) - .setContentTitle("Installed") - .setContentText(appLabel) - .build() - notificationManager?.notify( - notificationTag, - NOTIFICATION_ID_DOWNLOADING, - notification - ) - } - } - - PackageInstaller.STATUS_FAILURE_ABORTED -> { - // do nothing if user cancels - } - - else -> { - // problem occurred when installing/uninstalling package - val notification = builder - .setSmallIcon(android.R.drawable.stat_notify_error) - .setColor(Color.GREEN) - .setContentTitle("Unknown Error") - .setContentText(message) - .build() - notificationManager?.notify( - notificationTag, - NOTIFICATION_ID_DOWNLOADING, - notification - ) - } - } - } -} diff --git a/installer/src/main/java/com/looker/installer/notification/InstallNotification.kt b/installer/src/main/java/com/looker/installer/notification/InstallNotification.kt new file mode 100644 index 000000000..7f48e5b03 --- /dev/null +++ b/installer/src/main/java/com/looker/installer/notification/InstallNotification.kt @@ -0,0 +1,91 @@ +package com.looker.installer.notification + +import android.app.Notification +import android.app.NotificationManager +import android.content.Context +import android.graphics.Color +import androidx.core.app.NotificationCompat +import com.looker.core.common.Constants.NOTIFICATION_CHANNEL_INSTALL +import com.looker.core.common.Constants.NOTIFICATION_ID_INSTALL +import com.looker.installer.model.InstallState +import com.looker.installer.model.InstallState.Failed +import com.looker.installer.model.InstallState.Installed +import com.looker.installer.model.InstallState.Installing +import com.looker.installer.model.InstallState.Pending +import com.looker.core.common.R as CommonR + +fun NotificationManager.installNotification( + packageName: String, + notification: Notification, +) { + notify( + installTag(packageName), + NOTIFICATION_ID_INSTALL, + notification + ) +} + +fun NotificationManager.removeInstallNotification( + packageName: String, +) { + cancel(installTag(packageName), NOTIFICATION_ID_INSTALL) +} + +fun installTag(name: String): String = "install-${name.trim().replace(' ', '_')}" + +private const val SUCCESS_TIMEOUT = 5_000L + +fun Context.createInstallNotification( + appName: String, + state: InstallState, + isUninstall: Boolean = false, + autoCancel: Boolean = true, + block: NotificationCompat.Builder.() -> Unit = {}, +): Notification { + return NotificationCompat + .Builder(this, NOTIFICATION_CHANNEL_INSTALL) + .apply { + setAutoCancel(autoCancel) + setOngoing(false) + setOnlyAlertOnce(true) + setColor(Color.GREEN) + val (title, text) = if (isUninstall) { + setTimeoutAfter(SUCCESS_TIMEOUT) + setSmallIcon(CommonR.drawable.ic_delete) + getString(CommonR.string.uninstalled_application) to + getString(CommonR.string.uninstalled_application_DESC, appName) + } else { + when (state) { + Failed -> { + setSmallIcon(CommonR.drawable.ic_bug_report) + getString(CommonR.string.installation_failed) to + getString(CommonR.string.installation_failed_DESC, appName) + } + + Pending -> { + setSmallIcon(CommonR.drawable.ic_download) + getString(CommonR.string.downloaded_FORMAT, appName) to + getString(CommonR.string.tap_to_install_DESC) + } + + Installing -> { + setSmallIcon(CommonR.drawable.ic_download) + setProgress(-1, -1, true) + getString(CommonR.string.installing) to + appName + } + + Installed -> { + setTimeoutAfter(SUCCESS_TIMEOUT) + setSmallIcon(CommonR.drawable.ic_check) + getString(CommonR.string.installed) to + appName + } + } + } + setContentTitle(title) + setContentText(text) + block() + } + .build() +}