Skip to content

Commit 9b32b03

Browse files
lingqiqi5211Sevtinge
authored andcommitted
feat: add log viewer & refactor log levels
1 parent 66c74ab commit 9b32b03

16 files changed

Lines changed: 683 additions & 79 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,5 @@ kotlin.jvmToolchain(21)
196196
dependencies {
197197
implementation(libs.expansion)
198198
implementation(projects.library.core)
199+
implementation(projects.library.common)
199200
}

app/src/main/java/com/sevtinge/hyperceiler/Application.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@
2525
import android.os.Process;
2626

2727
import com.fan.common.logviewer.LogAppProxy;
28+
import com.fan.common.logviewer.LogEntry;
29+
import com.fan.common.logviewer.LogManager;
30+
import com.fan.common.logviewer.LogViewerActivity;
2831
import com.sevtinge.hyperceiler.common.utils.LSPosedScopeHelper;
32+
import com.sevtinge.hyperceiler.hook.utils.log.AndroidLogUtils;
2933
import com.sevtinge.hyperceiler.hook.utils.prefs.PrefsUtils;
3034
import com.sevtinge.hyperceiler.model.data.AppInfoCache;
3135
import com.sevtinge.hyperceiler.safemode.ExceptionCrashActivity;
36+
import com.sevtinge.hyperceiler.utils.log.XposedLogLoader;
3237

3338
import java.io.PrintWriter;
3439
import java.io.StringWriter;
@@ -47,6 +52,16 @@ public void onCreate() {
4752
super.onCreate();
4853
LogAppProxy.onCreate(this);
4954

55+
LogViewerActivity.setXposedLogLoader(XposedLogLoader::loadLogs);
56+
57+
AndroidLogUtils.setLogListener((level, tag, message) -> {
58+
try {
59+
LogManager logManager = LogManager.getInstance();
60+
logManager.addLog(new LogEntry(level, "App", "[" + tag + "] " + message, tag, true));
61+
} catch (Throwable ignored) {
62+
}
63+
});
64+
5065
new Thread(() -> AppInfoCache.getInstance(this).initAllAppInfos()).start();
5166

5267
LSPosedScopeHelper.init(this);

app/src/main/java/com/sevtinge/hyperceiler/main/page/settings/SettingsFragment.java

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
*/
1919
package com.sevtinge.hyperceiler.main.page.settings;
2020

21+
import static com.sevtinge.hyperceiler.hook.utils.api.ProjectApi.isBeta;
2122
import static com.sevtinge.hyperceiler.hook.utils.api.ProjectApi.isCanary;
23+
import static com.sevtinge.hyperceiler.hook.utils.api.ProjectApi.isRelease;
2224

2325
import android.app.Activity;
2426
import android.content.ComponentName;
@@ -77,24 +79,8 @@ public void initPrefs() {
7779
return true;
7880
});
7981

80-
if (isCanary()) {
81-
mLogLevel.setDefaultValue(3);
82-
mLogLevel.setEntries(new CharSequence[]{"Info", "Debug"});
83-
mLogLevel.setEntryValues(new CharSequence[]{"3", "4"});
84-
mLogLevel.setOnPreferenceChangeListener(
85-
(preference, o) -> {
86-
setLogLevel(Integer.parseInt((String) o));
87-
return true;
88-
}
89-
);
90-
} else {
91-
mLogLevel.setOnPreferenceChangeListener(
92-
(preference, o) -> {
93-
setLogLevel(Integer.parseInt((String) o));
94-
return true;
95-
}
96-
);
97-
}
82+
// 根据构建类型设置日志等级选项
83+
setupLogLevelPreference();
9884

9985
if (mHideAppIcon != null) {
10086
mHideAppIcon.setOnPreferenceChangeListener((preference, o) -> {
@@ -145,6 +131,60 @@ private void setLogLevel(int level) {
145131
LogManager.setLogLevel(level, requireContext().getApplicationInfo().dataDir);
146132
}
147133

134+
/**
135+
* 根据构建类型设置日志等级选项
136+
* Release: Disable, Error
137+
* Beta: Error, Debug
138+
* Canary: Info, Debug
139+
* Debug: Disable, Error, Warn, Info, Debug
140+
*/
141+
private void setupLogLevelPreference() {
142+
CharSequence[] entries;
143+
CharSequence[] entryValues;
144+
String defaultValue;
145+
146+
if (isRelease()) {
147+
entries = new CharSequence[]{"Disable", "Error"};
148+
entryValues = new CharSequence[]{"0", "1"};
149+
defaultValue = "1";
150+
} else if (isBeta()) {
151+
entries = new CharSequence[]{"Error", "Debug"};
152+
entryValues = new CharSequence[]{"1", "4"};
153+
defaultValue = "4";
154+
} else if (isCanary()) {
155+
entries = new CharSequence[]{"Info", "Debug"};
156+
entryValues = new CharSequence[]{"3", "4"};
157+
defaultValue = "3";
158+
} else {
159+
// Debug 构建类型:全部选项
160+
entries = new CharSequence[]{"Disable", "Error", "Warn", "Info", "Debug"};
161+
entryValues = new CharSequence[]{"0", "1", "2", "3", "4"};
162+
defaultValue = "4";
163+
}
164+
165+
mLogLevel.setEntries(entries);
166+
mLogLevel.setEntryValues(entryValues);
167+
mLogLevel.setDefaultValue(defaultValue);
168+
169+
// 如果当前值不在允许的范围内,重置为默认值
170+
String currentValue = mLogLevel.getValue();
171+
boolean isValidValue = false;
172+
for (CharSequence value : entryValues) {
173+
if (value.toString().equals(currentValue)) {
174+
isValidValue = true;
175+
break;
176+
}
177+
}
178+
if (!isValidValue || currentValue == null) {
179+
mLogLevel.setValue(defaultValue);
180+
}
181+
182+
mLogLevel.setOnPreferenceChangeListener((preference, o) -> {
183+
setLogLevel(Integer.parseInt((String) o));
184+
return true;
185+
});
186+
}
187+
148188
private void setIconMode(int mode) {
149189
mIconModeValue.setVisible(mode != 0);
150190
}
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/*
2+
* This file is part of HyperCeiler.
3+
4+
* HyperCeiler is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License.
8+
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
* Copyright (C) 2023-2025 HyperCeiler Contributions
18+
*/
19+
package com.sevtinge.hyperceiler.utils.log;
20+
21+
import android.content.Context;
22+
import android.util.Log;
23+
24+
import com.fan.common.logviewer.LogEntry;
25+
import com.fan.common.logviewer.LogManager;
26+
import com.sevtinge.hyperceiler.hook.utils.shell.ShellUtils;
27+
28+
import java.io.BufferedReader;
29+
import java.io.StringReader;
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
33+
/**
34+
* 用于加载 Xposed/LSPosed 日志并显示在 LogViewer 中
35+
*/
36+
public class XposedLogLoader {
37+
38+
private static final String TAG = "XposedLogLoader";
39+
40+
/**
41+
* 异步加载 Xposed 日志到 LogManager
42+
*
43+
* @param context 上下文
44+
* @param callback 加载完成回调(可为 null)
45+
*/
46+
public static void loadLogs(Context context, Runnable callback) {
47+
LogManager logManager = LogManager.getInstance(context);
48+
logManager.clearXposedLogs(); // 清除旧的 Xposed 日志
49+
50+
new Thread(() -> {
51+
try {
52+
// 查找最新的 Xposed 日志文件
53+
String logFileCmd = "ls -t /data/adb/lspd/log/modules_*.log 2>/dev/null | head -n 1";
54+
String logFilePath = ShellUtils.rootExecCmd(logFileCmd).trim();
55+
56+
if (logFilePath.isEmpty() || logFilePath.contains("No such file") || logFilePath.contains("ls:")) {
57+
logManager.addXposedLog(new LogEntry("W", "XposedLogLoader",
58+
"No Xposed log file found. Make sure LSPosed is installed and has generated logs.",
59+
"System", true));
60+
if (callback != null) callback.run();
61+
return;
62+
}
63+
64+
// 读取日志文件内容
65+
String content = ShellUtils.rootExecCmd("cat " + logFilePath);
66+
67+
if (content == null || content.isEmpty()) {
68+
logManager.addXposedLog(new LogEntry("W", "XposedLogLoader",
69+
"Xposed log file is empty.", "System", true));
70+
if (callback != null) callback.run();
71+
return;
72+
}
73+
74+
BufferedReader reader = new BufferedReader(new StringReader(content));
75+
String line;
76+
List<LogEntry> entries = new ArrayList<>();
77+
78+
while ((line = reader.readLine()) != null) {
79+
// 只处理包含 HyperCeiler 的日志行
80+
if (line.contains("HyperCeiler")) {
81+
LogEntry entry = parseXposedLogLine(line);
82+
if (entry != null) {
83+
entries.add(entry);
84+
}
85+
}
86+
}
87+
reader.close();
88+
89+
if (!entries.isEmpty()) {
90+
logManager.addXposedLogs(entries);
91+
Log.i(TAG, "Loaded " + entries.size() + " Xposed log entries");
92+
} else {
93+
logManager.addXposedLog(new LogEntry("I", "XposedLogLoader",
94+
"No HyperCeiler logs found in Xposed log file.", "System", true));
95+
}
96+
97+
} catch (Exception e) {
98+
Log.e(TAG, "Failed to load Xposed logs", e);
99+
logManager.addXposedLog(new LogEntry("E", "XposedLogLoader",
100+
"Failed to load logs: " + e.getMessage(), "System", true));
101+
}
102+
103+
if (callback != null) callback.run();
104+
}).start();
105+
}
106+
107+
/**
108+
* 同步加载 Xposed 日志(阻塞调用)
109+
*
110+
* @param context 上下文
111+
*/
112+
public static void loadLogsSync(Context context) {
113+
LogManager logManager = LogManager.getInstance(context);
114+
logManager.clearXposedLogs();
115+
116+
try {
117+
String logFileCmd = "ls -t /data/adb/lspd/log/modules_*.log 2>/dev/null | head -n 1";
118+
String logFilePath = ShellUtils.rootExecCmd(logFileCmd).trim();
119+
120+
if (logFilePath.isEmpty() || logFilePath.contains("No such file") || logFilePath.contains("ls:")) {
121+
logManager.addXposedLog(new LogEntry("W", "XposedLogLoader",
122+
"No Xposed log file found.", "System", true));
123+
return;
124+
}
125+
126+
String content = ShellUtils.rootExecCmd("cat " + logFilePath);
127+
128+
if (content == null || content.isEmpty()) {
129+
logManager.addXposedLog(new LogEntry("W", "XposedLogLoader",
130+
"Xposed log file is empty.", "System", true));
131+
return;
132+
}
133+
134+
BufferedReader reader = new BufferedReader(new StringReader(content));
135+
String line;
136+
List<LogEntry> entries = new ArrayList<>();
137+
138+
while ((line = reader.readLine()) != null) {
139+
if (line.contains("HyperCeiler")) {
140+
LogEntry entry = parseXposedLogLine(line);
141+
if (entry != null) {
142+
entries.add(entry);
143+
}
144+
}
145+
}
146+
reader.close();
147+
148+
if (!entries.isEmpty()) {
149+
logManager.addXposedLogs(entries);
150+
} else {
151+
logManager.addXposedLog(new LogEntry("I", "XposedLogLoader",
152+
"No HyperCeiler logs found.", "System", true));
153+
}
154+
155+
} catch (Exception e) {
156+
Log.e(TAG, "Failed to load Xposed logs", e);
157+
logManager.addXposedLog(new LogEntry("E", "XposedLogLoader",
158+
"Failed to load logs: " + e.getMessage(), "System", true));
159+
}
160+
}
161+
162+
/**
163+
* 解析 Xposed 日志行
164+
*
165+
* @param line 日志行
166+
* @return LogEntry 对象
167+
*/
168+
private static LogEntry parseXposedLogLine(String line) {
169+
// 解析日志等级
170+
String level = "V";
171+
172+
// LSPosed 日志格式通常包含 [HyperCeiler][E] 或 [HyperCeiler][I] 等标记
173+
if (line.contains("[E]") || line.contains("/E]") || line.contains("[E/")) {
174+
level = "E";
175+
} else if (line.contains("[W]") || line.contains("/W]") || line.contains("[W/")) {
176+
level = "W";
177+
} else if (line.contains("[I]") || line.contains("/I]") || line.contains("[I/")) {
178+
level = "I";
179+
} else if (line.contains("[D]") || line.contains("/D]") || line.contains("[D/")) {
180+
level = "D";
181+
}
182+
183+
// 提取消息部分
184+
String message = line;
185+
int tagIndex = line.indexOf("[HyperCeiler]");
186+
if (tagIndex != -1) {
187+
message = line.substring(tagIndex);
188+
}
189+
190+
// 提取模块/标签信息
191+
String module = "Other";
192+
// 尝试从日志中提取包名
193+
// 格式: [HyperCeiler][I][com.xxx.yyy][TagName]: message
194+
// 等级标记后的第一个中括号内容
195+
int levelEndIndex = -1;
196+
for (String lvl : new String[]{"[I]", "[D]", "[W]", "[E]", "[V]"}) {
197+
int idx = message.indexOf(lvl);
198+
if (idx != -1) {
199+
levelEndIndex = idx + lvl.length();
200+
break;
201+
}
202+
}
203+
204+
if (levelEndIndex != -1 && levelEndIndex < message.length() && message.charAt(levelEndIndex) == '[') {
205+
int pkgEndIndex = message.indexOf("]", levelEndIndex + 1);
206+
if (pkgEndIndex != -1) {
207+
String candidate = message.substring(levelEndIndex + 1, pkgEndIndex);
208+
// 验证是否为包名格式:
209+
// 1. "android" 特殊保留
210+
// 2. 包含点号且符合包名规范 (如 com.xxx.yyy)
211+
if (isValidPackageName(candidate)) {
212+
module = candidate;
213+
}
214+
}
215+
}
216+
217+
return new LogEntry(level, module, message, "Xposed", true);
218+
}
219+
220+
/**
221+
* 验证是否为有效的包名格式
222+
*
223+
* @param name 待验证的字符串
224+
* @return 是否为包名
225+
*/
226+
private static boolean isValidPackageName(String name) {
227+
if (name == null || name.isEmpty()) {
228+
return false;
229+
}
230+
231+
// "android" 是特殊的系统包名
232+
if ("android".equals(name)) {
233+
return true;
234+
}
235+
236+
// 包名必须包含点号
237+
if (!name.contains(".")) {
238+
return false;
239+
}
240+
241+
// 验证包名格式:以字母开头,只包含字母、数字、点、下划线
242+
// 且每个段都以字母开头
243+
String[] parts = name.split("\\.");
244+
for (String part : parts) {
245+
if (part.isEmpty()) {
246+
return false;
247+
}
248+
// 每个段必须以字母开头
249+
if (!Character.isLetter(part.charAt(0))) {
250+
return false;
251+
}
252+
// 每个字符只能是字母、数字、下划线
253+
for (char c : part.toCharArray()) {
254+
if (!Character.isLetterOrDigit(c) && c != '_') {
255+
return false;
256+
}
257+
}
258+
}
259+
260+
return true;
261+
}
262+
}

0 commit comments

Comments
 (0)