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
|
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 {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)) }
|
||||||
|
|
Loading…
Reference in New Issue