feat: send notifs when install ready if backgrounded

This commit is contained in:
rushiiMachine 2024-03-06 11:26:55 -08:00
parent 21d4d57975
commit 5ddd74be38
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
9 changed files with 158 additions and 13 deletions

View file

@ -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" />

View file

@ -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,
)
}
}
} }

View file

@ -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,

View file

@ -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
}

View file

@ -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 ->

View file

@ -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,
)
}
}
}

View file

@ -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
}

View file

@ -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>

View file

@ -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 = [