Skip to content

Commit 5ddd74b

Browse files
committed
feat: send notifs when install ready if backgrounded
1 parent 21d4d57 commit 5ddd74b

File tree

9 files changed

+158
-13
lines changed

9 files changed

+158
-13
lines changed

app/src/main/AndroidManifest.xml

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
<uses-permission android:name="android.permission.INTERNET" />
66
<uses-permission android:name="android.permission.WAKE_LOCK" />
7+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
78
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
89
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
910
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
@@ -41,6 +42,7 @@
4142
<activity
4243
android:name=".MainActivity"
4344
android:exported="true"
45+
android:launchMode="singleTask"
4446
android:theme="@style/Theme.AliucordManager.SplashScreen">
4547
<intent-filter>
4648
<action android:name="android.intent.action.MAIN" />

app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package com.aliucord.manager.installer.steps
22

3+
import android.content.Context
4+
import androidx.lifecycle.Lifecycle
5+
import androidx.lifecycle.ProcessLifecycleOwner
6+
import com.aliucord.manager.R
37
import com.aliucord.manager.installer.steps.base.Step
48
import com.aliucord.manager.manager.PreferencesManager
9+
import com.aliucord.manager.ui.util.InstallNotifications
510
import kotlinx.collections.immutable.ImmutableList
611
import kotlinx.coroutines.delay
712
import org.koin.core.component.KoinComponent
@@ -14,7 +19,10 @@ import org.koin.core.component.inject
1419
*/
1520
const val MINIMUM_STEP_DELAY: Long = 600L
1621

22+
const val ERROR_NOTIF_ID = 200002
23+
1724
abstract class StepRunner : KoinComponent {
25+
private val context: Context by inject()
1826
private val preferences: PreferencesManager by inject()
1927

2028
abstract val steps: ImmutableList<Step>
@@ -39,7 +47,10 @@ abstract class StepRunner : KoinComponent {
3947
suspend fun executeAll(): Throwable? {
4048
for (step in steps) {
4149
val error = step.executeCatching(this@StepRunner)
42-
if (error != null) return error
50+
if (error != null) {
51+
showErrorNotification()
52+
return error
53+
}
4354

4455
// Skip minimum run time when in dev mode
4556
if (!preferences.devMode && step.durationMs < MINIMUM_STEP_DELAY) {
@@ -49,4 +60,16 @@ abstract class StepRunner : KoinComponent {
4960

5061
return null
5162
}
63+
64+
private fun showErrorNotification() {
65+
// If app backgrounded
66+
if (ProcessLifecycleOwner.get().lifecycle.currentState == Lifecycle.State.CREATED) {
67+
InstallNotifications.createNotification(
68+
context = context,
69+
id = ERROR_NOTIF_ID,
70+
title = R.string.notif_install_fail_title,
71+
description = R.string.notif_install_fail_desc,
72+
)
73+
}
74+
}
5275
}

app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt

+19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.aliucord.manager.installer.steps.install
22

3+
import android.content.Context
4+
import androidx.lifecycle.*
35
import com.aliucord.manager.R
46
import com.aliucord.manager.installer.steps.StepGroup
57
import com.aliucord.manager.installer.steps.StepRunner
@@ -9,13 +11,17 @@ import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep
911
import com.aliucord.manager.installers.InstallerResult
1012
import com.aliucord.manager.manager.InstallerManager
1113
import com.aliucord.manager.manager.PreferencesManager
14+
import com.aliucord.manager.ui.util.InstallNotifications
1215
import org.koin.core.component.KoinComponent
1316
import org.koin.core.component.inject
1417

18+
private const val READY_NOTIF_ID = 200001
19+
1520
/**
1621
* Install the final APK with the system's PackageManager.
1722
*/
1823
class InstallStep : Step(), KoinComponent {
24+
private val context: Context by inject()
1925
private val installers: InstallerManager by inject()
2026
private val prefs: PreferencesManager by inject()
2127

@@ -25,6 +31,19 @@ class InstallStep : Step(), KoinComponent {
2531
override suspend fun execute(container: StepRunner) {
2632
val apk = container.getStep<CopyDependenciesStep>().patchedApk
2733

34+
// If app backgrounded, show notification
35+
if (ProcessLifecycleOwner.get().lifecycle.currentState == Lifecycle.State.CREATED) {
36+
InstallNotifications.createNotification(
37+
context = context,
38+
id = READY_NOTIF_ID,
39+
title = R.string.notif_install_ready_title,
40+
description = R.string.notif_install_ready_desc,
41+
)
42+
}
43+
44+
// Wait until app resumed
45+
ProcessLifecycleOwner.get().lifecycle.withResumed {}
46+
2847
val result = installers.getActiveInstaller().waitInstall(
2948
apks = listOf(apk),
3049
silent = !prefs.devMode,

app/src/main/kotlin/com/aliucord/manager/ui/components/Wakelock.kt

+1-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package com.aliucord.manager.ui.components
22

3-
import android.app.Activity
4-
import android.content.Context
5-
import android.content.ContextWrapper
63
import android.view.WindowManager
74
import androidx.compose.runtime.Composable
85
import androidx.compose.runtime.DisposableEffect
96
import androidx.compose.ui.platform.LocalContext
7+
import com.aliucord.manager.util.findActivity
108

119
/**
1210
* 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) {
2826
}
2927
}
3028
}
31-
32-
private fun Context.findActivity(): Activity? {
33-
var context = this
34-
while (context is ContextWrapper) {
35-
if (context is Activity) return context
36-
context = context.baseContext
37-
}
38-
return null
39-
}

app/src/main/kotlin/com/aliucord/manager/ui/screens/installopts/InstallOptionsScreen.kt

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import androidx.compose.runtime.*
88
import androidx.compose.ui.Alignment
99
import androidx.compose.ui.Modifier
1010
import androidx.compose.ui.draw.alpha
11+
import androidx.compose.ui.platform.LocalContext
1112
import androidx.compose.ui.res.painterResource
1213
import androidx.compose.ui.res.stringResource
1314
import androidx.compose.ui.text.style.TextAlign
@@ -33,9 +34,14 @@ class InstallOptionsScreen(
3334

3435
@Composable
3536
override fun Content() {
37+
val context = LocalContext.current
3638
val navigator = LocalNavigator.currentOrThrow
3739
val model = getScreenModel<InstallOptionsModel>()
3840

41+
LaunchedEffect(Unit) {
42+
InstallNotifications.requestPermissions(context)
43+
}
44+
3945
Scaffold(
4046
topBar = { InstallOptionsAppBar() },
4147
) { paddingValues ->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.aliucord.manager.ui.util
2+
3+
import android.Manifest
4+
import android.app.*
5+
import android.content.Context
6+
import android.content.Intent
7+
import android.content.pm.PackageManager
8+
import android.os.Build
9+
import android.util.Log
10+
import androidx.annotation.StringRes
11+
import androidx.core.app.*
12+
import androidx.core.content.ContextCompat
13+
import com.aliucord.manager.*
14+
import com.aliucord.manager.util.findActivity
15+
16+
object InstallNotifications {
17+
private const val CHANNEL_ID = "installation"
18+
19+
/**
20+
* Creates or replaces a notification with id [id] that brings
21+
* up the existing [MainActivity] when clicked upon.
22+
*
23+
* @param id A unique notification ID for different notifications
24+
* @param title Main notification title
25+
* @param description Notification description
26+
*/
27+
fun createNotification(
28+
context: Context,
29+
id: Int,
30+
@StringRes title: Int,
31+
@StringRes description: Int,
32+
) {
33+
val manager = NotificationManagerCompat.from(context)
34+
35+
// Create the target notification channel
36+
if (Build.VERSION.SDK_INT >= 26 && manager.getNotificationChannel(CHANNEL_ID) == null) {
37+
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManager.IMPORTANCE_HIGH)
38+
.setName(context.getString(R.string.notif_group_install_title))
39+
.setDescription(context.getString(R.string.notif_group_install_desc))
40+
.build()
41+
42+
manager.createNotificationChannel(channel)
43+
}
44+
45+
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
46+
.setDefaults(Notification.DEFAULT_LIGHTS or Notification.DEFAULT_SOUND)
47+
.setAutoCancel(true)
48+
.setSmallIcon(R.drawable.ic_aliucord_logo)
49+
.setContentTitle(context.getString(title))
50+
.setContentText(context.getString(description))
51+
.setContentIntent(
52+
PendingIntent.getActivity(
53+
/* context = */ context,
54+
/* requestCode = */ 0,
55+
/* intent = */
56+
Intent(context, MainActivity::class.java)
57+
.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT),
58+
/* flags = */ PendingIntent.FLAG_IMMUTABLE,
59+
)
60+
)
61+
.build()
62+
63+
try {
64+
manager.notify(id, notification)
65+
} catch (e: SecurityException) {
66+
Log.w(BuildConfig.TAG, "Failed to send install notification", e)
67+
}
68+
}
69+
70+
/**
71+
* Request the `POST_NOTIFICATIONS` permission if needed.
72+
*/
73+
fun requestPermissions(context: Context) {
74+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
75+
76+
val granted = ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
77+
val activity = context.findActivity() ?: return
78+
79+
if (granted != PackageManager.PERMISSION_GRANTED) {
80+
ActivityCompat.requestPermissions(
81+
activity,
82+
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
83+
0,
84+
)
85+
}
86+
}
87+
}

app/src/main/kotlin/com/aliucord/manager/util/Util.kt app/src/main/kotlin/com/aliucord/manager/util/Context.kt

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.aliucord.manager.util
22

3+
import android.app.Activity
34
import android.content.*
45
import android.os.Environment
56
import android.util.Log
@@ -44,3 +45,12 @@ fun Context.getPackageVersion(pkg: String): Pair<String, Int> {
4445
return packageManager.getPackageInfo(pkg, 0)
4546
.let { it.versionName to it.versionCode }
4647
}
48+
49+
fun Context.findActivity(): Activity? {
50+
var context = this
51+
while (context is ContextWrapper) {
52+
if (context is Activity) return context
53+
context = context.baseContext
54+
}
55+
return null
56+
}

app/src/main/res/values/strings.xml

+7
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,11 @@
146146
<string name="installopts_icon_desc">Changes the app\'s icon to use Aliucord branding instead of Discord\'s own.</string>
147147
<string name="installopts_divider_basic">Basic</string>
148148
<string name="installopts_divider_advanced">Advanced</string>
149+
150+
<string name="notif_group_install_title">Active installation</string>
151+
<string name="notif_group_install_desc">Progress notifications sent during a minimized installation</string>
152+
<string name="notif_install_ready_title">Aliucord installation ready!</string>
153+
<string name="notif_install_ready_desc">Click to install…</string>
154+
<string name="notif_install_fail_title">Aliucord failed to install</string>
155+
<string name="notif_install_fail_desc">Click to view more info…</string>
149156
</resources>

gradle/libs.versions.toml

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ accompanist-systemUiController = { module = "com.google.accompanist:accompanist-
2929
# AndroidX
3030
androidx-activity = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
3131
androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
32+
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" }
3233
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
3334
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" }
3435

@@ -80,6 +81,7 @@ androidx = [
8081
"androidx-core",
8182
"androidx-activity",
8283
"androidx-lifecycle",
84+
"androidx-lifecycle-process",
8385
"androidx-splashscreen",
8486
]
8587
compose = [

0 commit comments

Comments
 (0)