diff --git a/src/main/kotlin/com/looker/droidify/MainActivity.kt b/src/main/kotlin/com/looker/droidify/MainActivity.kt index 87414d42..897becd1 100644 --- a/src/main/kotlin/com/looker/droidify/MainActivity.kt +++ b/src/main/kotlin/com/looker/droidify/MainActivity.kt @@ -1,9 +1,15 @@ package com.looker.droidify +import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.content.pm.PackageInstaller +import android.net.Uri import com.looker.droidify.ContextWrapperX.Companion.wrap +import com.looker.droidify.installer.InstallerService import com.looker.droidify.screen.ScreenActivity +import com.looker.droidify.utility.extension.android.Android +import kotlinx.coroutines.withContext class MainActivity : ScreenActivity() { companion object { @@ -16,12 +22,35 @@ class MainActivity : ScreenActivity() { override fun handleIntent(intent: Intent?) { when (intent?.action) { ACTION_UPDATES -> handleSpecialIntent(SpecialIntent.Updates) - ACTION_INSTALL -> handleSpecialIntent( - SpecialIntent.Install( - intent.packageName, - intent.getStringExtra(EXTRA_CACHE_FILE_NAME) - ) - ) + ACTION_INSTALL -> { + // continue install prompt + 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, BuildConfig.APPLICATION_ID) + it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK ) + + startActivity(it) + } + + // TODO: send this back to the InstallerService to free up the UI + // prepare prompt intent +// val name = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) +// +// val pending = PendingIntent.getService( +// this, +// 0, +// Intent(this, InstallerService::class.java) +// .setData(Uri.parse("package:$name")) +// .putExtra(Intent.EXTRA_INTENT, promptIntent) +// .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), +// if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE +// else PendingIntent.FLAG_UPDATE_CURRENT +// ) +// +// pending.send() + } else -> super.handleIntent(intent) } } diff --git a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt index 00819462..d637b3a9 100644 --- a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt +++ b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt @@ -33,11 +33,15 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) { override suspend fun install(packageName: String, cacheFileName: String) { val cacheFile = Cache.getReleaseFile(context, cacheFileName) + // using packageName to store the app's name for the notification later down the line + intent.putExtra(InstallerService.KEY_APP_NAME, packageName) mDefaultInstaller(cacheFile) } - override suspend fun install(packageName: String, cacheFile: File) = + override suspend fun install(packageName: String, cacheFile: File) { + intent.putExtra(InstallerService.KEY_APP_NAME, packageName) mDefaultInstaller(cacheFile) + } override suspend fun uninstall(packageName: String) = mDefaultUninstaller(packageName) diff --git a/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt index 0a6e49ee..6946e8a2 100644 --- a/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt +++ b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt @@ -1,32 +1,41 @@ package com.looker.droidify.installer +import android.app.PendingIntent import android.app.Service import android.content.Intent import android.content.pm.PackageInstaller import android.content.pm.PackageManager +import android.net.Uri import android.os.IBinder import android.view.ContextThemeWrapper import androidx.core.app.NotificationCompat -import com.looker.droidify.Common +import com.looker.droidify.Common.NOTIFICATION_CHANNEL_DOWNLOADING +import com.looker.droidify.Common.NOTIFICATION_ID_DOWNLOADING import com.looker.droidify.R +import com.looker.droidify.MainActivity +import com.looker.droidify.utility.Utils +import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.extension.android.notificationManager import com.looker.droidify.utility.extension.resources.getColorFromAttr /** * Runs during or after a PackageInstaller session in order to handle completion, failure, or - * interruptions requiring user intervention (e.g. "Install Unknown Apps" permission requests). + * interruptions requiring user intervention, such as the package installer prompt. */ class InstallerService : Service() { companion object { const val KEY_ACTION = "installerAction" + const val KEY_APP_NAME = "appName" const val ACTION_UNINSTALL = "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 + // only trigger a prompt if in foreground or below Android 10, otherwise make notification + // launching a prompt in the background will fail silently + if ((!Android.sdk(29) || Utils.inForeground()) && status == PackageInstaller.STATUS_PENDING_USER_ACTION) { + // Triggers the installer prompt and "unknown apps" prompt if needed val promptIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT) promptIntent?.let { @@ -45,7 +54,10 @@ class InstallerService : Service() { } /** - * Notifies user of installer outcome. + * Notifies user of installer outcome. This can be success, error, or a request for user action + * if installation cannot proceed automatically. + * + * @param intent provided by PackageInstaller to the callback service/activity. */ private fun notifyStatus(intent: Intent) { // unpack from intent @@ -55,22 +67,23 @@ class InstallerService : Service() { val installerAction = intent.getStringExtra(KEY_ACTION) // 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 appLabel = intent.getStringExtra(KEY_APP_NAME) + ?: 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, Common.NOTIFICATION_CHANNEL_DOWNLOADING) + .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING) .setAutoCancel(true) .setColor( ContextThemeWrapper(this, R.style.Theme_Main_Light) @@ -78,10 +91,21 @@ class InstallerService : Service() { ) when (status) { + PackageInstaller.STATUS_PENDING_USER_ACTION -> { + // request user action with "downloaded" notification that triggers a working prompt + notificationManager.notify( + notificationTag, NOTIFICATION_ID_DOWNLOADING, builder + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setContentIntent(installIntent(intent)) + .setContentTitle(getString(R.string.downloaded_FORMAT, appLabel)) + .setContentText(getString(R.string.tap_to_install_DESC)) + .build() + ) + } PackageInstaller.STATUS_SUCCESS -> { if (installerAction == ACTION_UNINSTALL) // remove any notification for this app - notificationManager.cancel(notificationTag, Common.NOTIFICATION_ID_DOWNLOADING) + notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING) else { val notification = builder .setSmallIcon(android.R.drawable.stat_sys_download_done) @@ -90,11 +114,11 @@ class InstallerService : Service() { .build() notificationManager.notify( notificationTag, - Common.NOTIFICATION_ID_DOWNLOADING, + NOTIFICATION_ID_DOWNLOADING, notification ) Thread.sleep(5000) - notificationManager.cancel(notificationTag, Common.NOTIFICATION_ID_DOWNLOADING) + notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING) } } PackageInstaller.STATUS_FAILURE_ABORTED -> { @@ -109,7 +133,7 @@ class InstallerService : Service() { .build() notificationManager.notify( notificationTag, - Common.NOTIFICATION_ID_DOWNLOADING, + NOTIFICATION_ID_DOWNLOADING, notification ) } @@ -120,5 +144,33 @@ class InstallerService : Service() { return null } + /** + * Generates an intent that provides the specified activity information necessary to trigger + * the package manager's prompt, thus completing a staged installation requiring user + * intervention. + * + * @param intent the intent provided by PackageInstaller to the callback target passed to + * PackageInstaller.Session.commit(). + * @return a pending intent that can be attached to a background-accessible entry point such as + * a notification + */ + private fun installIntent(intent: Intent): PendingIntent { + // prepare prompt intent + val promptIntent : Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT) + val name = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) + + return PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java) + .setAction(MainActivity.ACTION_INSTALL) + .setData(Uri.parse("package:$name")) + .putExtra(Intent.EXTRA_INTENT, promptIntent) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + else PendingIntent.FLAG_UPDATE_CURRENT + ) + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/service/DownloadService.kt b/src/main/kotlin/com/looker/droidify/service/DownloadService.kt index 27db7c13..f045bd38 100644 --- a/src/main/kotlin/com/looker/droidify/service/DownloadService.kt +++ b/src/main/kotlin/com/looker/droidify/service/DownloadService.kt @@ -219,11 +219,13 @@ class DownloadService : ConnectionService() { .getColorFromAttr(R.attr.colorPrimary).defaultColor ) .setContentIntent( - PendingIntent.getBroadcast( + PendingIntent.getActivity( this, 0, - Intent(this, Receiver::class.java) - .setAction("$ACTION_OPEN.${task.packageName}"), + Intent(this, MainActivity::class.java) + .setAction(Intent.ACTION_VIEW) + .setData(Uri.parse("package:${task.packageName}")) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE else @@ -308,12 +310,10 @@ class DownloadService : ConnectionService() { consumed = true } if (!consumed) { - if (rootInstallerEnabled) { - scope.launch { - AppInstaller.getInstance(this@DownloadService) - ?.defaultInstaller?.install(task.release.cacheFileName) - } - } else showNotificationInstall(task) + scope.launch { + AppInstaller.getInstance(this@DownloadService) + ?.defaultInstaller?.install(task.name, task.release.cacheFileName) + } } } diff --git a/src/main/kotlin/com/looker/droidify/utility/Utils.kt b/src/main/kotlin/com/looker/droidify/utility/Utils.kt index 58e42d43..fff8f177 100644 --- a/src/main/kotlin/com/looker/droidify/utility/Utils.kt +++ b/src/main/kotlin/com/looker/droidify/utility/Utils.kt @@ -1,5 +1,8 @@ package com.looker.droidify.utility +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE import android.content.Context import android.content.pm.PackageInfo import android.content.pm.Signature @@ -166,4 +169,15 @@ object Utils { ) else -> Locale(localeCode) } + + /** + * Checks if app is currently considered to be in the foreground by Android. + */ + fun inForeground(): Boolean { + val appProcessInfo = ActivityManager.RunningAppProcessInfo() + ActivityManager.getMyMemoryState(appProcessInfo) + val importance = appProcessInfo.importance + return ((importance == IMPORTANCE_FOREGROUND) or (importance == IMPORTANCE_VISIBLE)) + } + }