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" flavorDimensions "vendor"
productFlavors { productFlavors {
play { play {
minSdk 23
} }
other { 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 { variant.outputs.configureEach {
outputFileName = (outputFileName as String).replace("-release", "") outputFileName = (outputFileName as String).replace("-release", "")
outputFileName = (outputFileName as String).replace("-play", "-play") outputFileName = (outputFileName as String).replace("-play", "-play")
outputFileName = (outputFileName as String).replace("-otherLegacy", "-legacy-android-5")
outputFileName = (outputFileName as String).replace("-other", "") outputFileName = (outputFileName as String).replace("-other", "")
} }
} }
} }
dependencies { 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" // API level specific versions
implementation 'androidx.compose.ui:ui' 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 "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.constraintlayout:constraintlayout:2.2.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.9.2" implementation "androidx.navigation:navigation-fragment-ktx:2.9.6"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2" implementation "androidx.navigation:navigation-ui-ktx:2.9.6"
implementation "androidx.navigation:navigation-fragment-ktx:2.9.3" implementation "com.google.zxing:core:3.5.4"
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.coordinatorlayout:coordinatorlayout:1.3.0" implementation "androidx.coordinatorlayout:coordinatorlayout:1.3.0"
implementation "androidx.preference:preference-ktx:1.2.1" 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-coroutines-android:1.10.2"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
// DO NOT UPDATE (minSdkVersion updated)
implementation "com.blacksquircle.ui:editorkit:2.2.0" implementation "com.blacksquircle.ui:editorkit:2.2.0"
implementation "com.blacksquircle.ui:language-json:2.2.0" implementation "com.blacksquircle.ui:language-json:2.2.0"
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") { implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
exclude group: "com.google.guava", module: "guava" 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.play:app-update-ktx:2.1.0"
playImplementation "com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1" playImplementation "com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1"
// Shizuku for silent install (other flavor only) // Shizuku (play and other flavors, API 23+ only)
otherImplementation 'dev.rikka.shizuku:api:13.1.5' def shizukuVersion = '12.2.0'
otherImplementation 'dev.rikka.shizuku:provider:13.1.5' 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' otherImplementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
// Compose dependencies // Compose dependencies - API 23+ (play/other)
def composeBom = platform('androidx.compose:compose-bom:2025.01.01') def composeBom23 = platform('androidx.compose:compose-bom:2025.12.01')
implementation composeBom def activityVersion23 = "1.12.2"
androidTestImplementation composeBom def lifecycleComposeVersion23 = "2.10.0"
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.ui:ui' playImplementation composeBom23
implementation 'androidx.compose.ui:ui-tooling-preview' 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' debugImplementation 'androidx.compose.ui:ui-tooling'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-test-manifest' debugImplementation 'androidx.compose.ui:ui-test-manifest'
implementation 'androidx.compose.material:material-icons-extended' androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
implementation 'androidx.activity:activity-compose:1.10.1'
implementation 'me.zhanghai.compose.preference:library:1.1.1' // Common Compose-related libraries
implementation "androidx.navigation:navigation-compose:2.9.3" implementation "sh.calvin.reorderable:reorderable:3.0.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "sh.calvin.reorderable:reorderable:2.3.3"
implementation "com.github.jeziellago:compose-markdown:0.5.4" implementation "com.github.jeziellago:compose-markdown:0.5.4"
implementation "org.kodein.emoji:emoji-kt:2.3.0" implementation "org.kodein.emoji:emoji-kt:2.3.0"
} }
def playCredentialsJSON = rootProject.file("service-account-credentials.json") 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 package io.nekohasekai.sfa.vendor
import android.os.Build
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.BuildConfig import io.nekohasekai.sfa.BuildConfig
import io.nekohasekai.sfa.ktx.unwrap import io.nekohasekai.sfa.ktx.unwrap
@@ -39,8 +40,11 @@ class GitHubUpdateChecker : Closeable {
return null return null
} }
val isLegacy = Build.VERSION.SDK_INT < Build.VERSION_CODES.M
val apkAsset = release.assets.find { asset -> 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( 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.spring
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState 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.Icons
import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.RestartAlt 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.BugReport
import androidx.compose.material.icons.outlined.Cable import androidx.compose.material.icons.outlined.Cable
import androidx.compose.material.icons.outlined.Download 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.BuildConfig
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DashboardSettingsBottomSheet( fun DashboardSettingsBottomSheet(
sheetState: SheetState, sheetState: SheetState,
@@ -282,8 +282,8 @@ fun DashboardSettingsBottomSheet(
dragOffset = 0f dragOffset = 0f
}, },
modifier = modifier =
Modifier.animateItemPlacement( animateItemCompat(
animationSpec = placementSpec =
spring( spring(
dampingRatio = Spring.DampingRatioMediumBouncy, dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow, stiffness = Spring.StiffnessLow,

View File

@@ -6,7 +6,7 @@ import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity 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.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.QRCodeWriter
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
@@ -46,7 +46,7 @@ suspend fun Context.shareProfile(profile: Profile) {
Intent(Intent.ACTION_SEND).setType("application/octet-stream") Intent(Intent.ACTION_SEND).setType("application/octet-stream")
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_STREAM, uri), .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, profile.typed.remoteURL,
) )
val imageSize = dp2px(256) 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 image = QRCodeWriter().encode(link, BarcodeFormat.QR_CODE, imageSize, imageSize, null)
val imageWidth = image.width val imageWidth = image.width
val imageHeight = image.height 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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> 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-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" /> <uses-sdk tools:overrideLibrary="rikka.shizuku.provider,rikka.shizuku.api,rikka.shizuku.aidl,rikka.shizuku.shared" />
@@ -16,14 +13,6 @@
android:exported="true" android:exported="true"
android:multiprocess="false" android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" /> 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> </application>
</manifest> </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 package io.nekohasekai.sfa.vendor
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.database.Settings
import java.io.File import java.io.File
import java.io.FileInputStream
enum class InstallMethod { enum class InstallMethod {
PACKAGE_INSTALLER, PACKAGE_INSTALLER,
@@ -29,12 +24,12 @@ object ApkInstaller {
return when (method) { return when (method) {
InstallMethod.SHIZUKU -> ShizukuInstaller.install(apkFile) InstallMethod.SHIZUKU -> ShizukuInstaller.install(apkFile)
InstallMethod.ROOT -> RootInstaller.install(apkFile) InstallMethod.ROOT -> RootInstaller.install(apkFile)
InstallMethod.PACKAGE_INSTALLER -> installWithPackageInstaller(context, apkFile) InstallMethod.PACKAGE_INSTALLER -> SystemPackageInstaller.install(context, apkFile)
} }
} }
fun canSystemSilentInstall(): Boolean { fun canSystemSilentInstall(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S return SystemPackageInstaller.canSystemSilentInstall()
} }
suspend fun canSilentInstall(): Boolean { suspend fun canSilentInstall(): Boolean {
@@ -45,40 +40,4 @@ object ApkInstaller {
InstallMethod.ROOT -> RootInstaller.checkAccess() 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)
}
}
}