From 73ac718ce2c205938ea36647232d7fe88bc5a2a9 Mon Sep 17 00:00:00 2001 From: Matthew Crossman Date: Fri, 31 Dec 2021 10:41:45 +1100 Subject: [PATCH 1/9] Fix install prompt notification, further automate updates/installs. Fixes "tap to install" notification not launching an activity. Install notifications moved to InstallerService so that we only show them for apps that need intervention (and lets us preserve our install session). All installs go to the installer, allowing for downloads to jump straight into installation if possible (auto updates or showing prompt on A9 and earlier). If a prompt is needed, the notification is still shown. Additional utility function checks if app is in the foreground. Used to ensure that we do not launch a prompt that cannot be launched. Miscellaneous comments have been added and improved. --- .../com/looker/droidify/MainActivity.kt | 41 +++++++-- .../droidify/installer/DefaultInstaller.kt | 6 +- .../droidify/installer/InstallerService.kt | 92 +++++++++++++++---- .../droidify/service/DownloadService.kt | 18 ++-- .../com/looker/droidify/utility/Utils.kt | 14 +++ 5 files changed, 135 insertions(+), 36 deletions(-) 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)) + } + } From 3e8e23e111df9f335f3f0a0145d23eb8bb7f1fef Mon Sep 17 00:00:00 2001 From: Matthew Crossman Date: Mon, 3 Jan 2022 14:51:31 +1100 Subject: [PATCH 2/9] Simplify install prompt handling Reverts most changes to MainActivity and uses handleSpecialIntent instead. handleSpecialIntent now defers installer prompt to InstallerService by starting it again. SpecialIntent has been modified to accommodate extra data needed to handle these callbacks. --- .../com/looker/droidify/MainActivity.kt | 41 ++++--------------- .../looker/droidify/screen/ScreenActivity.kt | 25 +++++++---- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/src/main/kotlin/com/looker/droidify/MainActivity.kt b/src/main/kotlin/com/looker/droidify/MainActivity.kt index 897becd1..5774c5dc 100644 --- a/src/main/kotlin/com/looker/droidify/MainActivity.kt +++ b/src/main/kotlin/com/looker/droidify/MainActivity.kt @@ -1,15 +1,10 @@ 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 { @@ -22,35 +17,13 @@ class MainActivity : ScreenActivity() { override fun handleIntent(intent: Intent?) { when (intent?.action) { ACTION_UPDATES -> handleSpecialIntent(SpecialIntent.Updates) - 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() - } + ACTION_INSTALL -> handleSpecialIntent( + SpecialIntent.Install( + intent.packageName, + intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1), + intent.getParcelableExtra(Intent.EXTRA_INTENT) + ) + ) else -> super.handleIntent(intent) } } diff --git a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt index ffb9d57a..424457b5 100644 --- a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt +++ b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt @@ -1,6 +1,7 @@ package com.looker.droidify.screen import android.content.Intent +import android.content.pm.PackageInstaller import android.os.Bundle import android.os.Parcel import android.view.ViewGroup @@ -13,7 +14,7 @@ import com.google.android.material.circularreveal.CircularRevealFrameLayout import com.looker.droidify.R import com.looker.droidify.content.Preferences import com.looker.droidify.database.CursorOwner -import com.looker.droidify.installer.AppInstaller +import com.looker.droidify.installer.InstallerService import com.looker.droidify.ui.fragments.AppDetailFragment import com.looker.droidify.utility.KParcelable import com.looker.droidify.utility.extension.resources.getDrawableFromAttr @@ -27,7 +28,7 @@ abstract class ScreenActivity : AppCompatActivity() { sealed class SpecialIntent { object Updates : SpecialIntent() - class Install(val packageName: String?, val cacheFileName: String?) : SpecialIntent() + class Install(val packageName: String?, val status: Int?, val promptIntent: Intent?) : SpecialIntent() } private class FragmentStackItem( @@ -217,14 +218,24 @@ abstract class ScreenActivity : AppCompatActivity() { } is SpecialIntent.Install -> { val packageName = specialIntent.packageName - if (!packageName.isNullOrEmpty()) { + val status = specialIntent.status + val promptIntent = specialIntent.promptIntent + if (!packageName.isNullOrEmpty() && status != null && promptIntent != null) { lifecycleScope.launch { - specialIntent.cacheFileName?.let { - AppInstaller.getInstance(this@ScreenActivity) - ?.defaultInstaller?.install(packageName, it) - } + startService( + Intent(baseContext, InstallerService::class.java) + .putExtra(PackageInstaller.EXTRA_STATUS, status) + .putExtra( + PackageInstaller.EXTRA_PACKAGE_NAME, + packageName + ) + .putExtra(Intent.EXTRA_INTENT, promptIntent) + ) } } + else { + throw IllegalArgumentException("Missing parameters needed to relaunch InstallerService and trigger prompt.") + } Unit } }::class From adf305ffc0e0124e06425f9f267eba062ccedc0c Mon Sep 17 00:00:00 2001 From: Matthew Crossman Date: Mon, 3 Jan 2022 19:20:00 +1100 Subject: [PATCH 3/9] Prevent DefaultInstaller from working on a non-existent cache file. Ensures that the cache file exists before attempting an install using it. This fixes a crash that can occur if the file is not yet available, likely due to an I/O-related race condition. --- .../droidify/installer/DefaultInstaller.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt index d637b3a9..503421cf 100644 --- a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt +++ b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt @@ -51,16 +51,18 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) { val session = sessionInstaller.openSession(id) - session.use { activeSession -> - activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream -> - cacheFile.inputStream().use { fileStream -> - fileStream.copyTo(packageStream) + if (cacheFile.exists()) { + session.use { activeSession -> + activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream -> + cacheFile.inputStream().use { fileStream -> + fileStream.copyTo(packageStream) + } } + + val pendingIntent = PendingIntent.getService(context, id, intent, flags) + + session.commit(pendingIntent.intentSender) } - - val pendingIntent = PendingIntent.getService(context, id, intent, flags) - - session.commit(pendingIntent.intentSender) } cacheFile.delete() } From fce311098dce1b341563c877b4bcb00c7ad75039 Mon Sep 17 00:00:00 2001 From: Matthew Crossman Date: Mon, 3 Jan 2022 20:21:36 +1100 Subject: [PATCH 4/9] Implement automatic updates after repository sync Initial implementation of fully automatic updates, allowing apps to be updated after a repo sync has been completed. It can be enabled with a new preference in settings. Implemented by greatly modifying SyncService to allow updates to be run on every completed sync before notifications would be outputted. Note that the update notification no longer appears if auto updates are enabled. BuildConfig.DEBUG is used to force syncing to run to completion, even if there are no changes to the repos. This allow reliable testing by turning the sync button into an "update all" button. This should be removed once an "Update all" button has been implemented in the updates tab. Cache file checking in DefaultInstaller is now more robust. --- .../looker/droidify/content/Preferences.kt | 3 + .../droidify/installer/DefaultInstaller.kt | 18 +- .../droidify/screen/SettingsFragment.kt | 4 + .../looker/droidify/service/SyncService.kt | 162 ++++++++++++------ src/main/res/values/strings.xml | 2 + 5 files changed, 129 insertions(+), 60 deletions(-) diff --git a/src/main/kotlin/com/looker/droidify/content/Preferences.kt b/src/main/kotlin/com/looker/droidify/content/Preferences.kt index 664635bf..afcd9a27 100644 --- a/src/main/kotlin/com/looker/droidify/content/Preferences.kt +++ b/src/main/kotlin/com/looker/droidify/content/Preferences.kt @@ -24,6 +24,7 @@ object Preferences { private val keys = sequenceOf( Key.Language, Key.AutoSync, + Key.AutoSyncInstall, Key.IncompatibleVersions, Key.ListAnimation, Key.ProxyHost, @@ -130,6 +131,8 @@ object Preferences { "auto_sync", Value.EnumerationValue(Preferences.AutoSync.Wifi) ) + object AutoSyncInstall : + Key("auto_sync_install", Value.BooleanValue(true)) object IncompatibleVersions : Key("incompatible_versions", Value.BooleanValue(false)) diff --git a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt index 503421cf..a600ab09 100644 --- a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt +++ b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt @@ -4,11 +4,13 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageInstaller.SessionParams +import android.util.Log import com.looker.droidify.content.Cache import com.looker.droidify.utility.extension.android.Android import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File +import java.io.FileNotFoundException class DefaultInstaller(context: Context) : BaseInstaller(context) { @@ -51,18 +53,20 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) { val session = sessionInstaller.openSession(id) - if (cacheFile.exists()) { - session.use { activeSession -> - activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream -> + session.use { activeSession -> + activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream -> + try { cacheFile.inputStream().use { fileStream -> fileStream.copyTo(packageStream) } + } catch (error: FileNotFoundException) { + Log.w("DefaultInstaller", "Cache file for DefaultInstaller does not seem to exist.") } - - val pendingIntent = PendingIntent.getService(context, id, intent, flags) - - session.commit(pendingIntent.intentSender) } + + val pendingIntent = PendingIntent.getService(context, id, intent, flags) + + session.commit(pendingIntent.intentSender) } cacheFile.delete() } diff --git a/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt b/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt index d6fa1e5a..83caa65c 100644 --- a/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt @@ -103,6 +103,10 @@ class SettingsFragment : ScreenFragment() { Preferences.AutoSync.Always -> getString(R.string.always) } } + addSwitch( + Preferences.Key.AutoSyncInstall, getString(R.string.sync_auto_install), + getString(R.string.sync_auto_install_summary) + ) addSwitch( Preferences.Key.UpdateNotify, getString(R.string.notify_about_updates), getString(R.string.notify_about_updates_summary) diff --git a/src/main/kotlin/com/looker/droidify/service/SyncService.kt b/src/main/kotlin/com/looker/droidify/service/SyncService.kt index 3982b6fb..4715366a 100644 --- a/src/main/kotlin/com/looker/droidify/service/SyncService.kt +++ b/src/main/kotlin/com/looker/droidify/service/SyncService.kt @@ -23,6 +23,7 @@ import com.looker.droidify.entity.ProductItem import com.looker.droidify.entity.Repository import com.looker.droidify.index.RepositoryUpdater import com.looker.droidify.utility.RxUtils +import com.looker.droidify.utility.Utils import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.extension.android.asSequence import com.looker.droidify.utility.extension.android.notificationManager @@ -73,6 +74,8 @@ class SyncService : ConnectionService() { private var updateNotificationBlockerFragment: WeakReference? = null + private val downloadConnection = Connection(DownloadService::class.java) + enum class SyncRequest { AUTO, MANUAL, FORCE } inner class Binder : android.os.Binder() { @@ -173,11 +176,13 @@ class SyncService : ConnectionService() { .let(notificationManager::createNotificationChannel) } + downloadConnection.bind(this) stateSubject.onEach { publishForegroundState(false, it) }.launchIn(scope) } override fun onDestroy() { super.onDestroy() + downloadConnection.unbind(this) cancelTasks { true } cancelCurrentTask { true } } @@ -366,14 +371,14 @@ class SyncService : ConnectionService() { if (throwable != null && task.manual) { showNotificationError(repository, throwable as Exception) } - handleNextTask(result == true || hasUpdates) + handleNextTask(BuildConfig.DEBUG || result == true || hasUpdates) } currentTask = CurrentTask(task, disposable, hasUpdates, initialState) } else { handleNextTask(hasUpdates) } } else if (started != Started.NO) { - if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) { + if (hasUpdates) { val disposable = RxUtils .querySingle { it -> Database.ProductAdapter @@ -396,9 +401,8 @@ class SyncService : ConnectionService() { throwable?.printStackTrace() currentTask = null handleNextTask(false) - val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true - if (!blocked && result != null && result.isNotEmpty()) { - displayUpdatesNotification(result) + if (result.isNotEmpty()) { + runAutoUpdate(result) } } currentTask = CurrentTask(null, disposable, true, State.Finishing) @@ -415,58 +419,110 @@ class SyncService : ConnectionService() { } } - private fun displayUpdatesNotification(productItems: List) { - val maxUpdates = 5 - fun T.applyHack(callback: T.() -> Unit): T = apply(callback) - notificationManager.notify( - Common.NOTIFICATION_ID_UPDATES, NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES) - .setSmallIcon(R.drawable.ic_new_releases) - .setContentTitle(getString(R.string.new_updates_available)) - .setContentText( - resources.getQuantityString( - R.plurals.new_updates_DESC_FORMAT, - productItems.size, productItems.size - ) + /** + * Performs automatic update after a repo sync if it is enabled. Otherwise, it continues on to + * displayUpdatesNotification. + * + * @param productItems a list of apps pending updates + * @see SyncService.displayUpdatesNotification + */ + private fun runAutoUpdate(productItems: List) { + if (Preferences[Preferences.Key.AutoSyncInstall]) { + // run startUpdate on every item + productItems.map { productItem -> + Pair( + Database.InstalledAdapter.get(productItem.packageName, null), + Database.RepositoryAdapter.get(productItem.repositoryId) ) - .setColor( - ContextThemeWrapper(this, R.style.Theme_Main_Light) - .getColorFromAttr(android.R.attr.colorPrimary).defaultColor - ) - .setContentIntent( - PendingIntent.getActivity( - this, - 0, - Intent(this, MainActivity::class.java) - .setAction(MainActivity.ACTION_UPDATES), - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - else - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - .setStyle(NotificationCompat.InboxStyle().applyHack { - for (productItem in productItems.take(maxUpdates)) { - val builder = SpannableStringBuilder(productItem.name) - builder.setSpan( - ForegroundColorSpan(Color.BLACK), 0, builder.length, - SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE + } + .filter { pair -> pair.first != null && pair.second != null } + .forEach { installedRepository -> + run { + // Redundant !! as linter doesn't recognise the above filter's effects + val installedItem = installedRepository.first!! + val repository = installedRepository.second!! + + val productRepository = Database.ProductAdapter.get( + installedItem.packageName, + null ) - builder.append(' ').append(productItem.version) - addLine(builder) - } - if (productItems.size > maxUpdates) { - val summary = - getString(R.string.plus_more_FORMAT, productItems.size - maxUpdates) - if (Android.sdk(24)) { - addLine(summary) - } else { - setSummaryText(summary) + .filter { product -> product.repositoryId == repository.id } + .map { product -> Pair(product, repository) } + + scope.launch { + Utils.startUpdate( + installedItem.packageName, + installedRepository.first, + productRepository, + downloadConnection + ) } } - }) - .build() - ) + } + } else { + displayUpdatesNotification(productItems) + } + } + + /** + * Displays summary of available updates. + * + * @param productItems list of apps pending updates + */ + private fun displayUpdatesNotification(productItems: List) { + if (updateNotificationBlockerFragment?.get()?.isAdded == true && Preferences[Preferences.Key.UpdateNotify]) { + val maxUpdates = 5 + fun T.applyHack(callback: T.() -> Unit): T = apply(callback) + notificationManager.notify( + Common.NOTIFICATION_ID_UPDATES, NotificationCompat + .Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES) + .setSmallIcon(R.drawable.ic_new_releases) + .setContentTitle(getString(R.string.new_updates_available)) + .setContentText( + resources.getQuantityString( + R.plurals.new_updates_DESC_FORMAT, + productItems.size, productItems.size + ) + ) + .setColor( + ContextThemeWrapper(this, R.style.Theme_Main_Light) + .getColorFromAttr(android.R.attr.colorPrimary).defaultColor + ) + .setContentIntent( + PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java) + .setAction(MainActivity.ACTION_UPDATES), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + else + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + .setStyle(NotificationCompat.InboxStyle().applyHack { + for (productItem in productItems.take(maxUpdates)) { + val builder = SpannableStringBuilder(productItem.name) + builder.setSpan( + ForegroundColorSpan(Color.BLACK), 0, builder.length, + SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE + ) + builder.append(' ').append(productItem.version) + addLine(builder) + } + if (productItems.size > maxUpdates) { + val summary = + getString(R.string.plus_more_FORMAT, productItems.size - maxUpdates) + if (Android.sdk(24)) { + addLine(summary) + } else { + setSummaryText(summary) + } + } + }) + .build() + ) + } } class Job : JobService() { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 83f48423..03ff6e52 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -180,4 +180,6 @@ Installed applications Sort & Filter New applications + Update apps after sync + Automatically install app updates after syncing repositories From e1fc3c656ab8e7154b9b138d1f7dd84de01695c6 Mon Sep 17 00:00:00 2001 From: Matthew Crossman Date: Tue, 4 Jan 2022 16:21:08 +1100 Subject: [PATCH 5/9] Make auto-update default on Android 12+ & stop background prompts. Sets preference for auto updates to true by default if device runs Android 12 and later. Reverts change allowing the install prompt to show up from the background and instead require the notification on all Android versions. --- src/main/kotlin/com/looker/droidify/content/Preferences.kt | 2 +- .../kotlin/com/looker/droidify/installer/InstallerService.kt | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/looker/droidify/content/Preferences.kt b/src/main/kotlin/com/looker/droidify/content/Preferences.kt index afcd9a27..cbc7dcfa 100644 --- a/src/main/kotlin/com/looker/droidify/content/Preferences.kt +++ b/src/main/kotlin/com/looker/droidify/content/Preferences.kt @@ -132,7 +132,7 @@ object Preferences { Value.EnumerationValue(Preferences.AutoSync.Wifi) ) object AutoSyncInstall : - Key("auto_sync_install", Value.BooleanValue(true)) + Key("auto_sync_install", Value.BooleanValue(Android.sdk(31))) object IncompatibleVersions : Key("incompatible_versions", Value.BooleanValue(false)) diff --git a/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt index 6946e8a2..2be4a9e8 100644 --- a/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt +++ b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt @@ -32,9 +32,8 @@ class InstallerService : Service() { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) - // 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) { + // only trigger a prompt if in foreground, otherwise make notification + if (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) From 375ab23edb4f8d562a7f9560848bf83650554f29 Mon Sep 17 00:00:00 2001 From: Matthew Crossman Date: Tue, 4 Jan 2022 20:26:07 +1100 Subject: [PATCH 6/9] Improve install notifications, improve DefaultInstaller, misc. clean-up Installer notifications have their own channel, their tags have been fixed, and the timeout has been properly set instead of using sleep. Ensured that DefaultInstaller's sessions use unique file names. Also improved error handling by including broken pipes and by preventing post-copy operations if an error has occurred. Some cleaning up has been done in Common, DownloadService, and SyncService. A few changes have been cherry-picked from master. --- src/main/kotlin/com/looker/droidify/Common.kt | 2 + .../droidify/installer/DefaultInstaller.kt | 43 ++++++-- .../droidify/installer/InstallerService.kt | 47 +++++--- .../droidify/screen/SettingsFragment.kt | 7 +- .../droidify/service/DownloadService.kt | 102 ++++-------------- .../looker/droidify/service/SyncService.kt | 28 ++--- 6 files changed, 112 insertions(+), 117 deletions(-) diff --git a/src/main/kotlin/com/looker/droidify/Common.kt b/src/main/kotlin/com/looker/droidify/Common.kt index 446a2f83..a6bc57c0 100644 --- a/src/main/kotlin/com/looker/droidify/Common.kt +++ b/src/main/kotlin/com/looker/droidify/Common.kt @@ -4,10 +4,12 @@ object Common { const val NOTIFICATION_CHANNEL_SYNCING = "syncing" const val NOTIFICATION_CHANNEL_UPDATES = "updates" const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading" + const val NOTIFICATION_CHANNEL_INSTALLER = "installed" const val NOTIFICATION_ID_SYNCING = 1 const val NOTIFICATION_ID_UPDATES = 2 const val NOTIFICATION_ID_DOWNLOADING = 3 + const val NOTIFICATION_ID_INSTALLER = 4 const val PREFS_LANGUAGE = "languages" const val PREFS_LANGUAGE_DEFAULT = "system" diff --git a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt index a600ab09..acbf2c9a 100644 --- a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt +++ b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt @@ -11,10 +11,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File import java.io.FileNotFoundException +import java.io.IOException class DefaultInstaller(context: Context) : BaseInstaller(context) { - private val sessionInstaller = context.packageManager.packageInstaller + private val packageManager = context.packageManager + private val sessionInstaller = packageManager.packageInstaller private val intent = Intent(context, InstallerService::class.java) companion object { @@ -48,27 +50,48 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) { override suspend fun uninstall(packageName: String) = mDefaultUninstaller(packageName) private fun mDefaultInstaller(cacheFile: File) { + // clean up inactive sessions + sessionInstaller.mySessions + .filter { session -> !session.isActive } + .forEach { session -> sessionInstaller.abandonSession(session.sessionId) } + // start new session val id = sessionInstaller.createSession(sessionParams) - val session = sessionInstaller.openSession(id) + // get package name + val packageInfo = packageManager.getPackageArchiveInfo(cacheFile.absolutePath, 0) + val packageName = packageInfo?.packageName ?: "unknown-package" + + // error flags + var hasErrors = false + session.use { activeSession -> - activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream -> + activeSession.openWrite(packageName, 0, cacheFile.length()).use { packageStream -> try { cacheFile.inputStream().use { fileStream -> fileStream.copyTo(packageStream) } - } catch (error: FileNotFoundException) { - Log.w("DefaultInstaller", "Cache file for DefaultInstaller does not seem to exist.") + } catch (e: FileNotFoundException) { + Log.w( + "DefaultInstaller", + "Cache file for DefaultInstaller does not seem to exist." + ) + hasErrors = true + } catch (e: IOException) { + Log.w( + "DefaultInstaller", + "Failed to perform cache to package copy due to a bad pipe." + ) + hasErrors = true } } - - val pendingIntent = PendingIntent.getService(context, id, intent, flags) - - session.commit(pendingIntent.intentSender) } - cacheFile.delete() + + if (!hasErrors) { + session.commit(PendingIntent.getService(context, id, intent, flags).intentSender) + cacheFile.delete() + } } private suspend fun mDefaultUninstaller(packageName: String) { diff --git a/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt index 2be4a9e8..306c2ce0 100644 --- a/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt +++ b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt @@ -1,16 +1,17 @@ package com.looker.droidify.installer +import android.app.NotificationChannel +import android.app.NotificationManager 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.NOTIFICATION_CHANNEL_DOWNLOADING -import com.looker.droidify.Common.NOTIFICATION_ID_DOWNLOADING +import com.looker.droidify.Common.NOTIFICATION_CHANNEL_INSTALLER +import com.looker.droidify.Common.NOTIFICATION_ID_INSTALLER import com.looker.droidify.R import com.looker.droidify.MainActivity import com.looker.droidify.utility.Utils @@ -27,6 +28,20 @@ class InstallerService : Service() { const val KEY_ACTION = "installerAction" const val KEY_APP_NAME = "appName" const val ACTION_UNINSTALL = "uninstall" + private const val INSTALLED_NOTIFICATION_TIMEOUT: Long = 10000 + private const val NOTIFICATION_TAG_PREFIX = "install-" + } + + override fun onCreate() { + super.onCreate() + + if (Android.sdk(26)) { + NotificationChannel( + NOTIFICATION_CHANNEL_INSTALLER, + getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW + ) + .let(notificationManager::createNotificationChannel) + } } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { @@ -61,12 +76,18 @@ class InstallerService : Service() { 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 sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1) + + // get package information from session + val sessionInstaller = this.packageManager.packageInstaller + val session = if (sessionId > 0) sessionInstaller.getSessionInfo(sessionId) else null + + val name = session?.appPackageName ?: intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) val installerAction = intent.getStringExtra(KEY_ACTION) // get application name for notifications - val appLabel = intent.getStringExtra(KEY_APP_NAME) + val appLabel = session?.appLabel ?: intent.getStringExtra(KEY_APP_NAME) ?: try { if (name != null) packageManager.getApplicationLabel( packageManager.getApplicationInfo( @@ -78,11 +99,11 @@ class InstallerService : Service() { null } - val notificationTag = "download-$name" + val notificationTag = "${NOTIFICATION_TAG_PREFIX}$name" // start building val builder = NotificationCompat - .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING) + .Builder(this, NOTIFICATION_CHANNEL_INSTALLER) .setAutoCancel(true) .setColor( ContextThemeWrapper(this, R.style.Theme_Main_Light) @@ -93,7 +114,7 @@ class InstallerService : Service() { PackageInstaller.STATUS_PENDING_USER_ACTION -> { // request user action with "downloaded" notification that triggers a working prompt notificationManager.notify( - notificationTag, NOTIFICATION_ID_DOWNLOADING, builder + notificationTag, NOTIFICATION_ID_INSTALLER, builder .setSmallIcon(android.R.drawable.stat_sys_download_done) .setContentIntent(installIntent(intent)) .setContentTitle(getString(R.string.downloaded_FORMAT, appLabel)) @@ -104,20 +125,19 @@ class InstallerService : Service() { PackageInstaller.STATUS_SUCCESS -> { if (installerAction == ACTION_UNINSTALL) // remove any notification for this app - notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING) + notificationManager.cancel(notificationTag, NOTIFICATION_ID_INSTALLER) else { val notification = builder .setSmallIcon(android.R.drawable.stat_sys_download_done) .setContentTitle(getString(R.string.installed)) .setContentText(appLabel) + .setTimeoutAfter(INSTALLED_NOTIFICATION_TIMEOUT) .build() notificationManager.notify( notificationTag, - NOTIFICATION_ID_DOWNLOADING, + NOTIFICATION_ID_INSTALLER, notification ) - Thread.sleep(5000) - notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING) } } PackageInstaller.STATUS_FAILURE_ABORTED -> { @@ -132,7 +152,7 @@ class InstallerService : Service() { .build() notificationManager.notify( notificationTag, - NOTIFICATION_ID_DOWNLOADING, + NOTIFICATION_ID_INSTALLER, notification ) } @@ -163,7 +183,6 @@ class InstallerService : Service() { 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 diff --git a/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt b/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt index 83caa65c..9d636faa 100644 --- a/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt @@ -41,6 +41,11 @@ class SettingsFragment : ScreenFragment() { private var preferenceBinding: PreferenceItemBinding? = null private val preferences = mutableMapOf, Preference<*>>() + override fun onResume() { + super.onResume() + preferences.forEach { (_, preference) -> preference.update() } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) preferenceBinding = PreferenceItemBinding.inflate(layoutInflater) @@ -216,7 +221,7 @@ class SettingsFragment : ScreenFragment() { } } - private fun LinearLayoutCompat.addCategory( + private inline fun LinearLayoutCompat.addCategory( title: String, callback: LinearLayoutCompat.() -> Unit, ) { diff --git a/src/main/kotlin/com/looker/droidify/service/DownloadService.kt b/src/main/kotlin/com/looker/droidify/service/DownloadService.kt index f045bd38..aaf4041b 100644 --- a/src/main/kotlin/com/looker/droidify/service/DownloadService.kt +++ b/src/main/kotlin/com/looker/droidify/service/DownloadService.kt @@ -3,14 +3,14 @@ package com.looker.droidify.service import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent import android.net.Uri import android.view.ContextThemeWrapper import androidx.core.app.NotificationCompat import com.looker.droidify.BuildConfig -import com.looker.droidify.Common +import com.looker.droidify.Common.NOTIFICATION_ID_DOWNLOADING +import com.looker.droidify.Common.NOTIFICATION_ID_SYNCING +import com.looker.droidify.Common.NOTIFICATION_CHANNEL_DOWNLOADING import com.looker.droidify.MainActivity import com.looker.droidify.R import com.looker.droidify.content.Cache @@ -19,61 +19,27 @@ import com.looker.droidify.entity.Repository import com.looker.droidify.installer.AppInstaller import com.looker.droidify.network.Downloader import com.looker.droidify.utility.Utils -import com.looker.droidify.utility.Utils.rootInstallerEnabled import com.looker.droidify.utility.extension.android.* import com.looker.droidify.utility.extension.resources.* import com.looker.droidify.utility.extension.text.* import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.* import java.io.File import java.security.MessageDigest import kotlin.math.* class DownloadService : ConnectionService() { companion object { - private const val ACTION_OPEN = "${BuildConfig.APPLICATION_ID}.intent.action.OPEN" - private const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL" private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL" - private const val EXTRA_CACHE_FILE_NAME = - "${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME" private val mutableDownloadState = MutableSharedFlow() private val downloadState = mutableDownloadState.asSharedFlow() } - val scope = CoroutineScope(Dispatchers.Default) - - class Receiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action.orEmpty() - when { - action.startsWith("$ACTION_OPEN.") -> { - val packageName = action.substring(ACTION_OPEN.length + 1) - context.startActivity( - Intent(context, MainActivity::class.java) - .setAction(Intent.ACTION_VIEW) - .setData(Uri.parse("package:$packageName")) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - ) - } - action.startsWith("$ACTION_INSTALL.") -> { - val packageName = action.substring(ACTION_INSTALL.length + 1) - val cacheFileName = intent.getStringExtra(EXTRA_CACHE_FILE_NAME) - context.startActivity( - Intent(context, MainActivity::class.java) - .setAction(MainActivity.ACTION_INSTALL) - .setData(Uri.parse("package:$packageName")) - .putExtra(MainActivity.EXTRA_CACHE_FILE_NAME, cacheFileName) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - ) - } - } - } - } + private val scope = CoroutineScope(Dispatchers.Default) + private val mainDispatcher = Dispatchers.Main sealed class State(val packageName: String, val name: String) { class Pending(packageName: String, name: String) : State(packageName, name) @@ -121,7 +87,7 @@ class DownloadService : ConnectionService() { } else { cancelTasks(packageName) cancelCurrentTask(packageName) - notificationManager.cancel(task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING) + notificationManager.cancel(task.notificationTag, NOTIFICATION_ID_DOWNLOADING) tasks += task if (currentTask == null) { handleDownload() @@ -146,16 +112,14 @@ class DownloadService : ConnectionService() { if (Android.sdk(26)) { NotificationChannel( - Common.NOTIFICATION_CHANNEL_DOWNLOADING, + NOTIFICATION_CHANNEL_DOWNLOADING, getString(R.string.downloading), NotificationManager.IMPORTANCE_LOW ) .apply { setShowBadge(false) } .let(notificationManager::createNotificationChannel) } - scope.launch { - downloadState.collect { publishForegroundState(false, it) } - } + downloadState.onEach { publishForegroundState(false, it) }.launchIn(scope) } override fun onDestroy() { @@ -176,7 +140,14 @@ class DownloadService : ConnectionService() { private fun cancelTasks(packageName: String?) { tasks.removeAll { (packageName == null || it.packageName == packageName) && run { - scope.launch { mutableStateSubject.emit(State.Cancel(it.packageName, it.name)) } + scope.launch(mainDispatcher) { + mutableStateSubject.emit( + State.Cancel( + it.packageName, + it.name + ) + ) + } true } } @@ -186,7 +157,7 @@ class DownloadService : ConnectionService() { currentTask?.let { if (packageName == null || it.task.packageName == packageName) { currentTask = null - scope.launch { + scope.launch(mainDispatcher) { mutableStateSubject.emit( State.Cancel( it.task.packageName, @@ -209,9 +180,9 @@ class DownloadService : ConnectionService() { private fun showNotificationError(task: Task, errorType: ErrorType) { notificationManager.notify(task.notificationTag, - Common.NOTIFICATION_ID_DOWNLOADING, + NOTIFICATION_ID_DOWNLOADING, NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) + .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING) .setAutoCancel(true) .setSmallIcon(android.R.drawable.stat_sys_warning) .setColor( @@ -276,36 +247,9 @@ class DownloadService : ConnectionService() { .build()) } - private fun showNotificationInstall(task: Task) { - notificationManager.notify( - task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING, NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) - .setAutoCancel(true) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setColor( - ContextThemeWrapper(this, R.style.Theme_Main_Light) - .getColorFromAttr(R.attr.colorPrimary).defaultColor - ) - .setContentIntent(installIntent(task)) - .setContentTitle(getString(R.string.downloaded_FORMAT, task.name)) - .setContentText(getString(R.string.tap_to_install_DESC)) - .build() - ) - } - - private fun installIntent(task: Task): PendingIntent = PendingIntent.getBroadcast( - this, - 0, - Intent(this, Receiver::class.java) - .setAction("$ACTION_INSTALL.${task.packageName}") - .putExtra(EXTRA_CACHE_FILE_NAME, task.release.cacheFileName), - if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - else PendingIntent.FLAG_UPDATE_CURRENT - ) - private fun publishSuccess(task: Task) { var consumed = false - scope.launch { + scope.launch(mainDispatcher) { mutableStateSubject.emit(State.Success(task.packageName, task.name, task.release)) consumed = true } @@ -367,7 +311,7 @@ class DownloadService : ConnectionService() { private val stateNotificationBuilder by lazy { NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) + .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING) .setSmallIcon(android.R.drawable.stat_sys_download) .setColor( ContextThemeWrapper(this, R.style.Theme_Main_Light) @@ -389,7 +333,7 @@ class DownloadService : ConnectionService() { private fun publishForegroundState(force: Boolean, state: State) { if (force || currentTask != null) { currentTask = currentTask?.copy(lastState = state) - startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { + startForeground(NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { when (state) { is State.Connecting -> { setContentTitle(getString(R.string.downloading_FORMAT, state.name)) diff --git a/src/main/kotlin/com/looker/droidify/service/SyncService.kt b/src/main/kotlin/com/looker/droidify/service/SyncService.kt index 4715366a..153267d8 100644 --- a/src/main/kotlin/com/looker/droidify/service/SyncService.kt +++ b/src/main/kotlin/com/looker/droidify/service/SyncService.kt @@ -7,14 +7,16 @@ import android.app.job.JobParameters import android.app.job.JobService import android.content.Intent import android.graphics.Color -import android.os.Build import android.text.SpannableStringBuilder import android.text.style.ForegroundColorSpan import android.view.ContextThemeWrapper import androidx.core.app.NotificationCompat import androidx.fragment.app.Fragment import com.looker.droidify.BuildConfig -import com.looker.droidify.Common +import com.looker.droidify.Common.NOTIFICATION_ID_UPDATES +import com.looker.droidify.Common.NOTIFICATION_ID_SYNCING +import com.looker.droidify.Common.NOTIFICATION_CHANNEL_SYNCING +import com.looker.droidify.Common.NOTIFICATION_CHANNEL_UPDATES import com.looker.droidify.MainActivity import com.looker.droidify.R import com.looker.droidify.content.Preferences @@ -123,7 +125,7 @@ class SyncService : ConnectionService() { fun setUpdateNotificationBlocker(fragment: Fragment?) { updateNotificationBlockerFragment = fragment?.let(::WeakReference) if (fragment != null) { - notificationManager.cancel(Common.NOTIFICATION_ID_UPDATES) + notificationManager.cancel(NOTIFICATION_ID_UPDATES) } } @@ -164,13 +166,13 @@ class SyncService : ConnectionService() { if (Android.sdk(26)) { NotificationChannel( - Common.NOTIFICATION_CHANNEL_SYNCING, + NOTIFICATION_CHANNEL_SYNCING, getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW ) .apply { setShowBadge(false) } .let(notificationManager::createNotificationChannel) NotificationChannel( - Common.NOTIFICATION_CHANNEL_UPDATES, + NOTIFICATION_CHANNEL_UPDATES, getString(R.string.updates), NotificationManager.IMPORTANCE_LOW ) .let(notificationManager::createNotificationChannel) @@ -215,8 +217,8 @@ class SyncService : ConnectionService() { private fun showNotificationError(repository: Repository, exception: Exception) { notificationManager.notify( - "repository-${repository.id}", Common.NOTIFICATION_ID_SYNCING, NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING) + "repository-${repository.id}", NOTIFICATION_ID_SYNCING, NotificationCompat + .Builder(this, NOTIFICATION_CHANNEL_SYNCING) .setSmallIcon(android.R.drawable.stat_sys_warning) .setColor( ContextThemeWrapper(this, R.style.Theme_Main_Light) @@ -242,7 +244,7 @@ class SyncService : ConnectionService() { private val stateNotificationBuilder by lazy { NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING) + .Builder(this, NOTIFICATION_CHANNEL_SYNCING) .setSmallIcon(R.drawable.ic_sync) .setColor( ContextThemeWrapper(this, R.style.Theme_Main_Light) @@ -253,7 +255,7 @@ class SyncService : ConnectionService() { this, 0, Intent(this, this::class.java).setAction(ACTION_CANCEL), - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT @@ -265,7 +267,7 @@ class SyncService : ConnectionService() { if (force || currentTask?.lastState != state) { currentTask = currentTask?.copy(lastState = state) if (started == Started.MANUAL) { - startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { + startForeground(NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { when (state) { is State.Connecting -> { setContentTitle(getString(R.string.syncing_FORMAT, state.name)) @@ -474,8 +476,8 @@ class SyncService : ConnectionService() { val maxUpdates = 5 fun T.applyHack(callback: T.() -> Unit): T = apply(callback) notificationManager.notify( - Common.NOTIFICATION_ID_UPDATES, NotificationCompat - .Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES) + NOTIFICATION_ID_UPDATES, NotificationCompat + .Builder(this, NOTIFICATION_CHANNEL_UPDATES) .setSmallIcon(R.drawable.ic_new_releases) .setContentTitle(getString(R.string.new_updates_available)) .setContentText( @@ -494,7 +496,7 @@ class SyncService : ConnectionService() { 0, Intent(this, MainActivity::class.java) .setAction(MainActivity.ACTION_UPDATES), - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT From 3ea8cd8c6662a7afe53fdd17ba486f5d8ab38c84 Mon Sep 17 00:00:00 2001 From: Matthew Crossman Date: Tue, 4 Jan 2022 20:50:48 +1100 Subject: [PATCH 7/9] Resolve SecurityException in installer, tweak installed notification. Catch and ignore SecurityExceptions when cleaning up old installer sessions. Catch and ignore session opening exceptions. Reduce timeout for "Installed" notifications to 5s (as it was before). Revert removal of intent data for the install prompt notification. --- .../droidify/installer/DefaultInstaller.kt | 58 +++++++++++++------ .../droidify/installer/InstallerService.kt | 6 +- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt index acbf2c9a..880ffc50 100644 --- a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt +++ b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt @@ -53,7 +53,17 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) { // clean up inactive sessions sessionInstaller.mySessions .filter { session -> !session.isActive } - .forEach { session -> sessionInstaller.abandonSession(session.sessionId) } + .forEach { session -> + try { + sessionInstaller.abandonSession(session.sessionId) + } + catch (_: SecurityException) { + Log.w( + "DefaultInstaller", + "Attempted to abandon a session we do not own." + ) + } + } // start new session val id = sessionInstaller.createSession(sessionParams) @@ -67,24 +77,38 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) { var hasErrors = false session.use { activeSession -> - activeSession.openWrite(packageName, 0, cacheFile.length()).use { packageStream -> - try { - cacheFile.inputStream().use { fileStream -> - fileStream.copyTo(packageStream) + try { + activeSession.openWrite(packageName, 0, cacheFile.length()).use { packageStream -> + try { + cacheFile.inputStream().use { fileStream -> + fileStream.copyTo(packageStream) + } + } catch (_: FileNotFoundException) { + Log.w( + "DefaultInstaller", + "Cache file does not seem to exist." + ) + hasErrors = true + } catch (_: IOException) { + Log.w( + "DefaultInstaller", + "Failed to perform cache to package copy due to a bad pipe." + ) + hasErrors = true } - } catch (e: FileNotFoundException) { - Log.w( - "DefaultInstaller", - "Cache file for DefaultInstaller does not seem to exist." - ) - hasErrors = true - } catch (e: IOException) { - Log.w( - "DefaultInstaller", - "Failed to perform cache to package copy due to a bad pipe." - ) - hasErrors = true } + } catch (_: SecurityException) { + Log.w( + "DefaultInstaller", + "Attempted to use a destroyed or sealed session when installing." + ) + hasErrors = true + } catch (_: IOException) { + Log.w( + "DefaultInstaller", + "Couldn't open up active session file for copying install data." + ) + hasErrors = true } } diff --git a/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt index 306c2ce0..f058ccd0 100644 --- a/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt +++ b/src/main/kotlin/com/looker/droidify/installer/InstallerService.kt @@ -7,13 +7,14 @@ 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.NOTIFICATION_CHANNEL_INSTALLER import com.looker.droidify.Common.NOTIFICATION_ID_INSTALLER -import com.looker.droidify.R import com.looker.droidify.MainActivity +import com.looker.droidify.R import com.looker.droidify.utility.Utils import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.extension.android.notificationManager @@ -28,7 +29,7 @@ class InstallerService : Service() { const val KEY_ACTION = "installerAction" const val KEY_APP_NAME = "appName" const val ACTION_UNINSTALL = "uninstall" - private const val INSTALLED_NOTIFICATION_TIMEOUT: Long = 10000 + private const val INSTALLED_NOTIFICATION_TIMEOUT: Long = 5000 private const val NOTIFICATION_TAG_PREFIX = "install-" } @@ -183,6 +184,7 @@ class InstallerService : Service() { 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 From 5fbb6dcb6107c1356946953947a2fe9371b54e37 Mon Sep 17 00:00:00 2001 From: Matthew Crossman Date: Tue, 4 Jan 2022 21:30:57 +1100 Subject: [PATCH 8/9] Improve SyncService post-sync behaviour, rewrite new preference. Moved the update query disposable to before the post-sync if-else block so that the debug flag can be removed and everything can operate as normal. Rewrote the title of the new preference for auto updates to reduce the use of jargon and to be a bit clearer. --- .../looker/droidify/service/SyncService.kt | 159 +++++++++--------- src/main/res/values/strings.xml | 2 +- 2 files changed, 81 insertions(+), 80 deletions(-) diff --git a/src/main/kotlin/com/looker/droidify/service/SyncService.kt b/src/main/kotlin/com/looker/droidify/service/SyncService.kt index 153267d8..4d000c05 100644 --- a/src/main/kotlin/com/looker/droidify/service/SyncService.kt +++ b/src/main/kotlin/com/looker/droidify/service/SyncService.kt @@ -373,40 +373,45 @@ class SyncService : ConnectionService() { if (throwable != null && task.manual) { showNotificationError(repository, throwable as Exception) } - handleNextTask(BuildConfig.DEBUG || result == true || hasUpdates) + handleNextTask(result == true || hasUpdates) } currentTask = CurrentTask(task, disposable, hasUpdates, initialState) } else { handleNextTask(hasUpdates) } } else if (started != Started.NO) { - if (hasUpdates) { - val disposable = RxUtils - .querySingle { it -> - Database.ProductAdapter - .query( - installed = true, - updates = true, - searchQuery = "", - section = ProductItem.Section.All, - order = ProductItem.Order.NAME, - signal = it - ) - .use { - it.asSequence().map(Database.ProductAdapter::transformItem) - .toList() - } - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { result, throwable -> - throwable?.printStackTrace() - currentTask = null - handleNextTask(false) - if (result.isNotEmpty()) { - runAutoUpdate(result) + val disposable = RxUtils + .querySingle { it -> + Database.ProductAdapter + .query( + installed = true, + updates = true, + searchQuery = "", + section = ProductItem.Section.All, + order = ProductItem.Order.NAME, + signal = it + ) + .use { + it.asSequence().map(Database.ProductAdapter::transformItem) + .toList() } + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { result, throwable -> + throwable?.printStackTrace() + currentTask = null + handleNextTask(false) + if (result.isNotEmpty()) { + if (Preferences[Preferences.Key.AutoSyncInstall]) + runAutoUpdate(result) + if (hasUpdates && Preferences[Preferences.Key.UpdateNotify] && + updateNotificationBlockerFragment?.get()?.isAdded == true + ) + displayUpdatesNotification(result) } + } + if (hasUpdates) { currentTask = CurrentTask(null, disposable, true, State.Finishing) } else { scope.launch { mutableFinishState.emit(Unit) } @@ -461,8 +466,6 @@ class SyncService : ConnectionService() { } } } - } else { - displayUpdatesNotification(productItems) } } @@ -472,59 +475,57 @@ class SyncService : ConnectionService() { * @param productItems list of apps pending updates */ private fun displayUpdatesNotification(productItems: List) { - if (updateNotificationBlockerFragment?.get()?.isAdded == true && Preferences[Preferences.Key.UpdateNotify]) { - val maxUpdates = 5 - fun T.applyHack(callback: T.() -> Unit): T = apply(callback) - notificationManager.notify( - NOTIFICATION_ID_UPDATES, NotificationCompat - .Builder(this, NOTIFICATION_CHANNEL_UPDATES) - .setSmallIcon(R.drawable.ic_new_releases) - .setContentTitle(getString(R.string.new_updates_available)) - .setContentText( - resources.getQuantityString( - R.plurals.new_updates_DESC_FORMAT, - productItems.size, productItems.size + val maxUpdates = 5 + fun T.applyHack(callback: T.() -> Unit): T = apply(callback) + notificationManager.notify( + NOTIFICATION_ID_UPDATES, NotificationCompat + .Builder(this, NOTIFICATION_CHANNEL_UPDATES) + .setSmallIcon(R.drawable.ic_new_releases) + .setContentTitle(getString(R.string.new_updates_available)) + .setContentText( + resources.getQuantityString( + R.plurals.new_updates_DESC_FORMAT, + productItems.size, productItems.size + ) + ) + .setColor( + ContextThemeWrapper(this, R.style.Theme_Main_Light) + .getColorFromAttr(android.R.attr.colorPrimary).defaultColor + ) + .setContentIntent( + PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java) + .setAction(MainActivity.ACTION_UPDATES), + if (Android.sdk(23)) + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + else + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + .setStyle(NotificationCompat.InboxStyle().applyHack { + for (productItem in productItems.take(maxUpdates)) { + val builder = SpannableStringBuilder(productItem.name) + builder.setSpan( + ForegroundColorSpan(Color.BLACK), 0, builder.length, + SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE ) - ) - .setColor( - ContextThemeWrapper(this, R.style.Theme_Main_Light) - .getColorFromAttr(android.R.attr.colorPrimary).defaultColor - ) - .setContentIntent( - PendingIntent.getActivity( - this, - 0, - Intent(this, MainActivity::class.java) - .setAction(MainActivity.ACTION_UPDATES), - if (Android.sdk(23)) - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - else - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - .setStyle(NotificationCompat.InboxStyle().applyHack { - for (productItem in productItems.take(maxUpdates)) { - val builder = SpannableStringBuilder(productItem.name) - builder.setSpan( - ForegroundColorSpan(Color.BLACK), 0, builder.length, - SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE - ) - builder.append(' ').append(productItem.version) - addLine(builder) + builder.append(' ').append(productItem.version) + addLine(builder) + } + if (productItems.size > maxUpdates) { + val summary = + getString(R.string.plus_more_FORMAT, productItems.size - maxUpdates) + if (Android.sdk(24)) { + addLine(summary) + } else { + setSummaryText(summary) } - if (productItems.size > maxUpdates) { - val summary = - getString(R.string.plus_more_FORMAT, productItems.size - maxUpdates) - if (Android.sdk(24)) { - addLine(summary) - } else { - setSummaryText(summary) - } - } - }) - .build() - ) - } + } + }) + .build() + ) } class Job : JobService() { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 03ff6e52..9fd2399a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -180,6 +180,6 @@ Installed applications Sort & Filter New applications - Update apps after sync + Install updates automatically Automatically install app updates after syncing repositories From 430b18e77b9eed6d525daa6982acaf5c74ad4a8d Mon Sep 17 00:00:00 2001 From: Matthew Crossman Date: Tue, 4 Jan 2022 21:44:24 +1100 Subject: [PATCH 9/9] Rename auto install preference to better reflect behaviour. Auto install preference always runs after a sync, even if it's manual. The names of the preference & strings have been updated to reflect this new behaviour. --- src/main/kotlin/com/looker/droidify/content/Preferences.kt | 4 ++-- .../kotlin/com/looker/droidify/screen/SettingsFragment.kt | 4 ++-- src/main/kotlin/com/looker/droidify/service/SyncService.kt | 4 ++-- src/main/res/values/strings.xml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/looker/droidify/content/Preferences.kt b/src/main/kotlin/com/looker/droidify/content/Preferences.kt index cbc7dcfa..6eec3171 100644 --- a/src/main/kotlin/com/looker/droidify/content/Preferences.kt +++ b/src/main/kotlin/com/looker/droidify/content/Preferences.kt @@ -24,7 +24,7 @@ object Preferences { private val keys = sequenceOf( Key.Language, Key.AutoSync, - Key.AutoSyncInstall, + Key.InstallAfterSync, Key.IncompatibleVersions, Key.ListAnimation, Key.ProxyHost, @@ -131,7 +131,7 @@ object Preferences { "auto_sync", Value.EnumerationValue(Preferences.AutoSync.Wifi) ) - object AutoSyncInstall : + object InstallAfterSync : Key("auto_sync_install", Value.BooleanValue(Android.sdk(31))) object IncompatibleVersions : diff --git a/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt b/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt index 9d636faa..92c1d8e0 100644 --- a/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/SettingsFragment.kt @@ -109,8 +109,8 @@ class SettingsFragment : ScreenFragment() { } } addSwitch( - Preferences.Key.AutoSyncInstall, getString(R.string.sync_auto_install), - getString(R.string.sync_auto_install_summary) + Preferences.Key.InstallAfterSync, getString(R.string.install_after_sync), + getString(R.string.install_after_sync_summary) ) addSwitch( Preferences.Key.UpdateNotify, getString(R.string.notify_about_updates), diff --git a/src/main/kotlin/com/looker/droidify/service/SyncService.kt b/src/main/kotlin/com/looker/droidify/service/SyncService.kt index 4d000c05..57a4f451 100644 --- a/src/main/kotlin/com/looker/droidify/service/SyncService.kt +++ b/src/main/kotlin/com/looker/droidify/service/SyncService.kt @@ -403,7 +403,7 @@ class SyncService : ConnectionService() { currentTask = null handleNextTask(false) if (result.isNotEmpty()) { - if (Preferences[Preferences.Key.AutoSyncInstall]) + if (Preferences[Preferences.Key.InstallAfterSync]) runAutoUpdate(result) if (hasUpdates && Preferences[Preferences.Key.UpdateNotify] && updateNotificationBlockerFragment?.get()?.isAdded == true @@ -434,7 +434,7 @@ class SyncService : ConnectionService() { * @see SyncService.displayUpdatesNotification */ private fun runAutoUpdate(productItems: List) { - if (Preferences[Preferences.Key.AutoSyncInstall]) { + if (Preferences[Preferences.Key.InstallAfterSync]) { // run startUpdate on every item productItems.map { productItem -> Pair( diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 9fd2399a..0a8d7b5a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -180,6 +180,6 @@ Installed applications Sort & Filter New applications - Install updates automatically - Automatically install app updates after syncing repositories + Install updates automatically + Automatically install app updates after syncing repositories