Fix saving apks
This commit is contained in:
parent
792a6f887a
commit
3b5fa88d17
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue