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
No known key found for this signature in database
GPG Key ID: C6B942B019794CC2
6 changed files with 112 additions and 117 deletions

View File

@ -4,10 +4,12 @@ object Common {
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 PREFS_LANGUAGE = "languages" const val PREFS_LANGUAGE = "languages"
const val PREFS_LANGUAGE_DEFAULT = "system" const val PREFS_LANGUAGE_DEFAULT = "system"

View File

@ -11,10 +11,12 @@ 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.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 {
@ -48,27 +50,48 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) {
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 -> sessionInstaller.abandonSession(session.sessionId) }
// 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 -> activeSession.openWrite(packageName, 0, cacheFile.length()).use { packageStream ->
try { try {
cacheFile.inputStream().use { fileStream -> cacheFile.inputStream().use { fileStream ->
fileStream.copyTo(packageStream) fileStream.copyTo(packageStream)
} }
} catch (error: FileNotFoundException) { } catch (e: FileNotFoundException) {
Log.w("DefaultInstaller", "Cache file for DefaultInstaller does not seem to exist.") 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) { private suspend fun mDefaultUninstaller(packageName: String) {

View File

@ -1,16 +1,17 @@
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.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.Common.NOTIFICATION_CHANNEL_DOWNLOADING import com.looker.droidify.Common.NOTIFICATION_CHANNEL_INSTALLER
import com.looker.droidify.Common.NOTIFICATION_ID_DOWNLOADING import com.looker.droidify.Common.NOTIFICATION_ID_INSTALLER
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.MainActivity import com.looker.droidify.MainActivity
import com.looker.droidify.utility.Utils import com.looker.droidify.utility.Utils
@ -27,6 +28,20 @@ class InstallerService : Service() {
const val KEY_ACTION = "installerAction" const val KEY_ACTION = "installerAction"
const val KEY_APP_NAME = "appName" const val KEY_APP_NAME = "appName"
const val ACTION_UNINSTALL = "uninstall" 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 { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
@ -61,12 +76,18 @@ class InstallerService : Service() {
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 = intent.getStringExtra(KEY_APP_NAME) val appLabel = session?.appLabel ?: intent.getStringExtra(KEY_APP_NAME)
?: try { ?: try {
if (name != null) packageManager.getApplicationLabel( if (name != null) packageManager.getApplicationLabel(
packageManager.getApplicationInfo( packageManager.getApplicationInfo(
@ -78,11 +99,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)
@ -93,7 +114,7 @@ class InstallerService : Service() {
PackageInstaller.STATUS_PENDING_USER_ACTION -> { PackageInstaller.STATUS_PENDING_USER_ACTION -> {
// request user action with "downloaded" notification that triggers a working prompt // request user action with "downloaded" notification that triggers a working prompt
notificationManager.notify( notificationManager.notify(
notificationTag, NOTIFICATION_ID_DOWNLOADING, builder notificationTag, NOTIFICATION_ID_INSTALLER, builder
.setSmallIcon(android.R.drawable.stat_sys_download_done) .setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentIntent(installIntent(intent)) .setContentIntent(installIntent(intent))
.setContentTitle(getString(R.string.downloaded_FORMAT, appLabel)) .setContentTitle(getString(R.string.downloaded_FORMAT, appLabel))
@ -104,20 +125,19 @@ class InstallerService : Service() {
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 -> {
@ -132,7 +152,7 @@ class InstallerService : Service() {
.build() .build()
notificationManager.notify( notificationManager.notify(
notificationTag, notificationTag,
NOTIFICATION_ID_DOWNLOADING, NOTIFICATION_ID_INSTALLER,
notification notification
) )
} }
@ -163,7 +183,6 @@ class InstallerService : Service() {
0, 0,
Intent(this, MainActivity::class.java) Intent(this, MainActivity::class.java)
.setAction(MainActivity.ACTION_INSTALL) .setAction(MainActivity.ACTION_INSTALL)
.setData(Uri.parse("package:$name"))
.putExtra(Intent.EXTRA_INTENT, promptIntent) .putExtra(Intent.EXTRA_INTENT, promptIntent)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

View File

@ -41,6 +41,11 @@ class SettingsFragment : ScreenFragment() {
private var preferenceBinding: PreferenceItemBinding? = null private var preferenceBinding: PreferenceItemBinding? = null
private val preferences = mutableMapOf<Preferences.Key<*>, Preference<*>>() private val preferences = mutableMapOf<Preferences.Key<*>, Preference<*>>()
override fun onResume() {
super.onResume()
preferences.forEach { (_, preference) -> preference.update() }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
preferenceBinding = PreferenceItemBinding.inflate(layoutInflater) preferenceBinding = PreferenceItemBinding.inflate(layoutInflater)
@ -216,7 +221,7 @@ class SettingsFragment : ScreenFragment() {
} }
} }
private fun LinearLayoutCompat.addCategory( private inline fun LinearLayoutCompat.addCategory(
title: String, title: String,
callback: LinearLayoutCompat.() -> Unit, callback: LinearLayoutCompat.() -> Unit,
) { ) {

View File

@ -3,14 +3,14 @@ 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
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.looker.droidify.BuildConfig 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.MainActivity
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.Cache 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.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.*
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.collect
import java.io.File import java.io.File
import java.security.MessageDigest import java.security.MessageDigest
import kotlin.math.* 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()
} }
val scope = CoroutineScope(Dispatchers.Default) 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) { 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)
@ -121,7 +87,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
} else { } else {
cancelTasks(packageName) cancelTasks(packageName)
cancelCurrentTask(packageName) cancelCurrentTask(packageName)
notificationManager.cancel(task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING) notificationManager.cancel(task.notificationTag, NOTIFICATION_ID_DOWNLOADING)
tasks += task tasks += task
if (currentTask == null) { if (currentTask == null) {
handleDownload() handleDownload()
@ -146,16 +112,14 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
if (Android.sdk(26)) { if (Android.sdk(26)) {
NotificationChannel( NotificationChannel(
Common.NOTIFICATION_CHANNEL_DOWNLOADING, NOTIFICATION_CHANNEL_DOWNLOADING,
getString(R.string.downloading), NotificationManager.IMPORTANCE_LOW getString(R.string.downloading), NotificationManager.IMPORTANCE_LOW
) )
.apply { setShowBadge(false) } .apply { setShowBadge(false) }
.let(notificationManager::createNotificationChannel) .let(notificationManager::createNotificationChannel)
} }
scope.launch { downloadState.onEach { publishForegroundState(false, it) }.launchIn(scope)
downloadState.collect { publishForegroundState(false, it) }
}
} }
override fun onDestroy() { override fun onDestroy() {
@ -176,7 +140,14 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
private fun cancelTasks(packageName: String?) { private fun cancelTasks(packageName: String?) {
tasks.removeAll { tasks.removeAll {
(packageName == null || it.packageName == packageName) && run { (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 true
} }
} }
@ -186,7 +157,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
currentTask?.let { currentTask?.let {
if (packageName == null || it.task.packageName == packageName) { if (packageName == null || it.task.packageName == packageName) {
currentTask = null currentTask = null
scope.launch { scope.launch(mainDispatcher) {
mutableStateSubject.emit( mutableStateSubject.emit(
State.Cancel( State.Cancel(
it.task.packageName, it.task.packageName,
@ -209,9 +180,9 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
private fun showNotificationError(task: Task, errorType: ErrorType) { private fun showNotificationError(task: Task, errorType: ErrorType) {
notificationManager.notify(task.notificationTag, notificationManager.notify(task.notificationTag,
Common.NOTIFICATION_ID_DOWNLOADING, NOTIFICATION_ID_DOWNLOADING,
NotificationCompat NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING)
.setAutoCancel(true) .setAutoCancel(true)
.setSmallIcon(android.R.drawable.stat_sys_warning) .setSmallIcon(android.R.drawable.stat_sys_warning)
.setColor( .setColor(
@ -276,36 +247,9 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
.build()) .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) { private fun publishSuccess(task: Task) {
var consumed = false var consumed = false
scope.launch { scope.launch(mainDispatcher) {
mutableStateSubject.emit(State.Success(task.packageName, task.name, task.release)) mutableStateSubject.emit(State.Success(task.packageName, task.name, task.release))
consumed = true consumed = true
} }
@ -367,7 +311,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
private val stateNotificationBuilder by lazy { private val stateNotificationBuilder by lazy {
NotificationCompat NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING) .Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING)
.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
.setColor( .setColor(
ContextThemeWrapper(this, R.style.Theme_Main_Light) ContextThemeWrapper(this, R.style.Theme_Main_Light)
@ -389,7 +333,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
private fun publishForegroundState(force: Boolean, state: State) { private fun publishForegroundState(force: Boolean, state: State) {
if (force || currentTask != null) { if (force || currentTask != null) {
currentTask = currentTask?.copy(lastState = state) currentTask = currentTask?.copy(lastState = state)
startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { startForeground(NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply {
when (state) { when (state) {
is State.Connecting -> { is State.Connecting -> {
setContentTitle(getString(R.string.downloading_FORMAT, state.name)) 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.app.job.JobService
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.looker.droidify.BuildConfig 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.MainActivity
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.Preferences import com.looker.droidify.content.Preferences
@ -123,7 +125,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
fun setUpdateNotificationBlocker(fragment: Fragment?) { fun setUpdateNotificationBlocker(fragment: Fragment?) {
updateNotificationBlockerFragment = fragment?.let(::WeakReference) updateNotificationBlockerFragment = fragment?.let(::WeakReference)
if (fragment != null) { 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)) { if (Android.sdk(26)) {
NotificationChannel( NotificationChannel(
Common.NOTIFICATION_CHANNEL_SYNCING, NOTIFICATION_CHANNEL_SYNCING,
getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW
) )
.apply { setShowBadge(false) } .apply { setShowBadge(false) }
.let(notificationManager::createNotificationChannel) .let(notificationManager::createNotificationChannel)
NotificationChannel( NotificationChannel(
Common.NOTIFICATION_CHANNEL_UPDATES, NOTIFICATION_CHANNEL_UPDATES,
getString(R.string.updates), NotificationManager.IMPORTANCE_LOW getString(R.string.updates), NotificationManager.IMPORTANCE_LOW
) )
.let(notificationManager::createNotificationChannel) .let(notificationManager::createNotificationChannel)
@ -215,8 +217,8 @@ class SyncService : ConnectionService<SyncService.Binder>() {
private fun showNotificationError(repository: Repository, exception: Exception) { private fun showNotificationError(repository: Repository, exception: Exception) {
notificationManager.notify( notificationManager.notify(
"repository-${repository.id}", Common.NOTIFICATION_ID_SYNCING, NotificationCompat "repository-${repository.id}", NOTIFICATION_ID_SYNCING, NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING) .Builder(this, NOTIFICATION_CHANNEL_SYNCING)
.setSmallIcon(android.R.drawable.stat_sys_warning) .setSmallIcon(android.R.drawable.stat_sys_warning)
.setColor( .setColor(
ContextThemeWrapper(this, R.style.Theme_Main_Light) ContextThemeWrapper(this, R.style.Theme_Main_Light)
@ -242,7 +244,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
private val stateNotificationBuilder by lazy { private val stateNotificationBuilder by lazy {
NotificationCompat NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING) .Builder(this, NOTIFICATION_CHANNEL_SYNCING)
.setSmallIcon(R.drawable.ic_sync) .setSmallIcon(R.drawable.ic_sync)
.setColor( .setColor(
ContextThemeWrapper(this, R.style.Theme_Main_Light) ContextThemeWrapper(this, R.style.Theme_Main_Light)
@ -253,7 +255,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
this, this,
0, 0,
Intent(this, this::class.java).setAction(ACTION_CANCEL), 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 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
else else
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
@ -265,7 +267,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
if (force || currentTask?.lastState != state) { if (force || currentTask?.lastState != state) {
currentTask = currentTask?.copy(lastState = state) currentTask = currentTask?.copy(lastState = state)
if (started == Started.MANUAL) { if (started == Started.MANUAL) {
startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply { startForeground(NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply {
when (state) { when (state) {
is State.Connecting -> { is State.Connecting -> {
setContentTitle(getString(R.string.syncing_FORMAT, state.name)) setContentTitle(getString(R.string.syncing_FORMAT, state.name))
@ -474,8 +476,8 @@ class SyncService : ConnectionService<SyncService.Binder>() {
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)
notificationManager.notify( notificationManager.notify(
Common.NOTIFICATION_ID_UPDATES, NotificationCompat NOTIFICATION_ID_UPDATES, NotificationCompat
.Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES) .Builder(this, NOTIFICATION_CHANNEL_UPDATES)
.setSmallIcon(R.drawable.ic_new_releases) .setSmallIcon(R.drawable.ic_new_releases)
.setContentTitle(getString(R.string.new_updates_available)) .setContentTitle(getString(R.string.new_updates_available))
.setContentText( .setContentText(
@ -494,7 +496,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
0, 0,
Intent(this, MainActivity::class.java) Intent(this, MainActivity::class.java)
.setAction(MainActivity.ACTION_UPDATES), .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 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
else else
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT