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.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.ui:ui:1.1.1")
implementation("androidx.compose.ui:ui-tooling:1.1.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha05")
implementation("androidx.navigation:navigation-compose:2.5.0-alpha03")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-beta01")
implementation("androidx.navigation:navigation-compose:2.5.0-beta01")
implementation("androidx.preference:preference:1.2.0")
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-permissions:0.24.5-alpha")
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.5-alpha")
implementation("com.google.android.material:material:1.5.0")
implementation("dev.rikka.shizuku:api:12.1.0")

View File

@ -7,10 +7,6 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application
android:name=".LSPApplication"
android:allowBackup="true"
@ -34,7 +30,8 @@
android:name=".manager.ModuleProvider"
android:authorities="org.lsposed.lspatch.provider"
android:enabled="true"
android:exported="true" />
android:exported="true"
tools:ignore="ExportedContentProvider" />
<provider
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
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.lsposed.lspatch.config.MyKeyStore
@ -53,38 +51,29 @@ object Patcher {
suspend fun patch(context: Context, logger: Logger, options: Options) {
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()
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)
.walk()
.filter { it.isFile }
.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 ->
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)
}
}
}
}
logger.i("Patched files are saved to $download")
logger.i("Patched files are saved to ${root.uri.lastPathSegment}")
}
}
}

View File

@ -1,22 +1,33 @@
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.filled.Add
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.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.TAG
import org.lsposed.lspatch.ui.util.LocalNavController
import java.io.IOException
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ManagePage() {
val navController = LocalNavController.current
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = { TopBar() },
floatingActionButton = {
Fab { navController.navigate(PageList.NewPatch.name) }
}
floatingActionButton = { Fab(snackbarHostState) },
snackbarHost = { SnackbarHost(snackbarHostState) }
) { innerPadding ->
}
@ -30,8 +41,68 @@ private fun TopBar() {
}
@Composable
private fun Fab(onClick: () -> Unit) {
FloatingActionButton(onClick = onClick) {
Icon(Icons.Filled.Add, stringResource(R.string.add))
private fun Fab(snackbarHostState: SnackbarHostState) {
val context = LocalContext.current
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.ClipboardManager
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.compose.animation.animateContentSize
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.lifecycle.viewmodel.compose.viewModel
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.R
import org.lsposed.lspatch.TAG
@ -54,7 +50,7 @@ private enum class PatchState {
SELECTING, CONFIGURING, SUBMITTING, PATCHING, FINISHED, ERROR
}
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewPatchPage(entry: NavBackStackEntry) {
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")
if (patchState == PatchState.SELECTING) {
LaunchedEffect(entry) {

View File

@ -24,8 +24,9 @@
<!-- New Patch Page -->
<string name="page_new_patch">New Patch</string>
<string name="patch_permission_title">Permission Application</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_title">Select storage 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_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>