559 lines
21 KiB
Java
559 lines
21 KiB
Java
package cc.winboll.studio.libappbase;
|
||
|
||
import android.app.Activity;
|
||
import android.app.Application;
|
||
import android.content.ActivityNotFoundException;
|
||
import android.content.ClipData;
|
||
import android.content.ClipboardManager;
|
||
import android.content.Context;
|
||
import android.content.Intent;
|
||
import android.content.pm.PackageInfo;
|
||
import android.content.pm.PackageManager;
|
||
import android.content.res.Resources;
|
||
import android.graphics.Color;
|
||
import android.os.Build;
|
||
import android.os.Bundle;
|
||
import android.os.Handler;
|
||
import android.os.Looper;
|
||
import android.text.TextUtils;
|
||
import android.view.Menu;
|
||
import android.view.MenuItem;
|
||
import android.view.ViewGroup;
|
||
import android.widget.HorizontalScrollView;
|
||
import android.widget.ScrollView;
|
||
import android.widget.TextView;
|
||
import android.widget.Toast;
|
||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||
import java.io.File;
|
||
import java.io.FileInputStream;
|
||
import java.io.FileOutputStream;
|
||
import java.io.IOException;
|
||
import java.io.ObjectInputStream;
|
||
import java.io.ObjectOutputStream;
|
||
import java.io.PrintWriter;
|
||
import java.io.StringWriter;
|
||
import java.lang.Thread.UncaughtExceptionHandler;
|
||
import java.text.SimpleDateFormat;
|
||
import java.util.Date;
|
||
import java.util.Locale;
|
||
|
||
/**
|
||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||
* @Date 2025/11/11 20:14
|
||
* @Describe * 应用全局崩溃处理类(单例逻辑)
|
||
* 核心功能:捕获应用未捕获异常,记录崩溃日志到文件,启动崩溃报告页面,
|
||
* 并通过「崩溃保险丝」机制防止重复崩溃,保障基础功能可用
|
||
*/
|
||
public final class CrashHandler {
|
||
|
||
/** 日志标签,用于当前类的日志输出标识 */
|
||
public static final String TAG = "CrashHandler";
|
||
|
||
/** 崩溃报告页面标题 */
|
||
public static final String TITTLE = "CrashReport";
|
||
|
||
/** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
|
||
public static final String EXTRA_CRASH_LOG = "crashInfo";
|
||
|
||
/** SharedPreferences 存储键(用于记录崩溃状态) */
|
||
final static String PREFS = CrashHandler.class.getName() + "PREFS";
|
||
/** SharedPreferences 中存储「是否发生崩溃」的键 */
|
||
final static String PREFS_CRASHHANDLER_ISCRASHHAPPEN = "PREFS_CRASHHANDLER_ISCRASHHAPPEN";
|
||
|
||
/** 崩溃保险丝状态文件路径(存储当前熔断等级) */
|
||
public static String _CrashCountFilePath;
|
||
|
||
/** 系统默认的未捕获异常处理器(用于降级处理,避免 CrashHandler 自身崩溃) */
|
||
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
||
|
||
/**
|
||
* 初始化崩溃处理器(默认存储路径)
|
||
* 调用重载方法,崩溃日志默认存储在应用外部私有目录的 crash 文件夹下
|
||
* @param app 全局 Application 实例(用于获取存储目录、包信息等)
|
||
*/
|
||
public static void init(Application app) {
|
||
// 初始化崩溃保险丝状态文件路径(外部存储/CrashHandler/IsCrashHandlerCrashHappen.dat)
|
||
_CrashCountFilePath = app.getExternalFilesDir("CrashHandler") + "/IsCrashHandlerCrashHappen.dat";
|
||
LogUtils.d(TAG, String.format("_CrashCountFilePath %s", _CrashCountFilePath));
|
||
// 调用带目录参数的初始化方法,传入 null 使用默认路径
|
||
init(app, null);
|
||
}
|
||
|
||
/**
|
||
* 初始化崩溃处理器(指定日志存储目录)
|
||
* 替换系统默认的未捕获异常处理器,自定义崩溃处理逻辑
|
||
* @param app 全局 Application 实例
|
||
* @param crashDir 崩溃日志存储目录(null 则使用默认路径)
|
||
*/
|
||
public static void init(final Application app, final String crashDir) {
|
||
// 设置自定义未捕获异常处理器
|
||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
|
||
@Override
|
||
public void uncaughtException(Thread thread, Throwable throwable) {
|
||
try {
|
||
// 尝试处理崩溃(捕获内部异常,避免 CrashHandler 自身崩溃)
|
||
tryUncaughtException(thread, throwable);
|
||
} catch (Throwable e) {
|
||
e.printStackTrace();
|
||
// 处理失败时,交给系统默认处理器兜底
|
||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
|
||
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 实际处理崩溃的核心方法
|
||
* 1. 熔断保险丝(记录崩溃次数);2. 收集崩溃信息;3. 写入日志文件;4. 启动崩溃报告页面
|
||
* @param thread 发生崩溃的线程
|
||
* @param throwable 崩溃异常对象(包含堆栈信息)
|
||
*/
|
||
private void tryUncaughtException(Thread thread, Throwable throwable) {
|
||
// 触发崩溃保险丝(每次崩溃熔断一次,降低防护等级)
|
||
AppCrashSafetyWire.getInstance().burnSafetyWire();
|
||
|
||
// 格式化崩溃发生时间(用于日志文件名和内容)
|
||
final String time = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss", Locale.getDefault()).format(new Date());
|
||
// 创建崩溃日志文件(默认路径:外部存储/crash/[时间].txt)
|
||
File crashFile = new File(
|
||
TextUtils.isEmpty(crashDir) ? new File(app.getExternalFilesDir(null), "crash") : new File(crashDir),
|
||
"crash_" + time + ".txt"
|
||
);
|
||
|
||
// 获取应用版本信息(版本名、版本号)
|
||
String versionName = "unknown";
|
||
long versionCode = 0;
|
||
try {
|
||
PackageInfo packageInfo = app.getPackageManager().getPackageInfo(app.getPackageName(), 0);
|
||
versionName = packageInfo.versionName;
|
||
// 适配 Android 9.0+(API 28)的版本号获取方式
|
||
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
||
} catch (PackageManager.NameNotFoundException ignored) {}
|
||
|
||
// 将异常堆栈信息转换为字符串
|
||
String fullStackTrace;
|
||
{
|
||
StringWriter sw = new StringWriter();
|
||
PrintWriter pw = new PrintWriter(sw);
|
||
throwable.printStackTrace(pw); // 将异常堆栈写入 PrintWriter
|
||
fullStackTrace = sw.toString();
|
||
pw.close();
|
||
}
|
||
|
||
// 拼接崩溃信息(设备信息 + 应用信息 + 堆栈信息)
|
||
StringBuilder sb = new StringBuilder();
|
||
sb.append("************* Crash Head ****************\n");
|
||
sb.append("Time Of Crash : ").append(time).append("\n");
|
||
sb.append("Device Manufacturer : ").append(Build.MANUFACTURER).append("\n"); // 设备厂商
|
||
sb.append("Device Model : ").append(Build.MODEL).append("\n"); // 设备型号
|
||
sb.append("Android Version : ").append(Build.VERSION.RELEASE).append("\n"); // Android 版本
|
||
sb.append("Android SDK : ").append(Build.VERSION.SDK_INT).append("\n"); // SDK 版本
|
||
sb.append("App VersionName : ").append(versionName).append("\n"); // 应用版本名
|
||
sb.append("App VersionCode : ").append(versionCode).append("\n"); // 应用版本号
|
||
sb.append("************* Crash Head ****************\n");
|
||
sb.append("\n").append(fullStackTrace); // 拼接异常堆栈
|
||
|
||
final String errorLog = sb.toString();
|
||
|
||
// 将崩溃日志写入文件(忽略写入失败)
|
||
try {
|
||
writeFile(crashFile, errorLog);
|
||
} catch (IOException ignored) {}
|
||
|
||
// 启动崩溃报告页面(标签用于代码块折叠)
|
||
gotoCrashActiviy: {
|
||
Intent intent = new Intent();
|
||
LogUtils.d(TAG, "gotoCrashActiviy: ");
|
||
|
||
// 根据保险丝状态选择启动的崩溃页面
|
||
if (AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
|
||
LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
|
||
// 保险丝正常:启动自定义样式的崩溃报告页面(GlobalCrashActivity)
|
||
intent.setClass(app, GlobalCrashActivity.class);
|
||
intent.putExtra(EXTRA_CRASH_LOG, errorLog); // 传递崩溃日志
|
||
} else {
|
||
LogUtils.d(TAG, "gotoCrashActiviy: else");
|
||
// 保险丝熔断:启动基础版崩溃页面(CrashActivity,避免复杂页面再次崩溃)
|
||
intent.setClass(app, CrashActivity.class);
|
||
intent.putExtra(EXTRA_CRASH_LOG, errorLog);
|
||
}
|
||
|
||
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
|
||
intent.addFlags(
|
||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||
| Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||
);
|
||
|
||
try {
|
||
if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
|
||
// 如果是 debug 版,启动崩溃页面窗口
|
||
app.startActivity(intent);
|
||
} else {
|
||
// 如果是 release 版,就只发送一个通知
|
||
CrashHandleNotifyUtils.handleUncaughtException(app, intent);
|
||
}
|
||
// 终止当前进程(确保完全重启)
|
||
android.os.Process.killProcess(android.os.Process.myPid());
|
||
System.exit(0);
|
||
|
||
} catch (ActivityNotFoundException e) {
|
||
// 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器
|
||
e.printStackTrace();
|
||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
|
||
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
||
}
|
||
} catch (Exception e) {
|
||
// 其他异常,兜底处理
|
||
e.printStackTrace();
|
||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
|
||
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将字符串内容写入文件(创建父目录、覆盖写入)
|
||
* @param file 目标文件(包含路径)
|
||
* @param content 待写入的内容(崩溃日志)
|
||
* @throws IOException 文件创建或写入失败时抛出
|
||
*/
|
||
private void writeFile(File file, String content) throws IOException {
|
||
File parentFile = file.getParentFile();
|
||
// 父目录不存在则创建
|
||
if (parentFile != null && !parentFile.exists()) {
|
||
parentFile.mkdirs();
|
||
}
|
||
file.createNewFile(); // 创建文件
|
||
FileOutputStream fos = new FileOutputStream(file);
|
||
fos.write(content.getBytes()); // 写入内容(默认 UTF-8 编码)
|
||
try {
|
||
fos.close(); // 关闭流
|
||
} catch (IOException e) {}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 应用崩溃保险丝内部类(单例)
|
||
* 核心作用:限制短时间内重复崩溃,通过「熔断等级」控制崩溃页面启动策略
|
||
* 等级范围:MINI(1)~ MAX(2),每次崩溃等级-1,熔断后启动基础版崩溃页面
|
||
*/
|
||
public static final class AppCrashSafetyWire {
|
||
|
||
/** 单例实例(volatile 保证多线程可见性) */
|
||
private static volatile AppCrashSafetyWire _AppCrashSafetyWire;
|
||
|
||
/** 当前熔断等级(1:最低防护;2:最高防护;≤0:熔断) */
|
||
private volatile Integer currentSafetyLevel;
|
||
/** 最低熔断等级(1,再崩溃则熔断) */
|
||
private static final int _MINI = 1;
|
||
/** 最高熔断等级(2,初始状态) */
|
||
private static final int _MAX = 2;
|
||
|
||
/**
|
||
* 私有构造方法(单例模式,禁止外部实例化)
|
||
* 初始化时加载本地存储的熔断等级
|
||
*/
|
||
private AppCrashSafetyWire() {
|
||
LogUtils.d(TAG, "AppCrashSafetyWire()");
|
||
currentSafetyLevel = loadCurrentSafetyLevel();
|
||
}
|
||
|
||
/**
|
||
* 获取单例实例(双重检查锁定,线程安全)
|
||
* @return AppCrashSafetyWire 单例
|
||
*/
|
||
public static synchronized AppCrashSafetyWire getInstance() {
|
||
if (_AppCrashSafetyWire == null) {
|
||
_AppCrashSafetyWire = new AppCrashSafetyWire();
|
||
}
|
||
return _AppCrashSafetyWire;
|
||
}
|
||
|
||
/**
|
||
* 设置当前熔断等级(内存中)
|
||
* @param currentSafetyLevel 目标等级(1~2)
|
||
*/
|
||
public void setCurrentSafetyLevel(int currentSafetyLevel) {
|
||
this.currentSafetyLevel = currentSafetyLevel;
|
||
}
|
||
|
||
/**
|
||
* 获取当前熔断等级(内存中)
|
||
* @return 当前等级(1~2 或 null)
|
||
*/
|
||
public int getCurrentSafetyLevel() {
|
||
return currentSafetyLevel;
|
||
}
|
||
|
||
/**
|
||
* 保存熔断等级到本地文件(持久化,重启应用生效)
|
||
* @param currentSafetyLevel 待保存的等级
|
||
*/
|
||
public void saveCurrentSafetyLevel(int currentSafetyLevel) {
|
||
LogUtils.d(TAG, "saveCurrentSafetyLevel()");
|
||
this.currentSafetyLevel = currentSafetyLevel;
|
||
try {
|
||
// 序列化等级到文件(ObjectOutputStream 写入 int)
|
||
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(_CrashCountFilePath));
|
||
oos.writeInt(currentSafetyLevel);
|
||
oos.flush();
|
||
oos.close();
|
||
LogUtils.d(TAG, String.format("saveCurrentSafetyLevel writeInt currentSafetyLevel %d", currentSafetyLevel));
|
||
} catch (IOException e) {
|
||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从本地文件加载熔断等级(应用启动时初始化)
|
||
* @return 加载的等级(文件不存在则初始化为 MAX(2))
|
||
*/
|
||
public int loadCurrentSafetyLevel() {
|
||
LogUtils.d(TAG, "loadCurrentSafetyLevel()");
|
||
try {
|
||
File f = new File(_CrashCountFilePath);
|
||
if (f.exists()) {
|
||
// 反序列化从文件读取等级
|
||
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(_CrashCountFilePath));
|
||
currentSafetyLevel = ois.readInt();
|
||
LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() readInt currentSafetyLevel %d", currentSafetyLevel));
|
||
} else {
|
||
// 文件不存在,初始化等级为最高(2)并保存
|
||
currentSafetyLevel = _MAX;
|
||
LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() currentSafetyLevel init to _MAX->%d", _MAX));
|
||
saveCurrentSafetyLevel(currentSafetyLevel);
|
||
}
|
||
} catch (IOException e) {
|
||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||
}
|
||
return currentSafetyLevel;
|
||
}
|
||
|
||
/**
|
||
* 熔断保险丝(每次崩溃调用,降低防护等级)
|
||
* @return 熔断后是否仍在防护范围内(true:是;false:已熔断)
|
||
*/
|
||
boolean burnSafetyWire() {
|
||
LogUtils.d(TAG, "burnSafetyWire()");
|
||
// 加载当前等级
|
||
int safeLevel = loadCurrentSafetyLevel();
|
||
// 若在防护范围内(1~2),等级-1 并保存
|
||
if (isSafetyWireWorking(safeLevel)) {
|
||
LogUtils.d(TAG, "burnSafetyWire() use");
|
||
saveCurrentSafetyLevel(safeLevel - 1);
|
||
// 返回熔断后的状态
|
||
return isSafetyWireWorking(safeLevel - 1);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 检查熔断等级是否在有效范围内(1~2)
|
||
* @param safetyLevel 待检查的等级
|
||
* @return true:在范围内(防护有效);false:超出范围(已熔断)
|
||
*/
|
||
boolean isSafetyWireWorking(int safetyLevel) {
|
||
LogUtils.d(TAG, "isSafetyWireOK()");
|
||
LogUtils.d(TAG, String.format("SafetyLevel %d", safetyLevel));
|
||
|
||
if (safetyLevel >= _MINI && safetyLevel <= _MAX) {
|
||
LogUtils.d(TAG, String.format("In Safety Level"));
|
||
return true;
|
||
}
|
||
LogUtils.d(TAG, String.format("Out of Safety Level"));
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 立即恢复熔断等级到最高(2)
|
||
* 用于重启应用后重置防护状态
|
||
*/
|
||
void resumeToMaximumImmediately() {
|
||
LogUtils.d(TAG, "resumeToMaximumImmediately() call saveCurrentSafetyLevel(_MAX)");
|
||
AppCrashSafetyWire.getInstance().saveCurrentSafetyLevel(_MAX);
|
||
}
|
||
|
||
/**
|
||
* 关闭防护(设置等级为最低(1))
|
||
* 下次崩溃直接熔断
|
||
*/
|
||
void off() {
|
||
LogUtils.d(TAG, "off()");
|
||
saveCurrentSafetyLevel(_MINI);
|
||
}
|
||
|
||
/**
|
||
* 检查当前保险丝是否有效(防护未熔断)
|
||
* @return true:有效(等级 1~2);false:已熔断
|
||
*/
|
||
boolean isAppCrashSafetyWireOK() {
|
||
LogUtils.d(TAG, "isAppCrashSafetyWireOK()");
|
||
currentSafetyLevel = loadCurrentSafetyLevel();
|
||
return isSafetyWireWorking(currentSafetyLevel);
|
||
}
|
||
|
||
/**
|
||
* 延迟恢复保险丝到最高等级(500ms 后)
|
||
* 核心作用:崩溃页面启动后,若下次即将熔断,提前恢复防护等级,避免持续崩溃
|
||
* @param context 上下文(用于获取主线程 Handler)
|
||
*/
|
||
void postResumeCrashSafetyWireHandler(final Context context) {
|
||
// 主线程延迟 500ms 执行(避免页面启动时阻塞)
|
||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||
@Override
|
||
public void run() {
|
||
LogUtils.d(TAG, "Handler run()");
|
||
// 检查:若当前等级-1 后超出防护范围(即将熔断),则恢复到最高等级
|
||
if (!AppCrashSafetyWire.getInstance().isSafetyWireWorking(currentSafetyLevel - 1)) {
|
||
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
|
||
LogUtils.d(TAG, "postResumeCrashSafetyWireHandler: 恢复保险丝到最高等级");
|
||
}
|
||
}
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 基础版崩溃报告页面(保险丝熔断时启动)
|
||
* 极简实现:仅展示崩溃日志,提供复制、重启功能,避免复杂布局导致二次崩溃
|
||
*/
|
||
public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
|
||
/** 菜单标识:复制崩溃日志 */
|
||
private static final int MENUITEM_COPY = 0;
|
||
/** 菜单标识:重启应用 */
|
||
private static final int MENUITEM_RESTART = 1;
|
||
|
||
/** 崩溃日志文本(从 CrashHandler 传递过来) */
|
||
private String mLog;
|
||
|
||
@Override
|
||
protected void onCreate(Bundle savedInstanceState) {
|
||
super.onCreate(savedInstanceState);
|
||
// 初始化崩溃保险丝延迟恢复机制
|
||
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
|
||
|
||
// 获取传递的崩溃日志
|
||
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
|
||
// 设置系统默认主题(避免自定义主题冲突)
|
||
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
|
||
|
||
// 动态创建布局(避免 XML 布局加载异常)
|
||
setContentView: {
|
||
// 垂直滚动视图(处理日志过长)
|
||
ScrollView contentView = new ScrollView(this);
|
||
contentView.setFillViewport(true);
|
||
|
||
// 水平滚动视图(处理日志行过长)
|
||
HorizontalScrollView hw = new HorizontalScrollView(this);
|
||
hw.setBackgroundColor(Color.GRAY); // 背景色设为灰色
|
||
|
||
// 日志显示文本框
|
||
TextView message = new TextView(this);
|
||
{
|
||
int padding = dp2px(16); // 内边距 16dp(适配不同屏幕)
|
||
message.setPadding(padding, padding, padding, padding);
|
||
message.setText(mLog); // 设置崩溃日志
|
||
message.setTextColor(Color.BLACK); // 文字黑色
|
||
message.setTextIsSelectable(true); // 支持文本选择(便于手动复制)
|
||
}
|
||
|
||
// 组装布局:TextView -> HorizontalScrollView -> ScrollView
|
||
hw.addView(message);
|
||
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||
// 设置当前 Activity 布局
|
||
setContentView(contentView);
|
||
|
||
// 配置 ActionBar 标题和副标题
|
||
getActionBar().setTitle(TITTLE);
|
||
getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重写返回键逻辑:点击返回键直接重启应用
|
||
*/
|
||
@Override
|
||
public void onBackPressed() {
|
||
restart();
|
||
}
|
||
|
||
/**
|
||
* 重启当前应用(与 GlobalCrashActivity 逻辑一致)
|
||
* 清除任务栈,启动主 Activity,终止当前进程
|
||
*/
|
||
private void restart() {
|
||
PackageManager pm = getPackageManager();
|
||
// 获取应用启动意图(默认启动主 Activity)
|
||
Intent intent = pm.getLaunchIntentForPackage(getPackageName());
|
||
if (intent != null) {
|
||
// 设置意图标志:清除原有任务栈,创建新任务
|
||
intent.addFlags(
|
||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||
| Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||
);
|
||
startActivity(intent);
|
||
}
|
||
// 关闭当前页面,终止进程,确保完全重启
|
||
finish();
|
||
android.os.Process.killProcess(android.os.Process.myPid());
|
||
System.exit(0);
|
||
}
|
||
|
||
/**
|
||
* dp 转 px(适配不同屏幕密度)
|
||
* @param dpValue dp 值
|
||
* @return 转换后的 px 值
|
||
*/
|
||
private int dp2px(final float dpValue) {
|
||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||
return (int) (dpValue * scale + 0.5f); // 四舍五入确保精度
|
||
}
|
||
|
||
/**
|
||
* 菜单点击事件回调(处理复制、重启)
|
||
* @param item 被点击的菜单项
|
||
* @return false:不消费事件(保持默认行为)
|
||
*/
|
||
@Override
|
||
public boolean onMenuItemClick(MenuItem item) {
|
||
switch (item.getItemId()) {
|
||
case MENUITEM_COPY:
|
||
// 复制日志到剪贴板
|
||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
|
||
break;
|
||
case MENUITEM_RESTART:
|
||
// 恢复保险丝到最高等级,然后重启应用
|
||
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
|
||
restart();
|
||
break;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 创建 ActionBar 菜单(添加复制、重启项)
|
||
* @param menu 菜单容器
|
||
* @return true:显示菜单
|
||
*/
|
||
@Override
|
||
public boolean onCreateOptionsMenu(Menu menu) {
|
||
// 添加「复制」菜单:有空间时显示在 ActionBar,否则放入溢出菜单
|
||
menu.add(0, MENUITEM_COPY, 0, "Copy")
|
||
.setOnMenuItemClickListener(this)
|
||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||
// 添加「重启」菜单:同上
|
||
menu.add(0, MENUITEM_RESTART, 0, "Restart")
|
||
.setOnMenuItemClickListener(this)
|
||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|