Fix saving apks

This commit is contained in:
Nullptr 2022-04-22 21:22:35 +08:00
parent 792a6f887a
commit 3b5fa88d17
8 changed files with 109 additions and 72 deletions

View File

@ -5,4 +5,4 @@ android.nonTransitiveRClass=true
android.enableR8.fullMode=true android.enableR8.fullMode=true
android.useAndroidX=true android.useAndroidX=true
agpVersion=7.1.2 agpVersion=7.1.3

View File

@ -74,12 +74,11 @@ dependencies {
implementation("androidx.compose.runtime:runtime-livedata:1.1.1") implementation("androidx.compose.runtime:runtime-livedata:1.1.1")
implementation("androidx.compose.ui:ui:1.1.1") implementation("androidx.compose.ui:ui:1.1.1")
implementation("androidx.compose.ui:ui-tooling:1.1.1") implementation("androidx.compose.ui:ui-tooling:1.1.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha05") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-beta01")
implementation("androidx.navigation:navigation-compose:2.5.0-alpha03") implementation("androidx.navigation:navigation-compose:2.5.0-beta01")
implementation("androidx.preference:preference:1.2.0") implementation("androidx.preference:preference:1.2.0")
implementation("com.google.accompanist:accompanist-drawablepainter:0.24.5-alpha") implementation("com.google.accompanist:accompanist-drawablepainter:0.24.5-alpha")
implementation("com.google.accompanist:accompanist-navigation-animation:0.24.5-alpha") implementation("com.google.accompanist:accompanist-navigation-animation:0.24.5-alpha")
implementation("com.google.accompanist:accompanist-permissions:0.24.5-alpha")
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.5-alpha") implementation("com.google.accompanist:accompanist-swiperefresh:0.24.5-alpha")
implementation("com.google.android.material:material:1.5.0") implementation("com.google.android.material:material:1.5.0")
implementation("dev.rikka.shizuku:api:12.1.0") implementation("dev.rikka.shizuku:api:12.1.0")

View File

@ -7,10 +7,6 @@
android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application <application
android:name=".LSPApplication" android:name=".LSPApplication"
android:allowBackup="true" android:allowBackup="true"
@ -34,7 +30,8 @@
android:name=".manager.ModuleProvider" android:name=".manager.ModuleProvider"
android:authorities="org.lsposed.lspatch.provider" android:authorities="org.lsposed.lspatch.provider"
android:enabled="true" android:enabled="true"
android:exported="true" /> android:exported="true"
tools:ignore="ExportedContentProvider" />
<provider <provider
android:name="rikka.shizuku.ShizukuProvider" android:name="rikka.shizuku.ShizukuProvider"

View File

@ -0,0 +1,6 @@
package org.lsposed.lspatch
object Constants {
const val PREFS_STORAGE_DIRECTORY = "storage_directory"
}

View File

@ -1,10 +1,8 @@
package org.lsposed.lspatch package org.lsposed.lspatch
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.os.Build import androidx.core.net.toUri
import android.os.Environment import androidx.documentfile.provider.DocumentFile
import android.provider.MediaStore
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.lsposed.lspatch.config.MyKeyStore import org.lsposed.lspatch.config.MyKeyStore
@ -45,7 +43,7 @@ object Patcher {
add("-m"); addAll(embeddedModules) add("-m"); addAll(embeddedModules)
} }
if (!MyKeyStore.useDefault) { if (!MyKeyStore.useDefault) {
addAll(arrayOf("-k", MyKeyStore.file.path, MyKeyStore.password, MyKeyStore.alias,MyKeyStore.aliasPassword)) addAll(arrayOf("-k", MyKeyStore.file.path, MyKeyStore.password, MyKeyStore.alias, MyKeyStore.aliasPassword))
} }
}.toTypedArray() }.toTypedArray()
} }
@ -53,38 +51,29 @@ object Patcher {
suspend fun patch(context: Context, logger: Logger, options: Options) { suspend fun patch(context: Context, logger: Logger, options: Options) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val download = "${Environment.DIRECTORY_DOWNLOADS}/LSPatch"
val externalStorageDir = Environment.getExternalStoragePublicDirectory(download)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) externalStorageDir.mkdirs()
options.outputPath = Files.createTempDirectory("patch").absolutePathString() options.outputPath = Files.createTempDirectory("patch").absolutePathString()
LSPatch(logger, *options.toStringArray()).doCommandLine() LSPatch(logger, *options.toStringArray()).doCommandLine()
val uri = LSPApplication.prefs.getString(Constants.PREFS_STORAGE_DIRECTORY, null)?.toUri()
?: throw IllegalStateException("Uri is null")
val root = DocumentFile.fromTreeUri(context, uri)
?: throw IllegalStateException("DocumentFile is null")
root.listFiles().forEach { it.delete() }
File(options.outputPath) File(options.outputPath)
.walk() .walk()
.filter { it.isFile } .filter { it.isFile }
.forEach { .forEach {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { val file = root.createFile("application/vnd.android.package-archive", it.name)
?: throw IllegalStateException("Failed to create output file")
val os = context.contentResolver.openOutputStream(file.uri)
?: throw IllegalStateException("Failed to open output stream")
os.use { output ->
it.inputStream().use { input -> it.inputStream().use { input ->
externalStorageDir.resolve(it.name).outputStream().use { output ->
input.copyTo(output)
}
}
} else {
val contentDetails = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, it.name)
put(MediaStore.Downloads.RELATIVE_PATH, download)
}
val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentDetails)
?: throw IllegalStateException("Failed to save files to Download")
it.inputStream().use { input ->
context.contentResolver.openOutputStream(uri)!!.use { output ->
input.copyTo(output) input.copyTo(output)
} }
} }
} }
} logger.i("Patched files are saved to ${root.uri.lastPathSegment}")
logger.i("Patched files are saved to $download")
} }
} }
} }

View File

@ -1,22 +1,33 @@
package org.lsposed.lspatch.ui.page package org.lsposed.lspatch.ui.page
import android.app.Activity
import android.content.Intent
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import kotlinx.coroutines.launch
import org.lsposed.lspatch.Constants
import org.lsposed.lspatch.LSPApplication
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
import org.lsposed.lspatch.TAG
import org.lsposed.lspatch.ui.util.LocalNavController import org.lsposed.lspatch.ui.util.LocalNavController
import java.io.IOException
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ManagePage() { fun ManagePage() {
val navController = LocalNavController.current val snackbarHostState = remember { SnackbarHostState() }
Scaffold( Scaffold(
topBar = { TopBar() }, topBar = { TopBar() },
floatingActionButton = { floatingActionButton = { Fab(snackbarHostState) },
Fab { navController.navigate(PageList.NewPatch.name) } snackbarHost = { SnackbarHost(snackbarHostState) }
}
) { innerPadding -> ) { innerPadding ->
} }
@ -30,8 +41,68 @@ private fun TopBar() {
} }
@Composable @Composable
private fun Fab(onClick: () -> Unit) { private fun Fab(snackbarHostState: SnackbarHostState) {
FloatingActionButton(onClick = onClick) { val context = LocalContext.current
Icon(Icons.Filled.Add, stringResource(R.string.add)) val navController = LocalNavController.current
val scope = rememberCoroutineScope()
var shouldSelectDirectory by remember { mutableStateOf(false) }
val errorText = stringResource(R.string.patch_select_dir_error)
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
try {
if (it.resultCode == Activity.RESULT_CANCELED) return@rememberLauncherForActivityResult
val uri = it.data?.data ?: throw IOException("No data")
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
LSPApplication.prefs.edit().putString(Constants.PREFS_STORAGE_DIRECTORY, uri.toString()).apply()
Log.i(TAG, "Storage directory: ${uri.path}")
navController.navigate(PageList.NewPatch.name)
} catch (e: Exception) {
Log.e(TAG, "Error when requesting saving directory", e)
scope.launch { snackbarHostState.showSnackbar(errorText) }
} }
}
if (shouldSelectDirectory) {
AlertDialog(
onDismissRequest = { shouldSelectDirectory = false },
confirmButton = {
TextButton(
content = { Text(stringResource(android.R.string.ok)) },
onClick = {
launcher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
shouldSelectDirectory = false
}
)
},
dismissButton = {
TextButton(
content = { Text(stringResource(android.R.string.cancel)) },
onClick = { shouldSelectDirectory = false }
)
},
title = { Text(stringResource(R.string.patch_select_dir_title)) },
text = { Text(stringResource(R.string.patch_select_dir_text)) }
)
}
FloatingActionButton(
content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) },
onClick = {
val uri = LSPApplication.prefs.getString(Constants.PREFS_STORAGE_DIRECTORY, null)?.toUri()
if (uri == null) {
shouldSelectDirectory = true
} else {
try {
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
navController.navigate(PageList.NewPatch.name)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to take persistable permission for saved uri", e)
LSPApplication.prefs.edit().putString(Constants.PREFS_STORAGE_DIRECTORY, null).apply()
shouldSelectDirectory = true
}
}
}
)
} }

View File

@ -3,7 +3,6 @@ package org.lsposed.lspatch.ui.page
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.os.Build
import android.util.Log import android.util.Log
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
@ -31,9 +30,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.Patcher
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
import org.lsposed.lspatch.TAG import org.lsposed.lspatch.TAG
@ -54,7 +50,7 @@ private enum class PatchState {
SELECTING, CONFIGURING, SUBMITTING, PATCHING, FINISHED, ERROR SELECTING, CONFIGURING, SUBMITTING, PATCHING, FINISHED, ERROR
} }
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NewPatchPage(entry: NavBackStackEntry) { fun NewPatchPage(entry: NavBackStackEntry) {
val navController = LocalNavController.current val navController = LocalNavController.current
@ -72,28 +68,6 @@ fun NewPatchPage(entry: NavBackStackEntry) {
} }
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val filePermissionState = rememberPermissionState(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (filePermissionState.status is PermissionStatus.Denied) {
AlertDialog(
onDismissRequest = {},
confirmButton = {
TextButton(onClick = { filePermissionState.launchPermissionRequest() }) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { navController.popBackStack() }) {
Text(stringResource(android.R.string.cancel))
}
},
title = { Text(stringResource(R.string.patch_permission_title)) },
text = { Text(stringResource(R.string.patch_permission_text)) }
)
return
}
}
Log.d(TAG, "NewPatchPage: $patchState") Log.d(TAG, "NewPatchPage: $patchState")
if (patchState == PatchState.SELECTING) { if (patchState == PatchState.SELECTING) {
LaunchedEffect(entry) { LaunchedEffect(entry) {

View File

@ -24,8 +24,9 @@
<!-- New Patch Page --> <!-- New Patch Page -->
<string name="page_new_patch">New Patch</string> <string name="page_new_patch">New Patch</string>
<string name="patch_permission_title">Permission Application</string> <string name="patch_select_dir_title">Select storage directory</string>
<string name="patch_permission_text">Please grant Write External Storage permission to allow saving patched apks to Download directory</string> <string name="patch_select_dir_text">Select a directory to store the patched apks</string>
<string name="patch_select_dir_error">Error when setting storage directory</string>
<string name="patch_mode">Patch Mode</string> <string name="patch_mode">Patch Mode</string>
<string name="patch_local">Local</string> <string name="patch_local">Local</string>
<string name="patch_local_desc">Patch an app without modules embedded.\nThe patched app need the manager running in background, and Xposed scope can be changed dynamically without re-patch.\nLocal patched apps can only run on the local device.</string> <string name="patch_local_desc">Patch an app without modules embedded.\nThe patched app need the manager running in background, and Xposed scope can be changed dynamically without re-patch.\nLocal patched apps can only run on the local device.</string>