1
0
Fork 0

Compatible with Android 10

This commit is contained in:
chinosk 2024-08-09 20:15:21 +08:00
parent 24480b6982
commit 8ddd6f53bc
Signed by: chinosk
GPG Key ID: 00610B08C1BF7BE9
5 changed files with 208 additions and 9 deletions

View File

@ -16,7 +16,7 @@ android {
minSdk 29 minSdk 29
targetSdk 34 targetSdk 34
versionCode 4 versionCode 4
versionName "v1.6" versionName "v1.6.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@ -6,6 +6,9 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@ -1,19 +1,29 @@
package io.github.chinosk.gakumas.localify package io.github.chinosk.gakumas.localify
import android.Manifest
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent 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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import io.github.chinosk.gakumas.localify.mainUtils.IOnShell import io.github.chinosk.gakumas.localify.mainUtils.IOnShell
import io.github.chinosk.gakumas.localify.mainUtils.LSPatchUtils 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.LSPatch
import org.lsposed.patch.util.Logger import org.lsposed.patch.util.Logger
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.attribute.PosixFilePermissions import java.nio.file.attribute.PosixFilePermissions
import java.util.concurrent.CountDownLatch
interface PatchCallback { interface PatchCallback {
@ -99,6 +111,137 @@ class PatchActivity : ComponentActivity() {
private var reservePatchFiles: Boolean = false private var reservePatchFiles: Boolean = false
var patchCallback: PatchCallback? = null 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) { private fun handleSelectedFile(uri: Uri) {
val fileName = uri.path?.substringAfterLast('/') val fileName = uri.path?.substringAfterLast('/')
if (fileName != null) { if (fileName != null) {
@ -110,6 +253,7 @@ class PatchActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
outputDir = "${filesDir.absolutePath}/output" outputDir = "${filesDir.absolutePath}/output"
// ShizukuApi.init() // ShizukuApi.init()
checkAndRequestWritePermission()
setContent { setContent {
GakumasLocalifyTheme(dynamicColor = false, darkTheme = false) { GakumasLocalifyTheme(dynamicColor = false, darkTheme = false) {
@ -414,7 +558,34 @@ class PatchActivity : ComponentActivity() {
return movedFiles 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?> { patchCallback: PatchCallback?): Pair<Int, String?> {
Log.i(TAG, "Perform install patched apks") Log.i(TAG, "Perform install patched apks")
var status = PackageInstaller.STATUS_FAILURE var status = PackageInstaller.STATUS_FAILURE
@ -424,13 +595,27 @@ class PatchActivity : ComponentActivity() {
runCatching { runCatching {
val sdcardPath = Environment.getExternalStorageDirectory().path val sdcardPath = Environment.getExternalStorageDirectory().path
val targetDirectory = File(sdcardPath, "Download/gkms_local_patch") val targetDirectory = File(sdcardPath, "Download/gkms_local_patch")
val savedFiles = saveFileTo(apkFiles, targetDirectory, true, false) // val savedFiles = saveFileTo(apkFiles, targetDirectory, true, false)
patchCallback?.onLog("Patched files: $savedFiles")
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) { if (!ShizukuApi.isPermissionGranted) {
status = PackageInstaller.STATUS_FAILURE status = PackageInstaller.STATUS_FAILURE
message = "Shizuku Not Ready." 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 return@runCatching
} }
@ -455,16 +640,26 @@ class PatchActivity : ComponentActivity() {
val action = if (reservePatchFiles) "cp" else "mv" val action = if (reservePatchFiles) "cp" else "mv"
val copyFilesCmd: MutableList<String> = mutableListOf() val copyFilesCmd: MutableList<String> = mutableListOf()
val movedFiles: 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 -> savedFiles.forEach { file ->
val movedFileName = "$installDS/${file.name}" val movedFileName = "$installDS/${file.name}"
movedFiles.add(movedFileName) movedFiles.add(movedFileName)
copyFilesCmd.add("$action ${file.absolutePath} $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(" && ") copyFilesCmd.joinToString(" && ")
Log.d(TAG, "moveFileCommand: $moveFileCommand") Log.d(TAG, "moveFileCommand: $moveFileCommand")
ShizukuShell(mutableListOf(), createDirCommand, ioShell).exec().destroy()
val cpFileShell = ShizukuShell(mutableListOf(), moveFileCommand, ioShell) val cpFileShell = ShizukuShell(mutableListOf(), moveFileCommand, ioShell)
cpFileShell.exec() cpFileShell.exec()
cpFileShell.destroy() cpFileShell.destroy()

View File

@ -23,7 +23,7 @@ class ShizukuShell(private var mOutput: MutableList<String>, private var mComman
val isBusy: Boolean val isBusy: Boolean
get() = mOutput.size > 0 && mOutput[mOutput.size - 1] != "aShell: Finish" get() = mOutput.size > 0 && mOutput[mOutput.size - 1] != "aShell: Finish"
fun exec() { fun exec(): ShizukuShell {
try { try {
Log.i(shellTag, "Execute: $mCommand") Log.i(shellTag, "Execute: $mCommand")
shellCallback?.onShellLine(mCommand) shellCallback?.onShellLine(mCommand)
@ -66,6 +66,7 @@ class ShizukuShell(private var mOutput: MutableList<String>, private var mComman
mProcess!!.waitFor() mProcess!!.waitFor()
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }
return this
} }
fun destroy() { fun destroy() {

View File

@ -27,7 +27,7 @@ import java.io.File
@Composable @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) { onFinish: (Int, String?) -> Unit) {
// val scope = rememberCoroutineScope() // val scope = rememberCoroutineScope()
// var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) } // var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) }