Skip to content

Commit 3304d58

Browse files
authored
Merge pull request #327 from OpenHub-Store/auto-update-impr
2 parents dee34a5 + 816d81e commit 3304d58

28 files changed

Lines changed: 431 additions & 1 deletion

File tree

composeApp/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ kotlin {
6969
implementation(libs.liquid)
7070
implementation(libs.jetbrains.compose.material.icons.extended)
7171

72+
implementation(libs.touchlab.kermit)
73+
7274
implementation(compose.runtime)
7375
implementation(compose.foundation)
7476
implementation(libs.jetbrains.compose.material3)

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,26 @@ import android.app.Application
44
import android.app.NotificationChannel
55
import android.app.NotificationManager
66
import android.os.Build
7+
import co.touchlab.kermit.Logger
8+
import kotlinx.coroutines.CoroutineScope
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.SupervisorJob
11+
import kotlinx.coroutines.flow.first
12+
import kotlinx.coroutines.launch
713
import org.koin.android.ext.android.get
814
import org.koin.android.ext.koin.androidContext
915
import zed.rainxch.core.data.services.PackageEventReceiver
1016
import zed.rainxch.core.data.services.UpdateScheduler
17+
import zed.rainxch.core.domain.model.InstallSource
18+
import zed.rainxch.core.domain.model.InstalledApp
1119
import zed.rainxch.core.domain.repository.InstalledAppsRepository
20+
import zed.rainxch.core.domain.repository.ThemesRepository
1221
import zed.rainxch.core.domain.system.PackageMonitor
1322
import zed.rainxch.githubstore.app.di.initKoin
1423

1524
class GithubStoreApp : Application() {
1625
private var packageEventReceiver: PackageEventReceiver? = null
26+
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
1727

1828
override fun onCreate() {
1929
super.onCreate()
@@ -25,6 +35,7 @@ class GithubStoreApp : Application() {
2535
createNotificationChannels()
2636
registerPackageEventReceiver()
2737
scheduleBackgroundUpdateChecks()
38+
registerSelfAsInstalledApp()
2839
}
2940

3041
private fun createNotificationChannels() {
@@ -70,10 +81,86 @@ class GithubStoreApp : Application() {
7081
}
7182

7283
private fun scheduleBackgroundUpdateChecks() {
73-
UpdateScheduler.schedule(context = this)
84+
appScope.launch {
85+
try {
86+
val intervalHours = get<ThemesRepository>().getUpdateCheckInterval().first()
87+
UpdateScheduler.schedule(
88+
context = this@GithubStoreApp,
89+
intervalHours = intervalHours,
90+
)
91+
} catch (e: Exception) {
92+
Logger.e(e) { "Failed to schedule background update checks" }
93+
}
94+
}
95+
}
96+
97+
private fun registerSelfAsInstalledApp() {
98+
appScope.launch {
99+
try {
100+
val repo = get<InstalledAppsRepository>()
101+
val selfPackageName = packageName
102+
val existing = repo.getAppByPackage(selfPackageName)
103+
104+
if (existing != null) return@launch
105+
106+
val packageMonitor = get<PackageMonitor>()
107+
val systemInfo = packageMonitor.getInstalledPackageInfo(selfPackageName)
108+
if (systemInfo == null) {
109+
Logger.w { "GithubStoreApp: Skip self-registration, package info missing for $selfPackageName" }
110+
return@launch
111+
}
112+
113+
val now = System.currentTimeMillis()
114+
val versionName = systemInfo.versionName
115+
val versionCode = systemInfo.versionCode
116+
117+
val selfApp =
118+
InstalledApp(
119+
packageName = selfPackageName,
120+
repoId = SELF_REPO_ID,
121+
repoName = SELF_REPO_NAME,
122+
repoOwner = SELF_REPO_OWNER,
123+
repoOwnerAvatarUrl = SELF_AVATAR_URL,
124+
repoDescription = "A cross-platform app store for GitHub releases",
125+
primaryLanguage = "Kotlin",
126+
repoUrl = "https://github.com/$SELF_REPO_OWNER/$SELF_REPO_NAME",
127+
installedVersion = versionName,
128+
installedAssetName = null,
129+
installedAssetUrl = null,
130+
latestVersion = null,
131+
latestAssetName = null,
132+
latestAssetUrl = null,
133+
latestAssetSize = null,
134+
appName = "GitHub Store",
135+
installSource = InstallSource.THIS_APP,
136+
installedAt = now,
137+
lastCheckedAt = 0L,
138+
lastUpdatedAt = now,
139+
isUpdateAvailable = false,
140+
updateCheckEnabled = true,
141+
releaseNotes = null,
142+
systemArchitecture = "",
143+
fileExtension = "apk",
144+
isPendingInstall = false,
145+
installedVersionName = versionName,
146+
installedVersionCode = versionCode,
147+
)
148+
149+
repo.saveInstalledApp(selfApp)
150+
Logger.i("GitHub Store App: App added")
151+
} catch (e: Exception) {
152+
Logger.e(e) { "GitHub Store App: Failed to register self as installed app" }
153+
}
154+
}
74155
}
75156

76157
companion object {
158+
private const val SELF_REPO_ID = 1101281251L
159+
private const val SELF_REPO_OWNER = "OpenHub-Store"
160+
private const val SELF_REPO_NAME = "GitHub-Store"
161+
private const val SELF_AVATAR_URL =
162+
@Suppress("ktlint:standard:max-line-length")
163+
"https://raw.githubusercontent.com/OpenHub-Store/GitHub-Store/refs/heads/main/media-resources/app_icon.png"
77164
const val UPDATES_CHANNEL_ID = "app_updates"
78165
const val UPDATE_SERVICE_CHANNEL_ID = "update_service"
79166
}

core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ import zed.rainxch.core.data.utils.AndroidBrowserHelper
2424
import zed.rainxch.core.data.utils.AndroidClipboardHelper
2525
import zed.rainxch.core.data.utils.AndroidShareManager
2626
import zed.rainxch.core.domain.network.Downloader
27+
import zed.rainxch.core.data.services.AndroidUpdateScheduleManager
2728
import zed.rainxch.core.domain.system.Installer
2829
import zed.rainxch.core.domain.system.InstallerStatusProvider
2930
import zed.rainxch.core.domain.system.PackageMonitor
31+
import zed.rainxch.core.domain.system.UpdateScheduleManager
3032
import zed.rainxch.core.domain.utils.AppLauncher
3133
import zed.rainxch.core.domain.utils.BrowserHelper
3234
import zed.rainxch.core.domain.utils.ClipboardHelper
@@ -122,4 +124,10 @@ actual val corePlatformModule = module {
122124
)
123125
}
124126

127+
single<UpdateScheduleManager> {
128+
AndroidUpdateScheduleManager(
129+
context = androidContext()
130+
)
131+
}
132+
125133
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package zed.rainxch.core.data.services
2+
3+
import android.content.Context
4+
import zed.rainxch.core.domain.system.UpdateScheduleManager
5+
6+
class AndroidUpdateScheduleManager(
7+
private val context: Context,
8+
) : UpdateScheduleManager {
9+
override fun reschedule(intervalHours: Long) {
10+
UpdateScheduler.reschedule(context, intervalHours)
11+
}
12+
}

core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateScheduler.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,43 @@ object UpdateScheduler {
6868
Logger.i { "UpdateScheduler: Scheduled periodic update check every ${intervalHours}h + immediate check" }
6969
}
7070

71+
/**
72+
* Force-reschedules the periodic update check with a new interval.
73+
* Uses UPDATE policy to replace the existing schedule immediately.
74+
* Call this when the user changes the update check interval in settings.
75+
*/
76+
fun reschedule(
77+
context: Context,
78+
intervalHours: Long,
79+
) {
80+
val constraints =
81+
Constraints
82+
.Builder()
83+
.setRequiredNetworkType(NetworkType.CONNECTED)
84+
.build()
85+
86+
val request =
87+
PeriodicWorkRequestBuilder<UpdateCheckWorker>(
88+
repeatInterval = intervalHours,
89+
repeatIntervalTimeUnit = TimeUnit.HOURS,
90+
).setConstraints(constraints)
91+
.setBackoffCriteria(
92+
BackoffPolicy.EXPONENTIAL,
93+
30,
94+
TimeUnit.MINUTES,
95+
).build()
96+
97+
WorkManager
98+
.getInstance(context)
99+
.enqueueUniquePeriodicWork(
100+
uniqueWorkName = UpdateCheckWorker.WORK_NAME,
101+
existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.UPDATE,
102+
request = request,
103+
)
104+
105+
Logger.i { "UpdateScheduler: Rescheduled periodic update check to every ${intervalHours}h" }
106+
}
107+
71108
/**
72109
* Enqueues a one-time [AutoUpdateWorker] to download and silently install
73110
* all available updates via Shizuku. Uses KEEP policy to avoid duplicate runs.

core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
44
import androidx.datastore.preferences.core.Preferences
55
import androidx.datastore.preferences.core.booleanPreferencesKey
66
import androidx.datastore.preferences.core.edit
7+
import androidx.datastore.preferences.core.longPreferencesKey
78
import androidx.datastore.preferences.core.stringPreferencesKey
89
import kotlinx.coroutines.flow.Flow
910
import kotlinx.coroutines.flow.map
@@ -22,6 +23,7 @@ class ThemesRepositoryImpl(
2223
private val AUTO_DETECT_CLIPBOARD_KEY = booleanPreferencesKey("auto_detect_clipboard_links")
2324
private val INSTALLER_TYPE_KEY = stringPreferencesKey("installer_type")
2425
private val AUTO_UPDATE_KEY = booleanPreferencesKey("auto_update_enabled")
26+
private val UPDATE_CHECK_INTERVAL_KEY = longPreferencesKey("update_check_interval_hours")
2527

2628
override fun getThemeColor(): Flow<AppTheme> =
2729
preferences.data.map { prefs ->
@@ -107,4 +109,19 @@ class ThemesRepositoryImpl(
107109
prefs[AUTO_UPDATE_KEY] = enabled
108110
}
109111
}
112+
113+
override fun getUpdateCheckInterval(): Flow<Long> =
114+
preferences.data.map { prefs ->
115+
prefs[UPDATE_CHECK_INTERVAL_KEY] ?: DEFAULT_UPDATE_CHECK_INTERVAL_HOURS
116+
}
117+
118+
override suspend fun setUpdateCheckInterval(hours: Long) {
119+
preferences.edit { prefs ->
120+
prefs[UPDATE_CHECK_INTERVAL_KEY] = hours
121+
}
122+
}
123+
124+
companion object {
125+
const val DEFAULT_UPDATE_CHECK_INTERVAL_HOURS = 6L
126+
}
110127
}

core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import zed.rainxch.core.data.services.DesktopFileLocationsProvider
1515
import zed.rainxch.core.data.services.DesktopInstaller
1616
import zed.rainxch.core.data.services.DesktopLocalizationManager
1717
import zed.rainxch.core.data.services.DesktopPackageMonitor
18+
import zed.rainxch.core.data.services.DesktopUpdateScheduleManager
1819
import zed.rainxch.core.data.services.FileLocationsProvider
1920
import zed.rainxch.core.domain.system.Installer
2021
import zed.rainxch.core.domain.system.InstallerStatusProvider
22+
import zed.rainxch.core.domain.system.UpdateScheduleManager
2123
import zed.rainxch.core.data.services.LocalizationManager
2224
import zed.rainxch.core.data.services.DesktopInstallerStatusProvider
2325
import zed.rainxch.core.data.utils.DesktopShareManager
@@ -92,4 +94,8 @@ actual val corePlatformModule = module {
9294
single<InstallerStatusProvider> {
9395
DesktopInstallerStatusProvider()
9496
}
97+
98+
single<UpdateScheduleManager> {
99+
DesktopUpdateScheduleManager()
100+
}
95101
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package zed.rainxch.core.data.services
2+
3+
import zed.rainxch.core.domain.system.UpdateScheduleManager
4+
5+
/**
6+
* No-op implementation for Desktop — WorkManager is Android-only.
7+
*/
8+
class DesktopUpdateScheduleManager : UpdateScheduleManager {
9+
override fun reschedule(intervalHours: Long) {
10+
// No background scheduler on Desktop
11+
}
12+
}

core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ThemesRepository.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ interface ThemesRepository {
2020
suspend fun setInstallerType(type: InstallerType)
2121
fun getAutoUpdateEnabled(): Flow<Boolean>
2222
suspend fun setAutoUpdateEnabled(enabled: Boolean)
23+
fun getUpdateCheckInterval(): Flow<Long>
24+
suspend fun setUpdateCheckInterval(hours: Long)
2325
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package zed.rainxch.core.domain.system
2+
3+
/**
4+
* Abstraction for rescheduling background update checks.
5+
* Android implementation delegates to WorkManager; Desktop is a no-op.
6+
*/
7+
interface UpdateScheduleManager {
8+
/**
9+
* Reschedules the periodic update check with a new interval.
10+
* Takes effect immediately (replaces existing schedule).
11+
*/
12+
fun reschedule(intervalHours: Long)
13+
}

0 commit comments

Comments
 (0)