From 8ddd6f53bc02ab9b81dc3d306c0a2a9f9f789fc4 Mon Sep 17 00:00:00 2001
From: chinosk <2248589280@qq.com>
Date: Fri, 9 Aug 2024 20:15:21 +0800
Subject: [PATCH] Compatible with Android 10
---
 app/build.gradle                              |   2 +-
 app/src/main/AndroidManifest.xml              |   3 +
 .../chinosk/gakumas/localify/PatchActivity.kt | 207 +++++++++++++++++-
 .../localify/mainUtils/ShizukuShell.kt        |   3 +-
 .../localify/ui/components/InstallDiag.kt     |   2 +-
 5 files changed, 208 insertions(+), 9 deletions(-)
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)) }