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