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_SYNCING = "syncing"
const val NOTIFICATION_CHANNEL_UPDATES = "updates" const val NOTIFICATION_CHANNEL_UPDATES = "updates"
const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading" const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading"
const val NOTIFICATION_CHANNEL_INSTALLER = "installed"
const val NOTIFICATION_ID_SYNCING = 1 const val NOTIFICATION_ID_SYNCING = 1
const val NOTIFICATION_ID_UPDATES = 2 const val NOTIFICATION_ID_UPDATES = 2
const val NOTIFICATION_ID_DOWNLOADING = 3 const val NOTIFICATION_ID_DOWNLOADING = 3
const val NOTIFICATION_ID_INSTALLER = 4
const val ROW_REPOSITORY_ID = "repository_id" const val ROW_REPOSITORY_ID = "repository_id"
const val ROW_PACKAGE_NAME = "package_name" const val ROW_PACKAGE_NAME = "package_name"

View File

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

View File

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

View File

@ -4,15 +4,19 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageInstaller.SessionParams
import android.util.Log
import com.looker.droidify.content.Cache import com.looker.droidify.content.Cache
import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.extension.android.Android
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
class DefaultInstaller(context: Context) : BaseInstaller(context) { 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) private val intent = Intent(context, InstallerService::class.java)
companion object { companion object {
@ -33,33 +37,86 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) {
override suspend fun install(packageName: String, cacheFileName: String) { override suspend fun install(packageName: String, cacheFileName: String) {
val cacheFile = Cache.getReleaseFile(context, cacheFileName) 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) 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) mDefaultInstaller(cacheFile)
}
override suspend fun uninstall(packageName: String) = mDefaultUninstaller(packageName) override suspend fun uninstall(packageName: String) = mDefaultUninstaller(packageName)
private fun mDefaultInstaller(cacheFile: File) { private fun mDefaultInstaller(cacheFile: File) {
// 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."
)
}
}
// start new session
val id = sessionInstaller.createSession(sessionParams) val id = sessionInstaller.createSession(sessionParams)
val session = sessionInstaller.openSession(id) 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 -> session.use { activeSession ->
activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream -> try {
activeSession.openWrite(packageName, 0, cacheFile.length()).use { packageStream ->
try {
cacheFile.inputStream().use { fileStream -> cacheFile.inputStream().use { fileStream ->
fileStream.copyTo(packageStream) 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
}
} }
val pendingIntent = PendingIntent.getService(context, id, intent, flags) if (!hasErrors) {
session.commit(PendingIntent.getService(context, id, intent, flags).intentSender)
session.commit(pendingIntent.intentSender)
}
cacheFile.delete() cacheFile.delete()
} }
}
private suspend fun mDefaultUninstaller(packageName: String) { private suspend fun mDefaultUninstaller(packageName: String) {
intent.putExtra(InstallerService.KEY_ACTION, InstallerService.ACTION_UNINSTALL) intent.putExtra(InstallerService.KEY_ACTION, InstallerService.ACTION_UNINSTALL)

View File

@ -1,33 +1,56 @@
package com.looker.droidify.installer package com.looker.droidify.installer
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.IBinder import android.os.IBinder
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.looker.droidify.NOTIFICATION_CHANNEL_DOWNLOADING import com.looker.droidify.NOTIFICATION_CHANNEL_INSTALLER
import com.looker.droidify.NOTIFICATION_ID_DOWNLOADING import com.looker.droidify.NOTIFICATION_ID_INSTALLER
import com.looker.droidify.MainActivity
import com.looker.droidify.R 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.android.notificationManager
import com.looker.droidify.utility.extension.resources.getColorFromAttr import com.looker.droidify.utility.extension.resources.getColorFromAttr
/** /**
* Runs during or after a PackageInstaller session in order to handle completion, failure, or * 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() { class InstallerService : Service() {
companion object { companion object {
const val KEY_ACTION = "installerAction" const val KEY_ACTION = "installerAction"
const val KEY_APP_NAME = "appName"
const val ACTION_UNINSTALL = "uninstall" 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 { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { // only trigger a prompt if in foreground, otherwise make notification
// prompts user to enable unknown source 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) val promptIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT)
promptIntent?.let { promptIntent?.let {
@ -46,17 +69,27 @@ 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) { private fun notifyStatus(intent: Intent) {
// unpack from intent // unpack from intent
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) 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 message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val installerAction = intent.getStringExtra(KEY_ACTION) val installerAction = intent.getStringExtra(KEY_ACTION)
// get application name for notifications // get application name for notifications
val appLabel = try { val appLabel = session?.appLabel ?: intent.getStringExtra(KEY_APP_NAME)
?: try {
if (name != null) packageManager.getApplicationLabel( if (name != null) packageManager.getApplicationLabel(
packageManager.getApplicationInfo( packageManager.getApplicationInfo(
name, name,
@ -67,11 +100,11 @@ class InstallerService : Service() {
null null
} }
val notificationTag = "download-$name" val notificationTag = "${NOTIFICATION_TAG_PREFIX}$name"
// start building // start building
val builder = NotificationCompat val builder = NotificationCompat
.Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING) .Builder(this, NOTIFICATION_CHANNEL_INSTALLER)
.setAutoCancel(true) .setAutoCancel(true)
.setColor( .setColor(
ContextThemeWrapper(this, R.style.Theme_Main_Light) ContextThemeWrapper(this, R.style.Theme_Main_Light)
@ -79,23 +112,33 @@ class InstallerService : Service() {
) )
when (status) { 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 -> { PackageInstaller.STATUS_SUCCESS -> {
if (installerAction == ACTION_UNINSTALL) if (installerAction == ACTION_UNINSTALL)
// remove any notification for this app // remove any notification for this app
notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING) notificationManager.cancel(notificationTag, NOTIFICATION_ID_INSTALLER)
else { else {
val notification = builder val notification = builder
.setSmallIcon(android.R.drawable.stat_sys_download_done) .setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(getString(R.string.installed)) .setContentTitle(getString(R.string.installed))
.setContentText(appLabel) .setContentText(appLabel)
.setTimeoutAfter(INSTALLED_NOTIFICATION_TIMEOUT)
.build() .build()
notificationManager.notify( notificationManager.notify(
notificationTag, notificationTag,
NOTIFICATION_ID_DOWNLOADING, NOTIFICATION_ID_INSTALLER,
notification notification
) )
Thread.sleep(5000)
notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING)
} }
} }
PackageInstaller.STATUS_FAILURE_ABORTED -> { PackageInstaller.STATUS_FAILURE_ABORTED -> {
@ -110,7 +153,7 @@ class InstallerService : Service() {
.build() .build()
notificationManager.notify( notificationManager.notify(
notificationTag, notificationTag,
NOTIFICATION_ID_DOWNLOADING, NOTIFICATION_ID_INSTALLER,
notification notification
) )
} }
@ -121,5 +164,33 @@ class InstallerService : Service() {
return null 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 package com.looker.droidify.screen
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Bundle import android.os.Bundle
import android.os.Parcel import android.os.Parcel
import android.view.ViewGroup import android.view.ViewGroup
@ -14,7 +15,7 @@ import com.looker.droidify.MainApplication
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.Preferences import com.looker.droidify.content.Preferences
import com.looker.droidify.database.CursorOwner 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.ui.fragments.AppDetailFragment
import com.looker.droidify.utility.KParcelable import com.looker.droidify.utility.KParcelable
import com.looker.droidify.utility.extension.resources.getDrawableFromAttr import com.looker.droidify.utility.extension.resources.getDrawableFromAttr
@ -31,7 +32,7 @@ abstract class ScreenActivity : AppCompatActivity() {
sealed class SpecialIntent { sealed class SpecialIntent {
object Updates : 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( private class FragmentStackItem(
@ -221,13 +222,23 @@ abstract class ScreenActivity : AppCompatActivity() {
} }
is SpecialIntent.Install -> { is SpecialIntent.Install -> {
val packageName = specialIntent.packageName val packageName = specialIntent.packageName
if (!packageName.isNullOrEmpty()) { val status = specialIntent.status
val promptIntent = specialIntent.promptIntent
if (!packageName.isNullOrEmpty() && status != null && promptIntent != null) {
lifecycleScope.launch { lifecycleScope.launch {
specialIntent.cacheFileName?.let { startService(
AppInstaller.getInstance(this@ScreenActivity) Intent(baseContext, InstallerService::class.java)
?.defaultInstaller?.install(packageName, it) .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 Unit
} }

View File

@ -108,6 +108,10 @@ class SettingsFragment : ScreenFragment() {
Preferences.AutoSync.Always -> getString(R.string.always) 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( addSwitch(
Preferences.Key.UpdateNotify, getString(R.string.notify_about_updates), Preferences.Key.UpdateNotify, getString(R.string.notify_about_updates),
getString(R.string.notify_about_updates_summary) 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.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
@ -16,7 +14,6 @@ import com.looker.droidify.entity.Repository
import com.looker.droidify.installer.AppInstaller import com.looker.droidify.installer.AppInstaller
import com.looker.droidify.network.Downloader import com.looker.droidify.network.Downloader
import com.looker.droidify.utility.Utils 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.android.*
import com.looker.droidify.utility.extension.resources.* import com.looker.droidify.utility.extension.resources.*
import com.looker.droidify.utility.extension.text.* import com.looker.droidify.utility.extension.text.*
@ -30,11 +27,7 @@ import kotlin.math.*
class DownloadService : ConnectionService<DownloadService.Binder>() { class DownloadService : ConnectionService<DownloadService.Binder>() {
companion object { 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 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 mutableDownloadState = MutableSharedFlow<State.Downloading>()
private val downloadState = mutableDownloadState.asSharedFlow() private val downloadState = mutableDownloadState.asSharedFlow()
@ -43,34 +36,6 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
private val scope = CoroutineScope(Dispatchers.Default) private val scope = CoroutineScope(Dispatchers.Default)
private val mainDispatcher = Dispatchers.Main 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) { sealed class State(val packageName: String, val name: String) {
class Pending(packageName: String, name: String) : State(packageName, name) class Pending(packageName: String, name: String) : State(packageName, name)
class Connecting(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 .getColorFromAttr(R.attr.colorPrimary).defaultColor
) )
.setContentIntent( .setContentIntent(
PendingIntent.getBroadcast( PendingIntent.getActivity(
this, this,
0, 0,
Intent(this, Receiver::class.java) Intent(this, MainActivity::class.java)
.setAction("$ACTION_OPEN.${task.packageName}"), .setAction(Intent.ACTION_VIEW)
.setData(Uri.parse("package:${task.packageName}"))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
if (Android.sdk(23)) if (Android.sdk(23))
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
else else
@ -275,33 +242,6 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
.build()) .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) { private fun publishSuccess(task: Task) {
var consumed = false var consumed = false
scope.launch(mainDispatcher) { scope.launch(mainDispatcher) {
@ -309,12 +249,10 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
consumed = true consumed = true
} }
if (!consumed) { if (!consumed) {
if (rootInstallerEnabled) {
scope.launch { scope.launch {
AppInstaller.getInstance(this@DownloadService) AppInstaller.getInstance(this@DownloadService)
?.defaultInstaller?.install(task.release.cacheFileName) ?.defaultInstaller?.install(task.name, task.release.cacheFileName)
} }
} else showNotificationInstall(task)
} }
} }

View File

@ -19,6 +19,7 @@ import com.looker.droidify.entity.ProductItem
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
import com.looker.droidify.index.RepositoryUpdater import com.looker.droidify.index.RepositoryUpdater
import com.looker.droidify.utility.RxUtils 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.Android
import com.looker.droidify.utility.extension.android.asSequence import com.looker.droidify.utility.extension.android.asSequence
import com.looker.droidify.utility.extension.android.notificationManager import com.looker.droidify.utility.extension.android.notificationManager
@ -70,6 +71,8 @@ class SyncService : ConnectionService<SyncService.Binder>() {
private var updateNotificationBlockerFragment: WeakReference<Fragment>? = null private var updateNotificationBlockerFragment: WeakReference<Fragment>? = null
private val downloadConnection = Connection(DownloadService::class.java)
enum class SyncRequest { AUTO, MANUAL, FORCE } enum class SyncRequest { AUTO, MANUAL, FORCE }
inner class Binder : android.os.Binder() { inner class Binder : android.os.Binder() {
@ -172,11 +175,13 @@ class SyncService : ConnectionService<SyncService.Binder>() {
.let(notificationManager::createNotificationChannel) .let(notificationManager::createNotificationChannel)
} }
downloadConnection.bind(this)
stateSubject.onEach { publishForegroundState(false, it) }.launchIn(scope) stateSubject.onEach { publishForegroundState(false, it) }.launchIn(scope)
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
downloadConnection.unbind(this)
cancelTasks { true } cancelTasks { true }
cancelCurrentTask { true } cancelCurrentTask { true }
} }
@ -372,7 +377,6 @@ class SyncService : ConnectionService<SyncService.Binder>() {
handleNextTask(hasUpdates) handleNextTask(hasUpdates)
} }
} else if (started != Started.NO) { } else if (started != Started.NO) {
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
val disposable = RxUtils val disposable = RxUtils
.querySingle { it -> .querySingle { it ->
db.productDao db.productDao
@ -385,7 +389,8 @@ class SyncService : ConnectionService<SyncService.Binder>() {
signal = it signal = it
) )
.use { .use {
it.asSequence().map { it.getProductItem() }.toList() it.asSequence().map { it.getProductItem() }
.toList()
} }
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -394,11 +399,16 @@ class SyncService : ConnectionService<SyncService.Binder>() {
throwable?.printStackTrace() throwable?.printStackTrace()
currentTask = null currentTask = null
handleNextTask(false) handleNextTask(false)
val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true if (result.isNotEmpty()) {
if (!blocked && result != null && result.isNotEmpty()) { if (Preferences[Preferences.Key.InstallAfterSync])
runAutoUpdate(result)
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify] &&
updateNotificationBlockerFragment?.get()?.isAdded == true
)
displayUpdatesNotification(result) displayUpdatesNotification(result)
} }
} }
if (hasUpdates) {
currentTask = CurrentTask(null, disposable, true, State.Finishing) currentTask = CurrentTask(null, disposable, true, State.Finishing)
} else { } else {
scope.launch { mutableFinishState.emit(Unit) } 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>) { private fun displayUpdatesNotification(productItems: List<ProductItem>) {
val maxUpdates = 5 val maxUpdates = 5
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback) fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)

View File

@ -1,5 +1,8 @@
package com.looker.droidify.utility 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.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.Signature import android.content.pm.Signature
@ -172,6 +175,17 @@ object Utils {
) )
else -> Locale(localeCode) 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)) fun Cursor.getProduct(): Product = getBlob(getColumnIndex(ROW_DATA))

View File

@ -181,4 +181,6 @@
<string name="installed_applications">Installed applications</string> <string name="installed_applications">Installed applications</string>
<string name="sort_filter">Sort &amp; Filter</string> <string name="sort_filter">Sort &amp; Filter</string>
<string name="new_applications">New applications</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> </resources>