diff --git a/gradle.properties b/gradle.properties index c43f29b..3e202fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.nonTransitiveRClass=true android.enableR8.fullMode=true android.useAndroidX=true -agpVersion=7.1.2 +agpVersion=7.1.3 diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index e99756a..5cf4912 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -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") diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml index 7953c1f..a980f8d 100644 --- a/manager/src/main/AndroidManifest.xml +++ b/manager/src/main/AndroidManifest.xml @@ -7,10 +7,6 @@ android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> - - + android:exported="true" + tools:ignore="ExportedContentProvider" /> 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 $download") + logger.i("Patched files are saved to ${root.uri.lastPathSegment}") } } } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/ManagePage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/ManagePage.kt index a90241f..f045b9a 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/ManagePage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/ManagePage.kt @@ -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 + } + } + } + ) } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt index 47ce0b4..5b125c2 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt @@ -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) { diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index 43872c3..556828b 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -24,8 +24,9 @@ New Patch - Permission Application - Please grant Write External Storage permission to allow saving patched apks to Download directory + Select storage directory + Select a directory to store the patched apks + Error when setting storage directory Patch Mode Local 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.