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"
|
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")
|
||||||
|
|||||||
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
|
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(
|
||||||
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.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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
<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>
|
||||||
@@ -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
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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