diff --git a/.gitignore b/.gitignore index 0a1b3f57..96309940 100644 --- a/.gitignore +++ b/.gitignore @@ -1941,3 +1941,21 @@ Thumbs.db /build-logic/convention/build/generated-sources/kotlin-dsl-plugins/kotlin/BuildLogic_android_basePlugin.kt /build-logic/convention/build/generated-sources/kotlin-dsl-plugins/kotlin/BuildLogic_android_libraryPlugin.kt /build-logic/convention/build/generated-sources/kotlin-dsl-plugins/kotlin/BuildLogic_rootProjectPlugin.kt + +# Local tooling and crash artifacts +/.kotlin/ +/.claude/ +/%APPDATA%/ +/202604*/ +/hs_err_pid*.log +/crash_hit_flag +/env_info +/java_threads +/java_trace +/java_trace_analysis +/logcat +/maps +/native_trace +/status +/summary +/trace_module diff --git a/app/src/main/java/moe/ono/activity/OUOSettingActivity.kt b/app/src/main/java/moe/ono/activity/OUOSettingActivity.kt index 34ab786f..96929606 100644 --- a/app/src/main/java/moe/ono/activity/OUOSettingActivity.kt +++ b/app/src/main/java/moe/ono/activity/OUOSettingActivity.kt @@ -9,6 +9,7 @@ import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper +import android.text.format.Formatter import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem @@ -49,6 +50,8 @@ import moe.ono.hooks.item.sigma.QQSurnamePredictor import moe.ono.hostInfo import moe.ono.isInHostProcess import moe.ono.ui.ThemeAttrUtils +import moe.ono.util.LogUtils +import moe.ono.util.SystemServiceUtils import moe.ono.ui.view.BgEffectPainter import moe.ono.util.Utils.convertTimestampToDate import moe.ono.util.Utils.jump @@ -191,14 +194,18 @@ open class OUOSettingActivity : BaseActivity() { val buildTime = findPreference("build_time") val buildUUID = findPreference("build_uuid") val enableLog = findPreference("prek_enable_log") + val logDirectory = findPreference("log_directory") + val clearLogs = findPreference("clear_logs") val hookPriority = findPreference("hook_priority") version?.setSummary(BuildConfig.VERSION_NAME) buildTime?.setSummary(convertTimestampToDate(BuildConfig.BUILD_TIMESTAMP)) buildUUID?.setSummary(BuildConfig.BUILD_UUID) enableLog?.isChecked = ConfigManager.getDefaultConfig().getBooleanOrFalse(PrekEnableLog) + refreshLogPreferences(logDirectory, clearLogs) enableLog?.setOnPreferenceChangeListener { _, newValue -> val isEnabled = newValue as Boolean ConfigManager.getDefaultConfig().edit().putBoolean(PrekEnableLog, isEnabled).apply() + refreshLogPreferences(logDirectory, clearLogs) true } @@ -223,7 +230,33 @@ open class OUOSettingActivity : BaseActivity() { jump(requireContext(), "https://github.com/cwuom/ono") return super.onPreferenceTreeClick(preference) } - "build_time", "build_uuid", "version","prek_enable_log", "hook_priority" -> { + "log_directory" -> { + val path = LogUtils.getLogRootDirectory() + SystemServiceUtils.copyToClipboard(requireContext(), path) + Toasts.success(requireContext(), "\u65E5\u5FD7\u76EE\u5F55\u5DF2\u590D\u5236") + return super.onPreferenceTreeClick(preference) + } + "clear_logs" -> { + MaterialAlertDialogBuilder(requireContext()) + .setTitle("\u786E\u5B9A\u6E05\u7A7A\u65E5\u5FD7\u5417\uFF1F") + .setMessage("\u8FD9\u4F1A\u5220\u9664\u5F53\u524D\u5DF2\u4FDD\u5B58\u7684\u8FD0\u884C\u65E5\u5FD7\u548C\u9519\u8BEF\u65E5\u5FD7") + .setPositiveButton("确定") { _, _ -> + val cleared = LogUtils.clearLogs() + refreshLogPreferences( + findPreference("log_directory"), + findPreference("clear_logs") + ) + if (cleared) { + Toasts.success(requireContext(), "\u65E5\u5FD7\u5DF2\u6E05\u7A7A") + } else { + Toasts.error(requireContext(), "\u65E5\u5FD7\u6E05\u7A7A\u5931\u8D25") + } + } + .setNegativeButton("取消", null) + .show() + return super.onPreferenceTreeClick(preference) + } + "build_time", "build_uuid", "version", "prek_enable_log", "hook_priority" -> { return super.onPreferenceTreeClick(preference) } } @@ -239,6 +272,13 @@ open class OUOSettingActivity : BaseActivity() { } return super.onPreferenceTreeClick(preference) } + + private fun refreshLogPreferences(logDirectory: Preference?, clearLogs: Preference?) { + val context = context ?: return + val size = Formatter.formatFileSize(context, LogUtils.getLogDirectorySize()) + logDirectory?.summary = LogUtils.getLogRootDirectory() + "\n当前占用:" + size + clearLogs?.summary = "删除已保存的运行日志和错误日志(当前占用:" + size + ")" + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -475,4 +515,4 @@ open class OUOSettingActivity : BaseActivity() { return intent } } -} \ No newline at end of file +} diff --git a/app/src/main/java/moe/ono/core/QLauncher.java b/app/src/main/java/moe/ono/core/QLauncher.java index 8368d6fc..421c3b4c 100644 --- a/app/src/main/java/moe/ono/core/QLauncher.java +++ b/app/src/main/java/moe/ono/core/QLauncher.java @@ -141,9 +141,12 @@ public static void injectLifecycleForProcess(Context ctx) { Parasitics.injectModuleResources(CacheConfig.getSplashActivity().getResources()); Parasitics.initForStubActivity(ctx); ServletPool.INSTANCE.injectServlet(); - QQInterfaces.Companion.update(); + try { + QQInterfaces.Companion.update(); + } catch (Throwable e) { + Logger.e("QQInterfaces.update failed during startup", e); + } }); } } } - diff --git a/app/src/main/java/moe/ono/hooks/base/api/QQMsgViewAdapter.kt b/app/src/main/java/moe/ono/hooks/base/api/QQMsgViewAdapter.kt index 642d5c94..db6fc165 100644 --- a/app/src/main/java/moe/ono/hooks/base/api/QQMsgViewAdapter.kt +++ b/app/src/main/java/moe/ono/hooks/base/api/QQMsgViewAdapter.kt @@ -11,14 +11,16 @@ import moe.ono.hooks._base.ApiHookItem import moe.ono.hooks._core.annotation.HookItem import moe.ono.reflex.ClassUtils import moe.ono.reflex.FieldUtils -import moe.ono.reflex.Ignore import moe.ono.reflex.MethodUtils import moe.ono.util.HostInfo +import moe.ono.util.Logger +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.util.LinkedHashSet @HookItem(path = "API/适配QQMsg内容ViewID") class QQMsgViewAdapter : ApiHookItem() { - companion object { private var contentViewId = 0 @@ -38,7 +40,7 @@ class QQMsgViewAdapter : ApiHookItem() { } } - private var unhook: XC_MethodHook.Unhook? = null + private val unhooks = ArrayList() private fun findContentViewId(): Int { return cGetInt( @@ -55,43 +57,99 @@ class QQMsgViewAdapter : ApiHookItem() { } override fun entry(loader: ClassLoader) { - if (findContentViewId() > 0) { - contentViewId = findContentViewId() + val cachedViewId = findContentViewId() + if (cachedViewId > 0) { + contentViewId = cachedViewId return } - val onMsgViewUpdate = - MethodUtils.create("com.tencent.mobileqq.aio.msglist.holder.AIOBubbleMsgItemVB") + + val holderClass = ClassUtils.findClass("com.tencent.mobileqq.aio.msglist.holder.AIOBubbleMsgItemVB") + val candidateMethods = collectCandidateMethods(holderClass) + if (candidateMethods.isEmpty()) { + Logger.e("适配QQMsg内容ViewID", "No candidate update method found in ${holderClass.name}") + return + } + + candidateMethods.forEach { method -> + unhooks.add(hookAfter(method) { param -> + handleItemViewUpdate(param.thisObject) + }) + } + } + + private fun collectCandidateMethods(holderClass: Class<*>): List { + val candidates = LinkedHashSet() + try { + MethodUtils.create(holderClass) + .methodName("handleUIState") .returnType(Void.TYPE) - .params(Int::class.java, Ignore::class.java, List::class.java, Bundle::class.java) - .first() - unhook = hookAfter(onMsgViewUpdate) { param -> - val thisObject = param.thisObject - val msgView = FieldUtils.create(thisObject) - .fieldType(View::class.java) - .firstValue(thisObject) - - val aioMsgItem = FieldUtils.create(thisObject) - .fieldType(ClassUtils.findClass("com.tencent.mobileqq.aio.msg.AIOMsgItem")) - .firstValue(thisObject) - - if (aioMsgItem == null || msgView == null) return@hookAfter - - val msgRecord: Any = MethodUtils.create(aioMsgItem.javaClass).methodName("getMsgRecord") - .callFirst(aioMsgItem) - - val elements: ArrayList = FieldUtils.getField( - msgRecord, "elements", - ArrayList::class.java - ) - - for (msgElement in elements) { - val type: Int = - FieldUtils.getField(msgElement, "elementType", Int::class.javaPrimitiveType) - //文本和图片类型的view 不解析其他类型的 否则解析不出来 - if (type <= 2) { - findContentView(msgView as ViewGroup) - break + .getResult() + .forEach { candidates.add(it) } + } catch (_: Throwable) { + } + + holderClass.declaredMethods + .filterTo(candidates) { isCandidateMethod(it) } + return candidates.toList() + } + + private fun isCandidateMethod(method: Method): Boolean { + if (Modifier.isStatic(method.modifiers) || method.returnType != Void.TYPE) { + return false + } + val parameterTypes = method.parameterTypes + if (parameterTypes.size !in 3..5) { + return false + } + val containsList = parameterTypes.any { List::class.java.isAssignableFrom(it) } + val containsBundle = parameterTypes.any { Bundle::class.java.isAssignableFrom(it) } + val containsInt = parameterTypes.any { + it == Int::class.javaPrimitiveType || it == Int::class.javaObjectType + } + return containsList && containsBundle && containsInt + } + + private fun handleItemViewUpdate(thisObject: Any) { + if (contentViewId > 0) { + clearHooks() + return + } + + val msgView = FieldUtils.create(thisObject) + .fieldType(View::class.java) + .firstValue(thisObject) ?: return + + val aioMsgItem = FieldUtils.create(thisObject) + .fieldType(ClassUtils.findClass("com.tencent.mobileqq.aio.msg.AIOMsgItem")) + .firstValue(thisObject) ?: return + + val msgRecord = try { + MethodUtils.create(aioMsgItem.javaClass) + .methodName("getMsgRecord") + .callFirst(aioMsgItem) + } catch (_: Throwable) { + return + } + + val elements = try { + FieldUtils.getField>(msgRecord, "elements", ArrayList::class.java) + } catch (_: Throwable) { + return + } + + val msgViewGroup = msgView as? ViewGroup ?: return + for (msgElement in elements) { + val type = try { + FieldUtils.getField(msgElement, "elementType", Int::class.javaPrimitiveType) + } catch (_: Throwable) { + continue + } + if (type <= 2) { + findContentView(msgViewGroup) + if (contentViewId > 0) { + clearHooks() } + break } } } @@ -102,11 +160,18 @@ class QQMsgViewAdapter : ApiHookItem() { if (child.javaClass.name == "com.tencent.qqnt.aio.holder.template.BubbleLayoutCompatPress") { contentViewId = child.id putContentViewId(child.id) - //解开hook - unhook?.unhook() break } } } -} \ No newline at end of file + private fun clearHooks() { + unhooks.forEach { + try { + it.unhook() + } catch (_: Throwable) { + } + } + unhooks.clear() + } +} diff --git a/app/src/main/java/moe/ono/service/QQInterfaces.kt b/app/src/main/java/moe/ono/service/QQInterfaces.kt index 124da4cd..505f009a 100644 --- a/app/src/main/java/moe/ono/service/QQInterfaces.kt +++ b/app/src/main/java/moe/ono/service/QQInterfaces.kt @@ -45,6 +45,38 @@ abstract class QQInterfaces { return null } + private fun isRuntimeReady(): Boolean { + return this::mRealHandlerReq.isInitialized + && this::mHandlerResponse.isInitialized + && this::mqqService.isInitialized + } + + private fun collectMissingMembers(): String { + val missingMembers = mutableListOf() + if (!this::mRealHandlerReq.isInitialized) { + missingMembers.add("mRealHandlerReq") + } + if (!this::mHandlerResponse.isInitialized) { + missingMembers.add("mHandlerResponse") + } + if (!this::mqqService.isInitialized) { + missingMembers.add("mqqService") + } + return missingMembers.joinToString() + } + + private fun ensureRuntimeReady(): Boolean { + if (isRuntimeReady()) { + return true + } + update() + if (isRuntimeReady()) { + return true + } + Logger.e("QQInterfaces: runtime unavailable -> ${collectMissingMembers()}") + return false + } + var app = (if (PlatformUtils.isMqqPackage()) MobileQQ.getMobileQQ().waitAppRuntime() else @@ -69,8 +101,6 @@ abstract class QQInterfaces { sendToServiceMsg(toServiceMsg) } - - // FIXME: 部分情况下发包失败 ,但无法精准复现 fun sendOidbSvcTrpcTcp( cmd: String, flag: Int, @@ -82,6 +112,9 @@ abstract class QQInterfaces { appSeq = generateSeq() } try { + if (!ensureRuntimeReady()) { + throw IllegalStateException("QQInterfaces runtime unavailable") + } sendReq(toServiceMsg) } catch (e: Exception) { Logger.e(e) @@ -92,9 +125,10 @@ abstract class QQInterfaces { } private fun sendReq(toServiceMsg: ToServiceMsg) { - // qq <= 8.9.8 + if (!ensureRuntimeReady()) { + throw IllegalStateException("QQInterfaces runtime unavailable") + } toServiceMsg.extraData.putBoolean("req_pb_protocol_flag", true) - // qq >= 8.9.10 toServiceMsg.attributes["req_pb_protocol_flag"] = true if (mRealHandlerReq.parameterTypes.size == 2) { MethodHandleUtil.invokeSpecial(mqqService, mRealHandlerReq, toServiceMsg, IServlet::class.java) @@ -104,10 +138,16 @@ abstract class QQInterfaces { } private fun decodeResponse(toServiceMsg: ToServiceMsg, fromServiceMsg: FromServiceMsg) { + if (!ensureRuntimeReady()) { + throw IllegalStateException("QQInterfaces runtime unavailable") + } MethodHandleUtil.invokeSpecial(mqqService, mHandlerResponse, fromServiceMsg.isSuccess, toServiceMsg, fromServiceMsg, null) } fun receive(seq: Int): JSONObject? { + if (!ensureRuntimeReady()) { + return null + } val startTime = System.currentTimeMillis() while (System.currentTimeMillis() - startTime < 5_000) { Thread.sleep(120) @@ -126,13 +166,18 @@ abstract class QQInterfaces { return null } - fun update(){ + fun update() { app = (if (PlatformUtils.isMqqPackage()) MobileQQ.getMobileQQ().waitAppRuntime() else MobileQQ.getMobileQQ().waitAppRuntime(null)) as AppInterface - val cBaseService = load("com.tencent.mobileqq.service.MobileQQServiceBase")!! + val cBaseService = load("com.tencent.mobileqq.service.MobileQQServiceBase") + if (cBaseService == null) { + Logger.e("QQInterfaces.update: MobileQQServiceBase not found") + return + } + cBaseService.getMethods(false).forEach { if (it.returnType == Void.TYPE && it.isPublic) { val paramTypes = it.parameterTypes @@ -155,33 +200,38 @@ abstract class QQInterfaces { } } } - app.getFields(false).forEach { + app.getFields().forEach { if (cBaseService.isAssignableFrom(it.type)) { Logger.d(it.name) it.isAccessible = true - mqqService = it.get(app)!! + val service = it.get(app) + if (service != null) { + mqqService = service + return@forEach + } } } - if (!this::mRealHandlerReq.isInitialized) { - throw RuntimeException("初始化失败 -> mRealHandlerReq") - } - if (!this::mHandlerResponse.isInitialized) { - throw RuntimeException("初始化失败 -> mHandlerResponse") - } - if (!this::mqqService.isInitialized) { // 9.2.70 修复 - app.getMethods(false).forEach { - if (it.returnType == cBaseService && it.isPublic - && it.name == "getMobileQQService" && it.paramCount == 0) { + if (!this::mqqService.isInitialized) { + app.getMethods().forEach { + if (it.isPublic + && it.name == "getMobileQQService" + && it.paramCount == 0 + && cBaseService.isAssignableFrom(it.returnType)) { Logger.d(it.toString()) it.isAccessible = true - mqqService = it.invoke(app)!! + val service = it.invoke(app) + if (service != null) { + mqqService = service + return@forEach + } } } } + if (!isRuntimeReady()) { + Logger.e("QQInterfaces.update: init failed -> ${collectMissingMembers()}") + } } } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/moe/ono/util/LogUtils.java b/app/src/main/java/moe/ono/util/LogUtils.java index 6418fd2d..ba504c0d 100644 --- a/app/src/main/java/moe/ono/util/LogUtils.java +++ b/app/src/main/java/moe/ono/util/LogUtils.java @@ -1,10 +1,12 @@ package moe.ono.util; - import static moe.ono.constants.Constants.PrekEnableLog; import android.annotation.SuppressLint; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.io.File; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -14,19 +16,48 @@ import moe.ono.util.io.FileUtils; public class LogUtils { - private static final String LOG_ROOT_DIRECTORY = PathTool.getModuleDataPath() + "/log/"; - private static String getRunLogDirectory() { - return LOG_ROOT_DIRECTORY + "RunLog" + File.separator; + private static final long LOG_RETENTION_MS = 7L * 24 * 60 * 60 * 1000; + private static final long CLEANUP_INTERVAL_MS = 6L * 60 * 60 * 1000; + private static volatile long sLastCleanupTimestamp; + + private LogUtils() { + } + + @NonNull + public static String getLogRootDirectory() { + return new File(PathTool.getModuleDataPath(), "log").getAbsolutePath() + File.separator; + } + + @NonNull + public static String getRunLogDirectory() { + return new File(getLogRootDirectory(), "RunLog").getAbsolutePath() + File.separator; } - private static String getErrorLogDirectory() { - return LOG_ROOT_DIRECTORY + "ErrorLog" + File.separator; + @NonNull + public static String getErrorLogDirectory() { + return new File(getLogRootDirectory(), "ErrorLog").getAbsolutePath() + File.separator; } + public static long getLogDirectorySize() { + return FileUtils.getDirSize(new File(getLogRootDirectory())); + } + + public static boolean clearLogs() { + try { + File rootDirectory = new File(getLogRootDirectory()); + if (!rootDirectory.exists()) { + return true; + } + FileUtils.deleteFile(rootDirectory); + return !rootDirectory.exists(); + } catch (Exception ignored) { + return false; + } + } /** - * @return 获取调用此方法的调用栈 + * @return 获取调用此方法的调用者 */ public static String getCallStack() { Throwable throwable = new Throwable(); @@ -39,78 +70,158 @@ public static String getCallStack() { * @param throwable new Throwable || Exception * @return 堆栈跟踪 */ - public static String getStackTrace(Throwable throwable) { + public static String getStackTrace(@NonNull Throwable throwable) { StringBuilder result = new StringBuilder(); result.append(throwable).append("\n"); StackTraceElement[] stackTraceElements = throwable.getStackTrace(); for (StackTraceElement stackTraceElement : stackTraceElements) { - //不把当前类加入结果中 - if (stackTraceElement.getClassName().equals(LogUtils.class.getName())) continue; + if (stackTraceElement.getClassName().equals(LogUtils.class.getName())) { + continue; + } result.append(stackTraceElement).append("\n"); } return result.toString(); } public static void addError(Throwable e) { - addError("Error Log", e.toString(), e); + writeLog(android.util.Log.ERROR, "Error Log", e == null ? "null" : e.toString(), e); } public static void addRunLog(Object content) { addRunLog("Run Log", content); } - /** - * 记录运行日志 确保能走到那一行代码 - * - * @param TAG(文件名) 内容 - */ - public static void addRunLog(String TAG, Object content) { - addLog(TAG, String.valueOf(content), content, false); + public static void addRunLog(String tag, Object content) { + writeLog(android.util.Log.DEBUG, tag, String.valueOf(content), null); } - /** - * 记录异常 - */ - public static void addError(String TAG, Throwable e) { - addLog(TAG, e.toString(), e, true); + public static void addError(String tag, Throwable e) { + writeLog(android.util.Log.ERROR, tag, e == null ? "null" : e.toString(), e); } - /** - * 记录异常 - * - * @param TAG (标签 文件名) - * @param Description 错误的相关描述 - * @param e Exception - */ - public static void addError(String TAG, String Description, Throwable e) { - addLog(TAG, Description, e, true); + public static void addError(String tag, String description, Throwable e) { + writeLog(android.util.Log.ERROR, tag, description, e); } + public static void addError(String tag, String msg) { + writeLog(android.util.Log.ERROR, tag, msg, null); + } - private static void addLog(String fileName, String Description, Object content, boolean isError) { - try { - if (!ConfigManager.getDefaultConfig().getBooleanOrFalse(PrekEnableLog)){ - return; - } - } catch (Exception ignored) {} + public static void writeLog(int priority, @Nullable String tag, @Nullable String message, @Nullable Throwable throwable) { + if (!isFileLoggingEnabled()) { + return; + } - String path = (isError ? getErrorLogDirectory() : getRunLogDirectory()) + fileName + ".log"; - StringBuilder stringBuffer = new StringBuilder(getTime()); - stringBuffer.append("\n").append(Description); - if (content instanceof Exception) { - stringBuffer.append("\n").append(getStackTrace((Exception) content)); + cleanupExpiredLogsIfNeeded(); + + boolean isError = priority >= android.util.Log.ERROR; + String safeTag = sanitizeTag(tag); + String logDirectory = isError ? getErrorLogDirectory() : getRunLogDirectory(); + String fileName = safeTag + '_' + getDate() + ".log"; + String path = new File(logDirectory, fileName).getAbsolutePath(); + + StringBuilder builder = new StringBuilder(); + builder.append(getTime()) + .append(' ') + .append(levelOf(priority)) + .append('/').append(safeTag) + .append(" [thread=").append(Thread.currentThread().getName()).append("]") + .append('\n') + .append(message == null ? "null" : message); + if (throwable != null) { + builder.append('\n').append(android.util.Log.getStackTraceString(throwable)); } - stringBuffer.append("\n\n"); - FileUtils.writeTextToFile(path, stringBuffer.toString(), true); + builder.append("\n\n"); + FileUtils.writeTextToFile(path, builder.toString(), true); } public static String getTime() { - @SuppressLint("SimpleDateFormat") SimpleDateFormat df = new SimpleDateFormat("[yyyy/MM/dd HH:mm:ss]"); + @SuppressLint("SimpleDateFormat") SimpleDateFormat df = new SimpleDateFormat("[yyyy/MM/dd HH:mm:ss]", Locale.getDefault()); Calendar calendar = Calendar.getInstance(); return df.format(calendar.getTime()); } - public static void addError(String TAG, String msg) { - addLog(TAG, msg, null, true); + @NonNull + private static String getDate() { + @SuppressLint("SimpleDateFormat") SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + return df.format(Calendar.getInstance().getTime()); + } + + private static boolean isFileLoggingEnabled() { + try { + return ConfigManager.getDefaultConfig().getBooleanOrFalse(PrekEnableLog); + } catch (Throwable ignored) { + return false; + } + } + + @NonNull + private static String sanitizeTag(@Nullable String tag) { + String fallbackTag = "common"; + if (tag == null) { + return fallbackTag; + } + String normalizedTag = tag.trim(); + if (normalizedTag.isEmpty()) { + return fallbackTag; + } + String safeTag = normalizedTag.replaceAll("[\\\\/:*?\"<>|\s]+", "_"); + if (safeTag.length() > 64) { + return safeTag.substring(0, 64); + } + return safeTag; + } + + @NonNull + private static String levelOf(int priority) { + switch (priority) { + case android.util.Log.VERBOSE: + return "V"; + case android.util.Log.DEBUG: + return "D"; + case android.util.Log.INFO: + return "I"; + case android.util.Log.WARN: + return "W"; + case android.util.Log.ERROR: + return "E"; + case android.util.Log.ASSERT: + return "A"; + default: + return "U"; + } + } + + private static void cleanupExpiredLogsIfNeeded() { + long now = System.currentTimeMillis(); + if (now - sLastCleanupTimestamp < CLEANUP_INTERVAL_MS) { + return; + } + synchronized (LogUtils.class) { + if (now - sLastCleanupTimestamp < CLEANUP_INTERVAL_MS) { + return; + } + deleteExpiredFiles(new File(getRunLogDirectory()), now); + deleteExpiredFiles(new File(getErrorLogDirectory()), now); + sLastCleanupTimestamp = now; + } + } + + private static void deleteExpiredFiles(@NonNull File directory, long now) { + File[] files = directory.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + if (file == null || !file.isFile()) { + continue; + } + if (now - file.lastModified() > LOG_RETENTION_MS) { + try { + file.delete(); + } catch (Exception ignored) { + } + } + } } } diff --git a/app/src/main/java/moe/ono/util/Logger.java b/app/src/main/java/moe/ono/util/Logger.java index 29ef7956..7229d5e2 100644 --- a/app/src/main/java/moe/ono/util/Logger.java +++ b/app/src/main/java/moe/ono/util/Logger.java @@ -1,104 +1,112 @@ package moe.ono.util; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import de.robv.android.xposed.XposedBridge; import moe.ono.BuildConfig; public class Logger { + private static final String TAG = BuildConfig.TAG; + private Logger() { } - private static final String TAG = BuildConfig.TAG; - public static void e(@NonNull String msg) { - android.util.Log.e(TAG, msg); - LogUtils.addError("common", msg); + log(android.util.Log.ERROR, null, msg, null); } public static void e(String tag, @NonNull String msg) { - android.util.Log.e(TAG, tag + ": "+ msg); + log(android.util.Log.ERROR, tag, msg, null); } public static void w(@NonNull String msg) { - android.util.Log.w(TAG, msg); - LogUtils.addRunLog("common", msg); + log(android.util.Log.WARN, null, msg, null); } + public static void w(String tag, @NonNull String msg) { - android.util.Log.w(TAG, tag + ": "+ msg); - LogUtils.addRunLog("common", msg); + log(android.util.Log.WARN, tag, msg, null); } public static void i(@NonNull String msg) { - android.util.Log.i(TAG, msg); - LogUtils.addRunLog("common", msg); + log(android.util.Log.INFO, null, msg, null); } + public static void i(String tag, @NonNull String msg) { - android.util.Log.i(TAG, tag + ": "+ msg); - LogUtils.addRunLog("common", msg); + log(android.util.Log.INFO, tag, msg, null); } - public static void d(@NonNull String msg) { - android.util.Log.d(TAG, msg); - LogUtils.addRunLog("common", msg); + log(android.util.Log.DEBUG, null, msg, null); } + public static void d(String tag, @NonNull String msg) { - android.util.Log.d(TAG, tag + ": "+ msg); - LogUtils.addRunLog("common", msg); + log(android.util.Log.DEBUG, tag, msg, null); } public static void v(@NonNull String msg) { - android.util.Log.v(TAG, msg); + log(android.util.Log.VERBOSE, null, msg, null); } + public static void v(String tag, @NonNull String msg) { - android.util.Log.v(TAG, tag + ": "+ msg); + log(android.util.Log.VERBOSE, tag, msg, null); } public static void e(@NonNull Throwable e) { - android.util.Log.e(TAG, e.toString(), e); - LogUtils.addError("common", e); + log(android.util.Log.ERROR, null, e.toString(), e); } public static void w(@NonNull Throwable e) { - android.util.Log.w(TAG, e.toString(), e); + log(android.util.Log.WARN, null, e.toString(), e); } public static void i(@NonNull Throwable e) { - android.util.Log.i(TAG, e.toString(), e); + log(android.util.Log.INFO, null, e.toString(), e); } public static void i(@NonNull Throwable e, boolean output) { - android.util.Log.i(TAG, e.toString(), e); - if (output){ + log(android.util.Log.INFO, null, e.toString(), e); + if (output) { XposedBridge.log(e); } } public static void d(@NonNull Throwable e) { - android.util.Log.d(TAG, e.toString(), e); + log(android.util.Log.DEBUG, null, e.toString(), e); } public static void e(@NonNull String msg, @NonNull Throwable e) { - android.util.Log.e(TAG, msg, e); - LogUtils.addError("common", e); + log(android.util.Log.ERROR, msg, msg, e); } public static void w(@NonNull String msg, @NonNull Throwable e) { - android.util.Log.w(TAG, msg, e); + log(android.util.Log.WARN, msg, msg, e); } public static void i(@NonNull String msg, @NonNull Throwable e) { - android.util.Log.i(TAG, msg, e); + log(android.util.Log.INFO, msg, msg, e); } public static void d(@NonNull String msg, @NonNull Throwable e) { - android.util.Log.d(TAG, msg, e); + log(android.util.Log.DEBUG, msg, msg, e); } @NonNull public static String getStackTraceString(@NonNull Throwable th) { return android.util.Log.getStackTraceString(th); } + + private static void log(int priority, @Nullable String subTag, @NonNull String msg, @Nullable Throwable throwable) { + String resolvedTag = subTag == null || subTag.trim().isEmpty() ? TAG : subTag.trim(); + String displayMessage = resolvedTag.equals(TAG) || resolvedTag.equals(msg) ? msg : resolvedTag + ": " + msg; + + if (throwable == null) { + android.util.Log.println(priority, TAG, displayMessage); + } else { + android.util.Log.println(priority, TAG, displayMessage + '\n' + android.util.Log.getStackTraceString(throwable)); + } + + LogUtils.writeLog(priority, resolvedTag, msg, throwable); + } } diff --git a/app/src/main/java/moe/ono/util/PathTool.java b/app/src/main/java/moe/ono/util/PathTool.java index 1a94902a..77496c77 100644 --- a/app/src/main/java/moe/ono/util/PathTool.java +++ b/app/src/main/java/moe/ono/util/PathTool.java @@ -24,14 +24,16 @@ public static String getModuleDataPath() { String directory = getStorageDirectory() + "/Android/data/" + HostInfo.getHostInfo().getPackageName() + "/ONO"; File file = new File(directory); if (!file.exists()) { - Logger.d("file.mkdirs(): " + file.mkdirs()); + file.mkdirs(); } return directory; } public static String getModuleCachePath(String dirName) { File cache = new File(getModuleDataPath() + "/cache/" + dirName); - if (!cache.exists()) Logger.d("cache.mkdirs(): " + cache.mkdirs()); + if (!cache.exists()) { + cache.mkdirs(); + } return cache.getAbsolutePath(); } diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 4b3e51b2..79d996b3 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -3,31 +3,31 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + android:title="设定"> + app:title="聊天与消息" /> + app:title="资料卡" /> + app:title="优化与修复" /> + app:title="开发者选项" /> + app:title="娱乐功能" /> + android:title="调试"> + android:persistent="false" /> + + + + android:title="兼容"> - + android:title="XC_MethodHook 优先级" + android:persistent="false" /> + android:title="关于">