* CI

* Add patch info to metadata

* Allow select local apks to patch

* Fix wrong indent
This commit is contained in:
Nullptr 2022-05-10 11:39:04 +08:00 committed by GitHub
parent 9e5f805d62
commit 6924c6a3b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 608 additions and 214 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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<Copy>("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")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -2,6 +2,8 @@ package org.lsposed.lspatch
object Constants {
const val PATCH_FILE_SUFFIX = "-lspatched.apk"
const val PREFS_KEYSTORE_PASSWORD = "keystore_password"
const val PREFS_KEYSTORE_ALIAS = "keystore_alias"
const val PREFS_KEYSTORE_ALIAS_PASSWORD = "keystore_alias_password"

View File

@ -7,6 +7,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.lsposed.hiddenapibypass.HiddenApiBypass
import org.lsposed.lspatch.util.ShizukuApi
import java.io.File
const val TAG = "LSPatch Manager"
@ -15,6 +16,7 @@ lateinit var lspApp: LSPApplication
class LSPApplication : Application() {
lateinit var prefs: SharedPreferences
lateinit var tmpApkDir: File
val globalScope = CoroutineScope(Dispatchers.Default)
@ -22,7 +24,9 @@ class LSPApplication : Application() {
super.onCreate()
HiddenApiBypass.addHiddenApiExemptions("");
lspApp = this
lspApp.filesDir.mkdir()
filesDir.mkdir()
tmpApkDir = cacheDir.resolve("apk")
tmpApkDir.mkdirs()
prefs = lspApp.getSharedPreferences("settings", Context.MODE_PRIVATE)
ShizukuApi.init()
}

View File

@ -1,17 +1,15 @@
package org.lsposed.lspatch
import android.content.Context
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.lsposed.lspatch.Constants.PATCH_FILE_SUFFIX
import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY
import org.lsposed.lspatch.config.MyKeyStore
import org.lsposed.patch.LSPatch
import org.lsposed.patch.util.Logger
import java.io.File
import java.io.IOException
import java.nio.file.Files
object Patcher {
@ -26,12 +24,10 @@ object Patcher {
private val verbose: Boolean,
private val embeddedModules: List<String>?
) {
lateinit var outputDir: File
fun toStringArray(): Array<String> {
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 ->

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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) }
)

View File

@ -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)

View File

@ -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<ManageViewModel>()
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<ManageViewModel>()
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
)
}
}
}
}
}

View File

@ -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<NewPatchViewModel>()
val snackbarHost = LocalSnackbarHost.current
val navController = LocalNavController.current
val lifecycleOwner = LocalLifecycleOwner.current
val isCancelled by entry.observeState<Boolean>("isCancelled")
LaunchedEffect(Unit) {
lspApp.tmpApkDir.listFiles()?.forEach(File::delete)
entry.savedStateHandle.getLiveData<AppInfo>("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<Pair<Int, String>>) : Lo
@Composable
private fun DoPatchBody(modifier: Modifier) {
val viewModel = viewModel<NewPatchViewModel>()
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)) }
)
},

View File

@ -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

View File

@ -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<SelectAppsViewModel>()
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 = {

View File

@ -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<Pair<AppInfo, PatchConfig>> 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")
}
}
}

View File

@ -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<AppInfo>
lateinit var patchOptions: Patcher.Options
private set
fun configurePatch(app: AppInfo) {
patchApp = app

View File

@ -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<AppInfo>()
private val appIcon = mutableMapOf<String, Drawable>()
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<AppInfo>()
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")
}
}

View File

@ -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<AppInfo>())
private set
private val appIcon = mutableMapOf<String, Drawable>()
suspend fun fetchAppList() {
withContext(Dispatchers.IO) {
val pm = lspApp.packageManager
val collection = mutableListOf<AppInfo>()
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<Int, String?> {
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<Uri>): Result<AppInfo> {
return withContext(Dispatchers.IO) {
runCatching {
val app = ApplicationInfo()
if (apks.size > 1) app.splitSourceDirs = Array<String?>(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
}
}
}
}

View File

@ -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) {

View File

@ -23,33 +23,32 @@
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="0.12397959"
android:scaleY="0.12397959"
android:translateX="-20.387754"
android:translateY="13.5">
android:scaleX="0.28265625"
android:scaleY="0.28265625"
android:translateX="17.82"
android:translateY="17.82">
<path
android:fillColor="#a697e8"
android:pathData="M280,0h640v640h-640z" />
<group>
<clip-path android:pathData="M280,0h640v640h-640z" />
<path
android:fillColor="#e4e0f7"
android:pathData="M600,1080m-600,0a600,600 0,1 1,1200 0a600,600 0,1 1,-1200 0" />
</group>
android:fillColor="#c9dc87"
android:pathData="M0,0h256v256h-256z" />
<path
android:fillColor="#607d8b"
android:pathData="M504.4,556.3l3.8,-21.8 -7.2,5.7 -0.7,-0.8c2.9,-3 5.8,-6.3 8.7,-9.7l0.2,-1.1 0.5,0.3c5,-5.9 9.9,-12.1 14.6,-18.6l5.1,6.2c-1.3,1.3 -2.7,2.4 -4.1,3.5h16.2c0.9,-0.9 1.8,-1.8 2.6,-2.7l4.3,6.5c-4.7,3 -9.6,5.8 -14.7,8.4h14.3l6.7,-3 -4.6,25.9 -5,-5.4h-9.7l2,2.2 -2.8,16.2h5.9l-0.1,0.3h7.8l10.4,-14.8 1.4,1.4 -12.9,18 -0.2,-0.3 -0.1,0.3h-11.3l0.1,-0.3c-3.5,0.5 -6.5,2.4 -9.1,5.7l1,-5.7 -0.3,0.3 4.1,-23.2h-0.5c-0.7,3.8 -2.7,6.9 -6,9.4 -4.3,3.8 -9.1,7.2 -14.5,10.2s-10.6,5.7 -15.7,8.1l-0.5,-1.6c4.4,-2.3 9.1,-5.3 14.2,-8.8 5.1,-3.5 9.3,-7 12.6,-10.6 1.8,-2 3,-4.2 3.6,-6.7h-12.9l-0.4,2.2 -7,4.6ZM512.3,546.6h12.9l2,-11.6h-12.9l-2,11.6ZM514.8,532.1h14.3c3.2,-3 6.4,-6.1 9.4,-9.2h-16.7c-3,2.7 -6.1,5.3 -9.2,7.8l2.2,1.4ZM532.7,539.4l-1.3,7.3h0.5l0.1,-0.5 0.4,0.5h12.9l2,-11.6h-18.6l3.8,4.3Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M573.1,523.2h16.2c9.5,0 16.8,3.3 14.9,14 -1.8,10.3 -10.4,14.8 -20,14.8h-6l-2.8,16.2h-10.2l7.9,-44.9ZM584.9,543.9c5.3,0 8.6,-2.3 9.4,-6.7 0.8,-4.4 -1.8,-5.9 -7.2,-5.9h-5.2l-2.2,12.6h5.2ZM582.9,549.3l8.2,-6.5 9.9,25.4h-11.4l-6.7,-18.9Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M607,551.1c2,-11.4 11.2,-17.9 19.8,-17.9s15.5,6.6 13.5,17.9c-2,11.4 -11.2,17.9 -19.8,17.9s-15.5,-6.6 -13.5,-17.9ZM629.9,551.1c1,-5.9 -0.4,-9.8 -4.6,-9.8s-6.9,3.8 -8,9.8c-1,5.9 0.4,9.7 4.6,9.7s6.9,-3.8 8,-9.7Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M645.2,551.1c2,-11.4 11.2,-17.9 19.8,-17.9s15.5,6.6 13.5,17.9c-2,11.4 -11.2,17.9 -19.8,17.9s-15.5,-6.6 -13.5,-17.9ZM668.2,551.1c1,-5.9 -0.4,-9.8 -4.6,-9.8s-6.9,3.8 -8,9.8c-1,5.9 0.4,9.7 4.6,9.7s6.9,-3.8 8,-9.7Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M684.2,555.7l2.4,-13.8h-4.7l1.3,-7.6 5.3,-0.4 2.8,-9.1h8.4l-1.6,9.1h8.2l-1.4,7.9h-8.2l-2.4,13.7c-0.7,3.9 0.8,5.4 3.5,5.4 1.1,0 2.4,-0.3 3.4,-0.6l0.3,7.4c-1.9,0.6 -4.5,1.2 -7.8,1.2 -8.4,0 -10.9,-5.3 -9.5,-13.3Z" />
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" />
<path
android:fillColor="#0e7c61"
android:pathData="M164.64,232.52l1.06,-6.01h-2.03l0.58,-3.29 2.31,-0.17 1.21,-3.95h3.65l-0.7,3.95h3.57l-0.61,3.46h-3.57l-1.05,5.96c-0.3,1.69 0.35,2.36 1.51,2.36 0.49,0 1.05,-0.14 1.46,-0.28l0.13,3.2c-0.83,0.25 -1.96,0.54 -3.4,0.54 -3.68,0 -4.73,-2.32 -4.12,-5.77Z" />
</group>
</vector>

View File

@ -23,42 +23,41 @@
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="0.12397959"
android:scaleY="0.12397959"
android:translateX="-20.387754"
android:translateY="13.5">
android:scaleX="0.28265625"
android:scaleY="0.28265625"
android:translateX="17.82"
android:translateY="17.82">
<path
android:fillColor="#a697e8"
android:pathData="M280,0h640v640h-640z" />
<group>
<clip-path android:pathData="M280,0h640v640h-640z" />
<path
android:fillColor="#e4e0f7"
android:pathData="M600,1080m-600,0a600,600 0,1 1,1200 0a600,600 0,1 1,-1200 0" />
</group>
android:fillColor="#c9dc87"
android:pathData="M0,0h256v256h-256z" />
<path
android:fillColor="#607d8b"
android:pathData="M485.6,524.2h14.1c8.8,0 15.4,3.1 15.4,11.5 0,11.8 -9.6,17 -20,17h-5.7l-3.3,16.5h-9.4l9,-44.9ZM495.6,545.2c6.7,0 10.1,-3.2 10.1,-8.1 0,-3.8 -2.7,-5.4 -7.5,-5.4h-4.7l-2.6,13.5h4.8ZM494,550.5l6.8,-6.1 9.7,24.7h-10l-6.6,-18.7Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M517,555.9c0,-13.2 9.8,-21.8 19.2,-21.8 8,0 13.3,5.6 13.3,14.1 0,13.2 -9.8,21.8 -19.2,21.8 -8,0 -13.3,-5.6 -13.3,-14.1ZM539.9,548.4c0,-4.2 -1.6,-6.7 -4.8,-6.7 -4.6,0 -8.6,6 -8.6,14 0,4.2 1.6,6.7 4.8,6.7 4.6,0 8.6,-6 8.6,-14Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M553.8,555.9c0,-13.2 9.8,-21.8 19.2,-21.8 8,0 13.3,5.6 13.3,14.1 0,13.2 -9.8,21.8 -19.2,21.8 -8,0 -13.3,-5.6 -13.3,-14.1ZM576.7,548.4c0,-4.2 -1.6,-6.7 -4.8,-6.7 -4.6,0 -8.6,6 -8.6,14 0,4.2 1.6,6.7 4.8,6.7 4.6,0 8.6,-6 8.6,-14Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M592.8,561c0,-1.5 0.3,-2.9 0.5,-4.3l3,-14.3h-4.6l1.4,-7 5,-0.4 2.9,-8.9h7.9l-1.7,8.9h7.7l-1.4,7.4h-8l-3,14.7c-0.1,0.8 -0.2,1.5 -0.2,2.3 0,2.3 1.1,3.3 3.6,3.3 1,0 1.9,-0.3 2.8,-0.8l1.7,6.6c-1.8,0.7 -4.5,1.5 -8,1.5 -6.9,0 -9.7,-3.8 -9.7,-9Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M616.3,563.3c0,-1.2 0.1,-2.5 0.5,-4.2l7.7,-38.3h9.4l-7.8,38.6c-0.1,0.7 -0.1,1 -0.1,1.3 0,1.2 0.6,1.6 1.3,1.6 0.4,0 0.6,0 1.2,-0.2l-0.3,7c-1.1,0.4 -2.8,0.8 -5,0.8 -4.9,0 -6.9,-2.5 -6.9,-6.7Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M653.8,534.1c8.2,0 11.1,5.7 11.1,12.8 0,3.2 -1.2,6.8 -1.7,7.8h-19.5c-0.2,5.7 3.2,8.1 7.7,8.1 2.1,0 4.6,-1.2 6.3,-2.4l3.3,5.9c-2.7,1.9 -7,3.6 -12.3,3.6 -8.3,0 -14,-5.5 -14,-14.6 0,-12.7 9.9,-21.2 19,-21.2ZM656.9,549c0.2,-0.7 0.3,-1.6 0.3,-2.5 0,-2.8 -1.2,-5.1 -4.6,-5.1 -3.3,0 -6.6,2.6 -8.2,7.6h12.4Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M666.4,563.4l5.5,-4.5c2.4,2.9 4.9,4.2 7.4,4.2s4.8,-1.4 4.8,-3.2c0,-2 -2,-2.9 -5.9,-5.1 -3.9,-2.2 -6.9,-5.2 -6.9,-9.5 0,-6.4 5.9,-11.1 13.3,-11.1 4.7,0 8.4,2.3 11.2,5.2l-5.1,4.9c-1.7,-1.7 -3.7,-3.1 -6.1,-3.1 -2.6,0 -4.4,1.5 -4.4,3.3 0,2.2 2.9,3.2 5.6,4.7 4.1,2.2 7.2,4.9 7.2,9.6 0,6.7 -6,11.2 -14.2,11.2 -4.1,0 -9.4,-2.4 -12.3,-6.6Z" />
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" />
<path
android:fillColor="#607d8b"
android:pathData="M695.6,563.4l5.5,-4.5c2.4,2.9 4.9,4.2 7.4,4.2s4.8,-1.4 4.8,-3.2c0,-2 -2,-2.9 -5.9,-5.1 -3.9,-2.2 -6.9,-5.2 -6.9,-9.5 0,-6.4 5.9,-11.1 13.3,-11.1 4.7,0 8.4,2.3 11.2,5.2l-5.1,4.9c-1.7,-1.7 -3.7,-3.1 -6.1,-3.1 -2.6,0 -4.4,1.5 -4.4,3.3 0,2.2 2.9,3.2 5.6,4.7 4.1,2.2 7.2,4.9 7.2,9.6 0,6.7 -6,11.2 -14.2,11.2 -4.1,0 -9.4,-2.4 -12.3,-6.6Z" />
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" />
<path
android:fillColor="#0e7c61"
android:pathData="M169.6,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" />
</group>
</vector>

View File

@ -18,19 +18,17 @@
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="1.2375"
android:scaleY="1.2375"
android:translateX="24.3"
android:translateY="24.3">
android:scaleX="0.27421874"
android:scaleY="0.27421874"
android:translateX="18.9"
android:translateY="18.9">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M24.1,27.6l-6.7,6.7c-0.4,0.5 -1,0.7 -1.6,0.7c-0.6,0 -1.2,-0.2 -1.6,-0.7l-4.4,-4.4C9.3,29.4 9,28.9 9,28.3c0,-0.6 0.2,-1.2 0.7,-1.6l6.6,-6.7l-6.6,-6.7c-0.4,-0.4 -0.7,-0.9 -0.7,-1.5c-0.1,-0.6 0.1,-1.1 0.5,-1.5l4.6,-4.6c0.4,-0.4 1,-0.7 1.6,-0.7c0.6,0 1.2,0.2 1.6,0.7l6.7,6.7l6.7,-6.7c0.4,-0.4 1,-0.7 1.6,-0.7c0.6,0 1.2,0.2 1.6,0.7l4.4,4.4c0.4,0.4 0.6,1 0.7,1.6c0,0.6 -0.2,1.2 -0.7,1.6L31.7,20l6.7,6.7c0.4,0.4 0.7,1 0.7,1.6c0,0.6 -0.2,1.2 -0.7,1.6l-4.6,4.6c-0.4,0.4 -0.9,0.6 -1.5,0.5c-0.6,-0.1 -1.1,-0.3 -1.5,-0.7L24.1,27.6zM24.1,18.7c0.4,0 0.8,-0.1 1,-0.4c0.3,-0.3 0.4,-0.6 0.4,-1s-0.1,-0.8 -0.4,-1c-0.3,-0.3 -0.6,-0.4 -1,-0.4c-0.4,0 -0.8,0.1 -1,0.4c-0.3,0.3 -0.4,0.6 -0.4,1c0,0.4 0.1,0.8 0.4,1C23.3,18.5 23.6,18.7 24.1,18.7zM17.8,18.5l4.7,-4.7l-6.7,-6.7c0,0 0,0 0,0c0,0 0,0 0,0l-4.6,4.6c0,0 0,0 0,0c0,0 0,0 0,0L17.8,18.5zM21.3,21.4c0.4,0 0.8,-0.1 1,-0.4s0.4,-0.6 0.4,-1s-0.1,-0.8 -0.4,-1s-0.6,-0.4 -1,-0.4c-0.4,0 -0.8,0.1 -1,0.4c-0.3,0.3 -0.4,0.6 -0.4,1s0.1,0.8 0.4,1C20.5,21.3 20.9,21.4 21.3,21.4zM24.1,24.1c0.4,0 0.8,-0.1 1,-0.4s0.4,-0.6 0.4,-1s-0.1,-0.8 -0.4,-1s-0.6,-0.4 -1,-0.4c-0.4,0 -0.8,0.1 -1,0.4c-0.3,0.3 -0.4,0.6 -0.4,1c0,0.4 0.1,0.8 0.4,1C23.3,24 23.6,24.1 24.1,24.1zM26.8,21.4c0.4,0 0.8,-0.1 1,-0.4c0.3,-0.3 0.4,-0.6 0.4,-1s-0.1,-0.8 -0.4,-1c-0.3,-0.3 -0.6,-0.4 -1,-0.4s-0.8,0.1 -1,0.4c-0.3,0.3 -0.4,0.6 -0.4,1s0.1,0.8 0.4,1C26,21.3 26.3,21.4 26.8,21.4zM25.5,26.1l6.7,6.7l0,0l0,0l4.6,-4.6l0,0l0,0l-6.7,-6.7L25.5,26.1zM19.4,15.4L19.4,15.4L19.4,15.4L19.4,15.4L19.4,15.4zM28.6,24.6C28.6,24.6 28.6,24.6 28.6,24.6S28.6,24.6 28.6,24.6S28.6,24.6 28.6,24.6S28.6,24.6 28.6,24.6z"
tools:ignore="VectorPath" />
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" />
</group>
</vector>

View File

@ -21,12 +21,16 @@
<!-- Manage Page -->
<string name="page_manage">Manage</string>
<string name="manage_loading">Loading</string>
<string name="manage_no_apps">No patched apps yet</string>
<!-- New Patch Page -->
<string name="page_new_patch">New Patch</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_from_storage">Select apk(s) from storage</string>
<string name="patch_from_applist">Select an installed app</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>

View File

@ -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<Jar>("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<Jar>("buildRelease") {
dependsOn(":appstub:copyRelease")
dependsOn(":patch-loader:copyRelease")
configure("release")
}

View File

@ -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");

View File

@ -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<Copy>("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)

View File

@ -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;
}
}

View File

@ -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() {
}
}