feat: add output log to media

底層實現了日誌檔案寫入。為管理器新增了 outputLog 選項,可以將日誌輸出到 Android/media 目錄。
This commit is contained in:
NkBe(HSSkyBoy) 2025-10-02 22:36:25 +08:00
parent 02c9d34f5a
commit 7613e7de10
No known key found for this signature in database
GPG Key ID: 525137026FF031DF
10 changed files with 80 additions and 9 deletions

View File

@ -109,6 +109,9 @@ jobs:
echo 'android.native.buildOutput=verbose' echo 'android.native.buildOutput=verbose'
} >> gradle.properties } >> gradle.properties
- name: Grant execution permission to gradlew
run: chmod +x gradlew
- name: Build dependencies with Gradle - name: Build dependencies with Gradle
working-directory: libxposed working-directory: libxposed
run: | run: |

2
.gitmodules vendored
View File

@ -4,5 +4,5 @@
branch = android10-release branch = android10-release
[submodule "core"] [submodule "core"]
path = core path = core
url = https://github.com/JingMatrix/LSPosed.git url = https://github.com/HSSkyBoy/LSPosed.git
branch = master branch = master

2
core

@ -1 +1 @@
Subproject commit fd894b94b2c0790f69aed8b75f13d2cc4f5c26b9 Subproject commit b168c71f7b05b81a05cefc8343ca10bfb5fb1dc9

View File

@ -359,6 +359,14 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) {
desc = stringResource(R.string.patch_inject_dex_desc) desc = stringResource(R.string.patch_inject_dex_desc)
) )
SettingsCheckBox(
modifier = Modifier.clickable { viewModel.outputLog = !viewModel.outputLog },
checked = viewModel.outputLog,
icon = Icons.Outlined.AddCard,
title = stringResource(R.string.patch_output_log_to_media),
desc = stringResource(R.string.patch_output_log_to_media_desc)
)
var bypassExpanded by remember { mutableStateOf(false) } var bypassExpanded by remember { mutableStateOf(false) }
AnywhereDropdown( AnywhereDropdown(
expanded = bypassExpanded, expanded = bypassExpanded,

View File

@ -33,10 +33,10 @@ fun checkIsApkFixedByLSP(context: Context, packageName: String): Boolean {
context.packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA) context.packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
(app.metaData?.containsKey("lspatch") != true) (app.metaData?.containsKey("lspatch") != true)
} catch (_: PackageManager.NameNotFoundException) { } catch (_: PackageManager.NameNotFoundException) {
Log.e("LSPatch", "Package not found: $packageName") Log.e("NPatch", "Package not found: $packageName")
false false
} catch (e: Exception) { } catch (e: Exception) {
Log.e("LSPatch", "Unexpected error in checkIsApkFixedByLSP", e) Log.e("NPatch", "Unexpected error in checkIsApkFixedByLSP", e)
false false
} }
} }

View File

@ -43,6 +43,7 @@ class NewPatchViewModel : ViewModel() {
var overrideVersionCode by mutableStateOf(false) var overrideVersionCode by mutableStateOf(false)
var sigBypassLevel by mutableStateOf(2) var sigBypassLevel by mutableStateOf(2)
var injectDex by mutableStateOf(false) var injectDex by mutableStateOf(false)
var outputLog by mutableStateOf(true)
var embeddedModules = emptyList<AppInfo>() var embeddedModules = emptyList<AppInfo>()
lateinit var patchApp: AppInfo lateinit var patchApp: AppInfo
@ -96,7 +97,7 @@ class NewPatchViewModel : ViewModel() {
if (useManager) embeddedModules = emptyList() if (useManager) embeddedModules = emptyList()
patchOptions = Patcher.Options( patchOptions = Patcher.Options(
injectDex = injectDex, injectDex = injectDex,
config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null), config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null, outputLog),
apkPaths = listOf(patchApp.app.sourceDir) + (patchApp.app.splitSourceDirs ?: emptyArray()), apkPaths = listOf(patchApp.app.sourceDir) + (patchApp.app.splitSourceDirs ?: emptyArray()),
embeddedModules = embeddedModules.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) } embeddedModules = embeddedModules.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) }
) )

View File

@ -112,6 +112,9 @@ public class LSPApplication {
// WARN: Since it uses `XResource`, the following class should not be initialized // WARN: Since it uses `XResource`, the following class should not be initialized
// before forkPostCommon is invoke. Otherwise, you will get failure of XResources // before forkPostCommon is invoke. Otherwise, you will get failure of XResources
if (config.outputLog){
XposedBridge.setLogPrinter(new XposedLogPrinter(0,"NPatch"));
}
Log.i(TAG, "Load modules"); Log.i(TAG, "Load modules");
LSPLoader.initModules(appLoadedApk); LSPLoader.initModules(appLoadedApk);
Log.i(TAG, "Modules initialized"); Log.i(TAG, "Modules initialized");
@ -119,7 +122,7 @@ public class LSPApplication {
switchAllClassLoader(); switchAllClassLoader();
SigBypass.doSigBypass(context, config.sigBypassLevel); SigBypass.doSigBypass(context, config.sigBypassLevel);
Log.i(TAG, "LSPatch bootstrap completed"); Log.i(TAG, "NPatch bootstrap completed");
} }
private static Context createLoadedApkWithContext() { private static Context createLoadedApkWithContext() {
@ -142,7 +145,7 @@ public class LSPApplication {
Log.i(TAG, "Use manager: " + config.useManager); Log.i(TAG, "Use manager: " + config.useManager);
Log.i(TAG, "Signature bypass level: " + config.sigBypassLevel); Log.i(TAG, "Signature bypass level: " + config.sigBypassLevel);
Path originPath = Paths.get(appInfo.dataDir, "cache/lspatch/origin/"); Path originPath = Paths.get(appInfo.dataDir, "cache/npatch/origin/");
Path cacheApkPath; Path cacheApkPath;
try (ZipFile sourceFile = new ZipFile(appInfo.sourceDir)) { try (ZipFile sourceFile = new ZipFile(appInfo.sourceDir)) {
cacheApkPath = originPath.resolve(sourceFile.getEntry(ORIGINAL_APK_ASSET_PATH).getCrc() + ".apk"); cacheApkPath = originPath.resolve(sourceFile.getEntry(ORIGINAL_APK_ASSET_PATH).getCrc() + ".apk");

View File

@ -0,0 +1,49 @@
package org.lsposed.lspatch.loader;
import android.app.ActivityThread;
import android.os.Environment;
import android.util.Log;
import android.util.LogPrinter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
public class XposedLogPrinter extends LogPrinter {
/**
* Create a new Printer that sends to the log with the given priority
* and tag.
*
* @param priority The desired log priority:
* {@link Log#VERBOSE Log.VERBOSE},
* {@link Log#DEBUG Log.DEBUG},
* {@link Log#INFO Log.INFO},
* {@link Log#WARN Log.WARN}, or
* {@link Log#ERROR Log.ERROR}.
* @param tag A string tag to associate with each printed log statement.
*/
public XposedLogPrinter(int priority, String tag) {
super(priority, tag);
}
@Override
public void println(String x) {
writeLine(x);
}
private static SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
private static FileOutputStream out;
private static synchronized void writeLine(String text){
try {
if (out == null){
File f = new File(Environment.getExternalStorageDirectory() + "/Android/media/" + ActivityThread.currentPackageName() + "/npatch/log/");
f.mkdirs();
out = new FileOutputStream(new File(f,format.format(new Date()) + ".log"),true);
}
out.write(text.getBytes());
out.write("\n".getBytes());
}catch (Exception ignored){ }
}
}

View File

@ -81,6 +81,9 @@ public class LSPatch {
@Parameter(names = {"--injectdex"}, description = "Inject directly the loder dex file into the original application package") @Parameter(names = {"--injectdex"}, description = "Inject directly the loder dex file into the original application package")
private boolean injectDex = false; private boolean injectDex = false;
@Parameter(names = {"--outputLog"}, description = "Output Log to Media")
private boolean outputLog = true;
@Parameter(names = {"-k", "--keystore"}, arity = 4, description = "Set custom signature keystore. Followed by 4 arguments: keystore path, keystore password, keystore alias, keystore alias password") @Parameter(names = {"-k", "--keystore"}, arity = 4, description = "Set custom signature keystore. Followed by 4 arguments: keystore path, keystore password, keystore alias, keystore alias password")
private List<String> keystoreArgs = Arrays.asList(null, "123456", "key0", "123456"); private List<String> keystoreArgs = Arrays.asList(null, "123456", "key0", "123456");
@ -96,6 +99,7 @@ public class LSPatch {
@Parameter(names = {"-m", "--embed"}, description = "Embed provided modules to apk") @Parameter(names = {"-m", "--embed"}, description = "Embed provided modules to apk")
private List<String> modules = new ArrayList<>(); private List<String> modules = new ArrayList<>();
private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml"; private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml";
private static final HashSet<String> ARCHES = new HashSet<>(Arrays.asList( private static final HashSet<String> ARCHES = new HashSet<>(Arrays.asList(
"armeabi-v7a", "armeabi-v7a",
@ -250,7 +254,7 @@ public class LSPatch {
logger.i("Patching apk..."); logger.i("Patching apk...");
// modify manifest // modify manifest
final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory); final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory, outputLog);
final var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8); final var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8);
final var metadata = Base64.getEncoder().encodeToString(configBytes); final var metadata = Base64.getEncoder().encodeToString(configBytes);
try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion))) { try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion))) {

View File

@ -5,6 +5,7 @@ public class PatchConfig {
public final boolean useManager; public final boolean useManager;
public final boolean debuggable; public final boolean debuggable;
public final boolean overrideVersionCode; public final boolean overrideVersionCode;
public final boolean outputLog;
public final int sigBypassLevel; public final int sigBypassLevel;
public final String originalSignature; public final String originalSignature;
public final String appComponentFactory; public final String appComponentFactory;
@ -17,7 +18,8 @@ public class PatchConfig {
boolean overrideVersionCode, boolean overrideVersionCode,
int sigBypassLevel, int sigBypassLevel,
String originalSignature, String originalSignature,
String appComponentFactory String appComponentFactory,
boolean outputLog
) { ) {
this.useManager = useManager; this.useManager = useManager;
this.debuggable = debuggable; this.debuggable = debuggable;
@ -27,5 +29,6 @@ public class PatchConfig {
this.appComponentFactory = appComponentFactory; this.appComponentFactory = appComponentFactory;
this.lspConfig = LSPConfig.instance; this.lspConfig = LSPConfig.instance;
this.managerPackageName = Constants.MANAGER_PACKAGE_NAME; this.managerPackageName = Constants.MANAGER_PACKAGE_NAME;
this.outputLog = outputLog;
} }
} }