Merge pull request #159: Automatic updates and installer improvements (closes #20)

This commit is contained in:
machiav3lli 2022-01-04 23:36:06 +01:00 committed by GitHub
commit 8dc4dcce0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 304 additions and 142 deletions

View File

@ -3,10 +3,12 @@ package com.looker.droidify
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 ROW_REPOSITORY_ID = "repository_id"
const val ROW_PACKAGE_NAME = "package_name"

View File

@ -2,6 +2,7 @@ package com.looker.droidify
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import com.looker.droidify.ContextWrapperX.Companion.wrap
import com.looker.droidify.screen.ScreenActivity
@ -19,7 +20,8 @@ class MainActivity : ScreenActivity() {
ACTION_INSTALL -> handleSpecialIntent(
SpecialIntent.Install(
intent.packageName,
intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1),
intent.getParcelableExtra(Intent.EXTRA_INTENT)
)
)
else -> super.handleIntent(intent)

View File

@ -24,6 +24,7 @@ object Preferences {
private val keys = sequenceOf(
Key.Language,
Key.AutoSync,
Key.InstallAfterSync,
Key.IncompatibleVersions,
Key.ListAnimation,
Key.ProxyHost,
@ -132,6 +133,8 @@ object Preferences {
"auto_sync",
Value.EnumerationValue(Preferences.AutoSync.Wifi)
)
object InstallAfterSync :
Key<Boolean>("auto_sync_install", Value.BooleanValue(Android.sdk(31)))
object IncompatibleVersions :
Key<Boolean>("incompatible_versions", Value.BooleanValue(false))

View File

@ -4,15 +4,19 @@ 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
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 {
@ -33,32 +37,85 @@ 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)
private fun mDefaultInstaller(cacheFile: File) {
val id = sessionInstaller.createSession(sessionParams)
val session = sessionInstaller.openSession(id)
session.use { activeSession ->
activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream ->
cacheFile.inputStream().use { fileStream ->
fileStream.copyTo(packageStream)
// clean up inactive sessions
sessionInstaller.mySessions
.filter { session -> !session.isActive }
.forEach { session ->
try {
sessionInstaller.abandonSession(session.sessionId)
}
catch (_: SecurityException) {
Log.w(
"DefaultInstaller",
"Attempted to abandon a session we do not own."
)
}
}
val pendingIntent = PendingIntent.getService(context, id, intent, flags)
// start new session
val id = sessionInstaller.createSession(sessionParams)
val session = sessionInstaller.openSession(id)
session.commit(pendingIntent.intentSender)
// get package name
val packageInfo = packageManager.getPackageArchiveInfo(cacheFile.absolutePath, 0)
val packageName = packageInfo?.packageName ?: "unknown-package"
// error flags
var hasErrors = false
session.use { activeSession ->
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 (_: 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
}
}
if (!hasErrors) {
session.commit(PendingIntent.getService(context, id, intent, flags).intentSender)
cacheFile.delete()
}
cacheFile.delete()
}
private suspend fun mDefaultUninstaller(packageName: String) {

View File

@ -1,33 +1,56 @@
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.NOTIFICATION_CHANNEL_DOWNLOADING
import com.looker.droidify.NOTIFICATION_ID_DOWNLOADING
import com.looker.droidify.NOTIFICATION_CHANNEL_INSTALLER
import com.looker.droidify.NOTIFICATION_ID_INSTALLER
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
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"
private const val INSTALLED_NOTIFICATION_TIMEOUT: Long = 5000
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 {
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, 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)
promptIntent?.let {
@ -46,32 +69,42 @@ 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
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 = try {
if (name != null) packageManager.getApplicationLabel(
packageManager.getApplicationInfo(
name,
PackageManager.GET_META_DATA
)
) else null
} catch (_: Exception) {
null
}
val appLabel = session?.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"
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)
@ -79,23 +112,33 @@ 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_INSTALLER, 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, 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 -> {
@ -110,7 +153,7 @@ class InstallerService : Service() {
.build()
notificationManager.notify(
notificationTag,
NOTIFICATION_ID_DOWNLOADING,
NOTIFICATION_ID_INSTALLER,
notification
)
}
@ -121,5 +164,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
)
}
}

View File

@ -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
@ -14,7 +15,7 @@ import com.looker.droidify.MainApplication
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
@ -31,7 +32,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(
@ -221,14 +222,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

View File

@ -108,6 +108,10 @@ class SettingsFragment : ScreenFragment() {
Preferences.AutoSync.Always -> getString(R.string.always)
}
}
addSwitch(
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),
getString(R.string.notify_about_updates_summary)

View File

@ -3,8 +3,6 @@ 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
@ -16,7 +14,6 @@ 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.*
@ -30,11 +27,7 @@ import kotlin.math.*
class DownloadService : ConnectionService<DownloadService.Binder>() {
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<State.Downloading>()
private val downloadState = mutableDownloadState.asSharedFlow()
@ -43,34 +36,6 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
private val scope = CoroutineScope(Dispatchers.Default)
private val mainDispatcher = Dispatchers.Main
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)
)
}
}
}
}
sealed class State(val packageName: String, val name: String) {
class Pending(packageName: String, name: String) : State(packageName, name)
class Connecting(packageName: String, name: String) : State(packageName, name)
@ -220,11 +185,13 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
.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
@ -275,33 +242,6 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
.build())
}
private fun showNotificationInstall(task: Task) {
notificationManager.notify(
task.notificationTag, NOTIFICATION_ID_DOWNLOADING, NotificationCompat
.Builder(this, 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(mainDispatcher) {
@ -309,12 +249,10 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
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)
}
}
}

View File

@ -19,6 +19,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
@ -70,6 +71,8 @@ class SyncService : ConnectionService<SyncService.Binder>() {
private var updateNotificationBlockerFragment: WeakReference<Fragment>? = null
private val downloadConnection = Connection(DownloadService::class.java)
enum class SyncRequest { AUTO, MANUAL, FORCE }
inner class Binder : android.os.Binder() {
@ -172,11 +175,13 @@ class SyncService : ConnectionService<SyncService.Binder>() {
.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 }
}
@ -372,33 +377,38 @@ class SyncService : ConnectionService<SyncService.Binder>() {
handleNextTask(hasUpdates)
}
} else if (started != Started.NO) {
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
val disposable = RxUtils
.querySingle { it ->
db.productDao
.query(
installed = true,
updates = true,
searchQuery = "",
section = ProductItem.Section.All,
order = ProductItem.Order.NAME,
signal = it
)
.use {
it.asSequence().map { it.getProductItem() }.toList()
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result, throwable ->
throwable?.printStackTrace()
currentTask = null
handleNextTask(false)
val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true
if (!blocked && result != null && result.isNotEmpty()) {
displayUpdatesNotification(result)
val disposable = RxUtils
.querySingle { it ->
db.productDao
.query(
installed = true,
updates = true,
searchQuery = "",
section = ProductItem.Section.All,
order = ProductItem.Order.NAME,
signal = it
)
.use {
it.asSequence().map { it.getProductItem() }
.toList()
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result, throwable ->
throwable?.printStackTrace()
currentTask = null
handleNextTask(false)
if (result.isNotEmpty()) {
if (Preferences[Preferences.Key.InstallAfterSync])
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) }
@ -413,6 +423,54 @@ class SyncService : ConnectionService<SyncService.Binder>() {
}
}
/**
* 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<ProductItem>) {
if (Preferences[Preferences.Key.InstallAfterSync]) {
// run startUpdate on every item
productItems.map { productItem ->
Pair(
Database.InstalledAdapter.get(productItem.packageName, null),
Database.RepositoryAdapter.get(productItem.repositoryId)
)
}
.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
)
.filter { product -> product.repositoryId == repository.id }
.map { product -> Pair(product, repository) }
scope.launch {
Utils.startUpdate(
installedItem.packageName,
installedRepository.first,
productRepository,
downloadConnection
)
}
}
}
}
}
/**
* Displays summary of available updates.
*
* @param productItems list of apps pending updates
*/
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
val maxUpdates = 5
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)

View File

@ -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
@ -172,6 +175,17 @@ 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))
}
}
fun Cursor.getProduct(): Product = getBlob(getColumnIndex(ROW_DATA))

View File

@ -181,4 +181,6 @@
<string name="installed_applications">Installed applications</string>
<string name="sort_filter">Sort &amp; Filter</string>
<string name="new_applications">New applications</string>
<string name="install_after_sync">Install updates automatically</string>
<string name="install_after_sync_summary">Automatically install app updates after syncing repositories</string>
</resources>