Skip to content

Commit 52248ba

Browse files
authored
feat: implement in-app automatic update downloader (#587)
This pull request introduces a comprehensive auto-update system for the app, including background update checks, user notifications, and streamlined update installation. It adds a new dependency for update management, integrates update logic into the UI, and introduces new services and receivers to handle update downloads and notifications. **Auto-update infrastructure:** - Added the `AutoUpdater` library dependency to manage update checks and installations. (`app/build.gradle.kts`) - Introduced `AppUpdateWorker` for periodic background update checks, which notifies users if an update is available and updates cached state. (`app/src/main/java/com/sameerasw/essentials/services/AppUpdateWorker.kt`) - Added `DownloadUpdateReceiver` to handle update download and installation requests via broadcast intents. (`app/src/main/java/com/sameerasw/essentials/services/receivers/DownloadUpdateReceiver.kt`, `AndroidManifest.xml`) [[1]](diffhunk://#diff-0a4b57680d9fb2620ea9cf1da508d0ecb87c77673470842af9ffd72b7048eecfR1-R41) [[2]](diffhunk://#diff-7fa6aef292187a049f7a4d6060d8df3ba212d838789c78940bd363344b1c38cdR1033-R1040) **Update checking and notification logic:** - Enhanced `UpdateRepository` to use `AutoUpdateManagerHelper` for update checks, fallback to GitHub if needed, and improved error handling. (`app/src/main/java/com/sameerasw/essentials/data/repository/UpdateRepository.kt`) [[1]](diffhunk://#diff-9af604644cfdd546a42e6abe4c147709a9e9f894b25db50d83afd83faeb40de2R3-R41) [[2]](diffhunk://#diff-9af604644cfdd546a42e6abe4c147709a9e9f894b25db50d83afd83faeb40de2L23-R53) [[3]](diffhunk://#diff-9af604644cfdd546a42e6abe4c147709a9e9f894b25db50d83afd83faeb40de2L35) - Registered the `REQUEST_INSTALL_PACKAGES` permission for update installations. (`app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt`) **UI improvements for updates:** - Updated both `MainActivity` and `SettingsActivity` to display update status, show animated badges when updates are available, and provide a button to check for or install updates. (`app/src/main/java/com/sameerasw/essentials/MainActivity.kt`, `app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt`) [[1]](diffhunk://#diff-ad4ba0c464c3da383256c4282daceee29c13d8de8e60416fef688e3a78f49effR246) [[2]](diffhunk://#diff-ad4ba0c464c3da383256c4282daceee29c13d8de8e60416fef688e3a78f49effL253-R273) [[3]](diffhunk://#diff-ad4ba0c464c3da383256c4282daceee29c13d8de8e60416fef688e3a78f49effR604-R639) [[4]](diffhunk://#diff-576ba73ddab3e3ff415e6dc1c0b3b670b182d923408666f9be5a506cd6f9f5a1R257) [[5]](diffhunk://#diff-576ba73ddab3e3ff415e6dc1c0b3b670b182d923408666f9be5a506cd6f9f5a1R422-R498) [[6]](diffhunk://#diff-576ba73ddab3e3ff415e6dc1c0b3b670b182d923408666f9be5a506cd6f9f5a1L882-L950) [[7]](diffhunk://#diff-ad4ba0c464c3da383256c4282daceee29c13d8de8e60416fef688e3a78f49effR31-R47) These changes together enable seamless update management, improve user experience around updates, and lay the groundwork for future enhancements to the app's update flow.
2 parents b21f7be + 7569e29 commit 52248ba

27 files changed

Lines changed: 667 additions & 102 deletions

File tree

app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ dependencies {
158158
implementation(libs.sentry.android)
159159
implementation(libs.androidx.graphics.shapes)
160160

161+
// AutoUpdater
162+
implementation("com.github.CSAbhiOnline:AutoUpdater:1.0.1")
163+
161164
// Media3 for Live Wallpaper
162165
implementation(libs.androidx.media3.exoplayer)
163166
implementation(libs.androidx.media3.common)

app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,14 @@
10301030
</intent-filter>
10311031
</receiver>
10321032

1033+
<receiver
1034+
android:name=".services.receivers.DownloadUpdateReceiver"
1035+
android:exported="false">
1036+
<intent-filter>
1037+
<action android:name="com.sameerasw.essentials.ACTION_DOWNLOAD_UPDATE" />
1038+
</intent-filter>
1039+
</receiver>
1040+
10331041
<receiver
10341042
android:name=".services.automation.receivers.TimeAutomationReceiver"
10351043
android:exported="false">

app/src/main/java/com/sameerasw/essentials/MainActivity.kt

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ import androidx.compose.foundation.layout.fillMaxSize
2828
import androidx.compose.foundation.layout.statusBars
2929
import androidx.compose.foundation.pager.HorizontalPager
3030
import androidx.compose.foundation.pager.rememberPagerState
31+
import androidx.compose.foundation.layout.offset
32+
import androidx.compose.foundation.layout.padding
33+
import androidx.compose.foundation.layout.size
34+
import androidx.compose.ui.draw.drawWithContent
35+
import androidx.compose.ui.graphics.BlendMode
36+
import android.graphics.PorterDuff
37+
import android.graphics.PorterDuffColorFilter
38+
import androidx.compose.ui.graphics.toArgb
39+
import com.airbnb.lottie.LottieProperty
40+
import com.airbnb.lottie.compose.LottieAnimation
41+
import com.airbnb.lottie.compose.LottieCompositionSpec
42+
import com.airbnb.lottie.compose.LottieConstants
43+
import com.airbnb.lottie.compose.animateLottieCompositionAsState
44+
import com.airbnb.lottie.compose.rememberLottieComposition
45+
import com.airbnb.lottie.compose.rememberLottieDynamicProperties
46+
import com.airbnb.lottie.compose.rememberLottieDynamicProperty
47+
import androidx.compose.material3.Badge
3148
import androidx.compose.material3.FloatingActionButton
3249
import androidx.compose.material3.FloatingToolbarDefaults
3350
import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Bottom
@@ -226,6 +243,7 @@ class MainActivity : AppCompatActivity() {
226243
var showUpdateSheet by remember { mutableStateOf(false) }
227244
var showInstructionsSheet by remember { mutableStateOf(false) }
228245
val updateInfo by viewModel.updateInfo
246+
val isUpdateAvailable by viewModel.isUpdateAvailable
229247

230248
var showGitHubAuthSheet by remember { mutableStateOf(false) }
231249
var showNewAutomationSheet by remember { mutableStateOf(false) }
@@ -250,7 +268,9 @@ class MainActivity : AppCompatActivity() {
250268
if (!viewModel.isPostNotificationsEnabled.value) {
251269
viewModel.requestNotificationPermission(this@MainActivity)
252270
}
253-
viewModel.checkForUpdates(context)
271+
if (!viewModel.isUpdateAvailable.value) {
272+
viewModel.checkForUpdates(context, manual = true)
273+
}
254274
updatesViewModel.loadTrackedRepos(context)
255275
}
256276

@@ -581,6 +601,42 @@ class MainActivity : AppCompatActivity() {
581601
}
582602
}
583603

604+
if (isUpdateAvailable && (currentTab == DIYTabs.ESSENTIALS || currentTab == DIYTabs.FREEZE)) {
605+
Badge(
606+
modifier = Modifier
607+
.align(Alignment.TopEnd)
608+
.offset(x = 6.dp, y = (-6).dp)
609+
.size(28.dp),
610+
containerColor = MaterialTheme.colorScheme.error,
611+
contentColor = MaterialTheme.colorScheme.onError
612+
) {
613+
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.update_motion))
614+
val progress by animateLottieCompositionAsState(
615+
composition = composition,
616+
iterations = LottieConstants.IterateForever
617+
)
618+
val onErrorColor = MaterialTheme.colorScheme.onError
619+
val dynamicProperties = rememberLottieDynamicProperties(
620+
rememberLottieDynamicProperty(
621+
property = LottieProperty.COLOR_FILTER,
622+
value = PorterDuffColorFilter(
623+
onErrorColor.toArgb(),
624+
PorterDuff.Mode.SRC_ATOP
625+
),
626+
keyPath = arrayOf("**")
627+
)
628+
)
629+
630+
LottieAnimation(
631+
composition = composition,
632+
progress = { progress },
633+
dynamicProperties = dynamicProperties,
634+
modifier = Modifier
635+
.fillMaxSize()
636+
.padding(2.dp)
637+
)
638+
}
639+
}
584640
}
585641
}
586642
)

app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt

Lines changed: 78 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ fun SettingsContent(
254254
var isPermissionsExpanded by remember { mutableStateOf(false) }
255255
var showUpdateSheet by remember { mutableStateOf(false) }
256256
val updateInfo by viewModel.updateInfo
257+
val isUpdateAvailable by viewModel.isUpdateAvailable
257258
val isAutoUpdateEnabled by viewModel.isAutoUpdateEnabled
258259
val isUpdateNotificationEnabled by viewModel.isUpdateNotificationEnabled
259260
val isPreReleaseCheckEnabled by viewModel.isPreReleaseCheckEnabled
@@ -418,6 +419,83 @@ fun SettingsContent(
418419
)
419420
}
420421

422+
// Updates Section
423+
Text(
424+
text = "Updates",
425+
style = MaterialTheme.typography.titleMedium,
426+
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp),
427+
color = MaterialTheme.colorScheme.onSurfaceVariant
428+
)
429+
430+
RoundedCardContainer {
431+
IconToggleItem(
432+
iconRes = R.drawable.rounded_mobile_check_24,
433+
title = "Auto check for updates",
434+
description = "Check for updates at app launch",
435+
isChecked = isAutoUpdateEnabled,
436+
onCheckedChange = { viewModel.setAutoUpdateEnabled(it, context) }
437+
)
438+
IconToggleItem(
439+
iconRes = R.drawable.rounded_experiment_24,
440+
title = context.getString(R.string.check_pre_releases_label),
441+
description = context.getString(R.string.check_pre_releases_desc),
442+
isChecked = isPreReleaseCheckEnabled,
443+
onCheckedChange = { viewModel.setPreReleaseCheckEnabled(it, context) }
444+
)
445+
IconToggleItem(
446+
iconRes = R.drawable.rounded_notifications_unread_24,
447+
title = "Notify for new updates",
448+
description = "Show a notification when an update is found",
449+
isChecked = isUpdateNotificationEnabled,
450+
onCheckedChange = { viewModel.setUpdateNotificationEnabled(it, context) }
451+
)
452+
453+
Row(
454+
modifier = Modifier
455+
.fillMaxWidth()
456+
.background(
457+
MaterialTheme.colorScheme.surfaceBright,
458+
shape = MaterialTheme.shapes.extraSmall
459+
)
460+
.padding(8.dp),
461+
verticalAlignment = Alignment.CenterVertically,
462+
horizontalArrangement = Arrangement.spacedBy(8.dp)
463+
) {
464+
val buttonText = if (isUpdateAvailable && !updateInfo?.versionName.isNullOrEmpty()) {
465+
stringResource(R.string.action_update_to_version, updateInfo?.versionName ?: "")
466+
} else {
467+
stringResource(R.string.action_check_for_updates)
468+
}
469+
470+
val buttonIconRes = R.drawable.rounded_mobile_arrow_down_24
471+
472+
Button(
473+
onClick = {
474+
HapticUtil.performVirtualKeyHaptic(view)
475+
viewModel.checkForUpdates(context, manual = true)
476+
showUpdateSheet = true
477+
},
478+
modifier = Modifier
479+
.fillMaxWidth()
480+
.height(52.dp)
481+
.padding(horizontal = 4.dp),
482+
contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp)
483+
) {
484+
Icon(
485+
painter = painterResource(id = buttonIconRes),
486+
contentDescription = null,
487+
modifier = Modifier.size(24.dp)
488+
)
489+
Spacer(modifier = Modifier.width(8.dp))
490+
Text(
491+
text = buttonText,
492+
style = MaterialTheme.typography.titleMedium,
493+
fontWeight = FontWeight.Bold
494+
)
495+
}
496+
}
497+
}
498+
421499
Spacer(modifier = Modifier.height(8.dp))
422500

423501
// App Settings Section
@@ -879,75 +957,6 @@ fun SettingsContent(
879957
}
880958
}
881959

882-
// Updates Section
883-
Text(
884-
text = "Updates",
885-
style = MaterialTheme.typography.titleMedium,
886-
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp),
887-
color = MaterialTheme.colorScheme.onSurfaceVariant
888-
)
889-
890-
RoundedCardContainer {
891-
IconToggleItem(
892-
iconRes = R.drawable.rounded_mobile_check_24,
893-
title = "Auto check for updates",
894-
description = "Check for updates at app launch",
895-
isChecked = isAutoUpdateEnabled,
896-
onCheckedChange = { viewModel.setAutoUpdateEnabled(it, context) }
897-
)
898-
IconToggleItem(
899-
iconRes = R.drawable.rounded_experiment_24,
900-
title = context.getString(R.string.check_pre_releases_label),
901-
description = context.getString(R.string.check_pre_releases_desc),
902-
isChecked = isPreReleaseCheckEnabled,
903-
onCheckedChange = { viewModel.setPreReleaseCheckEnabled(it, context) }
904-
)
905-
IconToggleItem(
906-
iconRes = R.drawable.rounded_notifications_unread_24,
907-
title = "Notify for new updates",
908-
description = "Show a notification when an update is found",
909-
isChecked = isUpdateNotificationEnabled,
910-
onCheckedChange = { viewModel.setUpdateNotificationEnabled(it, context) }
911-
)
912-
913-
Row(
914-
modifier = Modifier
915-
.fillMaxWidth()
916-
.background(
917-
MaterialTheme.colorScheme.surfaceBright,
918-
shape = MaterialTheme.shapes.extraSmall
919-
)
920-
.padding(8.dp),
921-
verticalAlignment = Alignment.CenterVertically,
922-
horizontalArrangement = Arrangement.spacedBy(8.dp)
923-
) {
924-
925-
// Check for updates button
926-
Button(
927-
onClick = {
928-
HapticUtil.performVirtualKeyHaptic(view)
929-
viewModel.checkForUpdates(context, manual = true)
930-
showUpdateSheet = true
931-
},
932-
modifier = Modifier
933-
.fillMaxWidth()
934-
.padding(6.dp)
935-
) {
936-
Icon(
937-
painter = painterResource(id = R.drawable.rounded_mobile_arrow_down_24),
938-
contentDescription = null,
939-
modifier = Modifier.size(24.dp)
940-
)
941-
Spacer(modifier = Modifier.width(8.dp))
942-
Text(
943-
"Check for updates",
944-
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
945-
)
946-
}
947-
948-
}
949-
}
950-
951960

952961
Spacer(modifier = Modifier.height(16.dp))
953962

app/src/main/java/com/sameerasw/essentials/data/repository/UpdateRepository.kt

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,44 @@
11
package com.sameerasw.essentials.data.repository
22

3+
import android.content.Context
34
import com.google.gson.Gson
45
import com.sameerasw.essentials.domain.model.UpdateInfo
6+
import com.sameerasw.essentials.utils.AutoUpdateManagerHelper
57
import kotlinx.coroutines.Dispatchers
68
import kotlinx.coroutines.withContext
9+
import java.net.HttpURLConnection
710
import java.net.URL
811

912
class UpdateRepository {
1013

1114
suspend fun checkForUpdates(
15+
context: Context,
16+
isPreReleaseCheckEnabled: Boolean,
17+
currentVersion: String
18+
): UpdateInfo? = withContext(Dispatchers.IO) {
19+
try {
20+
val autoUpdateHelper = AutoUpdateManagerHelper(context)
21+
val updateFeatures = autoUpdateHelper.checkForUpdate("https://sameerasw.com/essentials-update.json")
22+
23+
if (updateFeatures != null && updateFeatures.latestversion.isNotEmpty()) {
24+
val latestVersion = updateFeatures.latestversion
25+
val hasUpdate = isNewerVersion(currentVersion, latestVersion)
26+
return@withContext UpdateInfo(
27+
versionName = latestVersion,
28+
releaseNotes = updateFeatures.changelog,
29+
downloadUrl = updateFeatures.apk_url,
30+
releaseUrl = if (updateFeatures.changelog.startsWith("http")) updateFeatures.changelog else "https://github.com/sameerasw/essentials/releases",
31+
isUpdateAvailable = hasUpdate
32+
)
33+
}
34+
} catch (e: Exception) {
35+
e.printStackTrace()
36+
}
37+
38+
checkForUpdatesFromGitHub(isPreReleaseCheckEnabled, currentVersion)
39+
}
40+
41+
private suspend fun checkForUpdatesFromGitHub(
1242
isPreReleaseCheckEnabled: Boolean,
1343
currentVersion: String
1444
): UpdateInfo? = withContext(Dispatchers.IO) {
@@ -20,7 +50,7 @@ class UpdateRepository {
2050
}
2151

2252
val url = URL(urlString)
23-
val connection = url.openConnection() as java.net.HttpURLConnection
53+
val connection = url.openConnection() as HttpURLConnection
2454

2555
if (connection.responseCode != 200) {
2656
return@withContext null
@@ -32,7 +62,6 @@ class UpdateRepository {
3262
val releases = Gson().fromJson(releaseData, Array<Any>::class.java)
3363
.filterIsInstance<Map<String, Any>>()
3464

35-
// Find the true latest release using SemanticVersion comparison
3665
releases.maxByOrNull { rel ->
3766
val tagName = (rel["tag_name"] as? String)?.removePrefix("v") ?: "0.0.0"
3867
SemanticVersion.parse(tagName)

app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,7 @@ fun initPermissionRegistry() {
106106

107107
// Notification Snoozing feature
108108
PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_notification_snoozing_title)
109+
110+
// Install unknown packages feature
111+
PermissionRegistry.register("REQUEST_INSTALL_PACKAGES", R.string.tab_app_updates_title)
109112
}

0 commit comments

Comments
 (0)