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)
+ }
+ }
+}