diff --git a/app/build.gradle b/app/build.gradle index 3ffde37..86d0820 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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") diff --git a/app/src/github/AndroidManifest.xml b/app/src/github/AndroidManifest.xml new file mode 100644 index 0000000..85fdf1e --- /dev/null +++ b/app/src/github/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/ApkDownloader.kt b/app/src/github/java/io/nekohasekai/sfa/vendor/ApkDownloader.kt similarity index 100% rename from app/src/other/java/io/nekohasekai/sfa/vendor/ApkDownloader.kt rename to app/src/github/java/io/nekohasekai/sfa/vendor/ApkDownloader.kt diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/GitHubUpdateChecker.kt b/app/src/github/java/io/nekohasekai/sfa/vendor/GitHubUpdateChecker.kt similarity index 93% rename from app/src/other/java/io/nekohasekai/sfa/vendor/GitHubUpdateChecker.kt rename to app/src/github/java/io/nekohasekai/sfa/vendor/GitHubUpdateChecker.kt index fa1a432..6a0c818 100644 --- a/app/src/other/java/io/nekohasekai/sfa/vendor/GitHubUpdateChecker.kt +++ b/app/src/github/java/io/nekohasekai/sfa/vendor/GitHubUpdateChecker.kt @@ -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( diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/InstallResultReceiver.kt b/app/src/github/java/io/nekohasekai/sfa/vendor/InstallResultReceiver.kt similarity index 100% rename from app/src/other/java/io/nekohasekai/sfa/vendor/InstallResultReceiver.kt rename to app/src/github/java/io/nekohasekai/sfa/vendor/InstallResultReceiver.kt diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/RootInstaller.kt b/app/src/github/java/io/nekohasekai/sfa/vendor/RootInstaller.kt similarity index 100% rename from app/src/other/java/io/nekohasekai/sfa/vendor/RootInstaller.kt rename to app/src/github/java/io/nekohasekai/sfa/vendor/RootInstaller.kt diff --git a/app/src/github/java/io/nekohasekai/sfa/vendor/SystemPackageInstaller.kt b/app/src/github/java/io/nekohasekai/sfa/vendor/SystemPackageInstaller.kt new file mode 100644 index 0000000..bf62dce --- /dev/null +++ b/app/src/github/java/io/nekohasekai/sfa/vendor/SystemPackageInstaller.kt @@ -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 { + 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) + } + } +} diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/UpdateWorker.kt b/app/src/github/java/io/nekohasekai/sfa/vendor/UpdateWorker.kt similarity index 100% rename from app/src/other/java/io/nekohasekai/sfa/vendor/UpdateWorker.kt rename to app/src/github/java/io/nekohasekai/sfa/vendor/UpdateWorker.kt diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardSettingsBottomSheet.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardSettingsBottomSheet.kt index 50ccac9..ceffbe5 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardSettingsBottomSheet.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardSettingsBottomSheet.kt @@ -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, diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt index 029d6d2..f5120a8 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt @@ -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 diff --git a/app/src/minApi21/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt b/app/src/minApi21/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt new file mode 100644 index 0000000..cfeb2bf --- /dev/null +++ b/app/src/minApi21/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt @@ -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, +): Modifier = Modifier diff --git a/app/src/other/AndroidManifest.xml b/app/src/minApi23/AndroidManifest.xml similarity index 61% rename from app/src/other/AndroidManifest.xml rename to app/src/minApi23/AndroidManifest.xml index 697a21d..89cd700 100644 --- a/app/src/other/AndroidManifest.xml +++ b/app/src/minApi23/AndroidManifest.xml @@ -2,9 +2,6 @@ - - - @@ -16,14 +13,6 @@ android:exported="true" android:multiprocess="false" android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" /> - - - - - - diff --git a/app/src/other/java/android/content/IIntentReceiver.java b/app/src/minApi23/java/android/content/IIntentReceiver.java similarity index 100% rename from app/src/other/java/android/content/IIntentReceiver.java rename to app/src/minApi23/java/android/content/IIntentReceiver.java diff --git a/app/src/other/java/android/content/IIntentSender.java b/app/src/minApi23/java/android/content/IIntentSender.java similarity index 100% rename from app/src/other/java/android/content/IIntentSender.java rename to app/src/minApi23/java/android/content/IIntentSender.java diff --git a/app/src/other/java/android/content/pm/IPackageInstaller.java b/app/src/minApi23/java/android/content/pm/IPackageInstaller.java similarity index 100% rename from app/src/other/java/android/content/pm/IPackageInstaller.java rename to app/src/minApi23/java/android/content/pm/IPackageInstaller.java diff --git a/app/src/other/java/android/content/pm/IPackageInstallerSession.java b/app/src/minApi23/java/android/content/pm/IPackageInstallerSession.java similarity index 100% rename from app/src/other/java/android/content/pm/IPackageInstallerSession.java rename to app/src/minApi23/java/android/content/pm/IPackageInstallerSession.java diff --git a/app/src/minApi23/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt b/app/src/minApi23/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt new file mode 100644 index 0000000..d8b9a27 --- /dev/null +++ b/app/src/minApi23/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt @@ -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, +): Modifier = Modifier.animateItem(placementSpec = placementSpec) diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/ShizukuInstaller.kt b/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuInstaller.kt similarity index 100% rename from app/src/other/java/io/nekohasekai/sfa/vendor/ShizukuInstaller.kt rename to app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuInstaller.kt diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/hidden/IPackageManager.java b/app/src/minApi23/java/io/nekohasekai/sfa/vendor/hidden/IPackageManager.java similarity index 100% rename from app/src/other/java/io/nekohasekai/sfa/vendor/hidden/IPackageManager.java rename to app/src/minApi23/java/io/nekohasekai/sfa/vendor/hidden/IPackageManager.java diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt b/app/src/other/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt index 8774a61..458c6a3 100644 --- a/app/src/other/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt +++ b/app/src/other/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt @@ -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 { - 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) - } - } } diff --git a/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt b/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt new file mode 100644 index 0000000..df0ffb7 --- /dev/null +++ b/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt @@ -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 { + 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() + } + } +} diff --git a/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/Vendor.kt b/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/Vendor.kt new file mode 100644 index 0000000..196bf99 --- /dev/null +++ b/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/Vendor.kt @@ -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 { + 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) + } + } +}