Compare commits
2 Commits
appstore-v
...
appstore-v
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b756788c2 | |||
| df58e3b0df |
@@ -13,12 +13,14 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import uk.silverlabs.silverdroid.config.RemoteConfigLoader
|
import uk.silverlabs.silverdroid.config.RemoteConfigLoader
|
||||||
import uk.silverlabs.silverdroid.tor.TorManager
|
import uk.silverlabs.silverdroid.tor.TorManager
|
||||||
import uk.silverlabs.silverdroid.ui.theme.SilverDROIDTheme
|
import uk.silverlabs.silverdroid.ui.theme.SilverDROIDTheme
|
||||||
import uk.silverlabs.silverdroid.ui.webview.AppStoreJsBridge
|
import uk.silverlabs.silverdroid.ui.webview.AppStoreJsBridge
|
||||||
import uk.silverlabs.silverdroid.ui.webview.WasmWebView
|
import uk.silverlabs.silverdroid.ui.webview.WasmWebView
|
||||||
|
import uk.silverlabs.silverdroid.update.InstallerService
|
||||||
import uk.silverlabs.silverdroid.update.UpdateCheckerWorker
|
import uk.silverlabs.silverdroid.update.UpdateCheckerWorker
|
||||||
import uk.silverlabs.silverdroid.vpn.WireGuardManager
|
import uk.silverlabs.silverdroid.vpn.WireGuardManager
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -53,6 +55,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
updateWorkRequest
|
updateWorkRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Reconcile installed-apps DB with PackageManager (cleans up apps removed outside the AppStore)
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
InstallerService(applicationContext).reconcileWithPackageManager()
|
||||||
|
}
|
||||||
|
|
||||||
// Load configuration asynchronously with remote support
|
// Load configuration asynchronously with remote support
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val config = RemoteConfigLoader.loadConfigWithRemote(this@MainActivity)
|
val config = RemoteConfigLoader.loadConfigWithRemote(this@MainActivity)
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
package uk.silverlabs.silverdroid.ui.webview
|
package uk.silverlabs.silverdroid.ui.webview
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.WebView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import uk.silverlabs.silverdroid.data.PwaDatabase
|
import uk.silverlabs.silverdroid.data.PwaDatabase
|
||||||
import uk.silverlabs.silverdroid.data.model.InstalledApp
|
import uk.silverlabs.silverdroid.data.model.InstalledApp
|
||||||
@@ -36,13 +42,50 @@ class AppStoreJsBridge(
|
|||||||
private val installer = InstallerService(context)
|
private val installer = InstallerService(context)
|
||||||
private val db get() = PwaDatabase.getInstance(context)
|
private val db get() = PwaDatabase.getInstance(context)
|
||||||
|
|
||||||
|
/** Set by WasmWebView after the WebView is created, used to invoke JS callbacks. */
|
||||||
|
var webView: WebView? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the app is allowed to install packages from unknown sources.
|
||||||
|
* Pre-Oreo devices are always allowed via manifest permission.
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
fun canInstallApps(): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.packageManager.canRequestPackageInstalls()
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by the Blazor WASM app to install an APK natively.
|
* Called by the Blazor WASM app to install an APK natively.
|
||||||
|
* On Android 8+ checks REQUEST_INSTALL_PACKAGES at runtime and redirects to Settings if missing.
|
||||||
* Runs download + verify + PackageInstaller in a coroutine; registers the app in DB on success.
|
* Runs download + verify + PackageInstaller in a coroutine; registers the app in DB on success.
|
||||||
*/
|
*/
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun installApp(slug: String, downloadUrl: String, sha256: String, appName: String, version: String) {
|
fun installApp(slug: String, downloadUrl: String, sha256: String, appName: String, version: String) {
|
||||||
Log.i(TAG, "installApp: slug=$slug version=$version")
|
Log.i(TAG, "installApp: slug=$slug version=$version")
|
||||||
|
|
||||||
|
// Gate on runtime install permission (Android 8+)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||||
|
!context.packageManager.canRequestPackageInstalls()
|
||||||
|
) {
|
||||||
|
Log.w(TAG, "installApp: REQUEST_INSTALL_PACKAGES not granted — opening Settings for $slug")
|
||||||
|
val intent = Intent(
|
||||||
|
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||||
|
Uri.parse("package:${context.packageName}")
|
||||||
|
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
webView?.post {
|
||||||
|
webView?.evaluateJavascript(
|
||||||
|
"window.onInstallError && window.onInstallError('permission_required', '${slug.replace("'", "\\'")}')",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
val success = installer.downloadAndInstall(
|
val success = installer.downloadAndInstall(
|
||||||
slug = slug,
|
slug = slug,
|
||||||
@@ -86,13 +129,29 @@ class AppStoreJsBridge(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the installed version string for a slug, or "" if not tracked.
|
* Returns the installed version string for a slug, or "" if not tracked.
|
||||||
|
* Cross-checks PackageManager: cleans up stale DB records for uninstalled apps,
|
||||||
|
* and corrects the stored version if it diverges from the actual installed version.
|
||||||
* Called synchronously from JS (runs on background thread — safe per Android docs).
|
* Called synchronously from JS (runs on background thread — safe per Android docs).
|
||||||
*/
|
*/
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun getInstalledVersion(slug: String): String {
|
fun getInstalledVersion(slug: String): String {
|
||||||
return try {
|
return try {
|
||||||
kotlinx.coroutines.runBlocking(Dispatchers.IO) {
|
runBlocking(Dispatchers.IO) {
|
||||||
db.installedAppDao().getBySlug(slug)?.installedVersion ?: ""
|
val record = db.installedAppDao().getBySlug(slug) ?: return@runBlocking ""
|
||||||
|
try {
|
||||||
|
val info = context.packageManager.getPackageInfo(record.packageName, 0)
|
||||||
|
val actualVersion = info.versionName ?: record.installedVersion
|
||||||
|
if (actualVersion != record.installedVersion) {
|
||||||
|
Log.i(TAG, "getInstalledVersion: correcting DB for $slug: ${record.installedVersion} → $actualVersion")
|
||||||
|
db.installedAppDao().markUpdateInstalled(slug, actualVersion)
|
||||||
|
}
|
||||||
|
actualVersion
|
||||||
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
|
// Package was uninstalled outside the AppStore — remove stale record
|
||||||
|
Log.i(TAG, "getInstalledVersion: package ${record.packageName} not found, removing DB record for $slug")
|
||||||
|
db.installedAppDao().deleteBySlug(slug)
|
||||||
|
""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "getInstalledVersion failed for $slug", e)
|
Log.w(TAG, "getInstalledVersion failed for $slug", e)
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import android.annotation.SuppressLint
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.webkit.*
|
import android.webkit.*
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -16,7 +14,6 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WasmWebView(
|
fun WasmWebView(
|
||||||
url: String,
|
url: String,
|
||||||
@@ -28,182 +25,146 @@ fun WasmWebView(
|
|||||||
var webView by remember { mutableStateOf<WebView?>(null) }
|
var webView by remember { mutableStateOf<WebView?>(null) }
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
var loadProgress by remember { mutableIntStateOf(0) }
|
var loadProgress by remember { mutableIntStateOf(0) }
|
||||||
var pageTitle by remember { mutableStateOf(appName) }
|
|
||||||
var canGoBack by remember { mutableStateOf(false) }
|
var canGoBack by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
BackHandler(enabled = canGoBack) {
|
BackHandler(enabled = canGoBack) {
|
||||||
webView?.goBack()
|
webView?.goBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
// Top bar with navigation
|
AndroidView(
|
||||||
Surface(
|
factory = { context ->
|
||||||
tonalElevation = 3.dp,
|
WebView(context).apply {
|
||||||
shadowElevation = 3.dp
|
webView = this
|
||||||
) {
|
|
||||||
TopAppBar(
|
// Enable JavaScript
|
||||||
title = {
|
settings.javaScriptEnabled = true
|
||||||
Column {
|
|
||||||
Text(
|
// Enable WASM support
|
||||||
text = pageTitle,
|
settings.allowFileAccess = true
|
||||||
style = MaterialTheme.typography.titleMedium
|
settings.allowContentAccess = true
|
||||||
)
|
|
||||||
if (isLoading) {
|
// Enable DOM storage (required for PWAs)
|
||||||
LinearProgressIndicator(
|
settings.domStorageEnabled = true
|
||||||
progress = { loadProgress / 100f },
|
|
||||||
modifier = Modifier
|
// Enable database storage
|
||||||
.fillMaxWidth()
|
settings.databaseEnabled = true
|
||||||
.height(2.dp),
|
|
||||||
)
|
// Enable caching for offline support
|
||||||
|
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
|
|
||||||
|
// Enable mixed content (for development)
|
||||||
|
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||||
|
|
||||||
|
// Performance optimizations
|
||||||
|
settings.setRenderPriority(WebSettings.RenderPriority.HIGH)
|
||||||
|
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
|
|
||||||
|
// Disable zoom — native app feel
|
||||||
|
settings.setSupportZoom(false)
|
||||||
|
settings.builtInZoomControls = false
|
||||||
|
settings.displayZoomControls = false
|
||||||
|
|
||||||
|
// User agent (modern)
|
||||||
|
settings.userAgentString = settings.userAgentString +
|
||||||
|
" SilverAppStore/1.0 (AppStore Client)"
|
||||||
|
|
||||||
|
// Wire AppStore JS bridge so Blazor can call native install
|
||||||
|
jsInterface?.let {
|
||||||
|
addJavascriptInterface(it, AppStoreJsBridge.BRIDGE_NAME)
|
||||||
|
it.webView = this
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable wide viewport
|
||||||
|
settings.useWideViewPort = true
|
||||||
|
settings.loadWithOverviewMode = true
|
||||||
|
|
||||||
|
// WebView client
|
||||||
|
webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageStarted(
|
||||||
|
view: WebView?,
|
||||||
|
url: String?,
|
||||||
|
favicon: Bitmap?
|
||||||
|
) {
|
||||||
|
isLoading = true
|
||||||
|
loadProgress = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
isLoading = false
|
||||||
|
loadProgress = 100
|
||||||
|
canGoBack = view?.canGoBack() ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldOverrideUrlLoading(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?
|
||||||
|
): Boolean {
|
||||||
|
// Allow all navigation within WebView
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceivedError(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?,
|
||||||
|
error: WebResourceError?
|
||||||
|
) {
|
||||||
|
isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
navigationIcon = {
|
// Chrome client for progress
|
||||||
IconButton(onClick = {
|
webChromeClient = object : WebChromeClient() {
|
||||||
if (canGoBack) {
|
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||||
webView?.goBack()
|
loadProgress = newProgress
|
||||||
} else {
|
if (newProgress == 100) {
|
||||||
onBackPressed()
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||||
|
// Log console messages for debugging
|
||||||
|
consoleMessage?.let {
|
||||||
|
android.util.Log.d(
|
||||||
|
"WebView",
|
||||||
|
"${it.message()} -- From line ${it.lineNumber()} of ${it.sourceId()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = { webView?.reload() }) {
|
|
||||||
Icon(Icons.Default.Refresh, "Refresh")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the URL
|
||||||
|
loadUrl(url)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
update = { view ->
|
||||||
|
canGoBack = view.canGoBack()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Thin progress bar along top edge (no browser chrome)
|
||||||
|
if (isLoading) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { loadProgress / 100f },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(3.dp)
|
||||||
|
.align(Alignment.TopStart)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebView
|
// Full-screen splash only on cold WASM load
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
if (isLoading && loadProgress < 20) {
|
||||||
AndroidView(
|
Box(
|
||||||
factory = { context ->
|
modifier = Modifier
|
||||||
WebView(context).apply {
|
.fillMaxSize()
|
||||||
webView = this
|
.background(MaterialTheme.colorScheme.background),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
// Enable JavaScript
|
) {
|
||||||
settings.javaScriptEnabled = true
|
CircularProgressIndicator()
|
||||||
|
|
||||||
// Enable WASM support
|
|
||||||
settings.allowFileAccess = true
|
|
||||||
settings.allowContentAccess = true
|
|
||||||
|
|
||||||
// Enable DOM storage (required for PWAs)
|
|
||||||
settings.domStorageEnabled = true
|
|
||||||
|
|
||||||
// Enable database storage
|
|
||||||
settings.databaseEnabled = true
|
|
||||||
|
|
||||||
// Enable caching for offline support
|
|
||||||
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
|
||||||
|
|
||||||
// Enable mixed content (for development)
|
|
||||||
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
|
||||||
|
|
||||||
// Performance optimizations
|
|
||||||
settings.setRenderPriority(WebSettings.RenderPriority.HIGH)
|
|
||||||
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
|
||||||
|
|
||||||
// Enable zooming
|
|
||||||
settings.setSupportZoom(true)
|
|
||||||
settings.builtInZoomControls = true
|
|
||||||
settings.displayZoomControls = false
|
|
||||||
|
|
||||||
// User agent (modern)
|
|
||||||
settings.userAgentString = settings.userAgentString +
|
|
||||||
" SilverAppStore/1.0 (AppStore Client)"
|
|
||||||
|
|
||||||
// Wire AppStore JS bridge so Blazor can call native install
|
|
||||||
jsInterface?.let {
|
|
||||||
addJavascriptInterface(it, AppStoreJsBridge.BRIDGE_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable wide viewport
|
|
||||||
settings.useWideViewPort = true
|
|
||||||
settings.loadWithOverviewMode = true
|
|
||||||
|
|
||||||
// WebView client
|
|
||||||
webViewClient = object : WebViewClient() {
|
|
||||||
override fun onPageStarted(
|
|
||||||
view: WebView?,
|
|
||||||
url: String?,
|
|
||||||
favicon: Bitmap?
|
|
||||||
) {
|
|
||||||
isLoading = true
|
|
||||||
loadProgress = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
|
||||||
isLoading = false
|
|
||||||
loadProgress = 100
|
|
||||||
canGoBack = view?.canGoBack() ?: false
|
|
||||||
pageTitle = view?.title ?: appName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldOverrideUrlLoading(
|
|
||||||
view: WebView?,
|
|
||||||
request: WebResourceRequest?
|
|
||||||
): Boolean {
|
|
||||||
// Allow all navigation within WebView
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceivedError(
|
|
||||||
view: WebView?,
|
|
||||||
request: WebResourceRequest?,
|
|
||||||
error: WebResourceError?
|
|
||||||
) {
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chrome client for progress
|
|
||||||
webChromeClient = object : WebChromeClient() {
|
|
||||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
|
||||||
loadProgress = newProgress
|
|
||||||
if (newProgress == 100) {
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceivedTitle(view: WebView?, title: String?) {
|
|
||||||
pageTitle = title ?: appName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
|
||||||
// Log console messages for debugging
|
|
||||||
consoleMessage?.let {
|
|
||||||
android.util.Log.d(
|
|
||||||
"WebView",
|
|
||||||
"${it.message()} -- From line ${it.lineNumber()} of ${it.sourceId()}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the URL
|
|
||||||
loadUrl(url)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update = { view ->
|
|
||||||
canGoBack = view.canGoBack()
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Loading indicator
|
|
||||||
if (isLoading && loadProgress < 20) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,4 +181,28 @@ class InstallerService(private val context: Context) {
|
|||||||
dao.markUpdateInstalled(app.slug, newVersion)
|
dao.markUpdateInstalled(app.slug, newVersion)
|
||||||
Log.i(TAG, "Updated DB record for ${app.slug} to $newVersion")
|
Log.i(TAG, "Updated DB record for ${app.slug} to $newVersion")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile Room DB against PackageManager on app start.
|
||||||
|
* - Removes stale records for apps that were uninstalled outside the AppStore.
|
||||||
|
* - Corrects version strings that diverge from what PackageManager reports.
|
||||||
|
*/
|
||||||
|
suspend fun reconcileWithPackageManager() = withContext(Dispatchers.IO) {
|
||||||
|
val dao = PwaDatabase.getInstance(context).installedAppDao()
|
||||||
|
val allRecords = dao.getAllAppsOnce()
|
||||||
|
val pm = context.packageManager
|
||||||
|
for (record in allRecords) {
|
||||||
|
try {
|
||||||
|
val info = pm.getPackageInfo(record.packageName, 0)
|
||||||
|
val actualVersion = info.versionName ?: continue
|
||||||
|
if (actualVersion != record.installedVersion) {
|
||||||
|
Log.i(TAG, "Reconcile: updating ${record.slug} ${record.installedVersion} → $actualVersion")
|
||||||
|
dao.markUpdateInstalled(record.slug, actualVersion)
|
||||||
|
}
|
||||||
|
} catch (_: android.content.pm.PackageManager.NameNotFoundException) {
|
||||||
|
Log.i(TAG, "Reconcile: removing stale record for ${record.slug} (package not installed)")
|
||||||
|
dao.deleteBySlug(record.slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,21 @@ class UpdateCheckerWorker(
|
|||||||
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val dao = PwaDatabase.getInstance(context).installedAppDao()
|
val dao = PwaDatabase.getInstance(context).installedAppDao()
|
||||||
|
|
||||||
|
// Ensure SilverDROID itself is always tracked so it can detect its own updates
|
||||||
|
val selfSlug = "silverdroid"
|
||||||
|
val selfVersion = try {
|
||||||
|
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "1.0.0"
|
||||||
|
} catch (e: Exception) { "1.0.0" }
|
||||||
|
if (dao.getBySlug(selfSlug) == null) {
|
||||||
|
dao.insert(InstalledApp(
|
||||||
|
slug = selfSlug,
|
||||||
|
packageName = context.packageName,
|
||||||
|
appName = "SilverDROID",
|
||||||
|
installedVersion = selfVersion
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
val installedApps = dao.getAllAppsOnce()
|
val installedApps = dao.getAllAppsOnce()
|
||||||
|
|
||||||
if (installedApps.isEmpty()) return@withContext Result.success()
|
if (installedApps.isEmpty()) return@withContext Result.success()
|
||||||
|
|||||||
Reference in New Issue
Block a user