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.
This commit is contained in:
Matthew Crossman
2022-01-04 20:26:07 +11:00
parent e1fc3c656a
commit 375ab23edb
6 changed files with 112 additions and 117 deletions

View File

@ -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<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()
}
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<DownloadService.Binder>() {
} 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<DownloadService.Binder>() {
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<DownloadService.Binder>() {
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<DownloadService.Binder>() {
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<DownloadService.Binder>() {
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<DownloadService.Binder>() {
.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<DownloadService.Binder>() {
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<DownloadService.Binder>() {
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))

View File

@ -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<SyncService.Binder>() {
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<SyncService.Binder>() {
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<SyncService.Binder>() {
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<SyncService.Binder>() {
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<SyncService.Binder>() {
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<SyncService.Binder>() {
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<SyncService.Binder>() {
val maxUpdates = 5
fun <T> 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<SyncService.Binder>() {
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