diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 37f853b1c..ca025c83a 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml
index f3d3f14dd..daa739730 100644
--- a/manager/src/main/AndroidManifest.xml
+++ b/manager/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
+
+
+
+
+
+
+
@@ -44,6 +53,16 @@
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+
+
+
diff --git a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt
index e35023e29..c3e529138 100644
--- a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt
+++ b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt
@@ -18,6 +18,7 @@ class LSPApplication : Application() {
lateinit var prefs: SharedPreferences
lateinit var tmpApkDir: File
+ var targetApkFiles: ArrayList? = null
val globalScope = CoroutineScope(Dispatchers.Default)
diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt
index 15e6d3544..6799f6078 100644
--- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt
+++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt
@@ -1,5 +1,6 @@
package org.lsposed.lspatch
+import android.util.Log
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.Dispatchers
@@ -10,14 +11,14 @@ import org.lsposed.lspatch.share.Constants
import org.lsposed.lspatch.share.PatchConfig
import org.lsposed.patch.LSPatch
import org.lsposed.patch.util.Logger
+import java.io.File
import java.io.IOException
-import java.util.Collections.addAll
object Patcher {
class Options(
private val injectDex: Boolean,
- private val config: PatchConfig,
+ val config: PatchConfig,
private val apkPaths: List,
private val embeddedModules: List?
) {
@@ -52,6 +53,8 @@ object Patcher {
root.listFiles().forEach {
if (it.name?.endsWith(Constants.PATCH_FILE_SUFFIX) == true) it.delete()
}
+ lspApp.targetApkFiles?.clear()
+ val apkFileList = arrayListOf()
lspApp.tmpApkDir.walk()
.filter { it.name.endsWith(Constants.PATCH_FILE_SUFFIX) }
.forEach { apk ->
@@ -59,12 +62,16 @@ object Patcher {
?: throw IOException("Failed to create output file")
val output = lspApp.contentResolver.openOutputStream(file.uri)
?: throw IOException("Failed to open output stream")
+ val apkFile = File(lspApp.externalCacheDir, apk.name)
+ apk.copyTo(apkFile, overwrite = true)
+ apkFileList.add(apkFile)
output.use {
apk.inputStream().use { input ->
input.copyTo(output)
}
}
}
+ lspApp.targetApkFiles = apkFileList
logger.i("Patched files are saved to ${root.uri.lastPathSegment}")
}
}
diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt b/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt
index 017d2bb58..ed09eb520 100644
--- a/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt
+++ b/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt
@@ -1,5 +1,8 @@
package org.lsposed.lspatch.ui.activity
+import android.annotation.SuppressLint
+import android.content.IntentFilter
+import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -8,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -21,13 +25,22 @@ import org.lsposed.lspatch.ui.page.appCurrentDestinationAsState
import org.lsposed.lspatch.ui.page.destinations.Destination
import org.lsposed.lspatch.ui.page.startAppDestination
import org.lsposed.lspatch.ui.theme.LSPTheme
+import org.lsposed.lspatch.ui.util.InstallResultReceiver
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
class MainActivity : ComponentActivity() {
+ private val splitInstallReceiver by lazy { InstallResultReceiver() }
+
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS), RECEIVER_NOT_EXPORTED)
+ } else {
+ registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS))
+ }
setContent {
val navController = rememberAnimatedNavController()
LSPTheme {
@@ -47,6 +60,11 @@ class MainActivity : ComponentActivity() {
}
}
}
+
+ override fun onDestroy() {
+ super.onDestroy()
+ unregisterReceiver(splitInstallReceiver)
+ }
}
@Composable
diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt
index a1dc27ef4..91e87575a 100644
--- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt
+++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt
@@ -48,8 +48,12 @@ import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox
import org.lsposed.lspatch.ui.component.settings.SettingsItem
import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
+import org.lsposed.lspatch.ui.util.checkIsApkFixedByLSP
+import org.lsposed.lspatch.ui.util.installApk
+import org.lsposed.lspatch.ui.util.installApks
import org.lsposed.lspatch.ui.util.isScrolledToEnd
import org.lsposed.lspatch.ui.util.lastItemIndex
+import org.lsposed.lspatch.ui.util.uninstallApkByPackageName
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.ViewAction
@@ -121,7 +125,10 @@ fun NewPatchScreen(
when (viewModel.patchState) {
PatchState.INIT -> {
LaunchedEffect(Unit) {
- LSPPackageManager.cleanTmpApkDir()
+ LSPPackageManager.apply {
+ cleanTmpApkDir()
+ cleanExternalTmpApkDir()
+ }
when (id) {
ACTION_STORAGE -> {
storageLauncher.launch(arrayOf("application/vnd.android.package-archive"))
@@ -435,10 +442,10 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
val installSuccessfully = stringResource(R.string.patch_install_successfully)
val installFailed = stringResource(R.string.patch_install_failed)
val copyError = stringResource(R.string.copy_error)
- var installing by remember { mutableStateOf(false) }
- if (installing) InstallDialog(viewModel.patchApp) { status, message ->
+ var installing by remember { mutableStateOf(0) }
+ val onFinish: (Int, String?) -> Unit = { status, message ->
scope.launch {
- installing = false
+ installing = 0
if (status == PackageInstaller.STATUS_SUCCESS) {
lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) }
navigator.navigateUp()
@@ -451,6 +458,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
}
}
}
+ if (installing == 1) InstallDialog(viewModel.patchApp, onFinish) else if (installing == 2) InstallDialog2(viewModel.patchApp, onFinish)
Row(Modifier.padding(top = 12.dp)) {
Button(
modifier = Modifier.weight(1f),
@@ -462,11 +470,9 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
modifier = Modifier.weight(1f),
onClick = {
if (!ShizukuApi.isPermissionGranted) {
- scope.launch {
- snackbarHost.showSnackbar(shizukuUnavailable)
- }
+ installing = 2
} else {
- installing = true
+ installing = 1
}
},
content = { Text(stringResource(R.string.install)) }
@@ -572,3 +578,92 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
)
}
}
+
+@Composable
+private fun InstallDialog2(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
+ val scope = rememberCoroutineScope()
+ var uninstallFirst by remember {
+ mutableStateOf(
+ checkIsApkFixedByLSP(
+ lspApp,
+ patchApp.app.packageName
+ )
+ )
+ }
+
+ fun doInstall() {
+ val apkFiles = lspApp.targetApkFiles
+ if (apkFiles.isNullOrEmpty()){
+ onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "No target APK files found for installation")
+ return
+ }
+ if (apkFiles.size > 1) {
+ scope.launch {
+ val success = installApks(lspApp, apkFiles)
+ if (success) {
+ onFinish(
+ LSPPackageManager.STATUS_USER_CANCELLED,
+ "Split APKs installed successfully"
+ )
+ } else {
+ onFinish(
+ LSPPackageManager.STATUS_USER_CANCELLED,
+ "Failed to install split APKs"
+ )
+ }
+ }
+ } else {
+ installApk(lspApp, apkFiles.first())
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ if (!uninstallFirst) {
+ onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled")
+ doInstall()
+ }
+ }
+
+ if (uninstallFirst) {
+ AlertDialog(
+ onDismissRequest = {
+ onFinish(
+ LSPPackageManager.STATUS_USER_CANCELLED,
+ "User cancelled"
+ )
+ },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "Reset")
+ scope.launch {
+ Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}")
+ uninstallApkByPackageName(lspApp, patchApp.app.packageName)
+ uninstallFirst = false
+ }
+ },
+ content = { Text(stringResource(android.R.string.ok)) }
+ )
+ },
+ dismissButton = {
+ TextButton(
+ onClick = {
+ onFinish(
+ LSPPackageManager.STATUS_USER_CANCELLED,
+ "User cancelled"
+ )
+ },
+ content = { Text(stringResource(android.R.string.cancel)) }
+ )
+ },
+ title = {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.uninstall),
+ textAlign = TextAlign.Center
+ )
+ },
+ text = { Text(stringResource(R.string.patch_uninstall_text)) }
+ )
+ }
+}
\ No newline at end of file
diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt
index a062a374c..4535147ed 100644
--- a/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt
+++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt
@@ -192,11 +192,7 @@ fun AppManageBody(
onClick = {
expanded = false
scope.launch {
- if (!ShizukuApi.isPermissionGranted) {
- snackbarHost.showSnackbar(shizukuUnavailable)
- } else {
- viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second))
- }
+ viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second))
}
}
)
diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt b/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt
index 0a90329d0..dc3e6e4f3 100644
--- a/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt
+++ b/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt
@@ -1,6 +1,23 @@
package org.lsposed.lspatch.ui.util
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+import android.util.Log
import androidx.compose.foundation.lazy.LazyListState
+import androidx.core.content.FileProvider
+import androidx.core.net.toUri
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.lsposed.lspatch.BuildConfig
+import java.io.File
+import java.io.IOException
val LazyListState.lastVisibleItemIndex
get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index
@@ -10,3 +27,136 @@ val LazyListState.lastItemIndex
val LazyListState.isScrolledToEnd
get() = lastVisibleItemIndex == lastItemIndex
+
+fun checkIsApkFixedByLSP(context: Context, packageName: String): Boolean {
+ return try {
+ val app =
+ context.packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
+ (app.metaData?.containsKey("lspatch") != true)
+ } catch (_: PackageManager.NameNotFoundException) {
+ Log.e("LSPatch", "Package not found: $packageName")
+ false
+ } catch (e: Exception) {
+ Log.e("LSPatch", "Unexpected error in checkIsApkFixedByLSP", e)
+ false
+ }
+}
+
+fun installApk(context: Context, apkFile: File) {
+ try {
+ val apkUri =
+ FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apkFile)
+
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ addCategory("android.intent.category.DEFAULT")
+ setDataAndType(apkUri, "application/vnd.android.package-archive")
+ }
+ context.startActivity(intent)
+ } catch (e: Exception) {
+ Log.e("LSPatch", "installApk", e)
+ }
+}
+
+fun uninstallApkByPackageName(context: Context, packageName: String) = try {
+ val intent = Intent(Intent.ACTION_DELETE).apply {
+ data = "package:$packageName".toUri()
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(intent)
+} catch (e: Exception) {
+ Log.e("LSPatch", "uninstallApkByPackageName", e)
+}
+
+class InstallResultReceiver : BroadcastReceiver() {
+
+ companion object {
+ const val ACTION_INSTALL_STATUS = "${BuildConfig.APPLICATION_ID}.INSTALL_STATUS"
+
+ fun createPendingIntent(context: Context, sessionId: Int): PendingIntent {
+ val intent = Intent(context, InstallResultReceiver::class.java).apply {
+ action = ACTION_INSTALL_STATUS
+ }
+ val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ return PendingIntent.getBroadcast(context, sessionId, intent, flags)
+ }
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != ACTION_INSTALL_STATUS) {
+ return
+ }
+
+ val status =
+ intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
+ val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
+
+ when (status) {
+ PackageInstaller.STATUS_PENDING_USER_ACTION -> {
+ val confirmIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT)
+ if (confirmIntent != null) {
+ context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
+ }
+ }
+
+ PackageInstaller.STATUS_SUCCESS -> {
+ }
+
+ else -> {
+ }
+ }
+ }
+}
+
+suspend fun installApks(context: Context, apkFiles: List): Boolean {
+ if (!context.packageManager.canRequestPackageInstalls()) {
+ val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
+ data = "package:${context.packageName}".toUri()
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ context.startActivity(intent)
+ return false
+ }
+
+ apkFiles.forEach {
+ if (!it.exists()) {
+ return false
+ }
+ }
+
+ return withContext(Dispatchers.IO) {
+ val packageInstaller = context.packageManager.packageInstaller
+ var session: PackageInstaller.Session? = null
+ try {
+ val params =
+ PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
+ val sessionId = packageInstaller.createSession(params)
+ session = packageInstaller.openSession(sessionId)
+
+ apkFiles.forEach { apkFile ->
+ session.openWrite(apkFile.name, 0, apkFile.length()).use { outputStream ->
+ apkFile.inputStream().use { inputStream ->
+ inputStream.copyTo(outputStream)
+ session.fsync(outputStream)
+ }
+ }
+ }
+
+ val pendingIntent = InstallResultReceiver.createPendingIntent(context, sessionId)
+
+ session.commit(pendingIntent.intentSender)
+ true
+ } catch (_: IOException) {
+ session?.abandon()
+ false
+ } catch (_: Exception) {
+ session?.abandon()
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt
index 51da66e3c..34ada43ab 100644
--- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt
+++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt
@@ -17,6 +17,8 @@ import org.lsposed.lspatch.Patcher
import org.lsposed.lspatch.lspApp
import org.lsposed.lspatch.share.Constants
import org.lsposed.lspatch.share.PatchConfig
+import org.lsposed.lspatch.ui.util.installApk
+import org.lsposed.lspatch.ui.util.installApks
import org.lsposed.lspatch.ui.viewstate.ProcessingState
import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
@@ -86,7 +88,10 @@ class AppManageViewModel : ViewModel() {
updateLoaderState = ProcessingState.Processing
val result = runCatching {
withContext(Dispatchers.IO) {
- LSPPackageManager.cleanTmpApkDir()
+ LSPPackageManager.apply {
+ cleanTmpApkDir()
+ cleanExternalTmpApkDir()
+ }
val apkPaths = listOf(appInfo.app.sourceDir) + (appInfo.app.splitSourceDirs ?: emptyArray())
val patchPaths = mutableListOf()
val embeddedModulePaths = mutableListOf()
@@ -118,8 +123,21 @@ class AppManageViewModel : ViewModel() {
}
}
Patcher.patch(logger, Patcher.Options(false, config, patchPaths, embeddedModulePaths))
- val (status, message) = LSPPackageManager.install()
- if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message)
+ if (!ShizukuApi.isPermissionGranted) {
+ val apkFiles = lspApp.targetApkFiles
+ if (apkFiles.isNullOrEmpty()){
+ Log.e(TAG, "No patched APK files found")
+ throw RuntimeException("No patched APK files found")
+ }
+ if (apkFiles.size > 1) {
+ val success = installApks(lspApp, apkFiles)
+ } else {
+ installApk(lspApp, apkFiles.first())
+ }
+ } else {
+ val (status, message) = LSPPackageManager.install()
+ if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message)
+ }
}
}
updateLoaderState = ProcessingState.Done(result)
diff --git a/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt b/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt
index 5830b7920..fd61d8ac9 100644
--- a/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt
+++ b/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt
@@ -79,6 +79,12 @@ object LSPPackageManager {
}
}
+ suspend fun cleanExternalTmpApkDir(){
+ withContext(Dispatchers.IO) {
+ lspApp.externalCacheDir?.listFiles()?.forEach(File::delete)
+ }
+ }
+
suspend fun install(): Pair {
Log.i(TAG, "Perform install patched apks")
var status = PackageInstaller.STATUS_FAILURE
diff --git a/manager/src/main/res/xml/file_paths.xml b/manager/src/main/res/xml/file_paths.xml
new file mode 100644
index 000000000..b8ff724e0
--- /dev/null
+++ b/manager/src/main/res/xml/file_paths.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/meta-loader/src/main/java/org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub.java b/meta-loader/src/main/java/org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub.java
index d251eb226..0454af1dd 100644
--- a/meta-loader/src/main/java/org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub.java
+++ b/meta-loader/src/main/java/org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub.java
@@ -1,36 +1,30 @@
package org.lsposed.lspatch.metaloader;
import android.annotation.SuppressLint;
-import android.app.AppComponentFactory;
import android.app.ActivityThread;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
-import android.os.Build;
-import android.os.Process;
-import android.os.ServiceManager;
-import android.util.JsonReader;
+import android.app.AppComponentFactory;
import android.util.Log;
-import org.lsposed.hiddenapibypass.HiddenApiBypass;
import org.lsposed.lspatch.share.Constants;
import java.io.ByteArrayOutputStream;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Method;
-import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
-import java.util.zip.ZipFile;
@SuppressLint("UnsafeDynamicallyLoadedCode")
public class LSPAppComponentFactoryStub extends AppComponentFactory {
private static final String TAG = "LSPatch-MetaLoader";
- private static final Map archToLib = new HashMap(4);
+ private static final Map archToLib = Map.of(
+ "arm", "armeabi-v7a",
+ "arm64", "arm64-v8a",
+ "x86", "x86",
+ "x86_64", "x86_64"
+ );
public static byte[] dex;
@@ -45,11 +39,6 @@ public class LSPAppComponentFactoryStub extends AppComponentFactory {
private static void bootstrap() {
try {
- archToLib.put("arm", "armeabi-v7a");
- archToLib.put("arm64", "arm64-v8a");
- archToLib.put("x86", "x86");
- archToLib.put("x86_64", "x86_64");
-
var cl = Objects.requireNonNull(LSPAppComponentFactoryStub.class.getClassLoader());
Class> VMRuntime = Class.forName("dalvik.system.VMRuntime");
Method getRuntime = VMRuntime.getDeclaredMethod("getRuntime");
@@ -59,51 +48,17 @@ private static void bootstrap() {
String arch = (String) vmInstructionSet.invoke(getRuntime.invoke(null));
String libName = archToLib.get(arch);
- boolean useManager = false;
- String soPath;
-
- try (var is = cl.getResourceAsStream(Constants.CONFIG_ASSET_PATH);
- var reader = new JsonReader(new InputStreamReader(is))) {
- reader.beginObject();
- while (reader.hasNext()) {
- var name = reader.nextName();
- if (name.equals("useManager")) {
- useManager = reader.nextBoolean();
- break;
- } else {
- reader.skipValue();
- }
- }
- }
-
- if (useManager) {
- Log.i(TAG, "Bootstrap loader from manager");
- var ipm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
- ApplicationInfo manager;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- manager = (ApplicationInfo) HiddenApiBypass.invoke(IPackageManager.class, ipm, "getApplicationInfo", Constants.MANAGER_PACKAGE_NAME, 0L, Process.myUid() / 100000);
- } else {
- manager = ipm.getApplicationInfo(Constants.MANAGER_PACKAGE_NAME, 0, Process.myUid() / 100000);
- }
- try (var zip = new ZipFile(new File(manager.sourceDir));
- var is = zip.getInputStream(zip.getEntry(Constants.LOADER_DEX_ASSET_PATH));
- var os = new ByteArrayOutputStream()) {
- transfer(is, os);
- dex = os.toByteArray();
- }
- soPath = manager.sourceDir + "!/assets/lspatch/so/" + libName + "/liblspatch.so";
- } else {
- Log.i(TAG, "Bootstrap loader from embedment");
- try (var is = cl.getResourceAsStream(Constants.LOADER_DEX_ASSET_PATH);
- var os = new ByteArrayOutputStream()) {
- transfer(is, os);
- dex = os.toByteArray();
- }
- soPath = cl.getResource("assets/lspatch/so/" + libName + "/liblspatch.so").getPath().substring(5);
+ Log.i(TAG, "Bootstrap loader from embedment");
+ try (var is = cl.getResourceAsStream(Constants.LOADER_DEX_ASSET_PATH);
+ var os = new ByteArrayOutputStream()) {
+ transfer(is, os);
+ dex = os.toByteArray();
}
+ String soPath = cl.getResource("assets/lspatch/so/" + libName + "/liblspatch.so").getPath().substring(5);
System.load(soPath);
} catch (Throwable e) {
+ Log.e(TAG, "Error when loading liblspatch.so", e);
throw new ExceptionInInitializerError(e);
}
}
diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java
index ced217ff9..80b249658 100644
--- a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java
+++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java
@@ -6,20 +6,23 @@
import android.app.ActivityThread;
import android.app.LoadedApk;
import android.content.Context;
+import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.res.CompatibilityInfo;
import android.os.Build;
-import android.os.RemoteException;
import android.system.Os;
import android.util.Log;
+import org.json.JSONArray;
+import org.json.JSONObject;
import org.lsposed.lspatch.loader.util.FileUtils;
import org.lsposed.lspatch.loader.util.XLog;
+import org.lsposed.lspatch.service.FixedLocalApplicationService;
import org.lsposed.lspatch.service.LocalApplicationService;
import org.lsposed.lspatch.service.RemoteApplicationService;
import org.lsposed.lspd.core.Startup;
+import org.lsposed.lspd.models.Module;
import org.lsposed.lspd.service.ILSPApplicationService;
-import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
@@ -35,6 +38,7 @@
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
@@ -63,7 +67,7 @@ public static boolean isIsolated() {
return (android.os.Process.myUid() % PER_USER_RANGE) >= FIRST_APP_ZYGOTE_ISOLATED_UID;
}
- public static void onLoad() throws RemoteException, IOException {
+ public static void onLoad() throws IOException {
if (isIsolated()) {
XLog.d(TAG, "Skip isolated process");
return;
@@ -76,11 +80,38 @@ public static void onLoad() throws RemoteException, IOException {
}
Log.d(TAG, "Initialize service client");
- ILSPApplicationService service;
+ ILSPApplicationService service = null;
if (config.optBoolean("useManager")) {
- service = new RemoteApplicationService(context);
- } else {
- service = new LocalApplicationService(context);
+ try {
+ service = new RemoteApplicationService(context);
+ List m = service.getLegacyModulesList();
+ JSONArray modules = new JSONArray();
+ for (Module module : m) {
+ JSONObject json = new JSONObject();
+ json.put("packageName", module.packageName);
+ json.put("apkPath", module.apkPath);
+ modules.put(json);
+ }
+ Log.i(TAG, "Modules fetched from manager: " + modules.toString());
+ SharedPreferences prefs = context.getSharedPreferences("lspatch", Context.MODE_PRIVATE);
+ prefs.edit().putString("modules", modules.toString()).apply();
+ Log.i(TAG, "Modules saved to SharedPreferences");
+ } catch (Exception e) { // Catch RemoteException or others during remote service interaction
+ Log.e(TAG, "Failed to use RemoteApplicationService, fallback to fixed local service", e);
+ // Fallback service is created below if service is still null
+ }
+ }
+
+ if (service == null) {
+ if (config.optBoolean("useManager")) {
+ // If remote failed, use FixedLocalApplicationService
+ service = new FixedLocalApplicationService(context);
+ Log.i(TAG, "Using FixedLocalApplicationService as fallback.");
+ } else {
+ // If useManager was false, use LocalApplicationService
+ service = new LocalApplicationService(context);
+ Log.i(TAG, "Using LocalApplicationService.");
+ }
}
disableProfile(context);
@@ -239,4 +270,4 @@ private static void switchAllClassLoader() {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/service/FixedLocalApplicationService.java b/patch-loader/src/main/java/org/lsposed/lspatch/service/FixedLocalApplicationService.java
new file mode 100644
index 000000000..5dfd6a7ca
--- /dev/null
+++ b/patch-loader/src/main/java/org/lsposed/lspatch/service/FixedLocalApplicationService.java
@@ -0,0 +1,121 @@
+package org.lsposed.lspatch.service;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.lsposed.lspatch.util.ModuleLoader;
+import org.lsposed.lspd.models.Module;
+import org.lsposed.lspd.service.ILSPApplicationService;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class FixedLocalApplicationService extends ILSPApplicationService.Stub {
+
+ private static final String TAG = "LSPatch";
+ private static final String PREFS_NAME = "lspatch";
+ private static final String KEY_MODULES = "modules";
+
+ private final List modules = new ArrayList<>();
+
+ public FixedLocalApplicationService(Context context) {
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ String moduleString = prefs.getString(KEY_MODULES, "[]");
+ Log.i(TAG, "Using fixed local application service. Modules data: " + moduleString);
+
+ try {
+ JSONArray modulesArray = new JSONArray(moduleString);
+ PackageManager pm = context.getPackageManager();
+ for (int i = 0; i < modulesArray.length(); i++) {
+ JSONObject moduleObject = modulesArray.getJSONObject(i);
+ String packageName = moduleObject.getString("packageName");
+ String apkPath = moduleObject.getString("apkPath");
+ File apkFile = new File(apkPath);
+
+ if (!apkFile.exists()) {
+ Log.w(TAG, "Module APK not found at cached path: " + apkPath + " for package: " + packageName);
+ try {
+ ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+ apkPath = info.sourceDir;
+ apkFile = new File(apkPath);
+ if (apkFile.exists()) {
+ Log.i(TAG, "Found module APK via PackageManager: " + apkPath);
+ } else {
+ Log.e(TAG, "Module APK still not found via PackageManager for: " + packageName);
+ continue; // Skip this module if APK is not found
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Module package not found: " + packageName, e);
+ continue; // Skip this module
+ }
+ }
+
+ Module module = new Module();
+ module.apkPath = apkPath;
+ module.packageName = packageName;
+ try {
+ module.file = ModuleLoader.loadModule(apkPath);
+ if (module.file != null) {
+ modules.add(module);
+ Log.i(TAG, "Successfully loaded module: " + packageName + " from " + apkPath);
+ } else {
+ Log.e(TAG, "Failed to load module file for: " + packageName);
+ }
+ } catch (Exception loadException) {
+ Log.e(TAG, "Error loading module file for: " + packageName + " from " + apkPath, loadException);
+ }
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Error parsing modules JSON", e);
+ } catch (Exception e) { // Catch unexpected errors during initialization
+ Log.e(TAG, "Unexpected error initializing FixedLocalApplicationService", e);
+ }
+ }
+
+ @Override
+ public boolean isLogMuted() throws RemoteException {
+ // Consider making this configurable if needed
+ return false;
+ }
+
+ @Override
+ public List getLegacyModulesList() {
+ // Return an immutable list or a copy to prevent external modification
+ return Collections.unmodifiableList(modules);
+ }
+
+ @Override
+ public List getModulesList() {
+ // Currently returns an empty list, behavior maintained
+ return new ArrayList<>();
+ }
+
+ @Override
+ public String getPrefsPath(String packageName) {
+ // Consider validating packageName or handling potential exceptions
+ return new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/").getAbsolutePath();
+ }
+
+ @Override
+ public ParcelFileDescriptor requestInjectedManagerBinder(List binder) {
+ // Currently returns null, behavior maintained
+ return null;
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/service/RemoteApplicationService.java b/patch-loader/src/main/java/org/lsposed/lspatch/service/RemoteApplicationService.java
index ab3d027e9..98b38cc38 100644
--- a/patch-loader/src/main/java/org/lsposed/lspatch/service/RemoteApplicationService.java
+++ b/patch-loader/src/main/java/org/lsposed/lspatch/service/RemoteApplicationService.java
@@ -76,7 +76,7 @@ public void onServiceDisconnected(ComponentName name) {
if (!success) throw new TimeoutException("Bind service timeout");
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException |
InterruptedException | TimeoutException e) {
- Toast.makeText(context, "Unable to connect to Manager", Toast.LENGTH_SHORT).show();
+// Toast.makeText(context, "Unable to connect to Manager", Toast.LENGTH_SHORT).show();
var r = new RemoteException("Failed to get manager binder");
r.initCause(e);
throw r;
diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java
index c7907a84d..a7a4f07d1 100644
--- a/patch/src/main/java/org/lsposed/patch/LSPatch.java
+++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java
@@ -283,31 +283,31 @@ public void patch(File srcApkFile, File outputFile) throws PatchError, IOExcepti
}
if (!useManager) {
- logger.i("Adding loader dex...");
- try (var is = getClass().getClassLoader().getResourceAsStream(LOADER_DEX_ASSET_PATH)) {
- dstZFile.add(LOADER_DEX_ASSET_PATH, is);
- } catch (Throwable e) {
- throw new PatchError("Error when adding assets", e);
- }
-
- logger.i("Adding native lib...");
- // copy so and dex files into the unzipped apk
- // do not put liblspatch.so into apk!lib because x86 native bridge causes crash
- for (String arch : ARCHES) {
- String entryName = "assets/lspatch/so/" + arch + "/liblspatch.so";
- try (var is = getClass().getClassLoader().getResourceAsStream(entryName)) {
- dstZFile.add(entryName, is, false); // no compress for so
- } catch (Throwable e) {
- // More exception info
- throw new PatchError("Error when adding native lib", e);
- }
- logger.d("added " + entryName);
- }
-
logger.i("Embedding modules...");
embedModules(dstZFile);
}
+ logger.i("Adding loader dex...");
+ try (var is = getClass().getClassLoader().getResourceAsStream(LOADER_DEX_ASSET_PATH)) {
+ dstZFile.add(LOADER_DEX_ASSET_PATH, is);
+ } catch (Throwable e) {
+ throw new PatchError("Error when adding assets", e);
+ }
+
+ logger.i("Adding native lib...");
+ // copy so and dex files into the unzipped apk
+ // do not put liblspatch.so into apk!lib because x86 native bridge causes crash
+ for (String arch : ARCHES) {
+ String entryName = "assets/lspatch/so/" + arch + "/liblspatch.so";
+ try (var is = getClass().getClassLoader().getResourceAsStream(entryName)) {
+ dstZFile.add(entryName, is, false); // no compress for so
+ } catch (Throwable e) {
+ // More exception info
+ throw new PatchError("Error when adding native lib", e);
+ }
+ logger.d("added " + entryName);
+ }
+
// create zip link
logger.d("Creating nested apk link...");