This commit is contained in:
Felitendo
2025-05-20 15:22:58 +02:00
parent 8a6d5d19db
commit e65e82c85b
465 changed files with 0 additions and 37626 deletions

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,23 +0,0 @@
plugins {
alias(libs.plugins.looker.android.library)
alias(libs.plugins.looker.hilt)
alias(libs.plugins.looker.lint)
}
android {
namespace = "com.looker.installer"
}
dependencies {
modules(
Modules.coreCommon,
Modules.coreDatastore,
Modules.coreDomain,
)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.guava)
implementation(libs.libsu.core)
implementation(libs.shizuku.api)
api(libs.shizuku.provider)
}

View File

@@ -1,117 +0,0 @@
package com.looker.installer
import android.content.Context
import com.looker.core.common.extension.addAndCompute
import com.looker.core.common.extension.filter
import com.looker.core.common.extension.updateAsMutable
import com.looker.core.datastore.SettingsRepository
import com.looker.core.datastore.get
import com.looker.core.datastore.model.InstallerType
import com.looker.core.domain.model.PackageName
import com.looker.installer.installers.Installer
import com.looker.installer.installers.LegacyInstaller
import com.looker.installer.installers.root.RootInstaller
import com.looker.installer.installers.session.SessionInstaller
import com.looker.installer.installers.shizuku.ShizukuInstaller
import com.looker.installer.model.InstallItem
import com.looker.installer.model.InstallState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class InstallManager(
private val context: Context,
settingsRepository: SettingsRepository
) {
private val installItems = Channel<InstallItem>()
private val uninstallItems = Channel<PackageName>()
val state = MutableStateFlow<Map<PackageName, InstallState>>(emptyMap())
private var _installer: Installer? = null
set(value) {
field?.close()
field = value
}
private val installer: Installer get() = _installer!!
private val lock = Mutex()
private val installerPreference = settingsRepository.get { installerType }
suspend operator fun invoke() = coroutineScope {
setupInstaller()
installer()
uninstaller()
}
fun close() {
_installer = null
uninstallItems.close()
installItems.close()
}
suspend infix fun install(installItem: InstallItem) {
installItems.send(installItem)
}
suspend infix fun uninstall(packageName: PackageName) {
uninstallItems.send(packageName)
}
infix fun remove(packageName: PackageName) {
updateState { remove(packageName) }
}
private fun CoroutineScope.setupInstaller() = launch {
installerPreference.collectLatest(::setInstaller)
}
private fun CoroutineScope.installer() = launch {
val currentQueue = mutableSetOf<String>()
installItems.filter { item ->
currentQueue.addAndCompute(item.packageName.name) { isAdded ->
if (isAdded) {
updateState { put(item.packageName, InstallState.Pending) }
}
}
}.consumeEach { item ->
if (state.value.containsKey(item.packageName)) {
updateState { put(item.packageName, InstallState.Installing) }
val success = installer.use {
it.install(item)
}
updateState { put(item.packageName, success) }
currentQueue.remove(item.packageName.name)
}
}
}
private fun CoroutineScope.uninstaller() = launch {
uninstallItems.consumeEach {
installer.uninstall(it)
}
}
private suspend fun setInstaller(installerType: InstallerType) {
lock.withLock {
_installer = when (installerType) {
InstallerType.LEGACY -> LegacyInstaller(context)
InstallerType.SESSION -> SessionInstaller(context)
InstallerType.SHIZUKU -> ShizukuInstaller(context)
InstallerType.ROOT -> RootInstaller(context)
}
}
}
private inline fun updateState(block: MutableMap<PackageName, InstallState>.() -> Unit) {
state.update { it.updateAsMutable(block) }
}
}

View File

@@ -1,34 +0,0 @@
package com.looker.installer
import android.content.Context
import com.looker.core.datastore.SettingsRepository
import com.looker.installer.installers.root.RootPermissionHandler
import com.looker.installer.installers.shizuku.ShizukuPermissionHandler
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object InstallModule {
@Singleton
@Provides
fun providesInstaller(
@ApplicationContext context: Context,
settingsRepository: SettingsRepository
): InstallManager = InstallManager(context, settingsRepository)
@Singleton
@Provides
fun provideShizukuPermissionHandler(
@ApplicationContext context: Context
): ShizukuPermissionHandler = ShizukuPermissionHandler(context)
@Singleton
@Provides
fun provideRootPermissionHandler(): RootPermissionHandler = RootPermissionHandler()
}

View File

@@ -1,13 +0,0 @@
package com.looker.installer.installers
import com.looker.core.domain.model.PackageName
import com.looker.installer.model.InstallItem
import com.looker.installer.model.InstallState
interface Installer : AutoCloseable {
suspend fun install(installItem: InstallItem): InstallState
suspend fun uninstall(packageName: PackageName)
}

View File

@@ -1,70 +0,0 @@
package com.looker.installer.installers
import android.content.Context
import android.content.Intent
import android.util.AndroidRuntimeException
import androidx.core.net.toUri
import com.looker.core.common.SdkCheck
import com.looker.core.common.cache.Cache
import com.looker.core.domain.model.PackageName
import com.looker.installer.model.InstallItem
import com.looker.installer.model.InstallState
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
@Suppress("DEPRECATION")
internal class LegacyInstaller(private val context: Context) : Installer {
companion object {
private const val APK_MIME = "application/vnd.android.package-archive"
}
override suspend fun install(
installItem: InstallItem
): InstallState = suspendCancellableCoroutine { cont ->
val (uri, flags) = if (SdkCheck.isNougat) {
Cache.getReleaseUri(
context,
installItem.installFileName
) to Intent.FLAG_GRANT_READ_URI_PERMISSION
} else {
val file = Cache.getReleaseFile(context, installItem.installFileName)
file.toUri() to 0
}
try {
context.startActivity(
Intent(Intent.ACTION_INSTALL_PACKAGE).setDataAndType(uri, APK_MIME).setFlags(flags)
)
cont.resume(InstallState.Installed)
} catch (e: AndroidRuntimeException) {
context.startActivity(
Intent(Intent.ACTION_INSTALL_PACKAGE).setDataAndType(uri, APK_MIME)
.setFlags(flags or Intent.FLAG_ACTIVITY_NEW_TASK)
)
cont.resume(InstallState.Installed)
} catch (e: Exception) {
cont.resume(InstallState.Failed)
}
}
override suspend fun uninstall(packageName: PackageName) =
context.uninstallPackage(packageName)
override fun close() {}
}
internal suspend fun Context.uninstallPackage(packageName: PackageName) =
suspendCancellableCoroutine { cont ->
try {
startActivity(
Intent(
Intent.ACTION_UNINSTALL_PACKAGE,
"package:${packageName.name}".toUri()
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
cont.resume(Unit)
} catch (e: Exception) {
e.printStackTrace()
cont.resume(Unit)
}
}

View File

@@ -1,76 +0,0 @@
package com.looker.installer.installers.root
import android.content.Context
import com.looker.core.common.SdkCheck
import com.looker.core.common.cache.Cache
import com.looker.core.domain.model.PackageName
import com.looker.installer.installers.Installer
import com.looker.installer.installers.uninstallPackage
import com.looker.installer.model.InstallItem
import com.looker.installer.model.InstallState
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import kotlin.coroutines.resume
internal class RootInstaller(private val context: Context) : Installer {
private companion object {
const val ROOT_INSTALL_PACKAGE = "cat %s | pm install --user %s -t -r -S %s"
const val DELETE_PACKAGE = "%s rm %s"
val getCurrentUserState: String
get() = if (SdkCheck.isOreo) {
Shell.cmd("am get-current-user").exec().out[0]
} else {
Shell.cmd("dumpsys activity | grep -E \"mUserLru\"")
.exec().out[0].trim()
.removePrefix("mUserLru: [").removeSuffix("]")
}
val String.quote
get() = "\"${this.replace(Regex("""[\\$"`]""")) { c -> "\\${c.value}" }}\""
val getUtilBoxPath: String
get() {
listOf("toybox", "busybox").forEach {
val shellResult = Shell.cmd("which $it").exec()
if (shellResult.out.isNotEmpty()) {
val utilBoxPath = shellResult.out.joinToString("")
if (utilBoxPath.isNotEmpty()) return utilBoxPath.quote
}
}
return ""
}
fun installCmd(file: File): String = String.format(
ROOT_INSTALL_PACKAGE,
file.absolutePath,
getCurrentUserState,
file.length()
)
fun deleteCmd(file: File): String = String.format(
DELETE_PACKAGE,
getUtilBoxPath,
file.absolutePath.quote
)
}
override suspend fun install(
installItem: InstallItem
): InstallState = suspendCancellableCoroutine { cont ->
val releaseFile = Cache.getReleaseFile(context, installItem.installFileName)
Shell.cmd(installCmd(releaseFile)).submit { shellResult ->
val result = if (shellResult.isSuccess) InstallState.Installed
else InstallState.Failed
cont.resume(result)
Shell.cmd(deleteCmd(releaseFile)).submit()
}
}
override suspend fun uninstall(packageName: PackageName) =
context.uninstallPackage(packageName)
override fun close() {}
}

View File

@@ -1,12 +0,0 @@
package com.looker.installer.installers.root
import com.topjohnwu.superuser.Shell
class RootPermissionHandler {
val isGranted: Boolean
get() {
Shell.getCachedShell() ?: Shell.getShell()
return Shell.isAppGrantedRoot() ?: false
}
}

View File

@@ -1,111 +0,0 @@
package com.looker.installer.installers.session
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.looker.core.common.SdkCheck
import com.looker.core.common.cache.Cache
import com.looker.core.common.log
import com.looker.core.common.sdkAbove
import com.looker.core.domain.model.PackageName
import com.looker.installer.installers.Installer
import com.looker.installer.model.InstallItem
import com.looker.installer.model.InstallState
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
internal class SessionInstaller(private val context: Context) : Installer {
private val installer = context.packageManager.packageInstaller
private val intent = Intent(context, SessionInstallerReceiver::class.java)
companion object {
private var installerCallbacks: PackageInstaller.SessionCallback? = null
private val flags = if (SdkCheck.isSnowCake) PendingIntent.FLAG_MUTABLE else 0
private val sessionParams =
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
sdkAbove(sdk = Build.VERSION_CODES.S) {
setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
sdkAbove(sdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
setRequestUpdateOwnership(true)
}
}
}
override suspend fun install(
installItem: InstallItem
): InstallState = suspendCancellableCoroutine { cont ->
val cacheFile = Cache.getReleaseFile(context, installItem.installFileName)
val id = installer.createSession(sessionParams)
val installerCallback = object : PackageInstaller.SessionCallback() {
override fun onCreated(sessionId: Int) {}
override fun onBadgingChanged(sessionId: Int) {}
override fun onActiveChanged(sessionId: Int, active: Boolean) {}
override fun onProgressChanged(sessionId: Int, progress: Float) {}
override fun onFinished(sessionId: Int, success: Boolean) {
if (sessionId == id) cont.resume(InstallState.Installed)
}
}
installerCallbacks = installerCallback
installer.registerSessionCallback(
installerCallbacks!!,
Handler(Looper.getMainLooper())
)
val session = installer.openSession(id)
session.use { activeSession ->
val sizeBytes = cacheFile.length()
cacheFile.inputStream().use { fileStream ->
activeSession.openWrite(cacheFile.name, 0, sizeBytes).use { outputStream ->
if (cont.isActive) {
fileStream.copyTo(outputStream)
activeSession.fsync(outputStream)
}
}
}
val pendingIntent = PendingIntent.getBroadcast(context, id, intent, flags)
if (cont.isActive) activeSession.commit(pendingIntent.intentSender)
}
cont.invokeOnCancellation {
try {
installer.abandonSession(id)
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}
@SuppressLint("MissingPermission")
override suspend fun uninstall(packageName: PackageName) =
suspendCancellableCoroutine { cont ->
intent.putExtra(SessionInstallerReceiver.ACTION_UNINSTALL, true)
val pendingIntent = PendingIntent.getBroadcast(context, -1, intent, flags)
installer.uninstall(packageName.name, pendingIntent.intentSender)
cont.resume(Unit)
}
override fun close() {
installerCallbacks?.let {
installer.unregisterSessionCallback(it)
installerCallbacks = null
}
try {
installer.mySessions.forEach { installer.abandonSession(it.sessionId) }
} catch (e: SecurityException) {
log(e.message, type = Log.ERROR)
}
}
}

View File

@@ -1,106 +0,0 @@
package com.looker.installer.installers.session
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import com.looker.core.common.Constants.NOTIFICATION_CHANNEL_INSTALL
import com.looker.core.common.R
import com.looker.core.common.createNotificationChannel
import com.looker.core.common.extension.getPackageName
import com.looker.core.common.extension.notificationManager
import com.looker.core.domain.model.toPackageName
import com.looker.installer.InstallManager
import com.looker.installer.model.InstallState
import com.looker.installer.notification.createInstallNotification
import com.looker.installer.notification.installNotification
import com.looker.installer.notification.removeInstallNotification
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class SessionInstallerReceiver : BroadcastReceiver() {
// This is a cyclic dependency injection, I know but this is the best option for now
@Inject
lateinit var installManager: InstallManager
override fun onReceive(context: Context, intent: Intent) {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
// prompts user to enable unknown source
val promptIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT)
promptIntent?.let {
it.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
it.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, "com.android.vending")
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(it)
}
} else {
notifyStatus(intent, context)
}
}
private fun notifyStatus(intent: Intent, context: Context) {
val packageManager = context.packageManager
val notificationManager = context.notificationManager
context.createNotificationChannel(
id = NOTIFICATION_CHANNEL_INSTALL,
name = context.getString(R.string.install)
)
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val isUninstall = intent.getBooleanExtra(ACTION_UNINSTALL, false)
val appName = packageManager.getPackageName(packageName)
if (packageName != null) {
when (status) {
PackageInstaller.STATUS_SUCCESS -> {
notificationManager?.removeInstallNotification(packageName)
val notification = context.createInstallNotification(
appName = (appName ?: packageName.substringAfterLast('.')).toString(),
state = InstallState.Installed,
isUninstall = isUninstall,
) {
setTimeoutAfter(SUCCESS_TIMEOUT)
}
notificationManager?.installNotification(
packageName = packageName.toString(),
notification = notification,
)
}
PackageInstaller.STATUS_FAILURE_ABORTED -> {
installManager.remove(packageName.toPackageName())
}
else -> {
installManager.remove(packageName.toPackageName())
val notification = context.createInstallNotification(
appName = appName.toString(),
state = InstallState.Failed,
) {
setContentText(message)
}
notificationManager?.installNotification(
packageName = packageName,
notification = notification
)
}
}
}
}
companion object {
const val ACTION_UNINSTALL = "action_uninstall"
private const val SUCCESS_TIMEOUT = 5_000L
}
}

View File

@@ -1,88 +0,0 @@
package com.looker.installer.installers.shizuku
import android.content.Context
import com.looker.core.common.SdkCheck
import com.looker.core.common.cache.Cache
import com.looker.core.common.extension.size
import com.looker.core.domain.model.PackageName
import com.looker.installer.installers.Installer
import com.looker.installer.installers.uninstallPackage
import com.looker.installer.model.InstallItem
import com.looker.installer.model.InstallState
import kotlinx.coroutines.suspendCancellableCoroutine
import rikka.shizuku.Shizuku
import java.io.BufferedReader
import java.io.InputStream
import kotlin.coroutines.resume
internal class ShizukuInstaller(private val context: Context) : Installer {
companion object {
private val SESSION_ID_REGEX = Regex("(?<=\\[).+?(?=])")
}
override suspend fun install(
installItem: InstallItem
): InstallState = suspendCancellableCoroutine { cont ->
var sessionId: String? = null
val file = Cache.getReleaseFile(context, installItem.installFileName)
val packageName = installItem.packageName.name
try {
val fileSize = file.size ?: run {
cont.cancel()
error("File is not valid: Size ${file.size}")
}
if (cont.isCompleted) return@suspendCancellableCoroutine
file.inputStream().use {
val createCommand =
if (SdkCheck.isNougat) {
"pm install-create --user current -i $packageName -S $fileSize"
} else {
"pm install-create -i $packageName -S $fileSize"
}
val createResult = exec(createCommand)
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
?: run {
cont.cancel()
throw RuntimeException("Failed to create install session")
}
if (cont.isCompleted) return@suspendCancellableCoroutine
val writeResult = exec("pm install-write -S $fileSize $sessionId base -", it)
if (writeResult.resultCode != 0) {
cont.cancel()
throw RuntimeException("Failed to write APK to session $sessionId")
}
if (cont.isCompleted) return@suspendCancellableCoroutine
val commitResult = exec("pm install-commit $sessionId")
if (commitResult.resultCode != 0) {
cont.cancel()
throw RuntimeException("Failed to commit install session $sessionId")
}
if (cont.isCompleted) return@suspendCancellableCoroutine
cont.resume(InstallState.Installed)
}
} catch (e: Exception) {
if (sessionId != null) exec("pm install-abandon $sessionId")
cont.resume(InstallState.Failed)
}
}
override suspend fun uninstall(packageName: PackageName) =
context.uninstallPackage(packageName)
override fun close() {}
private data class ShellResult(val resultCode: Int, val out: String)
private fun exec(command: String, stdin: InputStream? = null): ShellResult {
val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null)
if (stdin != null) {
process.outputStream.use { stdin.copyTo(it) }
}
val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
val resultCode = process.waitFor()
return ShellResult(resultCode, output)
}
}

View File

@@ -1,81 +0,0 @@
package com.looker.installer.installers.shizuku
import android.content.Context
import android.content.pm.PackageManager
import com.looker.core.common.extension.getPackageInfoCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuProvider
class ShizukuPermissionHandler(
private val context: Context
) {
fun isInstalled(): Boolean =
context.packageManager.getPackageInfoCompat(ShizukuProvider.MANAGER_APPLICATION_ID) != null
val isBinderAlive: Flow<Boolean> = callbackFlow {
send(Shizuku.pingBinder())
val listener = Shizuku.OnBinderReceivedListener {
trySend(true)
}
Shizuku.addBinderReceivedListener(listener)
val deadListener = Shizuku.OnBinderDeadListener {
trySend(false)
}
Shizuku.addBinderDeadListener(deadListener)
awaitClose {
Shizuku.removeBinderReceivedListener(listener)
Shizuku.removeBinderDeadListener(deadListener)
}
}.flowOn(Dispatchers.Default).distinctUntilChanged().conflate()
private val isGranted: Flow<Boolean> = callbackFlow {
if (Shizuku.pingBinder()) {
send(Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED)
} else {
send(false)
}
val listener = Shizuku.OnRequestPermissionResultListener { requestCode, grantResult ->
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
trySend(grantResult == PackageManager.PERMISSION_GRANTED)
}
}
Shizuku.addRequestPermissionResultListener(listener)
awaitClose {
Shizuku.removeRequestPermissionResultListener(listener)
}
}.flowOn(Dispatchers.Default).distinctUntilChanged().conflate()
fun requestPermission() {
if (Shizuku.shouldShowRequestPermissionRationale()) {
}
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
}
val state: Flow<State> = combine(
flowOf(isInstalled()),
isBinderAlive,
isGranted
) { isInstalled, isAlive, isGranted ->
State(isGranted, isAlive, isInstalled)
}.distinctUntilChanged()
companion object {
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 87263
}
data class State(
val isPermissionGranted: Boolean,
val isAlive: Boolean,
val isInstalled: Boolean
)
}

View File

@@ -1,11 +0,0 @@
package com.looker.installer.model
import com.looker.core.domain.model.PackageName
import com.looker.core.domain.model.toPackageName
data class InstallItem(
val packageName: PackageName,
val installFileName: String
)
infix fun String.installFrom(fileName: String) = InstallItem(this.toPackageName(), fileName)

View File

@@ -1,6 +0,0 @@
package com.looker.installer.model
enum class InstallState { Failed, Pending, Installing, Installed }
inline val InstallState.isCancellable: Boolean
get() = this == InstallState.Pending

View File

@@ -1,91 +0,0 @@
package com.looker.installer.notification
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import android.graphics.Color
import androidx.core.app.NotificationCompat
import com.looker.core.common.Constants.NOTIFICATION_CHANNEL_INSTALL
import com.looker.core.common.Constants.NOTIFICATION_ID_INSTALL
import com.looker.installer.model.InstallState
import com.looker.installer.model.InstallState.Failed
import com.looker.installer.model.InstallState.Installed
import com.looker.installer.model.InstallState.Installing
import com.looker.installer.model.InstallState.Pending
import com.looker.core.common.R as CommonR
fun NotificationManager.installNotification(
packageName: String,
notification: Notification,
) {
notify(
installTag(packageName),
NOTIFICATION_ID_INSTALL,
notification
)
}
fun NotificationManager.removeInstallNotification(
packageName: String,
) {
cancel(installTag(packageName), NOTIFICATION_ID_INSTALL)
}
fun installTag(name: String): String = "install-${name.trim().replace(' ', '_')}"
private const val SUCCESS_TIMEOUT = 5_000L
fun Context.createInstallNotification(
appName: String,
state: InstallState,
isUninstall: Boolean = false,
autoCancel: Boolean = true,
block: NotificationCompat.Builder.() -> Unit = {},
): Notification {
return NotificationCompat
.Builder(this, NOTIFICATION_CHANNEL_INSTALL)
.apply {
setAutoCancel(autoCancel)
setOngoing(false)
setOnlyAlertOnce(true)
setColor(Color.GREEN)
val (title, text) = if (isUninstall) {
setTimeoutAfter(SUCCESS_TIMEOUT)
setSmallIcon(CommonR.drawable.ic_delete)
getString(CommonR.string.uninstalled_application) to
getString(CommonR.string.uninstalled_application_DESC, appName)
} else {
when (state) {
Failed -> {
setSmallIcon(CommonR.drawable.ic_bug_report)
getString(CommonR.string.installation_failed) to
getString(CommonR.string.installation_failed_DESC, appName)
}
Pending -> {
setSmallIcon(CommonR.drawable.ic_download)
getString(CommonR.string.downloaded_FORMAT, appName) to
getString(CommonR.string.tap_to_install_DESC)
}
Installing -> {
setSmallIcon(CommonR.drawable.ic_download)
setProgress(-1, -1, true)
getString(CommonR.string.installing) to
appName
}
Installed -> {
setTimeoutAfter(SUCCESS_TIMEOUT)
setSmallIcon(CommonR.drawable.ic_check)
getString(CommonR.string.installed) to
appName
}
}
}
setContentTitle(title)
setContentText(text)
block()
}
.build()
}