Split support for Android API 21 and 23
This commit is contained in:
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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" />
|
||||
|
||||
<application>
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
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>
|
||||
@@ -1,9 +0,0 @@
|
||||
package android.content;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.IInterface;
|
||||
|
||||
public interface IIntentReceiver extends IInterface {
|
||||
void performReceive(Intent intent, int resultCode, String data, Bundle extras,
|
||||
boolean ordered, boolean sticky, int sendingUser);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package android.content;
|
||||
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
|
||||
public interface IIntentSender extends IInterface {
|
||||
|
||||
void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
|
||||
IIntentReceiver finishedReceiver, String requiredPermission, Bundle options);
|
||||
|
||||
abstract class Stub extends Binder implements IIntentSender {
|
||||
public static IIntentSender asInterface(IBinder binder) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder asBinder() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package android.content.pm;
|
||||
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
import android.os.RemoteException;
|
||||
|
||||
public interface IPackageInstaller extends IInterface {
|
||||
|
||||
int createSession(PackageInstaller.SessionParams params, String installerPackageName, String installerAttributionTag, int userId) throws RemoteException;
|
||||
|
||||
IPackageInstallerSession openSession(int sessionId) throws RemoteException;
|
||||
|
||||
void abandonSession(int sessionId) throws RemoteException;
|
||||
|
||||
abstract class Stub extends Binder implements IPackageInstaller {
|
||||
public static IPackageInstaller asInterface(IBinder binder) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package android.content.pm;
|
||||
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
|
||||
public interface IPackageInstallerSession extends IInterface {
|
||||
|
||||
abstract class Stub extends Binder implements IPackageInstallerSession {
|
||||
public static IPackageInstallerSession asInterface(IBinder binder) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.Application
|
||||
import io.nekohasekai.sfa.update.UpdateState
|
||||
import io.nekohasekai.sfa.utils.HTTPClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
||||
class ApkDownloader : Closeable {
|
||||
private val client = Libbox.newHTTPClient().apply {
|
||||
modernTLS()
|
||||
keepAlive()
|
||||
}
|
||||
|
||||
suspend fun download(url: String): File = withContext(Dispatchers.IO) {
|
||||
val cacheDir = File(Application.application.cacheDir, "updates")
|
||||
cacheDir.mkdirs()
|
||||
val apkFile = File(cacheDir, "update.apk")
|
||||
|
||||
if (apkFile.exists()) apkFile.delete()
|
||||
|
||||
val request = client.newRequest()
|
||||
request.setUserAgent(HTTPClient.userAgent)
|
||||
request.setURL(url)
|
||||
|
||||
val response = request.execute()
|
||||
response.writeTo(apkFile.absolutePath)
|
||||
|
||||
if (!apkFile.exists() || apkFile.length() == 0L) {
|
||||
throw Exception("Download failed: empty file")
|
||||
}
|
||||
|
||||
UpdateState.saveApkPath(apkFile)
|
||||
apkFile
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.BuildConfig
|
||||
import io.nekohasekai.sfa.ktx.unwrap
|
||||
import io.nekohasekai.sfa.update.UpdateCheckException
|
||||
import io.nekohasekai.sfa.update.UpdateInfo
|
||||
import io.nekohasekai.sfa.update.UpdateTrack
|
||||
import io.nekohasekai.sfa.utils.HTTPClient
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.Closeable
|
||||
|
||||
class GitHubUpdateChecker : Closeable {
|
||||
companion object {
|
||||
private const val RELEASES_URL = "https://api.github.com/repos/SagerNet/sing-box/releases"
|
||||
private const val METADATA_FILENAME = "SFA-version-metadata.json"
|
||||
}
|
||||
|
||||
private val client = Libbox.newHTTPClient().apply {
|
||||
modernTLS()
|
||||
keepAlive()
|
||||
}
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun checkUpdate(track: UpdateTrack): UpdateInfo? {
|
||||
val includePrerelease = track == UpdateTrack.BETA
|
||||
val release = getLatestRelease(includePrerelease) ?: return null
|
||||
|
||||
if (!release.assets.any { it.name == METADATA_FILENAME }) {
|
||||
throw UpdateCheckException.TrackNotSupported()
|
||||
}
|
||||
|
||||
val metadata = downloadMetadata(release)!!
|
||||
|
||||
if (metadata.versionCode <= BuildConfig.VERSION_CODE) {
|
||||
return null
|
||||
}
|
||||
|
||||
val apkAsset = release.assets.find { asset ->
|
||||
asset.name.endsWith(".apk") && !asset.name.contains("play")
|
||||
}
|
||||
|
||||
return UpdateInfo(
|
||||
versionCode = metadata.versionCode,
|
||||
versionName = metadata.versionName,
|
||||
downloadUrl = apkAsset?.browserDownloadUrl ?: release.htmlUrl,
|
||||
releaseUrl = release.htmlUrl,
|
||||
releaseNotes = release.body,
|
||||
isPrerelease = release.prerelease,
|
||||
fileSize = apkAsset?.size ?: 0,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLatestRelease(includePrerelease: Boolean): GitHubRelease? {
|
||||
val request = client.newRequest()
|
||||
request.setURL(RELEASES_URL)
|
||||
request.setHeader("Accept", "application/vnd.github.v3+json")
|
||||
request.setUserAgent(HTTPClient.userAgent)
|
||||
|
||||
val response = request.execute()
|
||||
val content = response.content.unwrap
|
||||
|
||||
val releases = json.decodeFromString<List<GitHubRelease>>(content)
|
||||
|
||||
return if (includePrerelease) {
|
||||
releases.firstOrNull()
|
||||
} else {
|
||||
releases.firstOrNull { !it.prerelease && !it.draft }
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadMetadata(release: GitHubRelease): VersionMetadata? {
|
||||
val metadataAsset = release.assets.find { it.name == METADATA_FILENAME }
|
||||
?: return null
|
||||
|
||||
val request = client.newRequest()
|
||||
request.setURL(metadataAsset.browserDownloadUrl)
|
||||
request.setUserAgent(HTTPClient.userAgent)
|
||||
|
||||
val response = request.execute()
|
||||
val content = response.content.unwrap
|
||||
|
||||
return json.decodeFromString<VersionMetadata>(content)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
client.close()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GitHubRelease(
|
||||
@SerialName("tag_name") val tagName: String = "",
|
||||
val name: String = "",
|
||||
val body: String? = null,
|
||||
val draft: Boolean = false,
|
||||
val prerelease: Boolean = false,
|
||||
@SerialName("html_url") val htmlUrl: String = "",
|
||||
val assets: List<GitHubAsset> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GitHubAsset(
|
||||
val name: String = "",
|
||||
@SerialName("browser_download_url") val browserDownloadUrl: String = "",
|
||||
val size: Long = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VersionMetadata(
|
||||
@SerialName("version_code") val versionCode: Int = 0,
|
||||
@SerialName("version_name") val versionName: String = "",
|
||||
)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.util.Log
|
||||
import io.nekohasekai.sfa.update.UpdateState
|
||||
|
||||
class InstallResultReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
const val ACTION_INSTALL_COMPLETE = "io.nekohasekai.sfa.INSTALL_COMPLETE"
|
||||
private const val TAG = "InstallResultReceiver"
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != ACTION_INSTALL_COMPLETE) return
|
||||
|
||||
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
|
||||
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
|
||||
Log.d(TAG, "Install result: status=$status, message=$message")
|
||||
|
||||
when (status) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirmIntent = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
}
|
||||
confirmIntent?.let {
|
||||
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(it)
|
||||
}
|
||||
}
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
Log.d(TAG, "Installation successful")
|
||||
UpdateState.setInstallStatus(UpdateState.InstallStatus.Success)
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "Installation failed: $status - $message")
|
||||
UpdateState.setInstallStatus(UpdateState.InstallStatus.Failed(message ?: "Unknown error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
|
||||
object RootInstaller {
|
||||
|
||||
suspend fun checkAccess(): Boolean = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su -c echo test")
|
||||
val exitCode = process.waitFor()
|
||||
exitCode == 0
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun install(apkFile: File): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "pm install -r \"${apkFile.absolutePath}\""))
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val output = reader.readText()
|
||||
val exitCode = process.waitFor()
|
||||
|
||||
if (exitCode == 0 && output.contains("Success")) {
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(Exception("Installation failed: $output"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.content.pm.IPackageInstaller
|
||||
import android.content.pm.IPackageInstallerSession
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import io.nekohasekai.sfa.vendor.hidden.IPackageManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||
import rikka.shizuku.Shizuku
|
||||
import rikka.shizuku.ShizukuBinderWrapper
|
||||
import rikka.shizuku.SystemServiceHelper
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import android.content.IIntentSender
|
||||
|
||||
object ShizukuInstaller {
|
||||
|
||||
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 1001
|
||||
|
||||
fun isAvailable(): Boolean {
|
||||
return try {
|
||||
Shizuku.pingBinder()
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun checkPermission(): Boolean {
|
||||
return try {
|
||||
if (Shizuku.isPreV11()) {
|
||||
false
|
||||
} else {
|
||||
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPermission() {
|
||||
if (!Shizuku.isPreV11()) {
|
||||
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRunningAsRoot(): Boolean {
|
||||
return try {
|
||||
Shizuku.getUid() == 0
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPackageInstaller(): IPackageInstaller {
|
||||
val packageManagerBinder = SystemServiceHelper.getSystemService("package")
|
||||
val packageManager = IPackageManager.Stub.asInterface(ShizukuBinderWrapper(packageManagerBinder))
|
||||
val installerBinder = packageManager.packageInstaller.asBinder()
|
||||
return IPackageInstaller.Stub.asInterface(ShizukuBinderWrapper(installerBinder))
|
||||
}
|
||||
|
||||
private fun createPackageInstaller(
|
||||
installer: IPackageInstaller,
|
||||
installerPackageName: String,
|
||||
installerAttributionTag: String?,
|
||||
userId: Int
|
||||
): PackageInstaller {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return PackageInstaller::class.java
|
||||
.getConstructor(
|
||||
IPackageInstaller::class.java,
|
||||
String::class.java,
|
||||
String::class.java,
|
||||
Int::class.javaPrimitiveType
|
||||
)
|
||||
.newInstance(installer, installerPackageName, installerAttributionTag, userId)
|
||||
} else {
|
||||
return PackageInstaller::class.java
|
||||
.getConstructor(
|
||||
IPackageInstaller::class.java,
|
||||
String::class.java,
|
||||
Int::class.javaPrimitiveType
|
||||
)
|
||||
.newInstance(installer, installerPackageName, userId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSession(session: IPackageInstallerSession): PackageInstaller.Session {
|
||||
return PackageInstaller.Session::class.java
|
||||
.getConstructor(IPackageInstallerSession::class.java)
|
||||
.newInstance(session)
|
||||
}
|
||||
|
||||
private fun createIntentSender(onResult: (Intent) -> Unit): IntentSender {
|
||||
val sender = object : IIntentSender.Stub() {
|
||||
override fun send(
|
||||
code: Int,
|
||||
intent: Intent,
|
||||
resolvedType: String?,
|
||||
whitelistToken: android.os.IBinder?,
|
||||
finishedReceiver: android.content.IIntentReceiver?,
|
||||
requiredPermission: String?,
|
||||
options: android.os.Bundle?
|
||||
) {
|
||||
onResult(intent)
|
||||
}
|
||||
}
|
||||
return IntentSender::class.java
|
||||
.getConstructor(IIntentSender::class.java)
|
||||
.newInstance(sender)
|
||||
}
|
||||
|
||||
suspend fun install(apkFile: File): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
HiddenApiBypass.addHiddenApiExemptions("")
|
||||
}
|
||||
|
||||
val iPackageInstaller = getPackageInstaller()
|
||||
val isRoot = isRunningAsRoot()
|
||||
|
||||
val installerPackageName = if (isRoot) "io.nekohasekai.sfa" else "com.android.shell"
|
||||
val installerAttributionTag: String? = null
|
||||
val userId = if (isRoot) Process.myUserHandle().hashCode() else 0
|
||||
|
||||
val packageInstaller = createPackageInstaller(
|
||||
iPackageInstaller,
|
||||
installerPackageName,
|
||||
installerAttributionTag,
|
||||
userId
|
||||
)
|
||||
|
||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
val sessionId = packageInstaller.createSession(params)
|
||||
|
||||
val iSession = IPackageInstallerSession.Stub.asInterface(
|
||||
ShizukuBinderWrapper(iPackageInstaller.openSession(sessionId).asBinder())
|
||||
)
|
||||
val session = createSession(iSession)
|
||||
|
||||
try {
|
||||
FileInputStream(apkFile).use { inputStream ->
|
||||
session.openWrite("base.apk", 0, apkFile.length()).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
session.fsync(outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
val resultIntent = arrayOfNulls<Intent>(1)
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
val intentSender = createIntentSender { intent ->
|
||||
resultIntent[0] = intent
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
session.commit(intentSender)
|
||||
latch.await(60, TimeUnit.SECONDS)
|
||||
|
||||
val intent = resultIntent[0]
|
||||
?: return@withContext Result.failure(Exception("Installation timed out"))
|
||||
|
||||
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
|
||||
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
|
||||
if (status == PackageInstaller.STATUS_SUCCESS) {
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(Exception("Installation failed: $status - $message"))
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.update.UpdateState
|
||||
import io.nekohasekai.sfa.update.UpdateTrack
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UpdateWorker(
|
||||
private val appContext: Context,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
|
||||
companion object {
|
||||
private const val WORK_NAME = "AutoUpdate"
|
||||
private const val TAG = "UpdateWorker"
|
||||
|
||||
fun schedule(context: Context) {
|
||||
if (!Settings.autoUpdateEnabled) {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
|
||||
Log.d(TAG, "Auto update disabled, cancelled scheduled work")
|
||||
return
|
||||
}
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
|
||||
val workRequest = PeriodicWorkRequestBuilder<UpdateWorker>(
|
||||
24, TimeUnit.HOURS
|
||||
)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.HOURS)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
workRequest
|
||||
)
|
||||
Log.d(TAG, "Auto update scheduled")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
if (!Settings.autoUpdateEnabled) {
|
||||
Log.d(TAG, "Auto update disabled, skipping")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
Log.d(TAG, "Checking for updates...")
|
||||
|
||||
return try {
|
||||
val track = UpdateTrack.fromString(Settings.updateTrack)
|
||||
val updateInfo = GitHubUpdateChecker().use { it.checkUpdate(track) }
|
||||
|
||||
if (updateInfo == null) {
|
||||
Log.d(TAG, "No update available")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
Log.d(TAG, "Update available: ${updateInfo.versionName}")
|
||||
UpdateState.setUpdate(updateInfo)
|
||||
|
||||
if (Settings.silentInstallEnabled && ApkInstaller.canSilentInstall()) {
|
||||
Log.d(TAG, "Downloading update...")
|
||||
val apkFile = ApkDownloader().use { it.download(updateInfo.downloadUrl) }
|
||||
|
||||
Log.d(TAG, "Installing update...")
|
||||
val result = ApkInstaller.install(appContext, apkFile)
|
||||
|
||||
if (result.isSuccess) {
|
||||
Log.d(TAG, "Update installed successfully")
|
||||
} else {
|
||||
Log.e(TAG, "Update installation failed", result.exceptionOrNull())
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Silent install not available, update will be shown on next app launch")
|
||||
}
|
||||
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Auto update failed", e)
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package io.nekohasekai.sfa.vendor.hidden;
|
||||
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import android.content.pm.IPackageInstaller;
|
||||
|
||||
public interface IPackageManager extends IInterface {
|
||||
|
||||
IPackageInstaller getPackageInstaller() throws RemoteException;
|
||||
|
||||
abstract class Stub extends Binder implements IPackageManager {
|
||||
public static IPackageManager asInterface(IBinder binder) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user