From 6924c6a3b543d80794ab3250f0047212b351264f Mon Sep 17 00:00:00 2001 From: Nullptr <52071314+Dr-TSNG@users.noreply.github.com> Date: Tue, 10 May 2022 11:39:04 +0800 Subject: [PATCH] CI (#57) * CI * Add patch info to metadata * Allow select local apks to patch * Fix wrong indent --- .github/workflows/main.yml | 107 ++++++++++-- README.md | 4 +- build.gradle.kts | 4 + manager/build.gradle.kts | 12 +- manager/src/main/ic_launcher-playstore.png | Bin 25309 -> 26198 bytes .../java/org/lsposed/lspatch/Constants.kt | 2 + .../org/lsposed/lspatch/LSPApplication.kt | 6 +- .../main/java/org/lsposed/lspatch/Patcher.kt | 21 +-- .../lspatch/ui/activity/MainActivity.kt | 4 +- .../lsposed/lspatch/ui/component/AppItem.kt | 20 ++- .../lsposed/lspatch/ui/component/SearchBar.kt | 12 +- .../org/lsposed/lspatch/ui/page/HomePage.kt | 18 ++- .../org/lsposed/lspatch/ui/page/ManagePage.kt | 152 ++++++++++++++++-- .../lsposed/lspatch/ui/page/NewPatchPage.kt | 63 ++++++-- .../org/lsposed/lspatch/ui/page/PageList.kt | 7 +- .../lsposed/lspatch/ui/page/SelectAppsPage.kt | 11 +- .../lspatch/ui/viewmodel/ManageViewModel.kt | 27 ++++ .../lspatch/ui/viewmodel/NewPatchViewModel.kt | 2 + .../ui/viewmodel/SelectAppViewModel.kt | 45 +----- ...ckageInstaller.kt => LSPPackageManager.kt} | 92 ++++++++++- .../org/lsposed/lspatch/util/ShizukuApi.kt | 5 +- .../ic_launcher_background.xml | 43 +++-- .../res/drawable/ic_launcher_background.xml | 55 ++++--- .../res/drawable/ic_launcher_foreground.xml | 14 +- manager/src/main/res/values/strings.xml | 4 + patch-jar/build.gradle.kts | 31 ++-- .../main/java/org/lsposed/patch/LSPatch.java | 22 ++- share/java/build.gradle.kts | 22 +++ .../lsposed/lspatch/share/PatchConfig.java | 2 + .../org.lsposed.lspatch.share/LSPConfig.java | 15 ++ 30 files changed, 608 insertions(+), 214 deletions(-) create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/ManageViewModel.kt rename manager/src/main/java/org/lsposed/lspatch/util/{LSPPackageInstaller.kt => LSPPackageManager.kt} (54%) create mode 100644 share/java/src/template/java/org.lsposed.lspatch.share/LSPConfig.java diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1b589e9..ff96bb7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,7 @@ name: Android CI on: + workflow_dispatch: push: branches: [ master ] pull_request: @@ -9,6 +10,7 @@ jobs: build: name: Build on ${{ matrix.os }} runs-on: ${{ matrix.os }} + if: ${{ !startsWith(github.event.head_commit.message, '[skip ci]') }} strategy: fail-fast: false matrix: @@ -21,32 +23,115 @@ jobs: submodules: 'recursive' fetch-depth: 0 + - name: Write key + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' + run: | + if [ ! -z "${{ secrets.KEY_STORE }}" ]; then + echo androidStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> gradle.properties + echo androidKeyAlias='${{ secrets.ALIAS }}' >> gradle.properties + echo androidKeyPassword='${{ secrets.KEY_PASSWORD }}' >> gradle.properties + echo androidStoreFile='key.jks' >> gradle.properties + echo ${{ secrets.KEY_STORE }} | base64 --decode > key.jks + fi + - name: Set up JDK 11 uses: actions/setup-java@v2 with: java-version: '11' distribution: 'adopt' - - name: Build Debug + - name: Cache gradle dependencies + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + !~/.gradle/caches/build-cache-* + key: gradle-deps-core-${{ hashFiles('**/build.gradle.kts') }} + restore-keys: | + gradle-deps + + - name: Cache gradle build + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches/build-cache-* + ~/.gradle/buildOutputCleanup/cache.properties + key: gradle-builds-core-${{ github.sha }} + restore-keys: | + gradle-builds + + - name: Cache native build + uses: actions/cache@v2 + with: + path: | + ~/.ccache + patch-loader/build/.lto-cache + key: native-cache-${{ github.sha }} + restore-keys: native-cache- + + - name: Install dep run: | + sudo apt-get install -y ccache ninja-build + ccache -o max_size=1G + ccache -o hash_dir=false + ccache -o compiler_check='%compiler% -dumpmachine; %compiler% -dumpversion' + ccache -zp + + - name: Build with Gradle + run: | + [ $(du -s ~/.gradle/wrapper | awk '{ print $1 }') -gt 250000 ] && rm -rf ~/.gradle/wrapper/* || true + find ~/.gradle/caches -exec touch -d "2 days ago" {} + || true + echo 'org.gradle.caching=true' >> gradle.properties + echo 'org.gradle.parallel=true' >> gradle.properties + echo 'org.gradle.vfs.watch=true' >> gradle.properties echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties - ./gradlew buildDebug + echo 'android.native.buildOutput=verbose' >> gradle.properties + ln -s $(which ninja) $(dirname $(which cmake)) # https://issuetracker.google.com/issues/206099937 + echo "cmake.dir=$(dirname $(dirname $(which cmake)))" >> local.properties + ./gradlew buildAll + ccache -s + - name: Upload Debug artifact uses: actions/upload-artifact@v2 with: name: lspatch-debug - path: | - out/lspatch.jar - out/manager.apk + path: out/debug/* - - name: Build Release - run: | - echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties - ./gradlew buildRelease - name: Upload Release artifact uses: actions/upload-artifact@v2 with: name: lspatch-release + path: out/release/* + + - name: Upload mappings + uses: actions/upload-artifact@v2 + with: + name: mappings path: | - out/lspatch.jar - out/manager.apk + patch-loader/build/outputs/mapping + manager/build/outputs/mapping + + - name: Upload symbols + uses: actions/upload-artifact@v2 + with: + name: symbols + path: | + patch-loader/build/symbols + + - name: Post to channel + if: ${{ github.event_name != 'pull_request' && success() && github.ref == 'refs/heads/master' }} + env: + CHANNEL_ID: ${{ secrets.CHANNEL_ID }} + BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + COMMIT_URL: ${{ github.event.head_commit.url }} + run: | + if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then + export jarRelease=$(find out/release -name "*.jar") + export managerRelease=$(find out/release -name "*.apk") + export jarDebug=$(find out/debug -name "*.jar") + export managerDebug=$(find out/debug -name "*.apk") + ESCAPED=`python3 -c 'import json,os,urllib.parse; msg = json.dumps(os.environ["COMMIT_MESSAGE"]); print(urllib.parse.quote(msg if len(msg) <= 1024 else json.dumps(os.environ["COMMIT_URL"])))'` + curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FjarRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FmanagerRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FjarDebug%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FmanagerDebug%22%2C%22caption%22:${ESCAPED}%7D%5D" -F jarRelease="@$jarRelease" -F managerRelease="@$managerRelease" -F jarDebug="@$jarDebug" -F managerDebug="@$managerDebug" + fi diff --git a/README.md b/README.md index 9273f2a..871ace1 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ You can contribute translation [here](https://lsposed.crowdin.com/lspatch). ## Credits -- [LSPosed](https://github.com/LSPosed/LSPosed): core framework -- [Xpatch](https://github.com/WindySha/Xpatch): fork source +- [LSPosed](https://github.com/LSPosed/LSPosed): Core framework +- [Xpatch](https://github.com/WindySha/Xpatch): Fork source - [Apkzlib](https://android.googlesource.com/platform/tools/apkzlib): Repacking tool ## License diff --git a/build.gradle.kts b/build.gradle.kts index 93f55fc..c979f8c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -63,6 +63,10 @@ listOf("Debug", "Release").forEach { variant -> } } +tasks.register("buildAll") { + dependsOn("buildDebug", "buildRelease") +} + fun Project.configureBaseExtension() { extensions.findByType(BaseExtension::class)?.run { compileSdkVersion(androidCompileSdkVersion) diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index dd4221f..8925e5e 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -1,5 +1,7 @@ val defaultManagerPackageName: String by rootProject.extra val apiCode: Int by rootProject.extra +val verCode: Int by rootProject.extra +val verName: String by rootProject.extra val coreVerCode: Int by rootProject.extra val coreVerName: String by rootProject.extra @@ -13,10 +15,6 @@ plugins { android { defaultConfig { applicationId = defaultManagerPackageName - - buildConfigField("int", "API_CODE", """$apiCode""") - buildConfigField("int", "CORE_VERSION_CODE", """$coreVerCode""") - buildConfigField("String", "CORE_VERSION_NAME", """"$coreVerName"""") } buildTypes { @@ -58,8 +56,8 @@ afterEvaluate { task("build$variantCapped") { dependsOn(tasks["assemble$variantCapped"]) from(variant.outputs.map { it.outputFile }) - into("${rootProject.projectDir}/out") - rename(".*.apk", "manager.apk") + into("${rootProject.projectDir}/out/$variantLowered") + rename(".*.apk", "manager-v$verName-$verCode-$variantLowered.apk") } } } @@ -69,6 +67,7 @@ dependencies { implementation(projects.patch) implementation(projects.services.daemonService) implementation(projects.share.android) + implementation(projects.share.java) compileOnly("dev.rikka.hidden:stub:2.3.1") implementation("dev.rikka.hidden:compat:2.3.1") @@ -86,6 +85,7 @@ dependencies { implementation("com.google.accompanist:accompanist-navigation-animation: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.code.gson:gson:2.9.0") implementation("dev.rikka.shizuku:api:12.1.0") implementation("dev.rikka.shizuku:provider:12.1.0") implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") diff --git a/manager/src/main/ic_launcher-playstore.png b/manager/src/main/ic_launcher-playstore.png index 90798eea77b2a7a84a65731154367ea3f902dd74..8ea1a48c66b928e08bd9c10ba8bae39e7d8d7e6c 100644 GIT binary patch literal 26198 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 ) { - lateinit var outputDir: File - fun toStringArray(): Array { return buildList { addAll(apkPaths) - add("-o"); add(outputDir.absolutePath) + add("-o"); add(lspApp.tmpApkDir.absolutePath) if (debuggable) add("-d") add("-l"); add(sigbypassLevel.toString()) add("--v1"); add(v1.toString()) @@ -49,26 +45,23 @@ object Patcher { } } - suspend fun patch(context: Context, logger: Logger, options: Options) { + suspend fun patch(logger: Logger, options: Options) { withContext(Dispatchers.IO) { - options.outputDir = Files.createTempDirectory("patch").toFile() - options.outputDir.listFiles()?.forEach(File::delete) LSPatch(logger, *options.toStringArray()).doCommandLine() val uri = lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)?.toUri() ?: throw IOException("Uri is null") - val root = DocumentFile.fromTreeUri(context, uri) + val root = DocumentFile.fromTreeUri(lspApp, uri) ?: throw IOException("DocumentFile is null") root.listFiles().forEach { - if (it.name?.endsWith("-lspatched.apk") == true) it.delete() + if (it.name?.endsWith(PATCH_FILE_SUFFIX) == true) it.delete() } - options.outputDir - .walk() + lspApp.tmpApkDir.walk() .filter { it.isFile } .forEach { apk -> val file = root.createFile("application/vnd.android.package-archive", apk.name) ?: throw IOException("Failed to create output file") - val output = context.contentResolver.openOutputStream(file.uri) + val output = lspApp.contentResolver.openOutputStream(file.uri) ?: throw IOException("Failed to open output stream") output.use { apk.inputStream().use { input -> diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt b/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt index b7acf17..83e7378 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt @@ -66,7 +66,9 @@ private fun MainNavHost(navController: NavHostController, modifier: Modifier) { ) { PageList.values().forEach { page -> val sb = StringBuilder(page.name) - page.arguments.forEach { sb.append("/{${it.name}}") } + if (page.arguments.isNotEmpty()) { + sb.append(page.arguments.joinToString(",", "?") { "${it.name}={${it.name}}" }) + } composable(route = sb.toString(), arguments = page.arguments, content = page.body) } } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt index 3d02118..ce089f1 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.accompanist.drawablepainter.rememberDrawablePainter @@ -23,10 +24,10 @@ fun AppItem( icon: Drawable, label: String, packageName: String, - additionalInfo: (@Composable () -> Unit)? = null, onClick: () -> Unit, onLongClick: (() -> Unit)? = null, - checked: Boolean? = null + checked: Boolean? = null, + additionalContent: (@Composable () -> Unit)? = null, ) { Column( modifier = modifier @@ -47,10 +48,17 @@ fun AppItem( modifier = Modifier.size(32.dp), tint = Color.Unspecified ) - Column(Modifier.weight(1f)) { - Text(text = label, style = MaterialTheme.typography.bodyMedium) - Text(text = packageName, style = MaterialTheme.typography.bodySmall) - additionalInfo?.invoke() + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { + Text(label) + Text( + text = packageName, + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.bodySmall + ) + additionalContent?.invoke() } if (checked != null) { Checkbox( diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/SearchBar.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/SearchBar.kt index 15f4b89..04cdea7 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/component/SearchBar.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/SearchBar.kt @@ -41,8 +41,13 @@ fun SearchAppBar( val focusRequester = remember { FocusRequester() } var onSearch by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - if (onSearch) focusRequester.requestFocus() + if (onSearch) { + LaunchedEffect(Unit) { focusRequester.requestFocus() } + } + DisposableEffect(Unit) { + onDispose { + keyboardController?.hide() + } } SmallTopAppBar( @@ -75,8 +80,9 @@ fun SearchAppBar( trailingIcon = { IconButton( onClick = { - onClearClick() onSearch = false + keyboardController?.hide() + onClearClick() }, content = { Icon(Icons.Filled.Close, null) } ) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt index b9fd957..0565e98 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt @@ -13,7 +13,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -23,8 +26,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch -import org.lsposed.lspatch.BuildConfig import org.lsposed.lspatch.R +import org.lsposed.lspatch.share.LSPConfig import org.lsposed.lspatch.ui.util.HtmlText import org.lsposed.lspatch.ui.util.LocalSnackbarHost import org.lsposed.lspatch.util.ShizukuApi @@ -38,12 +41,11 @@ fun HomePage() { modifier = Modifier .padding(innerPadding) .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { ShizukuCard() - Spacer(Modifier.height(16.dp)) InfoCard() - Spacer(Modifier.height(16.dp)) SupportCard() } } @@ -164,13 +166,13 @@ private fun InfoCard() { Text(text = texts.second, style = MaterialTheme.typography.bodyMedium) } - infoCardContent(stringResource(R.string.home_api_version) to "${BuildConfig.API_CODE}") + infoCardContent(stringResource(R.string.home_api_version) to "${LSPConfig.instance.API_CODE}") Spacer(Modifier.height(24.dp)) - infoCardContent(stringResource(R.string.home_lspatch_version) to BuildConfig.VERSION_NAME + " (${BuildConfig.VERSION_CODE})") + infoCardContent(stringResource(R.string.home_lspatch_version) to LSPConfig.instance.VERSION_NAME + " (${LSPConfig.instance.VERSION_CODE})") Spacer(Modifier.height(24.dp)) - infoCardContent(stringResource(R.string.home_framework_version) to BuildConfig.CORE_VERSION_NAME + " (${BuildConfig.CORE_VERSION_CODE})") + infoCardContent(stringResource(R.string.home_framework_version) to LSPConfig.instance.CORE_VERSION_NAME + " (${LSPConfig.instance.CORE_VERSION_CODE})") Spacer(Modifier.height(24.dp)) infoCardContent(stringResource(R.string.home_system_version) to apiVersion) 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 202bb84..d246d2a 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 @@ -5,41 +5,56 @@ import android.content.Intent import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.lsposed.lspatch.* +import kotlinx.coroutines.withContext import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY import org.lsposed.lspatch.R +import org.lsposed.lspatch.TAG +import org.lsposed.lspatch.lspApp +import org.lsposed.lspatch.ui.component.AppItem import org.lsposed.lspatch.ui.util.LocalNavController import org.lsposed.lspatch.ui.util.LocalSnackbarHost +import org.lsposed.lspatch.ui.viewmodel.ManageViewModel +import org.lsposed.lspatch.util.LSPPackageManager import java.io.IOException @OptIn(ExperimentalMaterial3Api::class) @Composable fun ManagePage() { + val viewModel = viewModel() + Scaffold( topBar = { TopBar() }, floatingActionButton = { Fab() } ) { innerPadding -> - Text( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - text = "This page is not yet implemented", - textAlign = TextAlign.Center - ) + Box(Modifier.padding(innerPadding)) { + Body() + } } } @@ -57,6 +72,7 @@ private fun Fab() { val navController = LocalNavController.current val scope = rememberCoroutineScope() var shouldSelectDirectory by remember { mutableStateOf(false) } + var showNewPatchDialog by remember { mutableStateOf(false) } val errorText = stringResource(R.string.patch_select_dir_error) val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { @@ -67,7 +83,7 @@ private fun Fab() { context.contentResolver.takePersistableUriPermission(uri, takeFlags) lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, uri.toString()).apply() Log.i(TAG, "Storage directory: ${uri.path}") - navController.navigate(PageList.NewPatch.name) + showNewPatchDialog = true } catch (e: Exception) { Log.e(TAG, "Error when requesting saving directory", e) scope.launch { snackbarHost.showSnackbar(errorText) } @@ -103,6 +119,58 @@ private fun Fab() { ) } + if (showNewPatchDialog) { + AlertDialog( + onDismissRequest = { showNewPatchDialog = false }, + confirmButton = {}, + dismissButton = { + TextButton( + content = { Text(stringResource(android.R.string.cancel)) }, + onClick = { showNewPatchDialog = false } + ) + }, + title = { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.page_new_patch), + textAlign = TextAlign.Center + ) + }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + TextButton( + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), + onClick = { + navController.navigate(PageList.NewPatch.name + "?from=storage") + showNewPatchDialog = false + } + ) { + Text( + modifier = Modifier.padding(vertical = 8.dp), + text = stringResource(R.string.patch_from_storage), + style = MaterialTheme.typography.bodyLarge + ) + } + TextButton( + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), + onClick = { + navController.navigate(PageList.NewPatch.name + "?from=applist") + showNewPatchDialog = false + } + ) { + Text( + modifier = Modifier.padding(vertical = 8.dp), + text = stringResource(R.string.patch_from_applist), + style = MaterialTheme.typography.bodyLarge + ) + } + } + } + ) + } + FloatingActionButton( content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) }, onClick = { @@ -115,7 +183,7 @@ private fun Fab() { context.contentResolver.takePersistableUriPermission(uri, takeFlags) if (DocumentFile.fromTreeUri(context, uri)?.exists() == false) throw IOException("Storage directory was deleted") }.onSuccess { - navController.navigate(PageList.NewPatch.name) + showNewPatchDialog = true }.onFailure { Log.w(TAG, "Failed to take persistable permission for saved uri", it) lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, null).apply() @@ -125,3 +193,61 @@ private fun Fab() { } ) } + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun Body() { + val viewModel = viewModel() + + LaunchedEffect(Unit) { + if (LSPPackageManager.appList.isEmpty()) { + withContext(Dispatchers.IO) { + LSPPackageManager.fetchAppList() + } + } + } + + if (viewModel.appList.isEmpty()) { + Box(Modifier.fillMaxSize()) { + Text( + modifier = Modifier.align(Alignment.Center), + text = run { + if (LSPPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading) + else stringResource(R.string.manage_no_apps) + }, + fontFamily = FontFamily.Serif, + style = MaterialTheme.typography.headlineSmall + ) + } + } else { + LazyColumn { + items( + items = viewModel.appList, + key = { it.first.app.packageName } + ) { + AppItem( + modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)), + icon = LSPPackageManager.getIcon(it.first), + label = it.first.label, + packageName = it.first.app.packageName, + onClick = {} + ) { + val text = buildAnnotatedString { + val (text, color) = + if (it.second.useManager) stringResource(R.string.patch_local) to MaterialTheme.colorScheme.secondary + else stringResource(R.string.patch_portable) to MaterialTheme.colorScheme.tertiary + append(AnnotatedString(text, SpanStyle(color = color))) + append(" ") + append(it.second.lspConfig.VERSION_CODE.toString()) + } + Text( + text = text, + fontWeight = FontWeight.SemiBold, + fontFamily = FontFamily.Serif, + style = MaterialTheme.typography.bodySmall + ) + } + } + } + } +} 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 82a4435..40b05ca 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 @@ -6,6 +6,8 @@ import android.content.Context import android.content.pm.PackageInstaller import android.util.Log import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring @@ -35,6 +37,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavBackStackEntry import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.R import org.lsposed.lspatch.lspApp @@ -43,23 +46,26 @@ import org.lsposed.lspatch.ui.component.ShimmerAnimation import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox import org.lsposed.lspatch.ui.component.settings.SettingsItem import org.lsposed.lspatch.ui.util.* -import org.lsposed.lspatch.ui.viewmodel.AppInfo import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState -import org.lsposed.lspatch.util.LSPPackageInstaller +import org.lsposed.lspatch.util.LSPPackageManager +import org.lsposed.lspatch.util.LSPPackageManager.AppInfo import org.lsposed.lspatch.util.ShizukuApi import org.lsposed.patch.util.Logger +import java.io.File private const val TAG = "NewPatchPage" @OptIn(ExperimentalMaterial3Api::class) @Composable -fun NewPatchPage(entry: NavBackStackEntry) { +fun NewPatchPage(from: String, entry: NavBackStackEntry) { val viewModel = viewModel() + val snackbarHost = LocalSnackbarHost.current val navController = LocalNavController.current val lifecycleOwner = LocalLifecycleOwner.current val isCancelled by entry.observeState("isCancelled") LaunchedEffect(Unit) { + lspApp.tmpApkDir.listFiles()?.forEach(File::delete) entry.savedStateHandle.getLiveData("appInfo").observe(lifecycleOwner) { viewModel.configurePatch(it) } @@ -67,9 +73,28 @@ fun NewPatchPage(entry: NavBackStackEntry) { Log.d(TAG, "PatchState: ${viewModel.patchState}") if (viewModel.patchState == PatchState.SELECTING) { + val storageLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks -> + if (apks.isEmpty()) { + navController.popBackStack() + return@rememberLauncherForActivityResult + } + runBlocking { + LSPPackageManager.getAppInfoFromApks(apks) + .onSuccess { + viewModel.configurePatch(it) + } + .onFailure { + lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: "Unknown error") } + navController.popBackStack() + } + } + } LaunchedEffect(Unit) { if (isCancelled == true) navController.popBackStack() - else navController.navigate(PageList.SelectApps.name + "/false") + else when (from) { + "storage" -> storageLauncher.launch(arrayOf("application/vnd.android.package-archive")) + "applist" -> navController.navigate(PageList.SelectApps.name + "?multiSelect=false") + } } } else { Scaffold( @@ -173,7 +198,7 @@ private fun PatchOptionsBody(modifier: Modifier) { desc = stringResource(R.string.patch_portable_desc), extraContent = { TextButton( - onClick = { navController.navigate(PageList.SelectApps.name + "/true") }, + onClick = { navController.navigate(PageList.SelectApps.name + "?multiSelect=true") }, content = { Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge) } ) } @@ -265,7 +290,6 @@ private class PatchLogger(private val logs: MutableList>) : Lo @Composable private fun DoPatchBody(modifier: Modifier) { val viewModel = viewModel() - val context = LocalContext.current val snackbarHost = LocalSnackbarHost.current val navController = LocalNavController.current val scope = rememberCoroutineScope() @@ -274,14 +298,14 @@ private fun DoPatchBody(modifier: Modifier) { LaunchedEffect(Unit) { try { - Patcher.patch(context, logger, viewModel.patchOptions) + Patcher.patch(logger, viewModel.patchOptions) viewModel.finishPatch() } catch (t: Throwable) { logger.e(t.message.orEmpty()) logger.e(t.stackTraceToString()) viewModel.failPatch() } finally { - viewModel.patchOptions.outputDir.deleteRecursively() + lspApp.tmpApkDir.listFiles()?.forEach(File::delete) } } @@ -336,14 +360,15 @@ private fun DoPatchBody(modifier: Modifier) { var installing by rememberSaveable { mutableStateOf(false) } if (installing) InstallDialog(viewModel.patchApp) { status, message -> scope.launch { + LSPPackageManager.fetchAppList() installing = false if (status == PackageInstaller.STATUS_SUCCESS) { lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) } navController.popBackStack() - } else { + } else if (status != LSPPackageManager.STATUS_USER_CANCELLED) { val result = snackbarHost.showSnackbar(installFailed, copyError) if (result == SnackbarResult.ActionPerformed) { - val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("LSPatch", message)) } } @@ -382,7 +407,7 @@ private fun DoPatchBody(modifier: Modifier) { Button( modifier = Modifier.weight(1f), onClick = { - val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("LSPatch", logs.joinToString { it.second + "\n" })) }, content = { Text(stringResource(R.string.patch_copy_error)) } @@ -398,20 +423,26 @@ private fun DoPatchBody(modifier: Modifier) { @Composable private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { val scope = rememberCoroutineScope() - var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalled(patchApp.app.packageName)) } + var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) } var installing by remember { mutableStateOf(0) } val doInstall = suspend { Log.i(TAG, "Installing app ${patchApp.app.packageName}") installing = 1 - val (status, message) = LSPPackageInstaller.install() + val (status, message) = LSPPackageManager.install() installing = 0 Log.i(TAG, "Installation end: $status, $message") onFinish(status, message) } + LaunchedEffect(Unit) { + if (!uninstallFirst) { + doInstall() + } + } + if (uninstallFirst) { AlertDialog( - onDismissRequest = { onFinish(-2, "User cancelled") }, + onDismissRequest = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, confirmButton = { TextButton( onClick = { @@ -419,7 +450,7 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}") uninstallFirst = false installing = 2 - val (status, message) = LSPPackageInstaller.uninstall(patchApp.app.packageName) + val (status, message) = LSPPackageManager.uninstall(patchApp.app.packageName) installing = 0 Log.i(TAG, "Uninstallation end: $status, $message") if (status == PackageInstaller.STATUS_SUCCESS) { @@ -434,7 +465,7 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { }, dismissButton = { TextButton( - onClick = { onFinish(-2, "User cancelled") }, + onClick = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, content = { Text(stringResource(android.R.string.cancel)) } ) }, diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt index 1675bb2..937ef95 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt @@ -44,13 +44,16 @@ enum class PageList( body = { SettingsPage() } ), NewPatch( - body = { NewPatchPage(this) } + arguments = listOf( + navArgument("from") { type = NavType.StringType } + ), + body = { NewPatchPage(arguments!!.getString("from")!!, this) } ), SelectApps( arguments = listOf( navArgument("multiSelect") { type = NavType.BoolType } ), - body = { SelectAppsPage(this) } + body = { SelectAppsPage(arguments!!.getBoolean("multiSelect")) } ); val title: String diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsPage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsPage.kt index 44329df..cd2d372 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsPage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsPage.kt @@ -28,16 +28,15 @@ import org.lsposed.lspatch.ui.component.SearchAppBar import org.lsposed.lspatch.ui.util.LocalNavController import org.lsposed.lspatch.ui.util.observeState import org.lsposed.lspatch.ui.util.setState -import org.lsposed.lspatch.ui.viewmodel.AppInfo import org.lsposed.lspatch.ui.viewmodel.SelectAppsViewModel +import org.lsposed.lspatch.util.LSPPackageManager +import org.lsposed.lspatch.util.LSPPackageManager.AppInfo @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SelectAppsPage(entry: NavBackStackEntry) { +fun SelectAppsPage(multiSelect: Boolean) { val viewModel = viewModel() val navController = LocalNavController.current - val multiSelect = entry.arguments?.get("multiSelect") as? Boolean - ?: throw IllegalArgumentException("multiSelect is null") var searchPackage by remember { mutableStateOf("") } val filter: (AppInfo) -> Boolean = { @@ -113,7 +112,7 @@ private fun SingleSelect() { ) { AppItem( modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)), - icon = viewModel.getIcon(it), + icon = LSPPackageManager.getIcon(it), label = it.label, packageName = it.app.packageName, onClick = { @@ -140,7 +139,7 @@ private fun MultiSelect() { val checked = selected!!.contains(it) AppItem( modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)), - icon = viewModel.getIcon(it), + icon = LSPPackageManager.getIcon(it), label = it.label, packageName = it.app.packageName, onClick = { diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/ManageViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/ManageViewModel.kt new file mode 100644 index 0000000..623c87b --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/ManageViewModel.kt @@ -0,0 +1,27 @@ +package org.lsposed.lspatch.ui.viewmodel + +import android.util.Base64 +import android.util.Log +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.lifecycle.ViewModel +import com.google.gson.Gson +import org.lsposed.lspatch.share.PatchConfig +import org.lsposed.lspatch.util.LSPPackageManager +import org.lsposed.lspatch.util.LSPPackageManager.AppInfo + +private const val TAG = "ManageViewModel" + +class ManageViewModel : ViewModel() { + + val appList: List> by derivedStateOf { + LSPPackageManager.appList.mapNotNull { appInfo -> + appInfo.app.metaData?.getString("lspatch")?.let { + val json = Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) + appInfo to Gson().fromJson(json, PatchConfig::class.java) + } + }.also { + Log.d(TAG, "Loaded ${it.size} patched apps") + } + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt index 4c7f12f..3081e2e 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.ViewModel import org.lsposed.lspatch.Patcher +import org.lsposed.lspatch.util.LSPPackageManager.AppInfo class NewPatchViewModel : ViewModel() { @@ -27,6 +28,7 @@ class NewPatchViewModel : ViewModel() { private set lateinit var embeddedModules: SnapshotStateList lateinit var patchOptions: Patcher.Options + private set fun configurePatch(app: AppInfo) { patchApp = app diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/SelectAppViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/SelectAppViewModel.kt index aed9eb4..61bb2e7 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/SelectAppViewModel.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/SelectAppViewModel.kt @@ -1,31 +1,17 @@ package org.lsposed.lspatch.ui.viewmodel -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.os.Parcelable import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.parcelize.Parcelize -import org.lsposed.lspatch.lspApp -import java.text.Collator -import java.util.* +import org.lsposed.lspatch.util.LSPPackageManager +import org.lsposed.lspatch.util.LSPPackageManager.AppInfo private const val TAG = "SelectAppViewModel" -@Parcelize -class AppInfo(val app: ApplicationInfo, val label: String) : Parcelable - -private var appList = listOf() -private val appIcon = mutableMapOf() - class SelectAppsViewModel : ViewModel() { init { @@ -40,28 +26,13 @@ class SelectAppsViewModel : ViewModel() { fun filterAppList(refresh: Boolean, filter: (AppInfo) -> Boolean) { viewModelScope.launch { - if (appList.isEmpty() || refresh) refreshAppList() - filteredList = appList.filter(filter) - } - } - - fun getIcon(appInfo: AppInfo) = appIcon[appInfo.app.packageName]!! - - private suspend fun refreshAppList() { - Log.d(TAG, "Start refresh apps") - isRefreshing = true - val collection = mutableListOf() - withContext(Dispatchers.IO) { - val pm = lspApp.packageManager - pm.getInstalledApplications(PackageManager.GET_META_DATA).forEach { - val label = pm.getApplicationLabel(it) - appIcon[it.packageName] = pm.getApplicationIcon(it) - collection.add(AppInfo(it, label.toString())) + if (LSPPackageManager.appList.isEmpty() || refresh) { + isRefreshing = true + LSPPackageManager.fetchAppList() + isRefreshing = false } - collection.sortWith(compareBy(Collator.getInstance(Locale.getDefault())) { it.label }) + filteredList = LSPPackageManager.appList.filter(filter) + Log.d(TAG, "Filtered ${filteredList.size} apps") } - appList = collection - isRefreshing = false - Log.d(TAG, "Refreshed ${appList.size} apps") } } diff --git a/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageInstaller.kt b/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt similarity index 54% rename from manager/src/main/java/org/lsposed/lspatch/util/LSPPackageInstaller.kt rename to manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt index d8fd033..6a16c77 100644 --- a/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageInstaller.kt +++ b/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt @@ -1,20 +1,64 @@ package org.lsposed.lspatch.util import android.content.Intent +import android.content.pm.ApplicationInfo import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Parcelable +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import hidden.HiddenApiBridge import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import org.lsposed.lspatch.Constants.PATCH_FILE_SUFFIX import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY import org.lsposed.lspatch.lspApp +import org.lsposed.patch.util.ManifestParser +import java.io.File import java.io.IOException +import java.text.Collator +import java.util.* import java.util.concurrent.CountDownLatch +import java.util.zip.ZipFile import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -object LSPPackageInstaller { +object LSPPackageManager { + + const val TAG = "LSPPackageManager" + + @Parcelize + class AppInfo(val app: ApplicationInfo, val label: String) : Parcelable + + const val STATUS_USER_CANCELLED = -2 + + var appList by mutableStateOf(listOf()) + private set + + private val appIcon = mutableMapOf() + + suspend fun fetchAppList() { + withContext(Dispatchers.IO) { + val pm = lspApp.packageManager + val collection = mutableListOf() + pm.getInstalledApplications(PackageManager.GET_META_DATA).forEach { + val label = pm.getApplicationLabel(it) + collection.add(AppInfo(it, label.toString())) + appIcon[it.packageName] = pm.getApplicationIcon(it) + } + collection.sortWith(compareBy(Collator.getInstance(Locale.getDefault())) { it.label }) + appList = collection + } + } + + fun getIcon(appInfo: AppInfo) = appIcon[appInfo.app.packageName]!! suspend fun install(): Pair { var status = PackageInstaller.STATUS_FAILURE @@ -31,11 +75,12 @@ object LSPPackageInstaller { ?: throw IOException("Uri is null") val root = DocumentFile.fromTreeUri(lspApp, uri) ?: throw IOException("DocumentFile is null") - root.listFiles().forEach { apk -> - val input = lspApp.contentResolver.openInputStream(apk.uri) + root.listFiles().forEach { file -> + if (file.name?.endsWith(PATCH_FILE_SUFFIX) != true) return@forEach + val input = lspApp.contentResolver.openInputStream(file.uri) ?: throw IOException("Cannot open input stream") input.use { - session.openWrite(apk.name!!, 0, input.available().toLong()).use { output -> + session.openWrite(file.name!!, 0, input.available().toLong()).use { output -> input.copyTo(output) session.fsync(output) } @@ -94,4 +139,43 @@ object LSPPackageInstaller { } return Pair(status, message) } + + suspend fun getAppInfoFromApks(apks: List): Result { + return withContext(Dispatchers.IO) { + runCatching { + val app = ApplicationInfo() + if (apks.size > 1) app.splitSourceDirs = Array(apks.size - 1) { null } + apks.forEachIndexed { index, uri -> + val src = DocumentFile.fromSingleUri(lspApp, uri) + ?: throw IOException("DocumentFile is null") + val dst = lspApp.tmpApkDir.resolve(src.name!!) + val input = lspApp.contentResolver.openInputStream(uri) + ?: throw IOException("InputStream is null") + input.use { + dst.outputStream().use { output -> + input.copyTo(output) + } + } + ZipFile(dst).use { zip -> + val entry = zip.getEntry("AndroidManifest.xml") + ?: throw IOException("AndroidManifest.xml is not found") + zip.getInputStream(entry).use { + val info = ManifestParser.parseManifestFile(it) + if (app.packageName != null && app.packageName != info.packageName) { + throw IOException("Selected apks are not of the same app") + } + app.packageName = info.packageName + } + } + if (index == 0) app.sourceDir = dst.absolutePath + else app.splitSourceDirs[index - 1] = dst.absolutePath + } + AppInfo(app, app.packageName) + }.recoverCatching { t -> + lspApp.tmpApkDir.listFiles()?.forEach(File::delete) + Log.e(TAG, "Failed to load apks", t) + throw t + } + } + } } diff --git a/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt b/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt index 699a34d..47423fc 100644 --- a/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt +++ b/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt @@ -63,8 +63,9 @@ object ShizukuApi { return constructor.newInstance(iSession) } - fun isPackageInstalled(packageName: String): Boolean { - return iPackageManager.getPackageInfo(packageName, 0, 0 /* TODO: userId */) != null + fun isPackageInstalledWithoutPatch(packageName: String): Boolean { + val app = iPackageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA, 0 /* TODO: userId */) + return (app != null) && (app.metaData?.containsKey("lspatch") != true) } fun uninstallPackage(packageName: String, intentSender: IntentSender) { diff --git a/manager/src/main/res/drawable-zh-rCN/ic_launcher_background.xml b/manager/src/main/res/drawable-zh-rCN/ic_launcher_background.xml index 174257f..baa363e 100644 --- a/manager/src/main/res/drawable-zh-rCN/ic_launcher_background.xml +++ b/manager/src/main/res/drawable-zh-rCN/ic_launcher_background.xml @@ -23,33 +23,32 @@ android:viewportWidth="108" android:viewportHeight="108"> + android:scaleX="0.28265625" + android:scaleY="0.28265625" + android:translateX="17.82" + android:translateY="17.82"> - - - - + android:fillColor="#c9dc87" + android:pathData="M0,0h256v256h-256z" /> + android:fillAlpha="0.7" + android:fillColor="#fff" + android:pathData="M256,256v-27.38c-26.01,-15.34 -73.6,-25.62 -128,-25.62S26.01,213.28 0,228.62v27.38H256Z" + android:strokeAlpha="0.7" /> + android:fillColor="#0e7c61" + android:pathData="M86.37,232.77l1.67,-9.49 -3.13,2.46 -0.29,-0.35c1.25,-1.33 2.51,-2.73 3.79,-4.22l0.08,-0.47 0.21,0.12c2.17,-2.58 4.29,-5.27 6.35,-8.09l2.22,2.7c-0.57,0.55 -1.16,1.05 -1.79,1.52h7.03c0.38,-0.39 0.76,-0.78 1.14,-1.17l1.85,2.81c-2.03,1.33 -4.16,2.54 -6.38,3.63h6.21l2.92,-1.29 -1.98,11.25 -2.17,-2.34h-4.22l0.89,0.94 -1.24,7.03h2.58l-0.02,0.12h3.4l4.53,-6.45 0.6,0.59 -5.6,7.85 -0.1,-0.12 -0.02,0.12h-4.92l0.02,-0.12c-1.53,0.23 -2.84,1.05 -3.95,2.46l0.43,-2.46 -0.14,0.12 1.78,-10.08h-0.23c-0.29,1.64 -1.16,3.01 -2.6,4.1 -1.85,1.64 -3.95,3.13 -6.29,4.45 -2.34,1.33 -4.62,2.5 -6.83,3.52l-0.23,-0.7c1.9,-1.02 3.96,-2.29 6.18,-3.81 2.22,-1.52 4.06,-3.07 5.5,-4.63 0.78,-0.86 1.3,-1.83 1.57,-2.93h-5.62l-0.17,0.94 -3.05,1.99ZM89.81,228.55h5.62l0.89,-5.04h-5.62l-0.89,5.04ZM90.93,222.22h6.21c1.41,-1.33 2.77,-2.66 4.1,-3.98h-7.27c-1.3,1.17 -2.63,2.31 -4,3.4l0.95,0.59ZM98.69,225.38l-0.56,3.16h0.23l0.04,-0.23 0.19,0.23h5.62l0.89,-5.04h-8.09l1.66,1.88Z" /> + android:fillColor="#0e7c61" + android:pathData="M116.29,218.38h7.04c4.15,0 7.29,1.44 6.47,6.09 -0.79,4.49 -4.53,6.43 -8.68,6.43h-2.62l-1.24,7.04h-4.42l3.45,-19.55ZM121.38,227.38c2.33,0 3.75,-1 4.09,-2.92 0.34,-1.93 -0.79,-2.58 -3.12,-2.58h-2.26l-0.97,5.5h2.26ZM120.52,229.72l3.56,-2.83 4.29,11.03h-4.95l-2.9,-8.2Z" /> + android:fillColor="#0e7c61" + android:pathData="M131.02,230.49c0.87,-4.95 4.86,-7.81 8.63,-7.81s6.75,2.86 5.87,7.81c-0.87,4.94 -4.86,7.79 -8.62,7.79s-6.75,-2.86 -5.88,-7.79ZM141,230.49c0.45,-2.58 -0.16,-4.25 -1.98,-4.25s-3.03,1.67 -3.48,4.25c-0.45,2.58 0.16,4.24 1.98,4.24s3.02,-1.66 3.48,-4.24Z" /> + android:fillColor="#0e7c61" + android:pathData="M147.67,230.49c0.87,-4.95 4.86,-7.81 8.63,-7.81s6.75,2.86 5.87,7.81c-0.87,4.94 -4.86,7.79 -8.62,7.79s-6.75,-2.86 -5.88,-7.79ZM157.65,230.49c0.45,-2.58 -0.16,-4.25 -1.98,-4.25s-3.03,1.67 -3.48,4.25c-0.45,2.58 0.16,4.24 1.98,4.24s3.02,-1.66 3.48,-4.24Z" /> + diff --git a/manager/src/main/res/drawable/ic_launcher_background.xml b/manager/src/main/res/drawable/ic_launcher_background.xml index 229cb10..c07aa2a 100644 --- a/manager/src/main/res/drawable/ic_launcher_background.xml +++ b/manager/src/main/res/drawable/ic_launcher_background.xml @@ -23,42 +23,41 @@ android:viewportWidth="108" android:viewportHeight="108"> + android:scaleX="0.28265625" + android:scaleY="0.28265625" + android:translateX="17.82" + android:translateY="17.82"> - - - - + android:fillColor="#c9dc87" + android:pathData="M0,0h256v256h-256z" /> + android:fillAlpha="0.7" + android:fillColor="#fff" + android:pathData="M256,256v-27.38c-26.01,-15.34 -73.6,-25.62 -128,-25.62S26.01,213.28 0,228.62v27.38H256Z" + android:strokeAlpha="0.7" /> + android:fillColor="#0e7c61" + android:pathData="M78.19,218.79h6.13c3.84,0 6.69,1.36 6.69,5 0,5.12 -4.17,7.38 -8.73,7.38h-2.48l-1.42,7.17h-4.11l3.91,-19.55ZM82.56,227.91c2.91,0 4.39,-1.4 4.39,-3.52 0,-1.64 -1.16,-2.35 -3.26,-2.35h-2.06l-1.15,5.88h2.08ZM81.88,230.21l2.97,-2.63 4.22,10.76h-4.33l-2.85,-8.12Z" /> + android:fillColor="#0e7c61" + android:pathData="M91.9,232.57c0,-5.74 4.24,-9.47 8.34,-9.47 3.47,0 5.78,2.44 5.78,6.13 0,5.74 -4.24,9.47 -8.34,9.47 -3.47,0 -5.78,-2.44 -5.78,-6.13ZM101.87,229.32c0,-1.82 -0.7,-2.91 -2.08,-2.91 -2,0 -3.73,2.62 -3.73,6.09 0,1.82 0.7,2.91 2.08,2.91 2,0 3.73,-2.61 3.73,-6.09Z" /> + android:fillColor="#0e7c61" + android:pathData="M107.89,232.57c0,-5.74 4.24,-9.47 8.34,-9.47 3.47,0 5.78,2.44 5.78,6.13 0,5.74 -4.24,9.47 -8.34,9.47 -3.47,0 -5.78,-2.44 -5.78,-6.13ZM117.86,229.32c0,-1.82 -0.7,-2.91 -2.08,-2.91 -2,0 -3.73,2.62 -3.73,6.09 0,1.82 0.7,2.91 2.08,2.91 2,0 3.73,-2.61 3.73,-6.09Z" /> + android:fillColor="#0e7c61" + android:pathData="M124.86,234.76c0,-0.64 0.11,-1.25 0.22,-1.87l1.3,-6.23h-1.98l0.61,-3.03 2.19,-0.17 1.26,-3.89h3.44l-0.74,3.89h3.37l-0.62,3.2h-3.47l-1.3,6.39c-0.06,0.36 -0.07,0.67 -0.07,0.99 0,0.98 0.48,1.46 1.56,1.46 0.42,0 0.83,-0.15 1.21,-0.34l0.75,2.88c-0.79,0.3 -1.96,0.66 -3.49,0.66 -3,0 -4.21,-1.66 -4.21,-3.94Z" /> + android:fillColor="#0e7c61" + android:pathData="M135.1,235.79c0,-0.53 0.06,-1.07 0.22,-1.82l3.33,-16.65h4.1l-3.38,16.81c-0.06,0.3 -0.06,0.43 -0.06,0.57 0,0.51 0.27,0.69 0.57,0.69 0.17,0 0.28,0 0.53,-0.07l-0.13,3.04c-0.5,0.17 -1.22,0.33 -2.17,0.33 -2.13,0 -3.01,-1.1 -3.01,-2.91Z" /> + android:fillColor="#0e7c61" + android:pathData="M151.39,223.1c3.55,0 4.85,2.49 4.85,5.58 0,1.4 -0.53,2.94 -0.76,3.39h-8.48c-0.1,2.48 1.39,3.53 3.35,3.53 0.92,0 2,-0.51 2.73,-1.04l1.46,2.59c-1.19,0.84 -3.04,1.56 -5.34,1.56 -3.6,0 -6.09,-2.39 -6.09,-6.38 0,-5.51 4.31,-9.23 8.27,-9.23ZM152.75,229.56c0.07,-0.3 0.13,-0.7 0.13,-1.11 0,-1.2 -0.5,-2.2 -2,-2.2 -1.42,0 -2.87,1.12 -3.55,3.31h5.42Z" /> + android:fillColor="#0e7c61" + android:pathData="M156.88,235.81l2.39,-1.96c1.03,1.26 2.13,1.85 3.21,1.85s2.07,-0.61 2.07,-1.41c0,-0.85 -0.87,-1.24 -2.56,-2.2 -1.69,-0.95 -3.01,-2.25 -3.01,-4.14 0,-2.79 2.58,-4.85 5.76,-4.85 2.05,0 3.67,1.01 4.88,2.27l-2.23,2.13c-0.73,-0.74 -1.6,-1.35 -2.66,-1.35 -1.13,0 -1.9,0.64 -1.9,1.42 0,0.95 1.26,1.38 2.45,2.06 1.77,0.96 3.12,2.14 3.12,4.18 0,2.93 -2.59,4.89 -6.19,4.89 -1.8,0 -4.11,-1.04 -5.33,-2.89Z" /> + diff --git a/manager/src/main/res/drawable/ic_launcher_foreground.xml b/manager/src/main/res/drawable/ic_launcher_foreground.xml index 5cf924c..debdc46 100644 --- a/manager/src/main/res/drawable/ic_launcher_foreground.xml +++ b/manager/src/main/res/drawable/ic_launcher_foreground.xml @@ -18,19 +18,17 @@ --> + android:scaleX="0.27421874" + android:scaleY="0.27421874" + android:translateX="18.9" + android:translateY="18.9"> + android:fillColor="#fff" + android:pathData="M167.13,107.36l27.34,-27.34c2.65,-2.67 2.65,-6.97 0,-9.64l-29.8,-29.39c-2.67,-2.65 -6.97,-2.65 -9.64,0l-27.34,27.34 -27.34,-27.34c-1.22,-1.21 -2.86,-1.92 -4.58,-1.98 -1.79,0 -3.51,0.72 -4.79,1.98l-29.67,29.67c-2.47,2.63 -2.47,6.73 0,9.37l27.34,27.34 -27.34,27.34c-2.65,2.67 -2.65,6.97 0,9.64l29.67,29.67c2.67,2.65 6.97,2.65 9.64,0l27.34,-27.34 27.34,27.34c1.29,1.28 3.04,1.99 4.85,1.98 1.82,0.01 3.56,-0.7 4.85,-1.98l29.67,-29.67c2.65,-2.67 2.65,-6.97 0,-9.64l-27.55,-27.34ZM127.69,86.85c3.78,0 6.84,3.06 6.84,6.84s-3.06,6.84 -6.84,6.84 -6.84,-3.06 -6.84,-6.84 3.06,-6.84 6.84,-6.84ZM95.77,100.52l-24.81,-25.02 24.81,-24.81 25.09,24.75 -25.09,25.09ZM114.02,114.19c-3.78,0 -6.84,-3.06 -6.84,-6.84s3.06,-6.84 6.84,-6.84 6.84,3.06 6.84,6.84 -3.06,6.84 -6.84,6.84ZM127.69,127.86c-3.78,0 -6.84,-3.06 -6.84,-6.84s3.06,-6.84 6.84,-6.84 6.84,3.06 6.84,6.84 -3.06,6.84 -6.84,6.84ZM141.36,100.52c3.78,0 6.84,3.06 6.84,6.84s-3.06,6.84 -6.84,6.84 -6.84,-3.06 -6.84,-6.84 3.06,-6.84 6.84,-6.84ZM159.54,164.37l-24.81,-24.75 24.81,-24.81 24.75,24.75 -24.75,24.81Z" /> diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index d48278f..4759d90 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -21,12 +21,16 @@ Manage + Loading + No patched apps yet New Patch Select storage directory Select a directory to store the patched apks Error when setting storage directory + Select apk(s) from storage + Select an installed app 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. diff --git a/patch-jar/build.gradle.kts b/patch-jar/build.gradle.kts index 86fc9b0..2a7b37e 100644 --- a/patch-jar/build.gradle.kts +++ b/patch-jar/build.gradle.kts @@ -1,3 +1,5 @@ +val verCode: Int by rootProject.extra +val verName: String by rootProject.extra val androidSourceCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra @@ -14,9 +16,9 @@ dependencies { implementation(projects.patch) } -tasks.jar { - archiveBaseName.set("lspatch") - destinationDirectory.set(file("${rootProject.projectDir}/out")) +fun Jar.configure(variant: String) { + archiveBaseName.set("jar-v$verName-$verCode-$variant") + destinationDirectory.set(file("${rootProject.projectDir}/out/$variant")) manifest { attributes("Main-Class" to "org.lsposed.patch.LSPatch") } @@ -31,21 +33,14 @@ tasks.jar { exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "META-INF/*.MF", "META-INF/*.txt", "META-INF/versions/**") } -val jar = tasks.jar.get() - -tasks.register("buildDebug") { - jar.dependsOn(":appstub:copyDebug") - jar.dependsOn(":patch-loader:copyDebug") - dependsOn(tasks.build) +tasks.register("buildDebug") { + dependsOn(":appstub:copyDebug") + dependsOn(":patch-loader:copyDebug") + configure("debug") } -tasks.register("buildRelease") { - jar.dependsOn(":appstub:copyRelease") - jar.dependsOn(":patch-loader:copyRelease") - dependsOn(tasks.build) -} - -tasks["build"].doLast { - println("Build to " + jar.archiveFile) - println("Try \'java -jar " + jar.archiveFileName + "\' find more help") +tasks.register("buildRelease") { + dependsOn(":appstub:copyRelease") + dependsOn(":patch-loader:copyRelease") + configure("release") } diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java index e79de8e..2e9af92 100644 --- a/patch/src/main/java/org/lsposed/patch/LSPatch.java +++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java @@ -21,6 +21,7 @@ import com.wind.meditor.property.ModificationProperty; import com.wind.meditor.utils.NodeValue; import org.apache.commons.io.FilenameUtils; +import org.lsposed.lspatch.share.LSPConfig; import org.lsposed.lspatch.share.PatchConfig; import org.lsposed.patch.util.ApkSignatureHelper; import org.lsposed.patch.util.JavaLogger; @@ -38,8 +39,10 @@ import java.security.KeyStore; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.Set; @@ -160,7 +163,11 @@ public class LSPatch { var outputDir = new File(outputPath); outputDir.mkdirs(); - File outputFile = new File(outputDir, String.format("%s-lv%s-lspatched.apk", FilenameUtils.getBaseName(apkFileName), sigbypassLevel)).getAbsoluteFile(); + File outputFile = new File(outputDir, String.format( + Locale.getDefault(), "%s-%d-lspatched.apk", + FilenameUtils.getBaseName(apkFileName), + LSPConfig.instance.VERSION_CODE) + ).getAbsoluteFile(); if (outputFile.exists() && !forceOverwrite) throw new PatchError(outputPath + " exists. Use --force to overwrite"); @@ -237,7 +244,10 @@ public class LSPatch { logger.i("Patching apk..."); // modify manifest - try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open()))) { + var config = new PatchConfig(useManager, sigbypassLevel, null, appComponentFactory); + var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8); + var metadata = Base64.getEncoder().encodeToString(configBytes); + try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata))) { dstZFile.add(ANDROID_MANIFEST_XML, is); } catch (Throwable e) { throw new PatchError("Error when modifying manifest", e); @@ -273,9 +283,8 @@ public class LSPatch { } // save lspatch config to asset.. - var config = new PatchConfig(useManager, sigbypassLevel, originalSignature, appComponentFactory); - var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8); - + config = new PatchConfig(useManager, sigbypassLevel, originalSignature, appComponentFactory); + configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8); try (var is = new ByteArrayInputStream(configBytes)) { dstZFile.add(CONFIG_ASSET_PATH, is); } catch (Throwable e) { @@ -343,13 +352,14 @@ public class LSPatch { } } - private byte[] modifyManifestFile(InputStream is) throws IOException { + private byte[] modifyManifestFile(InputStream is, String metadata) throws IOException { ModificationProperty property = new ModificationProperty(); if (overrideVersionCode) property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, 1)); property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag)); property.addApplicationAttribute(new AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY)); + property.addMetaData(new ModificationProperty.MetaData("lspatch", metadata)); // TODO: replace query_all with queries -> manager property.addUsesPermission("android.permission.QUERY_ALL_PACKAGES"); diff --git a/share/java/build.gradle.kts b/share/java/build.gradle.kts index 6db7e78..0f7e994 100644 --- a/share/java/build.gradle.kts +++ b/share/java/build.gradle.kts @@ -1,3 +1,8 @@ +val apiCode: Int by rootProject.extra +val verCode: Int by rootProject.extra +val verName: String by rootProject.extra +val coreVerCode: Int by rootProject.extra +val coreVerName: String by rootProject.extra val androidSourceCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra @@ -9,3 +14,20 @@ java { sourceCompatibility = androidSourceCompatibility targetCompatibility = androidTargetCompatibility } + +val generateTask = task("generateJava") { + val template = mapOf( + "apiCode" to apiCode, + "verCode" to verCode, + "verName" to verName, + "coreVerCode" to coreVerCode, + "coreVerName" to coreVerName + ) + inputs.properties(template) + from("src/template/java") + into("$buildDir/generated/java") + expand(template) +} + +sourceSets["main"].java.srcDir("$buildDir/generated/java") +tasks["compileJava"].dependsOn(generateTask) diff --git a/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java b/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java index 406d71e..60cb892 100644 --- a/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java +++ b/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java @@ -6,11 +6,13 @@ public class PatchConfig { public final int sigBypassLevel; public final String originalSignature; public final String appComponentFactory; + public final LSPConfig lspConfig; public PatchConfig(boolean useManager, int sigBypassLevel, String originalSignature, String appComponentFactory) { this.useManager = useManager; this.sigBypassLevel = sigBypassLevel; this.originalSignature = originalSignature; this.appComponentFactory = appComponentFactory; + this.lspConfig = LSPConfig.instance; } } diff --git a/share/java/src/template/java/org.lsposed.lspatch.share/LSPConfig.java b/share/java/src/template/java/org.lsposed.lspatch.share/LSPConfig.java new file mode 100644 index 0000000..0e8c497 --- /dev/null +++ b/share/java/src/template/java/org.lsposed.lspatch.share/LSPConfig.java @@ -0,0 +1,15 @@ +package org.lsposed.lspatch.share; + +public class LSPConfig { + + public static final LSPConfig instance = new LSPConfig(); + + public final int API_CODE = ${apiCode}; + public final int VERSION_CODE = ${verCode}; + public final String VERSION_NAME = "${verName}"; + public final int CORE_VERSION_CODE = ${coreVerCode}; + public final String CORE_VERSION_NAME = "${coreVerName}"; + + private LSPConfig() { + } +}