diff --git a/app/build.gradle b/app/build.gradle
index a58605c..1fae95f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -16,7 +16,7 @@ android {
minSdk 29
targetSdk 34
versionCode 4
- versionName "v1.6"
+ versionName "v1.6.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4fb3e7e..8ff0e43 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,9 @@
+
+
+
+ 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, 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, targetFolder: String): List? {
+ val ret: MutableList = 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, reservePatchFiles: Boolean,
patchCallback: PatchCallback?): Pair {
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 = mutableListOf()
val movedFiles: MutableList = 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()
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/ShizukuShell.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/ShizukuShell.kt
index d699958..e51ab45 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/ShizukuShell.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/ShizukuShell.kt
@@ -23,7 +23,7 @@ class ShizukuShell(private var mOutput: MutableList, 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, private var mComman
mProcess!!.waitFor()
} catch (ignored: Exception) {
}
+ return this
}
fun destroy() {
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/InstallDiag.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/InstallDiag.kt
index 7d649ac..30cda34 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/InstallDiag.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/InstallDiag.kt
@@ -27,7 +27,7 @@ import java.io.File
@Composable
-fun InstallDiag(context: Context?, apkFiles: List, patchCallback: PatchCallback?, reservePatchFiles: Boolean,
+fun InstallDiag(context: PatchActivity?, apkFiles: List, patchCallback: PatchCallback?, reservePatchFiles: Boolean,
onFinish: (Int, String?) -> Unit) {
// val scope = rememberCoroutineScope()
// var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) }