feat: send notifs when install ready if backgrounded
This commit is contained in:
parent
21d4d57975
commit
5ddd74be38
9 changed files with 158 additions and 13 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
|
@ -41,6 +42,7 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/Theme.AliucordManager.SplashScreen">
|
android:theme="@style/Theme.AliucordManager.SplashScreen">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
package com.aliucord.manager.installer.steps
|
package com.aliucord.manager.installer.steps
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import com.aliucord.manager.R
|
||||||
import com.aliucord.manager.installer.steps.base.Step
|
import com.aliucord.manager.installer.steps.base.Step
|
||||||
import com.aliucord.manager.manager.PreferencesManager
|
import com.aliucord.manager.manager.PreferencesManager
|
||||||
|
import com.aliucord.manager.ui.util.InstallNotifications
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
@ -14,7 +19,10 @@ import org.koin.core.component.inject
|
||||||
*/
|
*/
|
||||||
const val MINIMUM_STEP_DELAY: Long = 600L
|
const val MINIMUM_STEP_DELAY: Long = 600L
|
||||||
|
|
||||||
|
const val ERROR_NOTIF_ID = 200002
|
||||||
|
|
||||||
abstract class StepRunner : KoinComponent {
|
abstract class StepRunner : KoinComponent {
|
||||||
|
private val context: Context by inject()
|
||||||
private val preferences: PreferencesManager by inject()
|
private val preferences: PreferencesManager by inject()
|
||||||
|
|
||||||
abstract val steps: ImmutableList<Step>
|
abstract val steps: ImmutableList<Step>
|
||||||
|
|
@ -39,7 +47,10 @@ abstract class StepRunner : KoinComponent {
|
||||||
suspend fun executeAll(): Throwable? {
|
suspend fun executeAll(): Throwable? {
|
||||||
for (step in steps) {
|
for (step in steps) {
|
||||||
val error = step.executeCatching(this@StepRunner)
|
val error = step.executeCatching(this@StepRunner)
|
||||||
if (error != null) return error
|
if (error != null) {
|
||||||
|
showErrorNotification()
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
// Skip minimum run time when in dev mode
|
// Skip minimum run time when in dev mode
|
||||||
if (!preferences.devMode && step.durationMs < MINIMUM_STEP_DELAY) {
|
if (!preferences.devMode && step.durationMs < MINIMUM_STEP_DELAY) {
|
||||||
|
|
@ -49,4 +60,16 @@ abstract class StepRunner : KoinComponent {
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showErrorNotification() {
|
||||||
|
// If app backgrounded
|
||||||
|
if (ProcessLifecycleOwner.get().lifecycle.currentState == Lifecycle.State.CREATED) {
|
||||||
|
InstallNotifications.createNotification(
|
||||||
|
context = context,
|
||||||
|
id = ERROR_NOTIF_ID,
|
||||||
|
title = R.string.notif_install_fail_title,
|
||||||
|
description = R.string.notif_install_fail_desc,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.aliucord.manager.installer.steps.install
|
package com.aliucord.manager.installer.steps.install
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.*
|
||||||
import com.aliucord.manager.R
|
import com.aliucord.manager.R
|
||||||
import com.aliucord.manager.installer.steps.StepGroup
|
import com.aliucord.manager.installer.steps.StepGroup
|
||||||
import com.aliucord.manager.installer.steps.StepRunner
|
import com.aliucord.manager.installer.steps.StepRunner
|
||||||
|
|
@ -9,13 +11,17 @@ import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep
|
||||||
import com.aliucord.manager.installers.InstallerResult
|
import com.aliucord.manager.installers.InstallerResult
|
||||||
import com.aliucord.manager.manager.InstallerManager
|
import com.aliucord.manager.manager.InstallerManager
|
||||||
import com.aliucord.manager.manager.PreferencesManager
|
import com.aliucord.manager.manager.PreferencesManager
|
||||||
|
import com.aliucord.manager.ui.util.InstallNotifications
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
private const val READY_NOTIF_ID = 200001
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install the final APK with the system's PackageManager.
|
* Install the final APK with the system's PackageManager.
|
||||||
*/
|
*/
|
||||||
class InstallStep : Step(), KoinComponent {
|
class InstallStep : Step(), KoinComponent {
|
||||||
|
private val context: Context by inject()
|
||||||
private val installers: InstallerManager by inject()
|
private val installers: InstallerManager by inject()
|
||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
|
|
||||||
|
|
@ -25,6 +31,19 @@ class InstallStep : Step(), KoinComponent {
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||||
|
|
||||||
|
// If app backgrounded, show notification
|
||||||
|
if (ProcessLifecycleOwner.get().lifecycle.currentState == Lifecycle.State.CREATED) {
|
||||||
|
InstallNotifications.createNotification(
|
||||||
|
context = context,
|
||||||
|
id = READY_NOTIF_ID,
|
||||||
|
title = R.string.notif_install_ready_title,
|
||||||
|
description = R.string.notif_install_ready_desc,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until app resumed
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.withResumed {}
|
||||||
|
|
||||||
val result = installers.getActiveInstaller().waitInstall(
|
val result = installers.getActiveInstaller().waitInstall(
|
||||||
apks = listOf(apk),
|
apks = listOf(apk),
|
||||||
silent = !prefs.devMode,
|
silent = !prefs.devMode,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
package com.aliucord.manager.ui.components
|
package com.aliucord.manager.ui.components
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.aliucord.manager.util.findActivity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maintain an active screen wakelock as long as [active] is true and this component is in scope.
|
* Maintain an active screen wakelock as long as [active] is true and this component is in scope.
|
||||||
|
|
@ -28,12 +26,3 @@ fun Wakelock(active: Boolean = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Context.findActivity(): Activity? {
|
|
||||||
var context = this
|
|
||||||
while (context is ContextWrapper) {
|
|
||||||
if (context is Activity) return context
|
|
||||||
context = context.baseContext
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
|
@ -33,9 +34,14 @@ class InstallOptionsScreen(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
|
val context = LocalContext.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val model = getScreenModel<InstallOptionsModel>()
|
val model = getScreenModel<InstallOptionsModel>()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
InstallNotifications.requestPermissions(context)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { InstallOptionsAppBar() },
|
topBar = { InstallOptionsAppBar() },
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.aliucord.manager.ui.util
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.*
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.app.*
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.aliucord.manager.*
|
||||||
|
import com.aliucord.manager.util.findActivity
|
||||||
|
|
||||||
|
object InstallNotifications {
|
||||||
|
private const val CHANNEL_ID = "installation"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or replaces a notification with id [id] that brings
|
||||||
|
* up the existing [MainActivity] when clicked upon.
|
||||||
|
*
|
||||||
|
* @param id A unique notification ID for different notifications
|
||||||
|
* @param title Main notification title
|
||||||
|
* @param description Notification description
|
||||||
|
*/
|
||||||
|
fun createNotification(
|
||||||
|
context: Context,
|
||||||
|
id: Int,
|
||||||
|
@StringRes title: Int,
|
||||||
|
@StringRes description: Int,
|
||||||
|
) {
|
||||||
|
val manager = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
|
// Create the target notification channel
|
||||||
|
if (Build.VERSION.SDK_INT >= 26 && manager.getNotificationChannel(CHANNEL_ID) == null) {
|
||||||
|
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManager.IMPORTANCE_HIGH)
|
||||||
|
.setName(context.getString(R.string.notif_group_install_title))
|
||||||
|
.setDescription(context.getString(R.string.notif_group_install_desc))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setDefaults(Notification.DEFAULT_LIGHTS or Notification.DEFAULT_SOUND)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setSmallIcon(R.drawable.ic_aliucord_logo)
|
||||||
|
.setContentTitle(context.getString(title))
|
||||||
|
.setContentText(context.getString(description))
|
||||||
|
.setContentIntent(
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
/* context = */ context,
|
||||||
|
/* requestCode = */ 0,
|
||||||
|
/* intent = */
|
||||||
|
Intent(context, MainActivity::class.java)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT),
|
||||||
|
/* flags = */ PendingIntent.FLAG_IMMUTABLE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
try {
|
||||||
|
manager.notify(id, notification)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(BuildConfig.TAG, "Failed to send install notification", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the `POST_NOTIFICATIONS` permission if needed.
|
||||||
|
*/
|
||||||
|
fun requestPermissions(context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
||||||
|
|
||||||
|
val granted = ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
val activity = context.findActivity() ?: return
|
||||||
|
|
||||||
|
if (granted != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
activity,
|
||||||
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.aliucord.manager.util
|
package com.aliucord.manager.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
@ -44,3 +45,12 @@ fun Context.getPackageVersion(pkg: String): Pair<String, Int> {
|
||||||
return packageManager.getPackageInfo(pkg, 0)
|
return packageManager.getPackageInfo(pkg, 0)
|
||||||
.let { it.versionName to it.versionCode }
|
.let { it.versionName to it.versionCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.findActivity(): Activity? {
|
||||||
|
var context = this
|
||||||
|
while (context is ContextWrapper) {
|
||||||
|
if (context is Activity) return context
|
||||||
|
context = context.baseContext
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
@ -146,4 +146,11 @@
|
||||||
<string name="installopts_icon_desc">Changes the app\'s icon to use Aliucord branding instead of Discord\'s own.</string>
|
<string name="installopts_icon_desc">Changes the app\'s icon to use Aliucord branding instead of Discord\'s own.</string>
|
||||||
<string name="installopts_divider_basic">Basic</string>
|
<string name="installopts_divider_basic">Basic</string>
|
||||||
<string name="installopts_divider_advanced">Advanced</string>
|
<string name="installopts_divider_advanced">Advanced</string>
|
||||||
|
|
||||||
|
<string name="notif_group_install_title">Active installation</string>
|
||||||
|
<string name="notif_group_install_desc">Progress notifications sent during a minimized installation</string>
|
||||||
|
<string name="notif_install_ready_title">Aliucord installation ready!</string>
|
||||||
|
<string name="notif_install_ready_desc">Click to install…</string>
|
||||||
|
<string name="notif_install_fail_title">Aliucord failed to install</string>
|
||||||
|
<string name="notif_install_fail_desc">Click to view more info…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ accompanist-systemUiController = { module = "com.google.accompanist:accompanist-
|
||||||
# AndroidX
|
# AndroidX
|
||||||
androidx-activity = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
androidx-activity = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||||
androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
|
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" }
|
||||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||||
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" }
|
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" }
|
||||||
|
|
||||||
|
|
@ -80,6 +81,7 @@ androidx = [
|
||||||
"androidx-core",
|
"androidx-core",
|
||||||
"androidx-activity",
|
"androidx-activity",
|
||||||
"androidx-lifecycle",
|
"androidx-lifecycle",
|
||||||
|
"androidx-lifecycle-process",
|
||||||
"androidx-splashscreen",
|
"androidx-splashscreen",
|
||||||
]
|
]
|
||||||
compose = [
|
compose = [
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue