Split support for Android API 21 and 23

This commit is contained in:
世界
2025-12-24 16:25:11 +08:00
parent cf771e1071
commit 456d35d969
22 changed files with 424 additions and 102 deletions

View File

@@ -66,8 +66,25 @@ android {
flavorDimensions "vendor"
productFlavors {
play {
minSdk 23
}
other {
minSdk 23
}
otherLegacy {
minSdk 21
}
}
sourceSets {
play {
java.srcDirs += ['src/minApi23/java']
}
other {
java.srcDirs += ['src/minApi23/java', 'src/github/java']
}
otherLegacy {
java.srcDirs += ['src/minApi21/java', 'src/github/java']
}
}
@@ -95,72 +112,145 @@ android {
variant.outputs.configureEach {
outputFileName = (outputFileName as String).replace("-release", "")
outputFileName = (outputFileName as String).replace("-play", "-play")
outputFileName = (outputFileName as String).replace("-otherLegacy", "-legacy-android-5")
outputFileName = (outputFileName as String).replace("-other", "")
}
}
}
dependencies {
implementation(fileTree("libs"))
// libbox
playImplementation(files("libs/libbox.aar"))
otherImplementation(files("libs/libbox.aar"))
otherLegacyImplementation(files("libs/libbox-legacy.aar"))
implementation "androidx.core:core-ktx:1.16.0"
implementation 'androidx.compose.ui:ui'
// API level specific versions
def lifecycleVersion23 = "2.10.0"
def roomVersion23 = "2.8.4"
def workVersion23 = "2.11.0"
def cameraVersion23 = "1.5.2"
def browserVersion23 = "1.9.0"
def lifecycleVersion21 = "2.9.4"
def roomVersion21 = "2.7.2"
def workVersion21 = "2.10.5"
def cameraVersion21 = "1.4.2"
def browserVersion21 = "1.9.0"
// Common dependencies (no API level difference)
implementation "androidx.core:core-ktx:1.17.0"
implementation "androidx.appcompat:appcompat:1.7.1"
implementation "com.google.android.material:material:1.12.0"
implementation "com.google.android.material:material:1.13.0"
implementation "androidx.constraintlayout:constraintlayout:2.2.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.9.2"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2"
implementation "androidx.navigation:navigation-fragment-ktx:2.9.3"
implementation "androidx.navigation:navigation-ui-ktx:2.9.3"
implementation "com.google.zxing:core:3.5.3"
implementation "androidx.room:room-runtime:2.7.2"
implementation "androidx.navigation:navigation-fragment-ktx:2.9.6"
implementation "androidx.navigation:navigation-ui-ktx:2.9.6"
implementation "com.google.zxing:core:3.5.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.3.0"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.camera:camera-view:1.4.2"
implementation "androidx.camera:camera-lifecycle:1.4.2"
implementation "androidx.camera:camera-camera2:1.4.2"
ksp "androidx.room:room-compiler:2.7.2"
implementation "androidx.work:work-runtime-ktx:2.10.3"
implementation "androidx.browser:browser:1.9.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
// DO NOT UPDATE (minSdkVersion updated)
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
implementation "com.blacksquircle.ui:editorkit:2.2.0"
implementation "com.blacksquircle.ui:language-json:2.2.0"
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
exclude group: "com.google.guava", module: "guava"
}
implementation "com.google.guava:guava:33.4.8-android"
implementation "com.google.guava:guava:33.5.0-android"
// API 23+ dependencies (play/other)
playImplementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion23"
playImplementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion23"
playImplementation "androidx.room:room-runtime:$roomVersion23"
playImplementation "androidx.work:work-runtime-ktx:$workVersion23"
playImplementation "androidx.camera:camera-view:$cameraVersion23"
playImplementation "androidx.camera:camera-lifecycle:$cameraVersion23"
playImplementation "androidx.camera:camera-camera2:$cameraVersion23"
playImplementation "androidx.browser:browser:$browserVersion23"
playAnnotationProcessor "androidx.room:room-compiler:$roomVersion23"
kspPlay "androidx.room:room-compiler:$roomVersion23"
otherImplementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion23"
otherImplementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion23"
otherImplementation "androidx.room:room-runtime:$roomVersion23"
otherImplementation "androidx.work:work-runtime-ktx:$workVersion23"
otherImplementation "androidx.camera:camera-view:$cameraVersion23"
otherImplementation "androidx.camera:camera-lifecycle:$cameraVersion23"
otherImplementation "androidx.camera:camera-camera2:$cameraVersion23"
otherImplementation "androidx.browser:browser:$browserVersion23"
kspOther "androidx.room:room-compiler:$roomVersion23"
// API 21 dependencies (otherLegacy)
otherLegacyImplementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion21"
otherLegacyImplementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion21"
otherLegacyImplementation "androidx.room:room-runtime:$roomVersion21"
otherLegacyImplementation "androidx.work:work-runtime-ktx:$workVersion21"
otherLegacyImplementation "androidx.camera:camera-view:$cameraVersion21"
otherLegacyImplementation "androidx.camera:camera-lifecycle:$cameraVersion21"
otherLegacyImplementation "androidx.camera:camera-camera2:$cameraVersion21"
otherLegacyImplementation "androidx.browser:browser:$browserVersion21"
kspOtherLegacy "androidx.room:room-compiler:$roomVersion21"
// Play Store specific
playImplementation "com.google.android.play:app-update-ktx:2.1.0"
playImplementation "com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1"
// Shizuku for silent install (other flavor only)
otherImplementation 'dev.rikka.shizuku:api:13.1.5'
otherImplementation 'dev.rikka.shizuku:provider:13.1.5'
// Shizuku (play and other flavors, API 23+ only)
def shizukuVersion = '12.2.0'
playImplementation "dev.rikka.shizuku:api:$shizukuVersion"
playImplementation "dev.rikka.shizuku:provider:$shizukuVersion"
playImplementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
otherImplementation "dev.rikka.shizuku:api:$shizukuVersion"
otherImplementation "dev.rikka.shizuku:provider:$shizukuVersion"
otherImplementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
// Compose dependencies
def composeBom = platform('androidx.compose:compose-bom:2025.01.01')
implementation composeBom
androidTestImplementation composeBom
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-tooling-preview'
// Compose dependencies - API 23+ (play/other)
def composeBom23 = platform('androidx.compose:compose-bom:2025.12.01')
def activityVersion23 = "1.12.2"
def lifecycleComposeVersion23 = "2.10.0"
playImplementation composeBom23
playImplementation 'androidx.compose.material3:material3'
playImplementation 'androidx.compose.ui:ui'
playImplementation 'androidx.compose.ui:ui-tooling-preview'
playImplementation 'androidx.compose.material:material-icons-extended'
playImplementation "androidx.activity:activity-compose:$activityVersion23"
playImplementation "androidx.navigation:navigation-compose:2.9.6"
playImplementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleComposeVersion23"
playImplementation 'androidx.compose.runtime:runtime-livedata'
otherImplementation composeBom23
otherImplementation 'androidx.compose.material3:material3'
otherImplementation 'androidx.compose.ui:ui'
otherImplementation 'androidx.compose.ui:ui-tooling-preview'
otherImplementation 'androidx.compose.material:material-icons-extended'
otherImplementation "androidx.activity:activity-compose:$activityVersion23"
otherImplementation "androidx.navigation:navigation-compose:2.9.6"
otherImplementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleComposeVersion23"
otherImplementation 'androidx.compose.runtime:runtime-livedata'
// Compose dependencies - API 21 (otherLegacy)
def composeBom21 = platform('androidx.compose:compose-bom:2025.01.00')
def activityVersion21 = "1.11.0"
def lifecycleComposeVersion21 = "2.9.4"
otherLegacyImplementation composeBom21
otherLegacyImplementation 'androidx.compose.material3:material3'
otherLegacyImplementation 'androidx.compose.ui:ui'
otherLegacyImplementation 'androidx.compose.ui:ui-tooling-preview'
otherLegacyImplementation 'androidx.compose.material:material-icons-extended'
otherLegacyImplementation "androidx.activity:activity-compose:$activityVersion21"
otherLegacyImplementation "androidx.navigation:navigation-compose:2.9.6"
otherLegacyImplementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleComposeVersion21"
otherLegacyImplementation 'androidx.compose.runtime:runtime-livedata'
// Debug/Test dependencies
debugImplementation 'androidx.compose.ui:ui-tooling'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
implementation 'androidx.compose.material:material-icons-extended'
implementation 'androidx.activity:activity-compose:1.10.1'
implementation 'me.zhanghai.compose.preference:library:1.1.1'
implementation "androidx.navigation:navigation-compose:2.9.3"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "sh.calvin.reorderable:reorderable:2.3.3"
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
// Common Compose-related libraries
implementation "sh.calvin.reorderable:reorderable:3.0.0"
implementation "com.github.jeziellago:compose-markdown:0.5.4"
implementation "org.kodein.emoji:emoji-kt:2.3.0"
}
def playCredentialsJSON = rootProject.file("service-account-credentials.json")

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<application>
<receiver
android:name=".vendor.InstallResultReceiver"
android:exported="false">
<intent-filter>
<action android:name="io.nekohasekai.sfa.INSTALL_COMPLETE" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -1,5 +1,6 @@
package io.nekohasekai.sfa.vendor
import android.os.Build
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.BuildConfig
import io.nekohasekai.sfa.ktx.unwrap
@@ -39,8 +40,11 @@ class GitHubUpdateChecker : Closeable {
return null
}
val isLegacy = Build.VERSION.SDK_INT < Build.VERSION_CODES.M
val apkAsset = release.assets.find { asset ->
asset.name.endsWith(".apk") && !asset.name.contains("play")
asset.name.endsWith(".apk") &&
!asset.name.contains("play") &&
asset.name.contains("legacy-android-5") == isLegacy
}
return UpdateInfo(

View File

@@ -0,0 +1,52 @@
package io.nekohasekai.sfa.vendor
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import java.io.File
import java.io.FileInputStream
import android.content.pm.PackageInstaller as AndroidPackageInstaller
object SystemPackageInstaller {
fun canSystemSilentInstall(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
}
fun install(context: Context, apkFile: File): Result<Unit> {
return try {
val packageInstaller = context.packageManager.packageInstaller
val params = AndroidPackageInstaller.SessionParams(AndroidPackageInstaller.SessionParams.MODE_FULL_INSTALL)
params.setAppPackageName(context.packageName)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
params.setRequireUserAction(AndroidPackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
val sessionId = packageInstaller.createSession(params)
packageInstaller.openSession(sessionId).use { session ->
session.openWrite("update.apk", 0, apkFile.length()).use { outputStream ->
FileInputStream(apkFile).use { inputStream ->
inputStream.copyTo(outputStream)
}
session.fsync(outputStream)
}
val intent = Intent(context, InstallResultReceiver::class.java).apply {
action = InstallResultReceiver.ACTION_INSTALL_COMPLETE
}
val pendingIntent = PendingIntent.getBroadcast(
context,
sessionId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
session.commit(pendingIntent.intentSender)
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -5,7 +5,6 @@ import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
@@ -28,6 +27,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.RestartAlt
import io.nekohasekai.sfa.compat.animateItemCompat
import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material.icons.outlined.Cable
import androidx.compose.material.icons.outlined.Download
@@ -67,7 +67,7 @@ import androidx.compose.ui.zIndex
import io.nekohasekai.sfa.BuildConfig
import io.nekohasekai.sfa.R
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DashboardSettingsBottomSheet(
sheetState: SheetState,
@@ -282,8 +282,8 @@ fun DashboardSettingsBottomSheet(
dragOffset = 0f
},
modifier =
Modifier.animateItemPlacement(
animationSpec =
animateItemCompat(
placementSpec =
spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow,

View File

@@ -6,7 +6,7 @@ import android.graphics.Bitmap
import android.graphics.Color
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity
import com.google.android.material.R
import androidx.appcompat.R as AppCompatR
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
import io.nekohasekai.libbox.Libbox
@@ -46,7 +46,7 @@ suspend fun Context.shareProfile(profile: Profile) {
Intent(Intent.ACTION_SEND).setType("application/octet-stream")
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_STREAM, uri),
getString(R.string.abc_shareactionprovider_share_with),
getString(AppCompatR.string.abc_shareactionprovider_share_with),
),
)
}
@@ -59,7 +59,7 @@ fun FragmentActivity.shareProfileURL(profile: Profile) {
profile.typed.remoteURL,
)
val imageSize = dp2px(256)
val color = getAttrColor(com.google.android.material.R.attr.colorPrimary)
val color = getAttrColor(androidx.appcompat.R.attr.colorPrimary)
val image = QRCodeWriter().encode(link, BarcodeFormat.QR_CODE, imageSize, imageSize, null)
val imageWidth = image.width
val imageHeight = image.height

View File

@@ -0,0 +1,11 @@
package io.nekohasekai.sfa.compat
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
@Suppress("UNUSED_PARAMETER")
fun LazyItemScope.animateItemCompat(
placementSpec: FiniteAnimationSpec<IntOffset>,
): Modifier = Modifier

View File

@@ -2,9 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
<uses-sdk tools:overrideLibrary="rikka.shizuku.provider,rikka.shizuku.api,rikka.shizuku.aidl,rikka.shizuku.shared" />
@@ -16,14 +13,6 @@
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<receiver
android:name=".vendor.InstallResultReceiver"
android:exported="false">
<intent-filter>
<action android:name="io.nekohasekai.sfa.INSTALL_COMPLETE" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -0,0 +1,10 @@
package io.nekohasekai.sfa.compat
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
fun LazyItemScope.animateItemCompat(
placementSpec: FiniteAnimationSpec<IntOffset>,
): Modifier = Modifier.animateItem(placementSpec = placementSpec)

View File

@@ -1,13 +1,8 @@
package io.nekohasekai.sfa.vendor
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import io.nekohasekai.sfa.database.Settings
import java.io.File
import java.io.FileInputStream
enum class InstallMethod {
PACKAGE_INSTALLER,
@@ -29,12 +24,12 @@ object ApkInstaller {
return when (method) {
InstallMethod.SHIZUKU -> ShizukuInstaller.install(apkFile)
InstallMethod.ROOT -> RootInstaller.install(apkFile)
InstallMethod.PACKAGE_INSTALLER -> installWithPackageInstaller(context, apkFile)
InstallMethod.PACKAGE_INSTALLER -> SystemPackageInstaller.install(context, apkFile)
}
}
fun canSystemSilentInstall(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
return SystemPackageInstaller.canSystemSilentInstall()
}
suspend fun canSilentInstall(): Boolean {
@@ -45,40 +40,4 @@ object ApkInstaller {
InstallMethod.ROOT -> RootInstaller.checkAccess()
}
}
private fun installWithPackageInstaller(context: Context, apkFile: File): Result<Unit> {
return try {
val packageInstaller = context.packageManager.packageInstaller
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
params.setAppPackageName(context.packageName)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
val sessionId = packageInstaller.createSession(params)
packageInstaller.openSession(sessionId).use { session ->
session.openWrite("update.apk", 0, apkFile.length()).use { outputStream ->
FileInputStream(apkFile).use { inputStream ->
inputStream.copyTo(outputStream)
}
session.fsync(outputStream)
}
val intent = Intent(context, InstallResultReceiver::class.java).apply {
action = InstallResultReceiver.ACTION_INSTALL_COMPLETE
}
val pendingIntent = PendingIntent.getBroadcast(
context,
sessionId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
session.commit(pendingIntent.intentSender)
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -0,0 +1,41 @@
package io.nekohasekai.sfa.vendor
import android.content.Context
import io.nekohasekai.sfa.database.Settings
import java.io.File
enum class InstallMethod {
PACKAGE_INSTALLER,
ROOT,
}
object ApkInstaller {
fun getConfiguredMethod(): InstallMethod {
return if (Settings.silentInstallEnabled) {
val method = Settings.silentInstallMethod
if (method == "SHIZUKU") InstallMethod.ROOT else InstallMethod.valueOf(method)
} else {
InstallMethod.PACKAGE_INSTALLER
}
}
suspend fun install(context: Context, apkFile: File, method: InstallMethod = getConfiguredMethod()): Result<Unit> {
return when (method) {
InstallMethod.ROOT -> RootInstaller.install(apkFile)
InstallMethod.PACKAGE_INSTALLER -> SystemPackageInstaller.install(context, apkFile)
}
}
fun canSystemSilentInstall(): Boolean {
return SystemPackageInstaller.canSystemSilentInstall()
}
suspend fun canSilentInstall(): Boolean {
val method = getConfiguredMethod()
return when (method) {
InstallMethod.PACKAGE_INSTALLER -> canSystemSilentInstall()
InstallMethod.ROOT -> RootInstaller.checkAccess()
}
}
}

View File

@@ -0,0 +1,149 @@
package io.nekohasekai.sfa.vendor
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.camera.core.ImageAnalysis
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.update.UpdateCheckException
import io.nekohasekai.sfa.update.UpdateInfo
import io.nekohasekai.sfa.update.UpdateState
import io.nekohasekai.sfa.update.UpdateTrack
object Vendor : VendorInterface {
private const val TAG = "Vendor"
override fun checkUpdate(
activity: Activity,
byUser: Boolean,
) {
try {
val updateInfo = checkUpdateAsync()
if (updateInfo != null) {
activity.runOnUiThread {
showUpdateDialog(activity, updateInfo)
}
} else if (byUser) {
activity.runOnUiThread {
showNoUpdatesDialog(activity)
}
}
} catch (e: UpdateCheckException.TrackNotSupported) {
Log.d(TAG, "checkUpdate: track not supported")
if (byUser) {
activity.runOnUiThread {
showTrackNotSupportedDialog(activity)
}
}
} catch (e: Exception) {
Log.e(TAG, "checkUpdate: ", e)
if (byUser) {
activity.runOnUiThread {
showNoUpdatesDialog(activity)
}
}
}
}
private fun showUpdateDialog(activity: Activity, updateInfo: UpdateInfo) {
val message = buildString {
append(activity.getString(R.string.new_version_available, updateInfo.versionName))
if (!updateInfo.releaseNotes.isNullOrBlank()) {
append("\n\n")
append(updateInfo.releaseNotes.take(500))
if (updateInfo.releaseNotes.length > 500) {
append("...")
}
}
}
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.check_update)
.setMessage(message)
.setPositiveButton(R.string.update) { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(updateInfo.releaseUrl))
activity.startActivity(intent)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun showNoUpdatesDialog(activity: Activity) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.check_update)
.setMessage(R.string.no_updates_available)
.setPositiveButton(R.string.ok, null)
.show()
}
private fun showTrackNotSupportedDialog(activity: Activity) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.check_update)
.setMessage(R.string.update_track_not_supported)
.setPositiveButton(R.string.ok, null)
.show()
}
override fun createQRCodeAnalyzer(
onSuccess: (String) -> Unit,
onFailure: (Exception) -> Unit,
): ImageAnalysis.Analyzer? {
return null
}
override fun isPerAppProxyAvailable(): Boolean {
return true
}
override fun supportsTrackSelection(): Boolean {
return true
}
override fun checkUpdateAsync(): UpdateInfo? {
val track = UpdateTrack.fromString(Settings.updateTrack)
return GitHubUpdateChecker().use { checker ->
checker.checkUpdate(track)
}
}
override fun supportsSilentInstall(): Boolean {
return true
}
override fun supportsAutoUpdate(): Boolean {
return true
}
override fun scheduleAutoUpdate() {
UpdateWorker.schedule(io.nekohasekai.sfa.Application.application)
}
override suspend fun verifySilentInstallMethod(method: String): Boolean {
return when (method) {
"PACKAGE_INSTALLER" -> {
ApkInstaller.canSystemSilentInstall() &&
Application.application.packageManager.canRequestPackageInstalls()
}
"ROOT" -> RootInstaller.checkAccess()
else -> false
}
}
override suspend fun downloadAndInstall(context: android.content.Context, downloadUrl: String): Result<Unit> {
return try {
val cachedApk = UpdateState.cachedApkFile.value
val apkFile = if (cachedApk != null && cachedApk.exists() && cachedApk.length() > 0) {
cachedApk
} else {
ApkDownloader().use { it.download(downloadUrl) }
}
ApkInstaller.install(context, apkFile)
} catch (e: Exception) {
Result.failure(e)
}
}
}