* 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 name: Android CI
on: on:
workflow_dispatch:
push: push:
branches: [ master ] branches: [ master ]
pull_request: pull_request:
@ -9,6 +10,7 @@ jobs:
build: build:
name: Build on ${{ matrix.os }} name: Build on ${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
if: ${{ !startsWith(github.event.head_commit.message, '[skip ci]') }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -21,32 +23,115 @@ jobs:
submodules: 'recursive' submodules: 'recursive'
fetch-depth: 0 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 - name: Set up JDK 11
uses: actions/setup-java@v2 uses: actions/setup-java@v2
with: with:
java-version: '11' java-version: '11'
distribution: 'adopt' 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: | 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 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 - name: Upload Debug artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: lspatch-debug name: lspatch-debug
path: | path: out/debug/*
out/lspatch.jar
out/manager.apk
- name: Build Release
run: |
echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties
./gradlew buildRelease
- name: Upload Release artifact - name: Upload Release artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: lspatch-release name: lspatch-release
path: out/release/*
- name: Upload mappings
uses: actions/upload-artifact@v2
with:
name: mappings
path: | path: |
out/lspatch.jar patch-loader/build/outputs/mapping
out/manager.apk 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 ## Credits
- [LSPosed](https://github.com/LSPosed/LSPosed): core framework - [LSPosed](https://github.com/LSPosed/LSPosed): Core framework
- [Xpatch](https://github.com/WindySha/Xpatch): fork source - [Xpatch](https://github.com/WindySha/Xpatch): Fork source
- [Apkzlib](https://android.googlesource.com/platform/tools/apkzlib): Repacking tool - [Apkzlib](https://android.googlesource.com/platform/tools/apkzlib): Repacking tool
## License ## License

View File

@ -63,6 +63,10 @@ listOf("Debug", "Release").forEach { variant ->
} }
} }
tasks.register("buildAll") {
dependsOn("buildDebug", "buildRelease")
}
fun Project.configureBaseExtension() { fun Project.configureBaseExtension() {
extensions.findByType(BaseExtension::class)?.run { extensions.findByType(BaseExtension::class)?.run {
compileSdkVersion(androidCompileSdkVersion) compileSdkVersion(androidCompileSdkVersion)

View File

@ -1,5 +1,7 @@
val defaultManagerPackageName: String by rootProject.extra val defaultManagerPackageName: String by rootProject.extra
val apiCode: Int 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 coreVerCode: Int by rootProject.extra
val coreVerName: String by rootProject.extra val coreVerName: String by rootProject.extra
@ -13,10 +15,6 @@ plugins {
android { android {
defaultConfig { defaultConfig {
applicationId = defaultManagerPackageName applicationId = defaultManagerPackageName
buildConfigField("int", "API_CODE", """$apiCode""")
buildConfigField("int", "CORE_VERSION_CODE", """$coreVerCode""")
buildConfigField("String", "CORE_VERSION_NAME", """"$coreVerName"""")
} }
buildTypes { buildTypes {
@ -58,8 +56,8 @@ afterEvaluate {
task<Copy>("build$variantCapped") { task<Copy>("build$variantCapped") {
dependsOn(tasks["assemble$variantCapped"]) dependsOn(tasks["assemble$variantCapped"])
from(variant.outputs.map { it.outputFile }) from(variant.outputs.map { it.outputFile })
into("${rootProject.projectDir}/out") into("${rootProject.projectDir}/out/$variantLowered")
rename(".*.apk", "manager.apk") rename(".*.apk", "manager-v$verName-$verCode-$variantLowered.apk")
} }
} }
} }
@ -69,6 +67,7 @@ dependencies {
implementation(projects.patch) implementation(projects.patch)
implementation(projects.services.daemonService) implementation(projects.services.daemonService)
implementation(projects.share.android) implementation(projects.share.android)
implementation(projects.share.java)
compileOnly("dev.rikka.hidden:stub:2.3.1") compileOnly("dev.rikka.hidden:stub:2.3.1")
implementation("dev.rikka.hidden:compat: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-navigation-animation:0.24.5-alpha")
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.5-alpha") implementation("com.google.accompanist:accompanist-swiperefresh:0.24.5-alpha")
implementation("com.google.android.material:material:1.5.0") implementation("com.google.android.material:material:1.5.0")
implementation("com.google.code.gson:gson:2.9.0")
implementation("dev.rikka.shizuku:api:12.1.0") implementation("dev.rikka.shizuku:api:12.1.0")
implementation("dev.rikka.shizuku:provider:12.1.0") implementation("dev.rikka.shizuku:provider:12.1.0")
implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") 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 { object Constants {
const val PATCH_FILE_SUFFIX = "-lspatched.apk"
const val PREFS_KEYSTORE_PASSWORD = "keystore_password" const val PREFS_KEYSTORE_PASSWORD = "keystore_password"
const val PREFS_KEYSTORE_ALIAS = "keystore_alias" const val PREFS_KEYSTORE_ALIAS = "keystore_alias"
const val PREFS_KEYSTORE_ALIAS_PASSWORD = "keystore_alias_password" const val PREFS_KEYSTORE_ALIAS_PASSWORD = "keystore_alias_password"

View File

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

View File

@ -1,17 +1,15 @@
package org.lsposed.lspatch package org.lsposed.lspatch
import android.content.Context
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.lsposed.lspatch.Constants.PATCH_FILE_SUFFIX
import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY
import org.lsposed.lspatch.config.MyKeyStore import org.lsposed.lspatch.config.MyKeyStore
import org.lsposed.patch.LSPatch import org.lsposed.patch.LSPatch
import org.lsposed.patch.util.Logger import org.lsposed.patch.util.Logger
import java.io.File
import java.io.IOException import java.io.IOException
import java.nio.file.Files
object Patcher { object Patcher {
@ -26,12 +24,10 @@ object Patcher {
private val verbose: Boolean, private val verbose: Boolean,
private val embeddedModules: List<String>? private val embeddedModules: List<String>?
) { ) {
lateinit var outputDir: File
fun toStringArray(): Array<String> { fun toStringArray(): Array<String> {
return buildList { return buildList {
addAll(apkPaths) addAll(apkPaths)
add("-o"); add(outputDir.absolutePath) add("-o"); add(lspApp.tmpApkDir.absolutePath)
if (debuggable) add("-d") if (debuggable) add("-d")
add("-l"); add(sigbypassLevel.toString()) add("-l"); add(sigbypassLevel.toString())
add("--v1"); add(v1.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) { withContext(Dispatchers.IO) {
options.outputDir = Files.createTempDirectory("patch").toFile()
options.outputDir.listFiles()?.forEach(File::delete)
LSPatch(logger, *options.toStringArray()).doCommandLine() LSPatch(logger, *options.toStringArray()).doCommandLine()
val uri = lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)?.toUri() val uri = lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)?.toUri()
?: throw IOException("Uri is null") ?: throw IOException("Uri is null")
val root = DocumentFile.fromTreeUri(context, uri) val root = DocumentFile.fromTreeUri(lspApp, uri)
?: throw IOException("DocumentFile is null") ?: throw IOException("DocumentFile is null")
root.listFiles().forEach { root.listFiles().forEach {
if (it.name?.endsWith("-lspatched.apk") == true) it.delete() if (it.name?.endsWith(PATCH_FILE_SUFFIX) == true) it.delete()
} }
options.outputDir lspApp.tmpApkDir.walk()
.walk()
.filter { it.isFile } .filter { it.isFile }
.forEach { apk -> .forEach { apk ->
val file = root.createFile("application/vnd.android.package-archive", apk.name) val file = root.createFile("application/vnd.android.package-archive", apk.name)
?: throw IOException("Failed to create output file") ?: 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") ?: throw IOException("Failed to open output stream")
output.use { output.use {
apk.inputStream().use { input -> apk.inputStream().use { input ->

View File

@ -66,7 +66,9 @@ private fun MainNavHost(navController: NavHostController, modifier: Modifier) {
) { ) {
PageList.values().forEach { page -> PageList.values().forEach { page ->
val sb = StringBuilder(page.name) 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) 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.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
@ -23,10 +24,10 @@ fun AppItem(
icon: Drawable, icon: Drawable,
label: String, label: String,
packageName: String, packageName: String,
additionalInfo: (@Composable () -> Unit)? = null,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,
checked: Boolean? = null checked: Boolean? = null,
additionalContent: (@Composable () -> Unit)? = null,
) { ) {
Column( Column(
modifier = modifier modifier = modifier
@ -47,10 +48,17 @@ fun AppItem(
modifier = Modifier.size(32.dp), modifier = Modifier.size(32.dp),
tint = Color.Unspecified tint = Color.Unspecified
) )
Column(Modifier.weight(1f)) { Column(
Text(text = label, style = MaterialTheme.typography.bodyMedium) modifier = Modifier.weight(1f),
Text(text = packageName, style = MaterialTheme.typography.bodySmall) verticalArrangement = Arrangement.spacedBy(1.dp)
additionalInfo?.invoke() ) {
Text(label)
Text(
text = packageName,
fontFamily = FontFamily.Monospace,
style = MaterialTheme.typography.bodySmall
)
additionalContent?.invoke()
} }
if (checked != null) { if (checked != null) {
Checkbox( Checkbox(

View File

@ -41,8 +41,13 @@ fun SearchAppBar(
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
var onSearch by remember { mutableStateOf(false) } var onSearch by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { if (onSearch) {
if (onSearch) focusRequester.requestFocus() LaunchedEffect(Unit) { focusRequester.requestFocus() }
}
DisposableEffect(Unit) {
onDispose {
keyboardController?.hide()
}
} }
SmallTopAppBar( SmallTopAppBar(
@ -75,8 +80,9 @@ fun SearchAppBar(
trailingIcon = { trailingIcon = {
IconButton( IconButton(
onClick = { onClick = {
onClearClick()
onSearch = false onSearch = false
keyboardController?.hide()
onClearClick()
}, },
content = { Icon(Icons.Filled.Close, null) } 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.CheckCircle
import androidx.compose.material.icons.outlined.Warning import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.* 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.lsposed.lspatch.BuildConfig
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
import org.lsposed.lspatch.share.LSPConfig
import org.lsposed.lspatch.ui.util.HtmlText import org.lsposed.lspatch.ui.util.HtmlText
import org.lsposed.lspatch.ui.util.LocalSnackbarHost import org.lsposed.lspatch.ui.util.LocalSnackbarHost
import org.lsposed.lspatch.util.ShizukuApi import org.lsposed.lspatch.util.ShizukuApi
@ -38,12 +41,11 @@ fun HomePage() {
modifier = Modifier modifier = Modifier
.padding(innerPadding) .padding(innerPadding)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
ShizukuCard() ShizukuCard()
Spacer(Modifier.height(16.dp))
InfoCard() InfoCard()
Spacer(Modifier.height(16.dp))
SupportCard() SupportCard()
} }
} }
@ -164,13 +166,13 @@ private fun InfoCard() {
Text(text = texts.second, style = MaterialTheme.typography.bodyMedium) 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)) 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)) 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)) Spacer(Modifier.height(24.dp))
infoCardContent(stringResource(R.string.home_system_version) to apiVersion) infoCardContent(stringResource(R.string.home_system_version) to apiVersion)

View File

@ -5,41 +5,56 @@ import android.content.Intent
import android.util.Log import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.animation.core.Spring
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.lsposed.lspatch.* import kotlinx.coroutines.withContext
import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY
import org.lsposed.lspatch.R 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.LocalNavController
import org.lsposed.lspatch.ui.util.LocalSnackbarHost import org.lsposed.lspatch.ui.util.LocalSnackbarHost
import org.lsposed.lspatch.ui.viewmodel.ManageViewModel
import org.lsposed.lspatch.util.LSPPackageManager
import java.io.IOException import java.io.IOException
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ManagePage() { fun ManagePage() {
val viewModel = viewModel<ManageViewModel>()
Scaffold( Scaffold(
topBar = { TopBar() }, topBar = { TopBar() },
floatingActionButton = { Fab() } floatingActionButton = { Fab() }
) { innerPadding -> ) { innerPadding ->
Text( Box(Modifier.padding(innerPadding)) {
modifier = Modifier Body()
.padding(innerPadding) }
.fillMaxSize(),
text = "This page is not yet implemented",
textAlign = TextAlign.Center
)
} }
} }
@ -57,6 +72,7 @@ private fun Fab() {
val navController = LocalNavController.current val navController = LocalNavController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var shouldSelectDirectory by remember { mutableStateOf(false) } var shouldSelectDirectory by remember { mutableStateOf(false) }
var showNewPatchDialog by remember { mutableStateOf(false) }
val errorText = stringResource(R.string.patch_select_dir_error) val errorText = stringResource(R.string.patch_select_dir_error)
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@ -67,7 +83,7 @@ private fun Fab() {
context.contentResolver.takePersistableUriPermission(uri, takeFlags) context.contentResolver.takePersistableUriPermission(uri, takeFlags)
lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, uri.toString()).apply() lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, uri.toString()).apply()
Log.i(TAG, "Storage directory: ${uri.path}") Log.i(TAG, "Storage directory: ${uri.path}")
navController.navigate(PageList.NewPatch.name) showNewPatchDialog = true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error when requesting saving directory", e) Log.e(TAG, "Error when requesting saving directory", e)
scope.launch { snackbarHost.showSnackbar(errorText) } 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( FloatingActionButton(
content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) }, content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) },
onClick = { onClick = {
@ -115,7 +183,7 @@ private fun Fab() {
context.contentResolver.takePersistableUriPermission(uri, takeFlags) context.contentResolver.takePersistableUriPermission(uri, takeFlags)
if (DocumentFile.fromTreeUri(context, uri)?.exists() == false) throw IOException("Storage directory was deleted") if (DocumentFile.fromTreeUri(context, uri)?.exists() == false) throw IOException("Storage directory was deleted")
}.onSuccess { }.onSuccess {
navController.navigate(PageList.NewPatch.name) showNewPatchDialog = true
}.onFailure { }.onFailure {
Log.w(TAG, "Failed to take persistable permission for saved uri", it) Log.w(TAG, "Failed to take persistable permission for saved uri", it)
lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, null).apply() 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.content.pm.PackageInstaller
import android.util.Log import android.util.Log
import androidx.activity.compose.BackHandler 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.animateContentSize
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.Patcher
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
import org.lsposed.lspatch.lspApp 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.SettingsCheckBox
import org.lsposed.lspatch.ui.component.settings.SettingsItem import org.lsposed.lspatch.ui.component.settings.SettingsItem
import org.lsposed.lspatch.ui.util.* 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
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState 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.lspatch.util.ShizukuApi
import org.lsposed.patch.util.Logger import org.lsposed.patch.util.Logger
import java.io.File
private const val TAG = "NewPatchPage" private const val TAG = "NewPatchPage"
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NewPatchPage(entry: NavBackStackEntry) { fun NewPatchPage(from: String, entry: NavBackStackEntry) {
val viewModel = viewModel<NewPatchViewModel>() val viewModel = viewModel<NewPatchViewModel>()
val snackbarHost = LocalSnackbarHost.current
val navController = LocalNavController.current val navController = LocalNavController.current
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
val isCancelled by entry.observeState<Boolean>("isCancelled") val isCancelled by entry.observeState<Boolean>("isCancelled")
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
lspApp.tmpApkDir.listFiles()?.forEach(File::delete)
entry.savedStateHandle.getLiveData<AppInfo>("appInfo").observe(lifecycleOwner) { entry.savedStateHandle.getLiveData<AppInfo>("appInfo").observe(lifecycleOwner) {
viewModel.configurePatch(it) viewModel.configurePatch(it)
} }
@ -67,9 +73,28 @@ fun NewPatchPage(entry: NavBackStackEntry) {
Log.d(TAG, "PatchState: ${viewModel.patchState}") Log.d(TAG, "PatchState: ${viewModel.patchState}")
if (viewModel.patchState == PatchState.SELECTING) { 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) { LaunchedEffect(Unit) {
if (isCancelled == true) navController.popBackStack() 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 { } else {
Scaffold( Scaffold(
@ -173,7 +198,7 @@ private fun PatchOptionsBody(modifier: Modifier) {
desc = stringResource(R.string.patch_portable_desc), desc = stringResource(R.string.patch_portable_desc),
extraContent = { extraContent = {
TextButton( 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) } 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 @Composable
private fun DoPatchBody(modifier: Modifier) { private fun DoPatchBody(modifier: Modifier) {
val viewModel = viewModel<NewPatchViewModel>() val viewModel = viewModel<NewPatchViewModel>()
val context = LocalContext.current
val snackbarHost = LocalSnackbarHost.current val snackbarHost = LocalSnackbarHost.current
val navController = LocalNavController.current val navController = LocalNavController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -274,14 +298,14 @@ private fun DoPatchBody(modifier: Modifier) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
try { try {
Patcher.patch(context, logger, viewModel.patchOptions) Patcher.patch(logger, viewModel.patchOptions)
viewModel.finishPatch() viewModel.finishPatch()
} catch (t: Throwable) { } catch (t: Throwable) {
logger.e(t.message.orEmpty()) logger.e(t.message.orEmpty())
logger.e(t.stackTraceToString()) logger.e(t.stackTraceToString())
viewModel.failPatch() viewModel.failPatch()
} finally { } 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) } var installing by rememberSaveable { mutableStateOf(false) }
if (installing) InstallDialog(viewModel.patchApp) { status, message -> if (installing) InstallDialog(viewModel.patchApp) { status, message ->
scope.launch { scope.launch {
LSPPackageManager.fetchAppList()
installing = false installing = false
if (status == PackageInstaller.STATUS_SUCCESS) { if (status == PackageInstaller.STATUS_SUCCESS) {
lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) } lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) }
navController.popBackStack() navController.popBackStack()
} else { } else if (status != LSPPackageManager.STATUS_USER_CANCELLED) {
val result = snackbarHost.showSnackbar(installFailed, copyError) val result = snackbarHost.showSnackbar(installFailed, copyError)
if (result == SnackbarResult.ActionPerformed) { 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)) cm.setPrimaryClip(ClipData.newPlainText("LSPatch", message))
} }
} }
@ -382,7 +407,7 @@ private fun DoPatchBody(modifier: Modifier) {
Button( Button(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = { 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" })) cm.setPrimaryClip(ClipData.newPlainText("LSPatch", logs.joinToString { it.second + "\n" }))
}, },
content = { Text(stringResource(R.string.patch_copy_error)) } content = { Text(stringResource(R.string.patch_copy_error)) }
@ -398,20 +423,26 @@ private fun DoPatchBody(modifier: Modifier) {
@Composable @Composable
private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
val scope = rememberCoroutineScope() 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) } var installing by remember { mutableStateOf(0) }
val doInstall = suspend { val doInstall = suspend {
Log.i(TAG, "Installing app ${patchApp.app.packageName}") Log.i(TAG, "Installing app ${patchApp.app.packageName}")
installing = 1 installing = 1
val (status, message) = LSPPackageInstaller.install() val (status, message) = LSPPackageManager.install()
installing = 0 installing = 0
Log.i(TAG, "Installation end: $status, $message") Log.i(TAG, "Installation end: $status, $message")
onFinish(status, message) onFinish(status, message)
} }
LaunchedEffect(Unit) {
if (!uninstallFirst) {
doInstall()
}
}
if (uninstallFirst) { if (uninstallFirst) {
AlertDialog( AlertDialog(
onDismissRequest = { onFinish(-2, "User cancelled") }, onDismissRequest = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
@ -419,7 +450,7 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}") Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}")
uninstallFirst = false uninstallFirst = false
installing = 2 installing = 2
val (status, message) = LSPPackageInstaller.uninstall(patchApp.app.packageName) val (status, message) = LSPPackageManager.uninstall(patchApp.app.packageName)
installing = 0 installing = 0
Log.i(TAG, "Uninstallation end: $status, $message") Log.i(TAG, "Uninstallation end: $status, $message")
if (status == PackageInstaller.STATUS_SUCCESS) { if (status == PackageInstaller.STATUS_SUCCESS) {
@ -434,7 +465,7 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
}, },
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onFinish(-2, "User cancelled") }, onClick = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") },
content = { Text(stringResource(android.R.string.cancel)) } content = { Text(stringResource(android.R.string.cancel)) }
) )
}, },

View File

@ -44,13 +44,16 @@ enum class PageList(
body = { SettingsPage() } body = { SettingsPage() }
), ),
NewPatch( NewPatch(
body = { NewPatchPage(this) } arguments = listOf(
navArgument("from") { type = NavType.StringType }
),
body = { NewPatchPage(arguments!!.getString("from")!!, this) }
), ),
SelectApps( SelectApps(
arguments = listOf( arguments = listOf(
navArgument("multiSelect") { type = NavType.BoolType } navArgument("multiSelect") { type = NavType.BoolType }
), ),
body = { SelectAppsPage(this) } body = { SelectAppsPage(arguments!!.getBoolean("multiSelect")) }
); );
val title: String 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.LocalNavController
import org.lsposed.lspatch.ui.util.observeState import org.lsposed.lspatch.ui.util.observeState
import org.lsposed.lspatch.ui.util.setState 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.ui.viewmodel.SelectAppsViewModel
import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SelectAppsPage(entry: NavBackStackEntry) { fun SelectAppsPage(multiSelect: Boolean) {
val viewModel = viewModel<SelectAppsViewModel>() val viewModel = viewModel<SelectAppsViewModel>()
val navController = LocalNavController.current val navController = LocalNavController.current
val multiSelect = entry.arguments?.get("multiSelect") as? Boolean
?: throw IllegalArgumentException("multiSelect is null")
var searchPackage by remember { mutableStateOf("") } var searchPackage by remember { mutableStateOf("") }
val filter: (AppInfo) -> Boolean = { val filter: (AppInfo) -> Boolean = {
@ -113,7 +112,7 @@ private fun SingleSelect() {
) { ) {
AppItem( AppItem(
modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)), modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)),
icon = viewModel.getIcon(it), icon = LSPPackageManager.getIcon(it),
label = it.label, label = it.label,
packageName = it.app.packageName, packageName = it.app.packageName,
onClick = { onClick = {
@ -140,7 +139,7 @@ private fun MultiSelect() {
val checked = selected!!.contains(it) val checked = selected!!.contains(it)
AppItem( AppItem(
modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)), modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)),
icon = viewModel.getIcon(it), icon = LSPPackageManager.getIcon(it),
label = it.label, label = it.label,
packageName = it.app.packageName, packageName = it.app.packageName,
onClick = { 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.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.Patcher
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
class NewPatchViewModel : ViewModel() { class NewPatchViewModel : ViewModel() {
@ -27,6 +28,7 @@ class NewPatchViewModel : ViewModel() {
private set private set
lateinit var embeddedModules: SnapshotStateList<AppInfo> lateinit var embeddedModules: SnapshotStateList<AppInfo>
lateinit var patchOptions: Patcher.Options lateinit var patchOptions: Patcher.Options
private set
fun configurePatch(app: AppInfo) { fun configurePatch(app: AppInfo) {
patchApp = app patchApp = app

View File

@ -1,31 +1,17 @@
package org.lsposed.lspatch.ui.viewmodel 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 android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import org.lsposed.lspatch.util.LSPPackageManager
import kotlinx.parcelize.Parcelize import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
import org.lsposed.lspatch.lspApp
import java.text.Collator
import java.util.*
private const val TAG = "SelectAppViewModel" 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() { class SelectAppsViewModel : ViewModel() {
init { init {
@ -40,28 +26,13 @@ class SelectAppsViewModel : ViewModel() {
fun filterAppList(refresh: Boolean, filter: (AppInfo) -> Boolean) { fun filterAppList(refresh: Boolean, filter: (AppInfo) -> Boolean) {
viewModelScope.launch { viewModelScope.launch {
if (appList.isEmpty() || refresh) refreshAppList() if (LSPPackageManager.appList.isEmpty() || refresh) {
filteredList = appList.filter(filter) isRefreshing = true
} LSPPackageManager.fetchAppList()
} isRefreshing = false
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()))
} }
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 package org.lsposed.lspatch.util
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInstaller 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.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import hidden.HiddenApiBridge import hidden.HiddenApiBridge
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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.Constants.PREFS_STORAGE_DIRECTORY
import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.lspApp
import org.lsposed.patch.util.ManifestParser
import java.io.File
import java.io.IOException import java.io.IOException
import java.text.Collator
import java.util.*
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.zip.ZipFile
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine 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?> { suspend fun install(): Pair<Int, String?> {
var status = PackageInstaller.STATUS_FAILURE var status = PackageInstaller.STATUS_FAILURE
@ -31,11 +75,12 @@ object LSPPackageInstaller {
?: throw IOException("Uri is null") ?: throw IOException("Uri is null")
val root = DocumentFile.fromTreeUri(lspApp, uri) val root = DocumentFile.fromTreeUri(lspApp, uri)
?: throw IOException("DocumentFile is null") ?: throw IOException("DocumentFile is null")
root.listFiles().forEach { apk -> root.listFiles().forEach { file ->
val input = lspApp.contentResolver.openInputStream(apk.uri) if (file.name?.endsWith(PATCH_FILE_SUFFIX) != true) return@forEach
val input = lspApp.contentResolver.openInputStream(file.uri)
?: throw IOException("Cannot open input stream") ?: throw IOException("Cannot open input stream")
input.use { 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) input.copyTo(output)
session.fsync(output) session.fsync(output)
} }
@ -94,4 +139,43 @@ object LSPPackageInstaller {
} }
return Pair(status, message) 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) return constructor.newInstance(iSession)
} }
fun isPackageInstalled(packageName: String): Boolean { fun isPackageInstalledWithoutPatch(packageName: String): Boolean {
return iPackageManager.getPackageInfo(packageName, 0, 0 /* TODO: userId */) != null 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) { fun uninstallPackage(packageName: String, intentSender: IntentSender) {

View File

@ -23,33 +23,32 @@
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<group <group
android:scaleX="0.12397959" android:scaleX="0.28265625"
android:scaleY="0.12397959" android:scaleY="0.28265625"
android:translateX="-20.387754" android:translateX="17.82"
android:translateY="13.5"> android:translateY="17.82">
<path <path
android:fillColor="#a697e8" android:fillColor="#c9dc87"
android:pathData="M280,0h640v640h-640z" /> android:pathData="M0,0h256v256h-256z" />
<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>
<path <path
android:fillColor="#607d8b" android:fillAlpha="0.7"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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> </group>
</vector> </vector>

View File

@ -23,42 +23,41 @@
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<group <group
android:scaleX="0.12397959" android:scaleX="0.28265625"
android:scaleY="0.12397959" android:scaleY="0.28265625"
android:translateX="-20.387754" android:translateX="17.82"
android:translateY="13.5"> android:translateY="17.82">
<path <path
android:fillColor="#a697e8" android:fillColor="#c9dc87"
android:pathData="M280,0h640v640h-640z" /> android:pathData="M0,0h256v256h-256z" />
<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>
<path <path
android:fillColor="#607d8b" android:fillAlpha="0.7"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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 <path
android:fillColor="#607d8b" android:fillColor="#0e7c61"
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: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> </group>
</vector> </vector>

View File

@ -18,19 +18,17 @@
--> -->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<group <group
android:scaleX="1.2375" android:scaleX="0.27421874"
android:scaleY="1.2375" android:scaleY="0.27421874"
android:translateX="24.3" android:translateX="18.9"
android:translateY="24.3"> android:translateY="18.9">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#fff"
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" 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" />
tools:ignore="VectorPath" />
</group> </group>
</vector> </vector>

View File

@ -21,12 +21,16 @@
<!-- Manage Page --> <!-- Manage Page -->
<string name="page_manage">Manage</string> <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 --> <!-- New Patch Page -->
<string name="page_new_patch">New Patch</string> <string name="page_new_patch">New Patch</string>
<string name="patch_select_dir_title">Select storage directory</string> <string name="patch_select_dir_title">Select storage directory</string>
<string name="patch_select_dir_text">Select a directory to store the patched apks</string> <string name="patch_select_dir_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_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_mode">Patch Mode</string>
<string name="patch_local">Local</string> <string name="patch_local">Local</string>
<string name="patch_local_desc">Patch an app without modules embedded.\nThe patched app need the manager running in background, and Xposed scope can be changed dynamically without re-patch.\nLocal patched apps can only run on the local device.</string> <string name="patch_local_desc">Patch an app without modules embedded.\nThe patched app need the manager running in background, and Xposed scope can be changed dynamically without re-patch.\nLocal patched apps can only run on the local device.</string>

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 androidSourceCompatibility: JavaVersion by rootProject.extra
val androidTargetCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra
@ -14,9 +16,9 @@ dependencies {
implementation(projects.patch) implementation(projects.patch)
} }
tasks.jar { fun Jar.configure(variant: String) {
archiveBaseName.set("lspatch") archiveBaseName.set("jar-v$verName-$verCode-$variant")
destinationDirectory.set(file("${rootProject.projectDir}/out")) destinationDirectory.set(file("${rootProject.projectDir}/out/$variant"))
manifest { manifest {
attributes("Main-Class" to "org.lsposed.patch.LSPatch") 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/**") 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<Jar>("buildDebug") {
dependsOn(":appstub:copyDebug")
tasks.register("buildDebug") { dependsOn(":patch-loader:copyDebug")
jar.dependsOn(":appstub:copyDebug") configure("debug")
jar.dependsOn(":patch-loader:copyDebug")
dependsOn(tasks.build)
} }
tasks.register("buildRelease") { tasks.register<Jar>("buildRelease") {
jar.dependsOn(":appstub:copyRelease") dependsOn(":appstub:copyRelease")
jar.dependsOn(":patch-loader:copyRelease") dependsOn(":patch-loader:copyRelease")
dependsOn(tasks.build) configure("release")
}
tasks["build"].doLast {
println("Build to " + jar.archiveFile)
println("Try \'java -jar " + jar.archiveFileName + "\' find more help")
} }

View File

@ -21,6 +21,7 @@ import com.wind.meditor.property.ModificationProperty;
import com.wind.meditor.utils.NodeValue; import com.wind.meditor.utils.NodeValue;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.lsposed.lspatch.share.LSPConfig;
import org.lsposed.lspatch.share.PatchConfig; import org.lsposed.lspatch.share.PatchConfig;
import org.lsposed.patch.util.ApkSignatureHelper; import org.lsposed.patch.util.ApkSignatureHelper;
import org.lsposed.patch.util.JavaLogger; import org.lsposed.patch.util.JavaLogger;
@ -38,8 +39,10 @@ import java.security.KeyStore;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@ -160,7 +163,11 @@ public class LSPatch {
var outputDir = new File(outputPath); var outputDir = new File(outputPath);
outputDir.mkdirs(); 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) if (outputFile.exists() && !forceOverwrite)
throw new PatchError(outputPath + " exists. Use --force to overwrite"); throw new PatchError(outputPath + " exists. Use --force to overwrite");
@ -237,7 +244,10 @@ public class LSPatch {
logger.i("Patching apk..."); logger.i("Patching apk...");
// modify manifest // 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); dstZFile.add(ANDROID_MANIFEST_XML, is);
} catch (Throwable e) { } catch (Throwable e) {
throw new PatchError("Error when modifying manifest", e); throw new PatchError("Error when modifying manifest", e);
@ -273,9 +283,8 @@ public class LSPatch {
} }
// save lspatch config to asset.. // save lspatch config to asset..
var config = new PatchConfig(useManager, sigbypassLevel, originalSignature, appComponentFactory); config = new PatchConfig(useManager, sigbypassLevel, originalSignature, appComponentFactory);
var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8); configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8);
try (var is = new ByteArrayInputStream(configBytes)) { try (var is = new ByteArrayInputStream(configBytes)) {
dstZFile.add(CONFIG_ASSET_PATH, is); dstZFile.add(CONFIG_ASSET_PATH, is);
} catch (Throwable e) { } 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(); ModificationProperty property = new ModificationProperty();
if (overrideVersionCode) if (overrideVersionCode)
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, 1)); property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, 1));
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag)); property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag));
property.addApplicationAttribute(new AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY)); property.addApplicationAttribute(new AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY));
property.addMetaData(new ModificationProperty.MetaData("lspatch", metadata));
// TODO: replace query_all with queries -> manager // TODO: replace query_all with queries -> manager
property.addUsesPermission("android.permission.QUERY_ALL_PACKAGES"); 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 androidSourceCompatibility: JavaVersion by rootProject.extra
val androidTargetCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra
@ -9,3 +14,20 @@ java {
sourceCompatibility = androidSourceCompatibility sourceCompatibility = androidSourceCompatibility
targetCompatibility = androidTargetCompatibility 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 int sigBypassLevel;
public final String originalSignature; public final String originalSignature;
public final String appComponentFactory; public final String appComponentFactory;
public final LSPConfig lspConfig;
public PatchConfig(boolean useManager, int sigBypassLevel, String originalSignature, String appComponentFactory) { public PatchConfig(boolean useManager, int sigBypassLevel, String originalSignature, String appComponentFactory) {
this.useManager = useManager; this.useManager = useManager;
this.sigBypassLevel = sigBypassLevel; this.sigBypassLevel = sigBypassLevel;
this.originalSignature = originalSignature; this.originalSignature = originalSignature;
this.appComponentFactory = appComponentFactory; 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() {
}
}