Fix saving apks
This commit is contained in:
parent
792a6f887a
commit
3b5fa88d17
|
|
@ -5,4 +5,4 @@ android.nonTransitiveRClass=true
|
|||
android.enableR8.fullMode=true
|
||||
android.useAndroidX=true
|
||||
|
||||
agpVersion=7.1.2
|
||||
agpVersion=7.1.3
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package org.lsposed.lspatch
|
||||
|
||||
object Constants {
|
||||
|
||||
const val PREFS_STORAGE_DIRECTORY = "storage_directory"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -45,7 +43,7 @@ object Patcher {
|
|||
add("-m"); addAll(embeddedModules)
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue