forked from chinosk/gkms-local
Compatible with Android 10
This commit is contained in:
parent
24480b6982
commit
8ddd6f53bc
|
@ -16,7 +16,7 @@ android {
|
|||
minSdk 29
|
||||
targetSdk 34
|
||||
versionCode 4
|
||||
versionName "v1.6"
|
||||
versionName "v1.6.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
package io.github.chinosk.gakumas.localify
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.IOnShell
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.LSPatchUtils
|
||||
|
@ -29,11 +39,13 @@ import kotlinx.coroutines.withContext
|
|||
import org.lsposed.patch.LSPatch
|
||||
import org.lsposed.patch.util.Logger
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.attribute.PosixFilePermissions
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
|
||||
interface PatchCallback {
|
||||
|
@ -99,6 +111,137 @@ class PatchActivity : ComponentActivity() {
|
|||
private var reservePatchFiles: Boolean = false
|
||||
var patchCallback: PatchCallback? = null
|
||||
|
||||
private val writePermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) { result ->
|
||||
if (result.resultCode != RESULT_OK) {
|
||||
Toast.makeText(this, "Permission Request Failed.", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private val writePermissionLauncherQ = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (!isGranted) {
|
||||
Toast.makeText(this, "Permission Request Failed.", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAndRequestWritePermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
/*
|
||||
// 针对 API 级别 30 及以上使用 MediaStore.createWriteRequest
|
||||
val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
val intentSender = MediaStore.createWriteRequest(contentResolver, listOf(uri)).intentSender
|
||||
writePermissionLauncher.launch(IntentSenderRequest.Builder(intentSender).build())*/
|
||||
}
|
||||
else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// 请求 WRITE_EXTERNAL_STORAGE 权限
|
||||
writePermissionLauncherQ.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun writeFileToDownloadFolder(
|
||||
sourceFile: File,
|
||||
targetFolder: String,
|
||||
targetFileName: String
|
||||
): Boolean {
|
||||
val downloadDirectory = Environment.DIRECTORY_DOWNLOADS
|
||||
val relativePath = "$downloadDirectory/$targetFolder/"
|
||||
val resolver = contentResolver
|
||||
|
||||
// 检查文件是否已经存在
|
||||
val existingUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
val query = resolver.query(
|
||||
existingUri,
|
||||
arrayOf(MediaStore.Files.FileColumns._ID),
|
||||
"${MediaStore.Files.FileColumns.RELATIVE_PATH}=? AND ${MediaStore.Files.FileColumns.DISPLAY_NAME}=?",
|
||||
arrayOf(relativePath, targetFileName),
|
||||
null
|
||||
)
|
||||
|
||||
query?.use {
|
||||
if (it.moveToFirst()) {
|
||||
// 如果文件存在,则删除
|
||||
val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
|
||||
val deleteUri = MediaStore.Files.getContentUri("external", id)
|
||||
resolver.delete(deleteUri, null, null)
|
||||
Log.d(patchTag, "query delete: $deleteUri")
|
||||
}
|
||||
}
|
||||
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.Downloads.DISPLAY_NAME, targetFileName)
|
||||
put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
|
||||
put(MediaStore.Downloads.RELATIVE_PATH, relativePath)
|
||||
}
|
||||
|
||||
var uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
Log.d(patchTag, "insert uri: $uri")
|
||||
|
||||
if (uri == null) {
|
||||
val latch = CountDownLatch(1)
|
||||
val downloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val downloadSaveDirectory = File(downloadDirectory, targetFolder)
|
||||
val downloadSaveFile = File(downloadSaveDirectory, targetFileName)
|
||||
MediaScannerConnection.scanFile(this, arrayOf(downloadSaveFile.absolutePath),
|
||||
null
|
||||
) { _, _ ->
|
||||
Log.d(patchTag, "scanFile finished.")
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
if (uri == null) {
|
||||
Log.e(patchTag, "uri is still null")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
resolver.openOutputStream(uri)?.use { outputStream ->
|
||||
FileInputStream(sourceFile).use { inputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
contentValues.clear()
|
||||
contentValues.put(MediaStore.Downloads.IS_PENDING, 0)
|
||||
resolver.update(uri, contentValues, null, null)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
resolver.delete(uri, null, null)
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun deleteFileInDownloadFolder(targetFolder: String, targetFileName: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val selection =
|
||||
"${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DISPLAY_NAME} = ?"
|
||||
val selectionArgs =
|
||||
arrayOf("${Environment.DIRECTORY_DOWNLOADS}/$targetFolder/", targetFileName)
|
||||
|
||||
val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
contentResolver.delete(uri, selection, selectionArgs)
|
||||
}
|
||||
else {
|
||||
val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "$targetFolder/$targetFileName")
|
||||
if (file.exists()) {
|
||||
if (file.delete()) {
|
||||
// Toast.makeText(this, "文件已删除", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSelectedFile(uri: Uri) {
|
||||
val fileName = uri.path?.substringAfterLast('/')
|
||||
if (fileName != null) {
|
||||
|
@ -110,6 +253,7 @@ class PatchActivity : ComponentActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
outputDir = "${filesDir.absolutePath}/output"
|
||||
// ShizukuApi.init()
|
||||
checkAndRequestWritePermission()
|
||||
|
||||
setContent {
|
||||
GakumasLocalifyTheme(dynamicColor = false, darkTheme = false) {
|
||||
|
@ -414,7 +558,34 @@ class PatchActivity : ComponentActivity() {
|
|||
return movedFiles
|
||||
}
|
||||
|
||||
suspend fun installSplitApks(context: Context, apkFiles: List<File>, reservePatchFiles: Boolean,
|
||||
private fun generateNonce(size: Int): String {
|
||||
val nonceScope = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
val scopeSize = nonceScope.length
|
||||
val nonceItem: (Int) -> Char = { nonceScope[(scopeSize * Math.random()).toInt()] }
|
||||
return Array(size, nonceItem).joinToString("")
|
||||
}
|
||||
|
||||
fun saveFilesToDownload(context: PatchActivity, apkFiles: List<File>, targetFolder: String): List<String>? {
|
||||
val ret: MutableList<String> = mutableListOf()
|
||||
apkFiles.forEach { f ->
|
||||
val success = context.writeFileToDownloadFolder(f, "gkms_local_patch", f.name)
|
||||
if (success) {
|
||||
ret.add(f.name)
|
||||
}
|
||||
else {
|
||||
val newName = "${generateNonce(6)}${f.name}"
|
||||
val success2 = context.writeFileToDownloadFolder(f, "gkms_local_patch",
|
||||
newName)
|
||||
if (!success2) {
|
||||
return null
|
||||
}
|
||||
ret.add(newName)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
suspend fun installSplitApks(context: PatchActivity, apkFiles: List<File>, reservePatchFiles: Boolean,
|
||||
patchCallback: PatchCallback?): Pair<Int, String?> {
|
||||
Log.i(TAG, "Perform install patched apks")
|
||||
var status = PackageInstaller.STATUS_FAILURE
|
||||
|
@ -424,13 +595,27 @@ class PatchActivity : ComponentActivity() {
|
|||
runCatching {
|
||||
val sdcardPath = Environment.getExternalStorageDirectory().path
|
||||
val targetDirectory = File(sdcardPath, "Download/gkms_local_patch")
|
||||
val savedFiles = saveFileTo(apkFiles, targetDirectory, true, false)
|
||||
patchCallback?.onLog("Patched files: $savedFiles")
|
||||
// val savedFiles = saveFileTo(apkFiles, targetDirectory, true, false)
|
||||
|
||||
val savedFileNames = saveFilesToDownload(context, apkFiles, "gkms_local_patch")
|
||||
if (savedFileNames == null) {
|
||||
status = PackageInstaller.STATUS_FAILURE
|
||||
message = "Save files failed."
|
||||
return@runCatching
|
||||
}
|
||||
|
||||
// patchCallback?.onLog("Patched files: $savedFiles")
|
||||
patchCallback?.onLog("Patched files: $apkFiles")
|
||||
|
||||
if (!ShizukuApi.isPermissionGranted) {
|
||||
status = PackageInstaller.STATUS_FAILURE
|
||||
message = "Shizuku Not Ready."
|
||||
if (!reservePatchFiles) savedFiles.forEach { file -> if (file.exists()) file.delete() }
|
||||
// if (!reservePatchFiles) savedFiles.forEach { file -> if (file.exists()) file.delete() }
|
||||
if (!reservePatchFiles) {
|
||||
savedFileNames.forEach { f ->
|
||||
context.deleteFileInDownloadFolder("gkms_local_patch", f)
|
||||
}
|
||||
}
|
||||
return@runCatching
|
||||
}
|
||||
|
||||
|
@ -455,16 +640,26 @@ class PatchActivity : ComponentActivity() {
|
|||
val action = if (reservePatchFiles) "cp" else "mv"
|
||||
val copyFilesCmd: MutableList<String> = mutableListOf()
|
||||
val movedFiles: MutableList<String> = mutableListOf()
|
||||
savedFileNames.forEach { file ->
|
||||
val movedFileName = "$installDS/${file}"
|
||||
movedFiles.add(movedFileName)
|
||||
val dlSaveFileName = File(targetDirectory, file)
|
||||
copyFilesCmd.add("$action ${dlSaveFileName.absolutePath} $movedFileName")
|
||||
}
|
||||
/*
|
||||
savedFiles.forEach { file ->
|
||||
val movedFileName = "$installDS/${file.name}"
|
||||
movedFiles.add(movedFileName)
|
||||
copyFilesCmd.add("$action ${file.absolutePath} $movedFileName")
|
||||
}
|
||||
val moveFileCommand = "mkdir $installDS && " +
|
||||
"chmod 777 $installDS && " +
|
||||
*/
|
||||
val createDirCommand = "mkdir $installDS"
|
||||
val moveFileCommand = "chmod 777 $installDS && " +
|
||||
copyFilesCmd.joinToString(" && ")
|
||||
Log.d(TAG, "moveFileCommand: $moveFileCommand")
|
||||
|
||||
ShizukuShell(mutableListOf(), createDirCommand, ioShell).exec().destroy()
|
||||
|
||||
val cpFileShell = ShizukuShell(mutableListOf(), moveFileCommand, ioShell)
|
||||
cpFileShell.exec()
|
||||
cpFileShell.destroy()
|
||||
|
|
|
@ -23,7 +23,7 @@ class ShizukuShell(private var mOutput: MutableList<String>, private var mComman
|
|||
val isBusy: Boolean
|
||||
get() = mOutput.size > 0 && mOutput[mOutput.size - 1] != "aShell: Finish"
|
||||
|
||||
fun exec() {
|
||||
fun exec(): ShizukuShell {
|
||||
try {
|
||||
Log.i(shellTag, "Execute: $mCommand")
|
||||
shellCallback?.onShellLine(mCommand)
|
||||
|
@ -66,6 +66,7 @@ class ShizukuShell(private var mOutput: MutableList<String>, private var mComman
|
|||
mProcess!!.waitFor()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
|
|
|
@ -27,7 +27,7 @@ import java.io.File
|
|||
|
||||
|
||||
@Composable
|
||||
fun InstallDiag(context: Context?, apkFiles: List<File>, patchCallback: PatchCallback?, reservePatchFiles: Boolean,
|
||||
fun InstallDiag(context: PatchActivity?, apkFiles: List<File>, patchCallback: PatchCallback?, reservePatchFiles: Boolean,
|
||||
onFinish: (Int, String?) -> Unit) {
|
||||
// val scope = rememberCoroutineScope()
|
||||
// var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) }
|
||||
|
|
Loading…
Reference in New Issue