Fix update check

This commit is contained in:
世界
2026-01-12 10:30:22 +08:00
parent 828be4aaf3
commit a2b3b846e0
7 changed files with 36 additions and 129 deletions

View File

@@ -27,14 +27,6 @@ class GitHubUpdateChecker : Closeable {
private val json = Json { ignoreUnknownKeys = true } private val json = Json { ignoreUnknownKeys = true }
fun checkUpdate(track: UpdateTrack): UpdateInfo? { fun checkUpdate(track: UpdateTrack): UpdateInfo? {
return getLatestUpdate(track, checkVersion = true)
}
fun forceGetLatestUpdate(track: UpdateTrack): UpdateInfo? {
return getLatestUpdate(track, checkVersion = false)
}
private fun getLatestUpdate(track: UpdateTrack, checkVersion: Boolean): UpdateInfo? {
val includePrerelease = track == UpdateTrack.BETA val includePrerelease = track == UpdateTrack.BETA
val release = getLatestRelease(includePrerelease) ?: return null val release = getLatestRelease(includePrerelease) ?: return null
@@ -44,7 +36,7 @@ class GitHubUpdateChecker : Closeable {
val metadata = downloadMetadata(release)!! val metadata = downloadMetadata(release)!!
if (checkVersion && metadata.versionCode <= BuildConfig.VERSION_CODE) { if (metadata.versionCode <= BuildConfig.VERSION_CODE) {
return null return null
} }

View File

@@ -2,7 +2,9 @@ package io.nekohasekai.sfa.vendor
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.ipc.RootService
import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.Application
@@ -77,12 +79,14 @@ object RootInstaller {
} }
} }
private data class RootServiceHandle( private class RootServiceHandle(
val connection: ServiceConnection, val connection: ServiceConnection,
val service: IRootService val service: IRootService
) : java.io.Closeable { ) : java.io.Closeable {
override fun close() { override fun close() {
RootService.unbind(connection) Handler(Looper.getMainLooper()).post {
RootService.unbind(connection)
}
} }
} }
} }

View File

@@ -3,8 +3,8 @@ package io.nekohasekai.sfa.compose.screen.settings
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log
import android.provider.Settings as AndroidSettings import android.provider.Settings as AndroidSettings
import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -249,6 +249,7 @@ fun AppSettingsScreen(navController: NavController) {
} }
showDownloadDialog = false showDownloadDialog = false
} catch (e: Exception) { } catch (e: Exception) {
Log.e("AppSettingsScreen", "Error downloading update", e)
downloadError = e.message downloadError = e.message
} }
} }
@@ -497,7 +498,7 @@ fun AppSettingsScreen(navController: NavController) {
), ),
) )
if (silentInstallEnabled && !xposedActivated) { if (silentInstallEnabled) {
ListItem( ListItem(
headlineContent = { headlineContent = {
Text( Text(
@@ -507,7 +508,9 @@ fun AppSettingsScreen(navController: NavController) {
}, },
supportingContent = { supportingContent = {
Text( Text(
when (silentInstallMethod) { if (xposedActivated) {
stringResource(R.string.install_method_root)
} else when (silentInstallMethod) {
"PACKAGE_INSTALLER" -> stringResource(R.string.install_method_package_installer) "PACKAGE_INSTALLER" -> stringResource(R.string.install_method_package_installer)
"SHIZUKU" -> stringResource(R.string.install_method_shizuku) "SHIZUKU" -> stringResource(R.string.install_method_shizuku)
"ROOT" -> stringResource(R.string.install_method_root) "ROOT" -> stringResource(R.string.install_method_root)
@@ -525,7 +528,7 @@ fun AppSettingsScreen(navController: NavController) {
}, },
modifier = modifier =
updateItemModifier() updateItemModifier()
.clickable { showInstallMethodMenu = true }, .let { if (!xposedActivated) it.clickable { showInstallMethodMenu = true } else it },
colors = colors =
ListItemDefaults.colors( ListItemDefaults.colors(
containerColor = Color.Transparent, containerColor = Color.Transparent,
@@ -731,76 +734,6 @@ fun AppSettingsScreen(navController: NavController) {
), ),
) )
if (BuildConfig.DEBUG && Vendor.supportsTrackSelection()) {
var isForceDownloading by remember { mutableStateOf(false) }
ListItem(
headlineContent = {
Text(
stringResource(R.string.force_download_install),
style = MaterialTheme.typography.bodyLarge,
)
},
leadingContent = {
Icon(
imageVector = Icons.Outlined.SystemUpdateAlt,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
},
trailingContent = {
if (isForceDownloading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp,
)
}
},
modifier =
Modifier
.clip(
if (hasUpdate) {
RoundedCornerShape(0.dp)
} else {
RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)
},
)
.clickable(enabled = !isForceDownloading) {
isForceDownloading = true
scope.launch {
try {
val latestUpdate = withContext(Dispatchers.IO) {
Vendor.forceGetLatestUpdate()
}
if (latestUpdate != null) {
showDownloadDialog = true
downloadError = null
downloadJob = scope.launch {
try {
withContext(Dispatchers.IO) {
Vendor.downloadAndInstall(context, latestUpdate.downloadUrl)
}
showDownloadDialog = false
} catch (e: Exception) {
downloadError = e.message
}
}
} else {
showErrorDialog = R.string.no_updates_available
}
} catch (_: UpdateCheckException.TrackNotSupported) {
showErrorDialog = R.string.update_track_not_supported
} catch (_: Exception) {
}
isForceDownloading = false
}
},
colors =
ListItemDefaults.colors(
containerColor = Color.Transparent,
),
)
}
if (hasUpdate && updateInfo != null) { if (hasUpdate && updateInfo != null) {
ListItem( ListItem(
headlineContent = { headlineContent = {

View File

@@ -19,15 +19,11 @@ import java.io.IOException
object PrivilegedServiceUtils { object PrivilegedServiceUtils {
const val SYSTEM_SERVICE_NAME = "sfa_privileged"
private fun getPackageManager(): Any { private fun getPackageManager(): Any {
val binder = SystemServiceHelperCompat.getSystemService("package") val binder = SystemServiceHelperCompat.getSystemService("package") ?: throw IllegalStateException("package service not available")
?: throw IllegalStateException("package service not available")
val stubClass = Class.forName("android.content.pm.IPackageManager\$Stub") val stubClass = Class.forName("android.content.pm.IPackageManager\$Stub")
val asInterface = stubClass.getMethod("asInterface", IBinder::class.java) val asInterface = stubClass.getMethod("asInterface", IBinder::class.java)
return asInterface.invoke(null, binder) return asInterface.invoke(null, binder) ?: throw IllegalStateException("IPackageManager is null")
?: throw IllegalStateException("IPackageManager is null")
} }
fun getInstalledPackages(flags: Int, userId: Int): List<PackageInfo> { fun getInstalledPackages(flags: Int, userId: Int): List<PackageInfo> {
@@ -37,14 +33,14 @@ object PrivilegedServiceUtils {
val method = iPackageManagerClass.getMethod( val method = iPackageManagerClass.getMethod(
"getInstalledPackages", "getInstalledPackages",
Long::class.javaPrimitiveType, Long::class.javaPrimitiveType,
Int::class.javaPrimitiveType Int::class.javaPrimitiveType,
) )
method.invoke(iPackageManager, flags.toLong(), userId) method.invoke(iPackageManager, flags.toLong(), userId)
} else { } else {
val method = iPackageManagerClass.getMethod( val method = iPackageManagerClass.getMethod(
"getInstalledPackages", "getInstalledPackages",
Int::class.javaPrimitiveType, Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType Int::class.javaPrimitiveType,
) )
method.invoke(iPackageManager, flags, userId) method.invoke(iPackageManager, flags, userId)
} }
@@ -61,14 +57,19 @@ object PrivilegedServiceUtils {
iPackageInstaller, iPackageInstaller,
installerPackageName, installerPackageName,
null, null,
targetUserId targetUserId,
) )
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
params.setAppPackageName(BuildConfig.APPLICATION_ID)
// Set INSTALL_REPLACE_EXISTING flag (value = 2)
val installFlagsField = PackageInstaller.SessionParams::class.java.getDeclaredField("installFlags")
installFlagsField.isAccessible = true
installFlagsField.setInt(params, installFlagsField.getInt(params) or 2)
val sessionId = packageInstaller.createSession(params) val sessionId = packageInstaller.createSession(params)
val iSession = IPackageInstallerSession.Stub.asInterface( val iSession = IPackageInstallerSession.Stub.asInterface(
iPackageInstaller.openSession(sessionId).asBinder() iPackageInstaller.openSession(sessionId).asBinder(),
) )
val session = createSession(iSession) val session = createSession(iSession)
@@ -91,8 +92,7 @@ object PrivilegedServiceUtils {
session.commit(intentSender) session.commit(intentSender)
latch.await(60, TimeUnit.SECONDS) latch.await(60, TimeUnit.SECONDS)
val intent = resultIntent[0] val intent = resultIntent[0] ?: throw IOException("Installation timed out")
?: throw IOException("Installation timed out")
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
if (status != PackageInstaller.STATUS_SUCCESS) { if (status != PackageInstaller.STATUS_SUCCESS) {
@@ -116,32 +116,26 @@ object PrivilegedServiceUtils {
installer: IPackageInstaller, installer: IPackageInstaller,
installerPackageName: String, installerPackageName: String,
installerAttributionTag: String?, installerAttributionTag: String?,
userId: Int userId: Int,
): PackageInstaller { ): PackageInstaller {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PackageInstaller::class.java PackageInstaller::class.java.getConstructor(
.getConstructor(
IPackageInstaller::class.java, IPackageInstaller::class.java,
String::class.java, String::class.java,
String::class.java, String::class.java,
Int::class.javaPrimitiveType Int::class.javaPrimitiveType,
) ).newInstance(installer, installerPackageName, installerAttributionTag, userId)
.newInstance(installer, installerPackageName, installerAttributionTag, userId)
} else { } else {
PackageInstaller::class.java PackageInstaller::class.java.getConstructor(
.getConstructor(
IPackageInstaller::class.java, IPackageInstaller::class.java,
String::class.java, String::class.java,
Int::class.javaPrimitiveType Int::class.javaPrimitiveType,
) ).newInstance(installer, installerPackageName, userId)
.newInstance(installer, installerPackageName, userId)
} }
} }
private fun createSession(session: IPackageInstallerSession): PackageInstaller.Session { private fun createSession(session: IPackageInstallerSession): PackageInstaller.Session {
return PackageInstaller.Session::class.java return PackageInstaller.Session::class.java.getConstructor(IPackageInstallerSession::class.java).newInstance(session)
.getConstructor(IPackageInstallerSession::class.java)
.newInstance(session)
} }
private fun createIntentSender(onResult: (Intent) -> Unit): IntentSender { private fun createIntentSender(onResult: (Intent) -> Unit): IntentSender {
@@ -153,14 +147,12 @@ object PrivilegedServiceUtils {
whitelistToken: android.os.IBinder?, whitelistToken: android.os.IBinder?,
finishedReceiver: android.content.IIntentReceiver?, finishedReceiver: android.content.IIntentReceiver?,
requiredPermission: String?, requiredPermission: String?,
options: Bundle? options: Bundle?,
) { ) {
onResult(intent) onResult(intent)
} }
} }
return IntentSender::class.java return IntentSender::class.java.getConstructor(IIntentSender::class.java).newInstance(sender)
.getConstructor(IIntentSender::class.java)
.newInstance(sender)
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")

View File

@@ -35,12 +35,6 @@ interface VendorInterface {
*/ */
fun checkUpdateAsync(): UpdateInfo? = null fun checkUpdateAsync(): UpdateInfo? = null
/**
* Force get latest update (ignores version check)
* @return UpdateInfo of the latest release, null if unavailable
*/
fun forceGetLatestUpdate(): UpdateInfo? = null
/** /**
* Check if silent install feature is available * Check if silent install feature is available
* @return true if silent install is supported (Other flavor only) * @return true if silent install is supported (Other flavor only)

View File

@@ -259,7 +259,6 @@
<!-- Updates --> <!-- Updates -->
<string name="check_update">Check Update</string> <string name="check_update">Check Update</string>
<string name="force_download_install">Force Download and Install</string>
<string name="check_update_automatic">Automatic Update Check</string> <string name="check_update_automatic">Automatic Update Check</string>
<string name="check_update_prompt_play">Would you like to enable automatic update checking from **Play Store**?</string> <string name="check_update_prompt_play">Would you like to enable automatic update checking from **Play Store**?</string>
<string name="check_update_prompt_github">Would you like to enable automatic update checking from **GitHub**?</string> <string name="check_update_prompt_github">Would you like to enable automatic update checking from **GitHub**?</string>

View File

@@ -108,13 +108,6 @@ object Vendor : VendorInterface {
} }
} }
override fun forceGetLatestUpdate(): UpdateInfo? {
val track = UpdateTrack.fromString(Settings.updateTrack)
return GitHubUpdateChecker().use { checker ->
checker.forceGetLatestUpdate(track)
}
}
override fun supportsSilentInstall(): Boolean { override fun supportsSilentInstall(): Boolean {
return true return true
} }