feat: Add data migration and robust system sync#136
Conversation
This introduces a robust data synchronization and migration mechanism at app startup and before checking for updates. Key changes: - **System Package Sync:** Before loading apps, it now cross-references the database with the system's installed packages. Apps that are no longer installed on the system are removed from the database. - **Data Migration:** For existing tracked apps missing `versionName` and `versionCode`, this change populates them using data from the package manager on Android or falls back to using the release tag on other platforms. - The `checkAllForUpdates` function now leverages the more efficient `installedAppsRepository.checkAllForUpdates()` after performing the sync and migration, instead of iterating and checking each app individually within the ViewModel.
WalkthroughAdds system-installed-package synchronization and migration to AppsViewModel and DetailsViewModel: on load and before update checks the view models compare system packages with DB entries, remove stale records, and populate missing version fields (Android vs desktop). AppsViewModel constructor gained a new Changes
Sequence DiagramsequenceDiagram
participant VM as ViewModel
participant Sys as SystemPackageManager
participant DB as InstalledAppsRepository / Database
participant Repo as AppsRepository
VM->>VM: loadApps() / loadInitial()
VM->>Sys: readInstalledPackages() (IO)
Sys-->>VM: installed package list
VM->>DB: queryInstalledApps()
DB-->>VM: DB app records
alt DB entries for packages not installed
VM->>DB: deleteStaleInstalledApps()
DB-->>VM: deletion confirmation
end
alt Missing version fields on DB entry
VM->>Sys: fetchVersionFromSystem() (Android) / deriveFromTag()
Sys-->>VM: version info
VM->>DB: updateInstalledAppVersion()
DB-->>VM: update confirmation
end
VM->>Repo: checkAllForUpdates()
Repo-->>VM: update results
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt (1)
136-153: Duplicated fallback logic between Android null-case and desktop.Lines 137-143 (Android when
systemInfo == null) and lines 146-151 (desktop) have identical logic. Consider extracting to reduce duplication.🔎 Proposed refactor
- } else if (app.installedVersionName == null) { - if (platform.type == PlatformType.ANDROID) { - val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName) - if (systemInfo != null) { - installedAppsRepository.updateApp(app.copy( - installedVersionName = systemInfo.versionName, - installedVersionCode = systemInfo.versionCode, - latestVersionName = systemInfo.versionName, - latestVersionCode = systemInfo.versionCode - )) - Logger.d { "Migrated ${app.packageName}: set versionName/code from system" } - } else { - installedAppsRepository.updateApp(app.copy( - installedVersionName = app.installedVersion, - installedVersionCode = 0L, - latestVersionName = app.installedVersion, - latestVersionCode = 0L - )) - Logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" } - } - } else { - installedAppsRepository.updateApp(app.copy( - installedVersionName = app.installedVersion, - installedVersionCode = 0L, - latestVersionName = app.installedVersion, - latestVersionCode = 0L - )) - Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag as versionName" } - } - } + } else if (app.installedVersionName == null) { + val systemInfo = if (platform.type == PlatformType.ANDROID) { + packageMonitor.getInstalledPackageInfo(app.packageName) + } else null + + if (systemInfo != null) { + installedAppsRepository.updateApp(app.copy( + installedVersionName = systemInfo.versionName, + installedVersionCode = systemInfo.versionCode, + latestVersionName = systemInfo.versionName, + latestVersionCode = systemInfo.versionCode + )) + Logger.d { "Migrated ${app.packageName}: set versionName/code from system" } + } else { + installedAppsRepository.updateApp(app.copy( + installedVersionName = app.installedVersion, + installedVersionCode = 0L, + latestVersionName = app.installedVersion, + latestVersionCode = 0L + )) + val platformLabel = if (platform.type == PlatformType.ANDROID) "" else " (desktop)" + Logger.d { "Migrated ${app.packageName}$platformLabel: fallback to tag as versionName" } + } + }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt
🔇 Additional comments (2)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt (2)
49-51: LGTM!The new
platformdependency is properly added and follows the existing pattern. This enables platform-specific migration logic in the sync function.
78-80: LGTM!Calling the sync/migration before collecting apps ensures the database is consistent with system state before UI population. The error handling in
syncSystemExistenceAndMigrate()ensures failures don't block app loading.
| private suspend fun syncSystemExistenceAndMigrate() { | ||
| withContext(Dispatchers.IO) { | ||
| try { | ||
| val installedPackageNames = packageMonitor.getAllInstalledPackageNames() | ||
| val appsInDb = installedAppsRepository.getAllInstalledApps().first() | ||
|
|
||
| appsInDb.forEach { app -> | ||
| if (!installedPackageNames.contains(app.packageName)) { | ||
| Logger.d { "App ${app.packageName} no longer installed (not in system packages), removing from DB" } | ||
| installedAppsRepository.deleteInstalledApp(app.packageName) | ||
| } else if (app.installedVersionName == null) { | ||
| if (platform.type == PlatformType.ANDROID) { | ||
| val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName) | ||
| if (systemInfo != null) { | ||
| installedAppsRepository.updateApp(app.copy( | ||
| installedVersionName = systemInfo.versionName, | ||
| installedVersionCode = systemInfo.versionCode, | ||
| latestVersionName = systemInfo.versionName, | ||
| latestVersionCode = systemInfo.versionCode | ||
| )) | ||
| Logger.d { "Migrated ${app.packageName}: set versionName/code from system" } | ||
| } else { | ||
| installedAppsRepository.updateApp(app.copy( | ||
| installedVersionName = app.installedVersion, | ||
| installedVersionCode = 0L, | ||
| latestVersionName = app.installedVersion, | ||
| latestVersionCode = 0L | ||
| )) | ||
| Logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" } | ||
| } | ||
| } else { | ||
| installedAppsRepository.updateApp(app.copy( | ||
| installedVersionName = app.installedVersion, | ||
| installedVersionCode = 0L, | ||
| latestVersionName = app.installedVersion, | ||
| latestVersionCode = 0L | ||
| )) | ||
| Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag as versionName" } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Logger.d { "Robust system existence sync and data migration completed" } | ||
| } catch (e: Exception) { | ||
| Logger.e { "Failed to sync existence or migrate data: ${e.message}" } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Consider re-throwing CancellationException to respect coroutine cancellation.
The catch block on line 158 catches all exceptions including CancellationException. This can cause the coroutine to appear successful when it was actually cancelled, potentially leading to inconsistent state during ViewModel cleanup.
🔎 Proposed fix
Logger.d { "Robust system existence sync and data migration completed" }
} catch (e: Exception) {
+ if (e is CancellationException) throw e
Logger.e { "Failed to sync existence or migrate data: ${e.message}" }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private suspend fun syncSystemExistenceAndMigrate() { | |
| withContext(Dispatchers.IO) { | |
| try { | |
| val installedPackageNames = packageMonitor.getAllInstalledPackageNames() | |
| val appsInDb = installedAppsRepository.getAllInstalledApps().first() | |
| appsInDb.forEach { app -> | |
| if (!installedPackageNames.contains(app.packageName)) { | |
| Logger.d { "App ${app.packageName} no longer installed (not in system packages), removing from DB" } | |
| installedAppsRepository.deleteInstalledApp(app.packageName) | |
| } else if (app.installedVersionName == null) { | |
| if (platform.type == PlatformType.ANDROID) { | |
| val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName) | |
| if (systemInfo != null) { | |
| installedAppsRepository.updateApp(app.copy( | |
| installedVersionName = systemInfo.versionName, | |
| installedVersionCode = systemInfo.versionCode, | |
| latestVersionName = systemInfo.versionName, | |
| latestVersionCode = systemInfo.versionCode | |
| )) | |
| Logger.d { "Migrated ${app.packageName}: set versionName/code from system" } | |
| } else { | |
| installedAppsRepository.updateApp(app.copy( | |
| installedVersionName = app.installedVersion, | |
| installedVersionCode = 0L, | |
| latestVersionName = app.installedVersion, | |
| latestVersionCode = 0L | |
| )) | |
| Logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" } | |
| } | |
| } else { | |
| installedAppsRepository.updateApp(app.copy( | |
| installedVersionName = app.installedVersion, | |
| installedVersionCode = 0L, | |
| latestVersionName = app.installedVersion, | |
| latestVersionCode = 0L | |
| )) | |
| Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag as versionName" } | |
| } | |
| } | |
| } | |
| Logger.d { "Robust system existence sync and data migration completed" } | |
| } catch (e: Exception) { | |
| Logger.e { "Failed to sync existence or migrate data: ${e.message}" } | |
| } | |
| } | |
| } | |
| private suspend fun syncSystemExistenceAndMigrate() { | |
| withContext(Dispatchers.IO) { | |
| try { | |
| val installedPackageNames = packageMonitor.getAllInstalledPackageNames() | |
| val appsInDb = installedAppsRepository.getAllInstalledApps().first() | |
| appsInDb.forEach { app -> | |
| if (!installedPackageNames.contains(app.packageName)) { | |
| Logger.d { "App ${app.packageName} no longer installed (not in system packages), removing from DB" } | |
| installedAppsRepository.deleteInstalledApp(app.packageName) | |
| } else if (app.installedVersionName == null) { | |
| if (platform.type == PlatformType.ANDROID) { | |
| val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName) | |
| if (systemInfo != null) { | |
| installedAppsRepository.updateApp(app.copy( | |
| installedVersionName = systemInfo.versionName, | |
| installedVersionCode = systemInfo.versionCode, | |
| latestVersionName = systemInfo.versionName, | |
| latestVersionCode = systemInfo.versionCode | |
| )) | |
| Logger.d { "Migrated ${app.packageName}: set versionName/code from system" } | |
| } else { | |
| installedAppsRepository.updateApp(app.copy( | |
| installedVersionName = app.installedVersion, | |
| installedVersionCode = 0L, | |
| latestVersionName = app.installedVersion, | |
| latestVersionCode = 0L | |
| )) | |
| Logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" } | |
| } | |
| } else { | |
| installedAppsRepository.updateApp(app.copy( | |
| installedVersionName = app.installedVersion, | |
| installedVersionCode = 0L, | |
| latestVersionName = app.installedVersion, | |
| latestVersionCode = 0L | |
| )) | |
| Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag as versionName" } | |
| } | |
| } | |
| } | |
| Logger.d { "Robust system existence sync and data migration completed" } | |
| } catch (e: Exception) { | |
| if (e is CancellationException) throw e | |
| Logger.e { "Failed to sync existence or migrate data: ${e.message}" } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt
around lines 115 to 162, the catch block currently swallows all Exceptions
including CancellationException; update the handler so that if the caught
exception is a CancellationException it is re-thrown (or not caught) to allow
coroutine cancellation to propagate, otherwise handle/log the error as before.
Ensure you import or reference kotlinx.coroutines.CancellationException if
needed and rethrow immediately when detected before logging or swallowing other
exceptions.
| private fun checkAllForUpdates() { | ||
| viewModelScope.launch { | ||
| try { | ||
| syncSystemExistenceAndMigrate() | ||
|
|
||
| installedAppsRepository.checkAllForUpdates() | ||
| } catch (e: Exception) { | ||
| Logger.e { "Check all for updates failed: ${e.message}" } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Same CancellationException handling issue.
Similar to syncSystemExistenceAndMigrate(), this catch block swallows cancellation. For consistency and proper coroutine behavior, re-throw CancellationException.
🔎 Proposed fix
viewModelScope.launch {
try {
syncSystemExistenceAndMigrate()
installedAppsRepository.checkAllForUpdates()
} catch (e: Exception) {
+ if (e is CancellationException) throw e
Logger.e { "Check all for updates failed: ${e.message}" }
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private fun checkAllForUpdates() { | |
| viewModelScope.launch { | |
| try { | |
| syncSystemExistenceAndMigrate() | |
| installedAppsRepository.checkAllForUpdates() | |
| } catch (e: Exception) { | |
| Logger.e { "Check all for updates failed: ${e.message}" } | |
| } | |
| } | |
| } | |
| private fun checkAllForUpdates() { | |
| viewModelScope.launch { | |
| try { | |
| syncSystemExistenceAndMigrate() | |
| installedAppsRepository.checkAllForUpdates() | |
| } catch (e: Exception) { | |
| if (e is CancellationException) throw e | |
| Logger.e { "Check all for updates failed: ${e.message}" } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt
around lines 164 to 174, the catch block for Exception swallows
CancellationException; change the catch to re-throw CancellationException and
only log other exceptions — i.e., inside the catch check if the caught exception
is a CancellationException and throw it, otherwise log the error message as
before.
This commit introduces a new `syncSystemExistenceAndMigrate` function to centralize application state management. This function is now called at the beginning of the `loadData` and `CheckForUpdates` actions to ensure the database accurately reflects the system's installed packages.
Key changes:
- **Removed Complex Sync Logic**: Deleted a large, complex block of code responsible for syncing application state from within the `loadData` flow.
- **Centralized Sync Function**: Created `syncSystemExistenceAndMigrate()` to handle:
- Removing apps from the database that are no longer installed on the system.
- Migrating legacy database entries that lack version name/code by populating them from system package info.
- **Simplified App Loading**: The process for loading an installed app's state is now simplified, relying on the newly synchronized database.
- **Dependency Injection**: Added the `Platform` dependency to the `DetailsViewModel` to support platform-specific migration logic.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (4)
84-85: Consider propagating sync failures for error recovery.The sync function catches exceptions internally but doesn't propagate them. If the sync fails,
loadInitialcontinues without knowledge of the failure, potentially leading to inconsistent state assumptions. Consider returning a success/failure indicator or propagating exceptions to allow the caller to decide whether to proceed or show an error to the user.
135-159: Redundant system check after synchronization.Lines 143-149 check if the package is installed on the system, but
syncSystemExistenceAndMigrate()has already removed apps not present in system packages (line 227-229). This additionalpackageMonitor.isPackageInstalled()check appears redundant and adds unnecessary I/O overhead.If the pending status clearing is the only goal here, consider whether the sync function should handle that as well, or simply trust that if a DB entry exists after sync, the package is installed.
264-266: Propagate errors to enable caller recovery.The catch block logs the error but doesn't propagate it or return a failure indicator. Callers (like
loadInitialandCheckForUpdates) have no way to know if the sync failed, which may lead them to operate on stale or inconsistent data.Consider either:
- Re-throwing the exception after logging
- Returning a
Result<Unit>to indicate success/failure- Emitting an error event to the UI
363-381: Consider caching or rate-limiting sync operations.Calling
syncSystemExistenceAndMigrate()on every "Check for Updates" action performs a full scan of all tracked apps and system packages, even though the user is only checking one app. If there are many tracked apps, this could introduce noticeable latency.Consider:
- Rate-limiting sync (e.g., skip if run within last 5 minutes)
- Only syncing on app startup and periodic background refresh
- Caching system package list with TTL
The current approach is safe and correct, but may feel slow to users with many tracked apps.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt
🧰 Additional context used
🧬 Code graph analysis (1)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (1)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt (1)
syncSystemExistenceAndMigrate(115-162)
🔇 Additional comments (2)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt (1)
250-251: LGTM! Dependency wiring follows established patterns.The addition of
platform = get()to the AppsViewModel instantiation is consistent with how other ViewModels in the file receive the platform dependency (MainViewModel, HomeViewModel, DetailsViewModel). The trailing comma on line 250 is good Kotlin practice.AppsViewModel constructor correctly includes the
platform: Platformparameter at line 50 and actively uses it in the migration logic.composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (1)
157-157: Improved error message clarity.The error message now correctly reflects the operation being performed ("load" rather than "check"), which improves debugging clarity.
| private suspend fun syncSystemExistenceAndMigrate() { | ||
| withContext(Dispatchers.IO) { | ||
| try { | ||
| val installedPackageNames = packageMonitor.getAllInstalledPackageNames() | ||
| val appsInDb = installedAppsRepository.getAllInstalledApps().first() | ||
|
|
||
| appsInDb.forEach { app -> | ||
| if (!installedPackageNames.contains(app.packageName)) { | ||
| Logger.d { "App ${app.packageName} no longer installed, removing from DB" } | ||
| installedAppsRepository.deleteInstalledApp(app.packageName) | ||
| } else if (app.installedVersionName == null) { | ||
| // Migration logic for apps missing version data | ||
| if (platform.type == PlatformType.ANDROID) { | ||
| val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName) | ||
| if (systemInfo != null) { | ||
| installedAppsRepository.updateApp(app.copy( | ||
| installedVersionName = systemInfo.versionName, | ||
| installedVersionCode = systemInfo.versionCode, | ||
| latestVersionName = systemInfo.versionName, | ||
| latestVersionCode = systemInfo.versionCode | ||
| )) | ||
| Logger.d { "Migrated ${app.packageName}: set versionName/code from system" } | ||
| } else { | ||
| installedAppsRepository.updateApp(app.copy( | ||
| installedVersionName = app.installedVersion, | ||
| installedVersionCode = 0L, | ||
| latestVersionName = app.installedVersion, | ||
| latestVersionCode = 0L | ||
| )) | ||
| Logger.d { "Migrated ${app.packageName}: fallback to tag" } | ||
| } | ||
| } else { | ||
| installedAppsRepository.updateApp(app.copy( | ||
| installedVersionName = app.installedVersion, | ||
| installedVersionCode = 0L, | ||
| latestVersionName = app.installedVersion, | ||
| latestVersionCode = 0L | ||
| )) | ||
| Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag" } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Logger.d { "System existence sync and data migration completed" } | ||
| } catch (e: Exception) { | ||
| Logger.e { "Failed to sync existence or migrate data: ${e.message}" } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Extract duplicated function to shared utility.
This function is identical to syncSystemExistenceAndMigrate() in AppsViewModel.kt (lines 114-161). Duplicating 48 lines of complex synchronization logic violates DRY principles and creates a maintenance burden—any bug fix or improvement must be applied in two places.
📝 Suggested approach
Extract this function to a shared use case or repository helper class that both ViewModels can depend on. For example:
// In a new file: SyncInstalledAppsUseCase.kt
class SyncInstalledAppsUseCase(
private val packageMonitor: PackageMonitor,
private val installedAppsRepository: InstalledAppsRepository,
private val platform: Platform
) {
suspend operator fun invoke() {
// ... existing implementation
}
}Then inject this use case into both ViewModels and call it instead of maintaining duplicate implementations.
Based on learnings, code duplication across ViewModels should be extracted to shared components.
| appsInDb.forEach { app -> | ||
| if (!installedPackageNames.contains(app.packageName)) { | ||
| Logger.d { "App ${app.packageName} no longer installed, removing from DB" } | ||
| installedAppsRepository.deleteInstalledApp(app.packageName) | ||
| } else if (app.installedVersionName == null) { | ||
| // Migration logic for apps missing version data | ||
| if (platform.type == PlatformType.ANDROID) { | ||
| val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName) | ||
| if (systemInfo != null) { | ||
| installedAppsRepository.updateApp(app.copy( | ||
| installedVersionName = systemInfo.versionName, | ||
| installedVersionCode = systemInfo.versionCode, | ||
| latestVersionName = systemInfo.versionName, | ||
| latestVersionCode = systemInfo.versionCode | ||
| )) | ||
| Logger.d { "Migrated ${app.packageName}: set versionName/code from system" } | ||
| } else { | ||
| installedAppsRepository.updateApp(app.copy( | ||
| installedVersionName = app.installedVersion, | ||
| installedVersionCode = 0L, | ||
| latestVersionName = app.installedVersion, | ||
| latestVersionCode = 0L | ||
| )) | ||
| Logger.d { "Migrated ${app.packageName}: fallback to tag" } | ||
| } | ||
| } else { | ||
| installedAppsRepository.updateApp(app.copy( | ||
| installedVersionName = app.installedVersion, | ||
| installedVersionCode = 0L, | ||
| latestVersionName = app.installedVersion, | ||
| latestVersionCode = 0L | ||
| )) | ||
| Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag" } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Add transaction boundaries to ensure database consistency.
The function performs multiple database operations (delete, update) in a loop without transaction boundaries. If the process fails midway (e.g., exception on iteration 5 of 10), the database will be in an inconsistent state—some apps removed or migrated, others not.
Consider wrapping the entire forEach loop in a database transaction or handling partial failures gracefully.
💡 Example transaction approach
// Pseudocode - adjust based on your database layer's transaction API
installedAppsRepository.runInTransaction {
appsInDb.forEach { app ->
if (!installedPackageNames.contains(app.packageName)) {
installedAppsRepository.deleteInstalledApp(app.packageName)
} else if (app.installedVersionName == null) {
// ... migration logic
}
}
}
This introduces a robust data synchronization and migration mechanism at app startup and before checking for updates.
Key changes:
versionNameandversionCode, this change populates them using data from the package manager on Android or falls back to using the release tag on other platforms.checkAllForUpdatesfunction now leverages the more efficientinstalledAppsRepository.checkAllForUpdates()after performing the sync and migration, instead of iterating and checking each app individually within the ViewModel.Summary by CodeRabbit
Bug Fixes
Refactor
Other
✏️ Tip: You can customize this high-level summary in your review settings.