Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,26 @@ import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import zed.rainxch.core.data.services.PackageEventReceiver
import zed.rainxch.core.data.services.UpdateScheduler
import zed.rainxch.core.domain.model.InstallSource
import zed.rainxch.core.domain.model.InstalledApp
import zed.rainxch.core.domain.repository.InstalledAppsRepository
import zed.rainxch.core.domain.repository.ThemesRepository
import zed.rainxch.core.domain.system.PackageMonitor
import zed.rainxch.githubstore.app.di.initKoin

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

override fun onCreate() {
super.onCreate()
Expand All @@ -25,6 +35,7 @@ class GithubStoreApp : Application() {
createNotificationChannels()
registerPackageEventReceiver()
scheduleBackgroundUpdateChecks()
registerSelfAsInstalledApp()
}

private fun createNotificationChannels() {
Expand Down Expand Up @@ -70,10 +81,73 @@ class GithubStoreApp : Application() {
}

private fun scheduleBackgroundUpdateChecks() {
UpdateScheduler.schedule(context = this)
appScope.launch {
val intervalHours = get<ThemesRepository>().getUpdateCheckInterval().first()
UpdateScheduler.schedule(context = this@GithubStoreApp, intervalHours = intervalHours)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Automatically registers GitHub Store itself as a tracked installed app on first launch.
* This allows the app to track its own updates and auto-update via Shizuku.
*/
private fun registerSelfAsInstalledApp() {
appScope.launch {
try {
val repo = get<InstalledAppsRepository>()
val existing = repo.getAppByPackage(SELF_PACKAGE_NAME)
if (existing != null) return@launch

val packageMonitor = get<PackageMonitor>()
val systemInfo = packageMonitor.getInstalledPackageInfo(SELF_PACKAGE_NAME)

val now = System.currentTimeMillis()
val versionName = systemInfo?.versionName ?: ""
val versionCode = systemInfo?.versionCode ?: 0L

val selfApp = InstalledApp(
packageName = SELF_PACKAGE_NAME,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
repoId = 0L,
repoName = SELF_REPO_NAME,
repoOwner = SELF_REPO_OWNER,
repoOwnerAvatarUrl = "https://avatars.githubusercontent.com/u/221085707",
repoDescription = "A cross-platform app store for GitHub releases",
primaryLanguage = "Kotlin",
repoUrl = "https://github.com/$SELF_REPO_OWNER/$SELF_REPO_NAME",
installedVersion = versionName,
installedAssetName = null,
installedAssetUrl = null,
latestVersion = null,
latestAssetName = null,
latestAssetUrl = null,
latestAssetSize = null,
appName = "GitHub Store",
installSource = InstallSource.THIS_APP,
installedAt = now,
lastCheckedAt = 0L,
lastUpdatedAt = now,
isUpdateAvailable = false,
updateCheckEnabled = true,
releaseNotes = null,
systemArchitecture = "",
fileExtension = "apk",
isPendingInstall = false,
installedVersionName = versionName,
installedVersionCode = versionCode,
)

repo.saveInstalledApp(selfApp)
Logger.i { "GithubStoreApp: Registered self as tracked installed app (v$versionName)" }
} catch (e: Exception) {
Logger.e { "GithubStoreApp: Failed to register self: ${e.message}" }
}
}
}

companion object {
private const val SELF_PACKAGE_NAME = "zed.rainxch.githubstore"
private const val SELF_REPO_OWNER = "OpenHub-Store"
private const val SELF_REPO_NAME = "GitHub-Store"
const val UPDATES_CHANNEL_ID = "app_updates"
const val UPDATE_SERVICE_CHANNEL_ID = "update_service"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import zed.rainxch.core.data.utils.AndroidBrowserHelper
import zed.rainxch.core.data.utils.AndroidClipboardHelper
import zed.rainxch.core.data.utils.AndroidShareManager
import zed.rainxch.core.domain.network.Downloader
import zed.rainxch.core.data.services.AndroidUpdateScheduleManager
import zed.rainxch.core.domain.system.Installer
import zed.rainxch.core.domain.system.InstallerStatusProvider
import zed.rainxch.core.domain.system.PackageMonitor
import zed.rainxch.core.domain.system.UpdateScheduleManager
import zed.rainxch.core.domain.utils.AppLauncher
import zed.rainxch.core.domain.utils.BrowserHelper
import zed.rainxch.core.domain.utils.ClipboardHelper
Expand Down Expand Up @@ -122,4 +124,10 @@ actual val corePlatformModule = module {
)
}

single<UpdateScheduleManager> {
AndroidUpdateScheduleManager(
context = androidContext()
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package zed.rainxch.core.data.services

import android.content.Context
import zed.rainxch.core.domain.system.UpdateScheduleManager

class AndroidUpdateScheduleManager(
private val context: Context,
) : UpdateScheduleManager {
override fun reschedule(intervalHours: Long) {
UpdateScheduler.reschedule(context, intervalHours)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,43 @@ object UpdateScheduler {
Logger.i { "UpdateScheduler: Scheduled periodic update check every ${intervalHours}h + immediate check" }
}

/**
* Force-reschedules the periodic update check with a new interval.
* Uses UPDATE policy to replace the existing schedule immediately.
* Call this when the user changes the update check interval in settings.
*/
fun reschedule(
context: Context,
intervalHours: Long,
) {
val constraints =
Constraints
.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val request =
PeriodicWorkRequestBuilder<UpdateCheckWorker>(
repeatInterval = intervalHours,
repeatIntervalTimeUnit = TimeUnit.HOURS,
).setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30,
TimeUnit.MINUTES,
).build()

WorkManager
.getInstance(context)
.enqueueUniquePeriodicWork(
uniqueWorkName = UpdateCheckWorker.WORK_NAME,
existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.UPDATE,
request = request,
)

Logger.i { "UpdateScheduler: Rescheduled periodic update check to every ${intervalHours}h" }
}

/**
* Enqueues a one-time [AutoUpdateWorker] to download and silently install
* all available updates via Shizuku. Uses KEEP policy to avoid duplicate runs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
Expand All @@ -22,6 +23,7 @@ class ThemesRepositoryImpl(
private val AUTO_DETECT_CLIPBOARD_KEY = booleanPreferencesKey("auto_detect_clipboard_links")
private val INSTALLER_TYPE_KEY = stringPreferencesKey("installer_type")
private val AUTO_UPDATE_KEY = booleanPreferencesKey("auto_update_enabled")
private val UPDATE_CHECK_INTERVAL_KEY = longPreferencesKey("update_check_interval_hours")

override fun getThemeColor(): Flow<AppTheme> =
preferences.data.map { prefs ->
Expand Down Expand Up @@ -107,4 +109,19 @@ class ThemesRepositoryImpl(
prefs[AUTO_UPDATE_KEY] = enabled
}
}

override fun getUpdateCheckInterval(): Flow<Long> =
preferences.data.map { prefs ->
prefs[UPDATE_CHECK_INTERVAL_KEY] ?: DEFAULT_UPDATE_CHECK_INTERVAL_HOURS
}

override suspend fun setUpdateCheckInterval(hours: Long) {
preferences.edit { prefs ->
prefs[UPDATE_CHECK_INTERVAL_KEY] = hours
}
}

companion object {
const val DEFAULT_UPDATE_CHECK_INTERVAL_HOURS = 6L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import zed.rainxch.core.data.services.DesktopFileLocationsProvider
import zed.rainxch.core.data.services.DesktopInstaller
import zed.rainxch.core.data.services.DesktopLocalizationManager
import zed.rainxch.core.data.services.DesktopPackageMonitor
import zed.rainxch.core.data.services.DesktopUpdateScheduleManager
import zed.rainxch.core.data.services.FileLocationsProvider
import zed.rainxch.core.domain.system.Installer
import zed.rainxch.core.domain.system.InstallerStatusProvider
import zed.rainxch.core.domain.system.UpdateScheduleManager
import zed.rainxch.core.data.services.LocalizationManager
import zed.rainxch.core.data.services.DesktopInstallerStatusProvider
import zed.rainxch.core.data.utils.DesktopShareManager
Expand Down Expand Up @@ -92,4 +94,8 @@ actual val corePlatformModule = module {
single<InstallerStatusProvider> {
DesktopInstallerStatusProvider()
}

single<UpdateScheduleManager> {
DesktopUpdateScheduleManager()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package zed.rainxch.core.data.services

import zed.rainxch.core.domain.system.UpdateScheduleManager

/**
* No-op implementation for Desktop — WorkManager is Android-only.
*/
class DesktopUpdateScheduleManager : UpdateScheduleManager {
override fun reschedule(intervalHours: Long) {
// No background scheduler on Desktop
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ interface ThemesRepository {
suspend fun setInstallerType(type: InstallerType)
fun getAutoUpdateEnabled(): Flow<Boolean>
suspend fun setAutoUpdateEnabled(enabled: Boolean)
fun getUpdateCheckInterval(): Flow<Long>
suspend fun setUpdateCheckInterval(hours: Long)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package zed.rainxch.core.domain.system

/**
* Abstraction for rescheduling background update checks.
* Android implementation delegates to WorkManager; Desktop is a no-op.
*/
interface UpdateScheduleManager {
/**
* Reschedules the periodic update check with a new interval.
* Takes effect immediately (replaces existing schedule).
*/
fun reschedule(intervalHours: Long)
}
Original file line number Diff line number Diff line change
Expand Up @@ -525,4 +525,12 @@
<string name="shizuku_install_failed_fallback">Shizuku install failed, using standard installer</string>
<string name="auto_update_title">Auto-update apps</string>
<string name="auto_update_description">Automatically download and install updates in background via Shizuku</string>

<string name="section_updates">Updates</string>
<string name="update_check_interval_title">Update check interval</string>
<string name="update_check_interval_description">How often to check for app updates in background</string>
<string name="interval_3h">3h</string>
<string name="interval_6h">6h</string>
<string name="interval_12h">12h</string>
<string name="interval_24h">24h</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ sealed interface ProfileAction {
data class OnInstallerTypeSelected(val type: InstallerType) : ProfileAction
data object OnRequestShizukuPermission : ProfileAction
data class OnAutoUpdateToggled(val enabled: Boolean) : ProfileAction
data class OnUpdateCheckIntervalChanged(val hours: Long) : ProfileAction

data class OnAutoDetectClipboardToggled(
val enabled: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import zed.rainxch.githubstore.core.presentation.res.*
import zed.rainxch.profile.presentation.components.LogoutDialog
import zed.rainxch.profile.presentation.components.sections.about
import zed.rainxch.profile.presentation.components.sections.installationSection
import zed.rainxch.profile.presentation.components.sections.updatesSection
import zed.rainxch.profile.presentation.components.sections.logout
import zed.rainxch.profile.presentation.components.sections.networkSection
import zed.rainxch.profile.presentation.components.sections.othersSection
Expand Down Expand Up @@ -206,6 +207,11 @@ fun ProfileScreen(
onAction = onAction
)

updatesSection(
state = state,
onAction = onAction
)

item {
Spacer(Modifier.height(16.dp))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ data class ProfileState(
val installerType: InstallerType = InstallerType.DEFAULT,
val shizukuAvailability: ShizukuAvailability = ShizukuAvailability.UNAVAILABLE,
val autoUpdateEnabled: Boolean = false,
val updateCheckIntervalHours: Long = 6L,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import zed.rainxch.core.domain.model.ProxyConfig
import zed.rainxch.core.domain.repository.ProxyRepository
import zed.rainxch.core.domain.repository.ThemesRepository
import zed.rainxch.core.domain.system.InstallerStatusProvider
import zed.rainxch.core.domain.system.UpdateScheduleManager
import zed.rainxch.core.domain.utils.BrowserHelper
import zed.rainxch.githubstore.core.presentation.res.Res
import zed.rainxch.githubstore.core.presentation.res.failed_to_save_proxy_settings
Expand All @@ -30,6 +31,7 @@ class ProfileViewModel(
private val profileRepository: ProfileRepository,
private val installerStatusProvider: InstallerStatusProvider,
private val proxyRepository: ProxyRepository,
private val updateScheduleManager: UpdateScheduleManager,
) : ViewModel() {
private var userProfileJob: Job? = null

Expand All @@ -48,6 +50,7 @@ class ProfileViewModel(
loadInstallerPreference()
observeShizukuStatus()
loadAutoUpdatePreference()
loadUpdateCheckInterval()

hasLoadedInitialData = true
}
Expand Down Expand Up @@ -229,6 +232,16 @@ class ProfileViewModel(
}
}

private fun loadUpdateCheckInterval() {
viewModelScope.launch {
themesRepository.getUpdateCheckInterval().collect { hours ->
_state.update {
it.copy(updateCheckIntervalHours = hours)
}
}
}
}
Comment on lines +235 to +243
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate interval values before persisting/rescheduling.

hours is currently trusted end-to-end. Please normalize to supported values (3/6/12/24) before writing state/repository and before scheduling.

Suggested hardening
+    private val supportedUpdateIntervals = setOf(3L, 6L, 12L, 24L)
+    private const val defaultUpdateIntervalHours = 6L
+
+    private fun normalizeUpdateInterval(hours: Long): Long =
+        hours.takeIf { it in supportedUpdateIntervals } ?: defaultUpdateIntervalHours
+
     private fun loadUpdateCheckInterval() {
         viewModelScope.launch {
             themesRepository.getUpdateCheckInterval().collect { hours ->
+                val normalized = normalizeUpdateInterval(hours)
                 _state.update {
-                    it.copy(updateCheckIntervalHours = hours)
+                    it.copy(updateCheckIntervalHours = normalized)
                 }
             }
         }
     }
@@
             is ProfileAction.OnUpdateCheckIntervalChanged -> {
+                val normalized = normalizeUpdateInterval(action.hours)
                 viewModelScope.launch {
-                    themesRepository.setUpdateCheckInterval(action.hours)
-                    updateScheduleManager.reschedule(action.hours)
+                    themesRepository.setUpdateCheckInterval(normalized)
+                    updateScheduleManager.reschedule(normalized)
                 }
             }

Also applies to: 418-423

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt`
around lines 235 - 243, Normalize and validate the incoming update-interval
value to the supported set {3,6,12,24} before you persist it, update _state, or
pass it to any scheduler: in loadUpdateCheckInterval() wrap the collected hours
from themesRepository.getUpdateCheckInterval().collect with a normalization
function (e.g., normalizeUpdateInterval(hours)) and use the normalized value
when calling _state.update and when any scheduling functions are invoked; do the
same in the code path that writes/updates the repository (the method that saves
the interval) and in the scheduler invocation (e.g., scheduleUpdateCheck or
similar) so all writes and scheduling always use the normalized value.


fun onAction(action: ProfileAction) {
when (action) {
ProfileAction.OnHelpClick -> {
Expand Down Expand Up @@ -402,6 +415,13 @@ class ProfileViewModel(
}
}

is ProfileAction.OnUpdateCheckIntervalChanged -> {
viewModelScope.launch {
themesRepository.setUpdateCheckInterval(action.hours)
updateScheduleManager.reschedule(action.hours)
}
}

ProfileAction.OnProxySave -> {
val currentState = _state.value
val port =
Expand Down
Loading