Split support for Android API 21 and 23
This commit is contained in:
170
app/build.gradle
170
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")
|
||||
|
||||
17
app/src/github/AndroidManifest.xml
Normal file
17
app/src/github/AndroidManifest.xml
Normal 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>
|
||||
@@ -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(
|
||||
52
app/src/github/java/io/nekohasekai/sfa/vendor/SystemPackageInstaller.kt
vendored
Normal file
52
app/src/github/java/io/nekohasekai/sfa/vendor/SystemPackageInstaller.kt
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt
vendored
Normal file
41
app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt
vendored
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
149
app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/Vendor.kt
vendored
Normal file
149
app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/Vendor.kt
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user