恢复APPBase_Bck20251205_123340_337版最后提交的源码

This commit is contained in:
2025-12-06 14:55:37 +08:00
parent 7f030cafda
commit 2aaacc88c3
16 changed files with 1326 additions and 937 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sat Dec 06 14:24:56 HKT 2025 #Sat Dec 06 06:54:32 GMT 2025
stageCount=2 stageCount=2
libraryProject=libappbase libraryProject=libappbase
baseVersion=15.12 baseVersion=15.12
publishVersion=15.12.1 publishVersion=15.12.1
buildCount=0 buildCount=2
baseBetaVersion=15.12.2 baseBetaVersion=15.12.2

View File

@@ -2,7 +2,7 @@
<manifest <manifest
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.appbase"> package="cc.winboll.studio.appbase">
<application <application
android:name=".App" android:name=".App"
android:icon="@drawable/ic_winboll" android:icon="@drawable/ic_winboll"
@@ -11,7 +11,7 @@
android:resizeableActivity="true" android:resizeableActivity="true"
android:process=":App"> android:process=":App">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:exported="true" android:exported="true"
@@ -29,13 +29,15 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".GlobalApplication$CrashActivity"/> <activity android:name=".GlobalApplication$CrashActivity"/>
<meta-data <meta-data
android:name="android.max_aspect" android:name="android.max_aspect"
android:value="4.0"/> android:value="4.0"/>
<activity android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"/>
</application> </application>
</manifest> </manifest>

View File

@@ -2,6 +2,7 @@ package cc.winboll.studio.appbase;
import cc.winboll.studio.libappbase.GlobalApplication; import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.BuildConfig;
/** /**
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>
@@ -21,6 +22,8 @@ public class App extends GlobalApplication {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置) super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置)
//setIsDebugging(false);
setIsDebugging(BuildConfig.DEBUG);
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用) // 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext()); ToastUtils.init(getApplicationContext());
} }

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sat Dec 06 14:24:33 HKT 2025 #Sat Dec 06 06:54:32 GMT 2025
stageCount=2 stageCount=2
libraryProject=libappbase libraryProject=libappbase
baseVersion=15.12 baseVersion=15.12
publishVersion=15.12.1 publishVersion=15.12.1
buildCount=0 buildCount=2
baseBetaVersion=15.12.2 baseBetaVersion=15.12.2

View File

@@ -3,8 +3,9 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.libappbase"> package="cc.winboll.studio.libappbase">
<application> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<activity <activity
android:name=".CrashHandler$CrashActivity" android:name=".CrashHandler$CrashActivity"
android:label="CrashActivity" android:label="CrashActivity"

View File

@@ -23,6 +23,7 @@ import android.widget.HorizontalScrollView;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@@ -52,7 +53,7 @@ public final class CrashHandler {
public static final String TITTLE = "CrashReport"; public static final String TITTLE = "CrashReport";
/** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */ /** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
public static final String EXTRA_CRASH_INFO = "crashInfo"; public static final String EXTRA_CRASH_LOG = "crashInfo";
/** SharedPreferences 存储键(用于记录崩溃状态) */ /** SharedPreferences 存储键(用于记录崩溃状态) */
final static String PREFS = CrashHandler.class.getName() + "PREFS"; final static String PREFS = CrashHandler.class.getName() + "PREFS";
@@ -169,12 +170,12 @@ public final class CrashHandler {
LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK"); LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
// 保险丝正常启动自定义样式的崩溃报告页面GlobalCrashActivity // 保险丝正常启动自定义样式的崩溃报告页面GlobalCrashActivity
intent.setClass(app, GlobalCrashActivity.class); intent.setClass(app, GlobalCrashActivity.class);
intent.putExtra(EXTRA_CRASH_INFO, errorLog); // 传递崩溃日志 intent.putExtra(EXTRA_CRASH_LOG, errorLog); // 传递崩溃日志
} else { } else {
LogUtils.d(TAG, "gotoCrashActiviy: else"); LogUtils.d(TAG, "gotoCrashActiviy: else");
// 保险丝熔断启动基础版崩溃页面CrashActivity避免复杂页面再次崩溃 // 保险丝熔断启动基础版崩溃页面CrashActivity避免复杂页面再次崩溃
intent.setClass(app, CrashActivity.class); intent.setClass(app, CrashActivity.class);
intent.putExtra(EXTRA_CRASH_INFO, errorLog); intent.putExtra(EXTRA_CRASH_LOG, errorLog);
} }
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面) // 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
@@ -185,10 +186,17 @@ public final class CrashHandler {
); );
try { try {
// 启动崩溃页面,终止当前进程(确保完全重启) if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
app.startActivity(intent); // 如果是 debug 版,启动崩溃页面窗口
app.startActivity(intent);
} else {
// 如果是 release 版,就只发送一个通知
CrashHandleNotifyUtils.handleUncaughtException(app, intent);
}
// 终止当前进程(确保完全重启)
android.os.Process.killProcess(android.os.Process.myPid()); android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0); System.exit(0);
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
// 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器 // 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器
e.printStackTrace(); e.printStackTrace();
@@ -428,7 +436,7 @@ public final class CrashHandler {
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext()); AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
// 获取传递的崩溃日志 // 获取传递的崩溃日志
mLog = getIntent().getStringExtra(EXTRA_CRASH_INFO); mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
// 设置系统默认主题(避免自定义主题冲突) // 设置系统默认主题(避免自定义主题冲突)
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar); setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);

View File

@@ -85,6 +85,7 @@ public class GlobalApplication extends Application {
super.onCreate(); super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成) // 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this; sInstance = this;
// 初始化基础组件日志、崩溃处理、Toast // 初始化基础组件日志、崩溃处理、Toast
initCoreComponents(); initCoreComponents();
@@ -169,6 +170,7 @@ public class GlobalApplication extends Application {
// 释放单例引用(可选,避免内存泄漏风险) // 释放单例引用(可选,避免内存泄漏风险)
sInstance = null; sInstance = null;
LogUtils.d(TAG, "GlobalApplication 终止,单例实例已释放"); LogUtils.d(TAG, "GlobalApplication 终止,单例实例已释放");
} }
} }

View File

@@ -51,7 +51,7 @@ public final class GlobalCrashActivity extends Activity implements MenuItem.OnMe
.postResumeCrashSafetyWireHandler(getApplicationContext()); .postResumeCrashSafetyWireHandler(getApplicationContext());
// 从 Intent 中获取崩溃日志数据EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键) // 从 Intent 中获取崩溃日志数据EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键)
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_INFO); mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
// 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构) // 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构)
setContentView(R.layout.activity_globalcrash); setContentView(R.layout.activity_globalcrash);

View File

@@ -1,5 +1,10 @@
package cc.winboll.studio.libappbase; package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen@QQ.COM
* @Date 2024/08/12 14:36:18
* @Describe 日志视图类,继承 RelativeLayout 类。
*/
import android.content.ClipData; import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
@@ -21,6 +26,9 @@ import android.widget.RelativeLayout;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.R;
import cc.winboll.studio.libappbase.views.HorizontalListView;
import java.text.Collator; import java.text.Collator;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -28,49 +36,27 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/08/12 14:36:18
* @Describe 日志可视化自定义 View继承 RelativeLayout
* 核心功能日志展示、日志级别筛选、TAG 过滤(启用/禁用、TAG 搜索定位、日志清理/复制、视图交互控制
* 依赖 LogUtils 进行日志读写,通过 LogViewThread 监听日志文件变化并自动刷新
*/
public class LogView extends RelativeLayout { public class LogView extends RelativeLayout {
/** 当前 View 的日志 TAG用于调试输出 */
public static final String TAG = "LogView"; public static final String TAG = "LogView";
/** 日志处理中标志避免并发刷新volatile 保证多线程可见性) */ public volatile boolean mIsHandling;
private volatile boolean mIsHandling; public volatile boolean mIsAddNewLog;
/** 新日志添加标志标记有未处理的新日志volatile 保证多线程可见性) */
private volatile boolean mIsAddNewLog;
/** 上下文对象(用于布局加载、系统服务获取) */ Context mContext;
private Context mContext; ScrollView mScrollView;
/** 日志滚动视图(包裹日志文本,支持垂直滚动) */ TextView mTextView;
private ScrollView mLogScrollView; EditText metTagSearch;
/** 日志文本展示控件(显示所有日志内容) */ CheckBox mSelectableCheckBox;
private TextView mLogTextView; CheckBox mSelectAllTAGCheckBox;
/** TAG 搜索输入框(用于搜索并定位目标 TAG */ TAGListAdapter mTAGListAdapter;
private EditText mTagSearchEt; LogViewThread mLogViewThread;
/** 文本选择开关(控制是否允许选中日志文本) */ LogViewHandler mLogViewHandler;
private CheckBox mTextSelectableCb; Spinner mLogLevelSpinner;
/** 全选 TAG 开关(控制所有 TAG 的启用/禁用) */ ArrayAdapter<CharSequence> mLogLevelSpinnerAdapter;
private CheckBox mSelectAllTagCb; // 标签列表
/** TAG 列表适配器(绑定 TAG 数据与视图,处理勾选状态) */ HorizontalListView mListViewTags;
private TAGListAdapter mTagListAdapter;
/** 日志监听线程(监听日志文件变化,触发视图刷新) */
private LogViewThread mLogViewThread;
/** 日志视图 Handler主线程更新 UI避免跨线程操作 */
private LogViewHandler mLogViewHandler;
/** 日志级别选择下拉框(用于切换全局日志输出级别) */
private Spinner mLogLevelSpinner;
/** 日志级别适配器(绑定 LogUtils.LOG_LEVEL 枚举与 Spinner */
private ArrayAdapter<CharSequence> mLogLevelAdapter;
/** TAG 水平列表视图(横向展示所有 TAG支持滚动 */
private HorizontalListView mTagHorizontalListView;
// ====================== 构造方法(初始化视图) ======================
public LogView(Context context) { public LogView(Context context) {
super(context); super(context);
initView(context); initView(context);
@@ -91,307 +77,258 @@ public class LogView extends RelativeLayout {
initView(context); initView(context);
} }
/**
* 启动日志监听与展示
* 1. 初始化并启动 LogViewThread监听日志文件变化
* 2. 初始加载并展示日志内容。
*/
public void start() { public void start() {
mLogViewThread = new LogViewThread(this); mLogViewThread = new LogViewThread(LogView.this);
mLogViewThread.start(); mLogViewThread.start();
showAndScrollLogView(); // 初始显示日志并滚动到底部 // 显示日志
showAndScrollLogView();
} }
/** public void scrollLogUp() {
* 滚动日志到底部(确保最新日志可见) mScrollView.post(new Runnable() {
* 运行在主线程,通过 post 提交 Runnable 避免 UI 线程阻塞 @Override
*/ public void run() {
private void scrollLogToBottom() { mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
mLogScrollView.post(new Runnable() { // 日志显示结束
@Override mLogViewHandler.setIsHandling(false);
public void run() { // 检查是否添加了新日志
// 滚动到 ScrollView 底部FOCUS_DOWN 表示聚焦到底部) if (mLogViewHandler.isAddNewLog()) {
mLogScrollView.fullScroll(ScrollView.FOCUS_DOWN); // 有新日志添加,先更改新日志标志
// 标记日志处理完成 mLogViewHandler.setIsAddNewLog(false);
mLogViewHandler.setIsHandling(false); // 再次发送显示日志的显示
// 检查是否有未处理的新日志,有则再次触发刷新 Message message = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOGVIEW_UPDATE);
if (mLogViewHandler.isAddNewLog()) { mLogViewHandler.sendMessage(message);
mLogViewHandler.setIsAddNewLog(false); }
Message refreshMsg = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOG_REFRESH); }
mLogViewHandler.sendMessage(refreshMsg); });
}
}
});
} }
/** void initView(Context context) {
* 初始化视图组件(加载布局、绑定控件、设置监听)
* @param context 上下文对象
*/
private void initView(Context context) {
mContext = context; mContext = context;
mLogViewHandler = new LogViewHandler(); // 初始化主线程 Handler mLogViewHandler = new LogViewHandler();
// 加载视图布局
addView(inflate(mContext, cc.winboll.studio.libappbase.R.layout.view_log, null));
// 初始化日志子控件视图
//
mScrollView = findViewById(cc.winboll.studio.libappbase.R.id.viewlogScrollViewLog);
mTextView = findViewById(cc.winboll.studio.libappbase.R.id.viewlogTextViewLog);
metTagSearch = findViewById(cc.winboll.studio.libappbase.R.id.tagsearch_et);
// 获取Log Level spinner实例
mLogLevelSpinner = findViewById(cc.winboll.studio.libappbase.R.id.viewlogSpinner1);
// 加载日志视图布局R.layout.view_log 为自定义布局文件) metTagSearch.addTextChangedListener(new TextWatcher() {
View rootView = LayoutInflater.from(context).inflate(R.layout.view_log, this, true);
// 绑定布局控件(通过 ID 找到对应组件)
bindViews(rootView);
// 设置 TAG 搜索输入框监听(实时搜索并定位 TAG @Override
setupTagSearchListener(); public void afterTextChanged(Editable editable) {
// 设置功能按钮监听(清理日志、复制日志) }
setupFunctionButtonListeners(rootView);
// 设置文本选择开关监听(控制日志文本是否可选中) @Override
setupTextSelectableListener(); public void beforeTextChanged(CharSequence charSequence, int p, int p1, int p2) {
// 初始化日志级别下拉框(绑定级别数据,设置默认值) }
initLogLevelSpinner();
// 初始化 TAG 列表(加载所有 TAG设置全选状态 @Override
initTagListView(); public void onTextChanged(CharSequence s, int start, int before, int count) {
// 设置默认交互模式(默认禁止子视图获取焦点,避免误触) LogUtils.d(TAG, s.toString());
if (s.length() > 0) {
scrollToTag(s.toString());
} else {
HorizontalScrollView hsRoot = findViewById(R.id.viewlogHorizontalScrollView1);
hsRoot.smoothScrollTo(0, 0);
mListViewTags.resetScrollToStart();
}
// mListViewTags.postDelayed(new Runnable() {
// @Override
// public void run() {
// mListViewTags.scrollToItem(5);
// }
// }, 100);
}
// 其他方法留空或按需实现
});
(findViewById(cc.winboll.studio.libappbase.R.id.viewlogButtonClean)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
LogUtils.cleanLog();
LogUtils.d(TAG, "Log is cleaned.");
}
});
(findViewById(cc.winboll.studio.libappbase.R.id.viewlogButtonCopy)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
ClipboardManager cm = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog()));
LogUtils.d(TAG, "Log is copied.");
}
});
mSelectableCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBoxSelectable);
mSelectableCheckBox.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
if (mSelectableCheckBox.isChecked()) {
setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
} else {
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
}
});
// 设置日志级别列表
ArrayList<String> adapterItems = new ArrayList<>();
for (LogUtils.LOG_LEVEL e : LogUtils.LOG_LEVEL.values()) {
adapterItems.add(e.name());
}
// 假设你有一个字符串数组作为选项列表
//String[] options = {"Option 1", "Option 2", "Option 3"};
// 创建一个ArrayAdapter来绑定数据到spinner
mLogLevelSpinnerAdapter = ArrayAdapter.createFromResource(
context, cc.winboll.studio.libappbase.R.array.enum_loglevel_array, android.R.layout.simple_spinner_item);
mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// 设置适配器并将它应用到spinner上
mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // 设置下拉视图样式
mLogLevelSpinner.setAdapter(mLogLevelSpinnerAdapter);
// 为Spinner添加监听器
mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
//String selectedOption = mLogLevelSpinnerAdapter.getItem(position);
// 处理选中的选项...
LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// 如果没有选择,则执行此操作...
}
});
// 获取默认值的索引
int defaultValueIndex = LogUtils.getLogLevel().ordinal();
if (defaultValueIndex != -1) {
// 如果找到了默认值,设置默认选项
mLogLevelSpinner.setSelection(defaultValueIndex);
}
// 加载标签列表
Map<String, Boolean> mapTAGList = LogUtils.getMapTAGList();
boolean isAllSelect = true;
for (Map.Entry<String, Boolean> entry : mapTAGList.entrySet()) {
if (entry.getValue() == false) {
isAllSelect = false;
break;
}
}
CheckBox cbALLTAG = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1);
cbALLTAG.setChecked(isAllSelect);
// 加载标签表
mListViewTags = findViewById(cc.winboll.studio.libappbase.R.id.tags_listview);
mListViewTags.setVerticalOffset(10);
mTAGListAdapter = new TAGListAdapter(mContext, mapTAGList);
mListViewTags.setAdapter(mTAGListAdapter);
// 可以添加点击监听器来处理勾选框状态变化后的逻辑,比如获取当前勾选情况等
mTAGListAdapter.notifyDataSetChanged();
mSelectAllTAGCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1);
mSelectAllTAGCheckBox.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
LogUtils.setALlTAGListEnable(mSelectAllTAGCheckBox.isChecked());
//LogUtils.setALlTAGListEnable(false);
//mTAGListAdapter.notifyDataSetChanged();
mTAGListAdapter.reload();
//ToastUtils.show(String.format("onClick\nmSelectAllTAGCheckBox.isChecked() : %s", mSelectAllTAGCheckBox.isChecked()));
}
});
// 设置滚动时不聚焦日志
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
} }
/**
* 绑定布局控件(通过 ID 查找并初始化所有子组件)
* @param rootView 根布局视图
*/
private void bindViews(View rootView) {
mLogScrollView = rootView.findViewById(R.id.viewlogScrollViewLog);
mLogTextView = rootView.findViewById(R.id.viewlogTextViewLog);
mTagSearchEt = rootView.findViewById(R.id.tagsearch_et);
mLogLevelSpinner = rootView.findViewById(R.id.viewlogSpinner1);
mTextSelectableCb = rootView.findViewById(R.id.viewlogCheckBoxSelectable);
mSelectAllTagCb = rootView.findViewById(R.id.viewlogCheckBox1);
mTagHorizontalListView = rootView.findViewById(R.id.tags_listview);
}
/**
* 设置 TAG 搜索输入框监听(文本变化时触发 TAG 定位)
*/
private void setupTagSearchListener() {
mTagSearchEt.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String searchText = s.toString().trim();
LogUtils.d(TAG, "TAG 搜索内容:" + searchText);
if (!searchText.isEmpty()) {
// 搜索文本非空,定位匹配的 TAG
scrollToTargetTag(searchText);
} else {
// 搜索文本为空,重置滚动位置
HorizontalScrollView parentHs = findViewById(R.id.viewlogHorizontalScrollView1);
parentHs.smoothScrollTo(0, 0);
mTagHorizontalListView.resetScrollToStart();
}
}
@Override
public void afterTextChanged(Editable s) {}
});
}
/**
* 设置功能按钮监听(清理日志、复制日志)
*/
private void setupFunctionButtonListeners(View rootView) {
// 清理日志按钮(点击清空所有历史日志)
rootView.findViewById(R.id.viewlogButtonClean).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.cleanLog();
LogUtils.d(TAG, "日志已清理");
}
});
// 复制日志按钮(点击复制所有日志到剪贴板)
rootView.findViewById(R.id.viewlogButtonCopy).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
// 将日志内容复制到剪贴板(标签为应用包名)
clipboard.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog()));
LogUtils.d(TAG, "日志已复制到剪贴板");
}
});
}
/**
* 设置文本选择开关监听(控制日志文本是否可选中复制)
*/
private void setupTextSelectableListener() {
mTextSelectableCb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mTextSelectableCb.isChecked()) {
// 允许文本选择:子视图优先获取焦点
setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
} else {
// 禁止文本选择:阻止子视图获取焦点
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
}
});
}
/**
* 初始化日志级别下拉框Spinner
* 1. 绑定 LogUtils.LOG_LEVEL 枚举数据;
* 2. 设置默认选中当前全局日志级别;
* 3. 监听级别变化,更新 LogUtils 全局配置。
*/
private void initLogLevelSpinner() {
// 从资源文件加载日志级别数组R.array.enum_loglevel_array 与 LOG_LEVEL 枚举对应)
mLogLevelAdapter = ArrayAdapter.createFromResource(
mContext, R.array.enum_loglevel_array, android.R.layout.simple_spinner_item);
// 设置下拉列表样式
mLogLevelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mLogLevelSpinner.setAdapter(mLogLevelAdapter);
// 监听下拉框选择变化
mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// 根据选择的位置设置全局日志级别position 与 LOG_LEVEL 枚举索引对应)
LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
// 设置默认选中当前日志级别
int defaultLevelIndex = LogUtils.getLogLevel().ordinal();
if (defaultLevelIndex >= 0) {
mLogLevelSpinner.setSelection(defaultLevelIndex);
}
}
/**
* 初始化 TAG 水平列表
* 1. 加载 LogUtils 中的所有 TAG 及其启用状态;
* 2. 初始化 TAG 列表适配器;
* 3. 设置全选 TAG 开关监听。
*/
private void initTagListView() {
// 获取 LogUtils 中的 TAG 启用状态映射表
Map<String, Boolean> tagEnableMap = LogUtils.getTagEnableMap();
// 判断是否所有 TAG 都已启用(初始化全选开关状态)
boolean isAllTagEnabled = isAllTagsEnabled(tagEnableMap);
mSelectAllTagCb.setChecked(isAllTagEnabled);
// 初始化 TAG 水平列表(设置垂直偏移,绑定适配器)
mTagHorizontalListView.setVerticalOffset(10);
mTagListAdapter = new TAGListAdapter(mContext, tagEnableMap);
mTagHorizontalListView.setAdapter(mTagListAdapter);
mTagListAdapter.notifyDataSetChanged(); // 刷新列表数据
// 全选 TAG 开关监听(点击时启用/禁用所有 TAG
mSelectAllTagCb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean isSelectAll = mSelectAllTagCb.isChecked();
LogUtils.setAllTagsEnable(isSelectAll); // 批量更新所有 TAG 状态
mTagListAdapter.reload(); // 重新加载 TAG 数据并刷新视图
}
});
}
/**
* 判断是否所有 TAG 都已启用
* @param tagEnableMap TAG 启用状态映射表
* @return true所有 TAG 均启用false存在未启用的 TAG
*/
private boolean isAllTagsEnabled(Map<String, Boolean> tagEnableMap) {
for (Map.Entry<String, Boolean> entry : tagEnableMap.entrySet()) {
if (!entry.getValue()) {
return false;
}
}
return true;
}
/**
* 更新日志视图(由 LogViewThread 触发,通知有新日志)
* 避免并发刷新:正在处理时标记新日志,处理完成后再次刷新
*/
public void updateLogView() { public void updateLogView() {
if (mLogViewHandler.isHandling()) { if (mLogViewHandler.isHandling() == true) {
// 正在处理日志刷新,标记有新日志待处理 // 正在处理日志显示,
// 就先设置一个新日志标志位
// 以便日志显示完后,再次显示新日志内容
mLogViewHandler.setIsAddNewLog(true); mLogViewHandler.setIsAddNewLog(true);
} else { } else {
// 发送刷新消息到主线程 //LogUtils.d(TAG, "LogListener showLog(String path)");
Message refreshMsg = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOG_REFRESH); Message message = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOGVIEW_UPDATE);
mLogViewHandler.sendMessage(refreshMsg); mLogViewHandler.sendMessage(message);
mLogViewHandler.setIsAddNewLog(false); mLogViewHandler.setIsAddNewLog(false);
} }
} }
/** void showAndScrollLogView() {
* 显示日志并滚动到底部 mTextView.setText(LogUtils.loadLog());
* 1. 从 LogUtils 加载所有历史日志; scrollLogUp();
* 2. 设置到文本控件并滚动到底部。
*/
private void showAndScrollLogView() {
mLogTextView.setText(LogUtils.loadLog()); // 加载并显示日志
scrollLogToBottom(); // 滚动到底部,显示最新日志
} }
/** public void scrollToTag(final String prefix) {
* 滚动到目标 TAG根据搜索文本定位匹配的 TAG 并滚动显示) if (mTAGListAdapter == null || prefix == null || prefix.length() == 0) {
* @param prefix 搜索文本TAG 前缀) LogUtils.d(TAG, "参数为空,无法滚动");
*/
private void scrollToTargetTag(final String prefix) {
if (mTagListAdapter == null || prefix == null || prefix.isEmpty()) {
LogUtils.d(TAG, "TAG 搜索参数为空,无法定位");
return; return;
} }
final List<TAGItemModel> tagItemList = mTagListAdapter.getItemList(); final List<TAGItemModel> itemList = mTAGListAdapter.getItemList();
mTagHorizontalListView.post(new Runnable() {
@Override
public void run() {
int targetPosition = -1;
// 遍历 TAG 列表,查找前缀匹配的 TAG忽略大小写
for (int i = 0; i < tagItemList.size(); i++) {
String tag = tagItemList.get(i).getTag();
if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) {
targetPosition = i;
break;
}
}
if (targetPosition != -1) { mListViewTags.post(new Runnable() {
final int targetPositionFinal = targetPosition; @Override
// 延迟滚动(确保布局完成,避免滚动失效) public void run() {
mTagHorizontalListView.postDelayed(new Runnable() { // 查找匹配的标签位置
@Override int targetPosition = -1;
public void run() {
LogUtils.d(TAG, "定位到 TAG 位置:" + targetPositionFinal); for (int i = 0; i < itemList.size(); i++) {
mTagHorizontalListView.scrollToItem(targetPositionFinal); String tag = itemList.get(i).getTag();
} if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) {
}, 100); targetPosition = i;
} else {
LogUtils.d(TAG, "未找到匹配前缀的 TAG" + prefix); break;
} }
} }
});
if (targetPosition != -1) {
// 优化滚动逻辑
//mListViewTags.setSelection(targetPosition);
//mListViewTags.invalidateViews(); // 强制刷新所有可见项
// 单独刷新目标视图
// View targetView = mListViewTags.getChildAt(targetPosition);
// if (targetView != null) {
// targetView.requestLayout();
// targetView.requestFocus();
// }
final int scrollPosition = targetPosition;
// 延迟滚动确保布局完成
mListViewTags.postDelayed(new Runnable() {
@Override
public void run() {
LogUtils.d(TAG, String.format("scrollPosition %d", scrollPosition));
mListViewTags.scrollToItem(scrollPosition);
}
}, 100);
} else {
LogUtils.d(TAG, "未找到匹配的标签前缀:" + prefix);
}
}
});
} }
// ====================== 内部类:日志视图 Handler主线程更新 UI ======================
/**
* 日志视图 Handler运行在主线程处理日志刷新消息 class LogViewHandler extends Handler {
* 避免跨线程操作 UI通过标志位控制并发刷新
*/ final static int MSG_LOGVIEW_UPDATE = 0;
private class LogViewHandler extends Handler { volatile boolean isHandling;
/** 日志刷新消息标识 */ volatile boolean isAddNewLog;
private static final int MSG_LOG_REFRESH = 0;
/** 日志处理中标志(与外部 mIsHandling 同步) */
private volatile boolean isHandling;
/** 新日志添加标志(与外部 mIsAddNewLog 同步) */
private volatile boolean isAddNewLog;
public LogViewHandler() { public LogViewHandler() {
setIsHandling(false); setIsHandling(false);
@@ -414,32 +351,24 @@ public class LogView extends RelativeLayout {
return isAddNewLog; return isAddNewLog;
} }
@Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) { switch (msg.what) {
case MSG_LOG_REFRESH: case MSG_LOGVIEW_UPDATE:{
// 未处理日志刷新时,标记为处理中并触发显示 if (isHandling() == false) {
if (!isHandling()) { setIsHandling(true);
setIsHandling(true); showAndScrollLogView();
showAndScrollLogView(); }
break;
} }
break;
default: default:
break; break;
} }
super.handleMessage(msg);
} }
} }
// ====================== 内部类TAG 数据模型(封装 TAG 名称与状态) ====================== public class TAGItemModel {
/**
* TAG 列表项数据模型
* 封装单个 TAG 的名称及其启用状态(用于 Adapter 数据绑定)
*/
private class TAGItemModel {
/** TAG 名称(如 "LogViewThread"、"LogUtils" */
private String tag; private String tag;
/** TAG 启用状态true启用false禁用 */
private boolean isChecked; private boolean isChecked;
public TAGItemModel(String tag, boolean isChecked) { public TAGItemModel(String tag, boolean isChecked) {
@@ -463,17 +392,18 @@ public class LogView extends RelativeLayout {
isChecked = checked; isChecked = checked;
} }
/** // getter/setter...
* 重写 equals 方法(按 TAG 名称判断相等)
* @param o 比较对象
* @return trueTAG 名称相同false不同
*/
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) {
if (o == null || getClass() != o.getClass()) return false; return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TAGItemModel that = (TAGItemModel) o; TAGItemModel that = (TAGItemModel) o;
// 手动处理空值比较(兼容 Java 7不依赖 Objects.equals // 手动处理空值比较Java 6 不支持 Objects.equals
if (tag == null) { if (tag == null) {
return that.tag == null; return that.tag == null;
} else { } else {
@@ -481,174 +411,106 @@ public class LogView extends RelativeLayout {
} }
} }
/**
* 重写 hashCode 方法(基于 TAG 名称生成哈希值)
* @return 哈希值(空 TAG 返回 0
*/
@Override @Override
public int hashCode() { public int hashCode() {
return tag == null ? 0 : tag.hashCode(); return tag == null ? 0 : tag.hashCode(); // 手动处理空值
} }
} }
// ====================== 内部类TAG 列表适配器(绑定数据与视图) ======================
/** public class TAGListAdapter extends BaseAdapter {
* TAG 水平列表适配器(继承 BaseAdapter
* 负责 TAG 数据与列表项视图的绑定,处理勾选状态变化
*/
private class TAGListAdapter extends BaseAdapter {
/** 上下文对象(用于加载列表项布局) */
private Context context; private Context context;
/** 原始 TAG 启用状态映射表(来自 LogUtils */ private Map<String, Boolean> mapOrigin;
private Map<String, Boolean> originTagMap; private List<TAGItemModel> itemList;
/** TAG 列表项数据(转换为 TAGItemModel 列表,便于排序和绑定) */
private List<TAGItemModel> tagItemList;
/** public TAGListAdapter(Context context, Map<String, Boolean> map) {
* 构造方法(初始化数据并加载到列表)
* @param context 上下文
* @param tagMap TAG 启用状态映射表
*/
public TAGListAdapter(Context context, Map<String, Boolean> tagMap) {
this.context = context; this.context = context;
this.originTagMap = tagMap; mapOrigin = map;
loadTagData(originTagMap); // 加载并转换数据 loadMap(mapOrigin);
} }
/**
* 获取 TAG 列表项数据(供外部定位 TAG 使用)
* @return TAGItemModel 列表
*/
public List<TAGItemModel> getItemList() { public List<TAGItemModel> getItemList() {
return tagItemList; return itemList;
} }
// ====================== BaseAdapter 抽象方法实现 ======================
@Override @Override
public int getCount() { public int getCount() {
return tagItemList == null ? 0 : tagItemList.size(); return itemList.size();
} }
@Override @Override
public Object getItem(int position) { public Object getItem(int p) {
return tagItemList.get(position); return itemList.get(p);
} }
@Override @Override
public long getItemId(int position) { public long getItemId(int p) {
return position; return p;
} }
/** void loadMap(Map<String, Boolean> map) {
* 加载 TAG 数据(将 Map 转换为 List 并排序) itemList = new ArrayList<TAGItemModel>();
* @param tagMap TAG 启用状态映射表 for (Map.Entry<String, Boolean> entry : map.entrySet()) {
*/ itemList.add(new TAGItemModel(entry.getKey(), entry.getValue()));
private void loadTagData(Map<String, Boolean> tagMap) {
tagItemList = new ArrayList<>();
// 遍历 Map转换为 TAGItemModel 并添加到列表
for (Map.Entry<String, Boolean> entry : tagMap.entrySet()) {
tagItemList.add(new TAGItemModel(entry.getKey(), entry.getValue()));
} }
// 按 TAG 名称升序排序(中文排序兼容) // 添加排序功能按照tag进行升序排序
Collections.sort(tagItemList, new TagAscComparator(true)); Collections.sort(itemList, new SortMapEntryByKeyString(true));
//Collections.sort(itemList, new SortMapEntryByKeyString(false));
} }
/**
* 重新加载 TAG 数据(用于全选/反选后刷新列表)
*/
public void reload() { public void reload() {
loadTagData(originTagMap); // 重新加载数据 loadMap(mapOrigin);
notifyDataSetChanged(); // 通知视图刷新 super.notifyDataSetChanged();
} }
/**
* 创建/复用列表项视图(优化性能,避免重复 inflate
* @param position 列表项位置
* @param convertView 复用视图(可为 null
* @param parent 父容器
* @return 列表项视图
*/
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder; ViewHolder holder;
// 复用视图(减少布局加载开销)
if (convertView == null) { if (convertView == null) {
// 加载列表项布局R.layout.item_logtag 为 TAG 项自定义布局)
convertView = LayoutInflater.from(context).inflate(R.layout.item_logtag, parent, false); convertView = LayoutInflater.from(context).inflate(R.layout.item_logtag, parent, false);
holder = new ViewHolder(); holder = new ViewHolder();
// 绑定列表项控件TAG 文本和勾选框) holder.tvText = convertView.findViewById(R.id.viewlogtagTextView1);
holder.tagTv = convertView.findViewById(R.id.viewlogtagTextView1); holder.cbChecked = convertView.findViewById(R.id.viewlogtagCheckBox1);
holder.tagCb = convertView.findViewById(R.id.viewlogtagCheckBox1); convertView.setTag(holder);
convertView.setTag(holder); // 保存 ViewHolder 到视图
} else { } else {
holder = (ViewHolder) convertView.getTag(); // 复用 ViewHolder holder = (ViewHolder) convertView.getTag();
} }
// 绑定数据到视图 final TAGItemModel item = itemList.get(position);
final TAGItemModel item = tagItemList.get(position); holder.tvText.setText(item.getTag());
holder.tagTv.setText(item.getTag()); // 设置 TAG 名称 holder.cbChecked.setChecked(item.isChecked());
holder.tagCb.setChecked(item.isChecked()); // 设置勾选状态 holder.cbChecked.setOnClickListener(new View.OnClickListener(){
// 勾选框点击监听(更新 TAG 启用状态) @Override
holder.tagCb.setOnClickListener(new View.OnClickListener() { public void onClick(View v) {
@Override LogUtils.setTAGListEnable(item.getTag(), ((CheckBox)v).isChecked());
public void onClick(View v) { }
boolean isChecked = ((CheckBox) v).isChecked(); });
// 调用 LogUtils 更新该 TAG 的启用状态
LogUtils.setTagEnable(item.getTag(), isChecked);
// 同步更新本地模型状态(避免刷新后状态不一致)
item.setChecked(isChecked);
}
});
return convertView; return convertView;
} }
/** public class ViewHolder {
* 列表项 ViewHolder缓存控件提升列表滑动性能 TextView tvText;
*/ CheckBox cbChecked;
private class ViewHolder {
TextView tagTv; // TAG 名称文本控件
CheckBox tagCb; // TAG 启用状态勾选框
} }
} }
// ====================== 内部类TAG 排序比较器(中文兼容) ====================== class SortMapEntryByKeyString implements Comparator<TAGItemModel> {
/** private boolean mIsDesc = true;
* TAG 名称排序比较器(实现 Comparator // isDesc 是否降序排列
* 支持中文排序(基于系统默认中文 Locale可选择升序/降序 public SortMapEntryByKeyString(boolean isDesc) {
*/ mIsDesc = isDesc;
private class TagAscComparator implements Comparator<TAGItemModel> {
/** 排序方向true升序false降序 */
private boolean isAsc;
/** 中文排序器(兼容中文汉字排序) */
private Collator chineseCollator = Collator.getInstance(java.util.Locale.CHINA);
public TagAscComparator(boolean isAsc) {
this.isAsc = isAsc;
} }
Collator cmp = Collator.getInstance(java.util.Locale.CHINA);
/**
* 比较两个 TAGItemModel按 TAG 名称排序)
* @param o1 第一个比较对象
* @param o2 第二个比较对象
* @return 比较结果正数o1 在 o2 后负数o1 在 o2 前0相等
*/
@Override @Override
public int compare(TAGItemModel o1, TAGItemModel o2) { public int compare(TAGItemModel o1, TAGItemModel o2) {
String tag1 = o1.getTag(); if (mIsDesc) {
String tag2 = o2.getTag(); return o1.getTag().compareTo(o2.getTag());
// 处理空值(空 TAG 排在最前)
if (tag1 == null) return -1;
if (tag2 == null) return 1;
// 根据排序方向返回比较结果
if (isAsc) {
return chineseCollator.compare(tag1, tag2); // 升序
} else { } else {
return chineseCollator.compare(tag2, tag1); // 降序 return o2.getTag().compareTo(o1.getTag());
} }
} }
} }
} }

View File

@@ -121,6 +121,18 @@ public class ToastUtils {
LogUtils.d(TAG, "ToastUtils 初始化完成,上下文已设置"); LogUtils.d(TAG, "ToastUtils 初始化完成,上下文已设置");
} }
// ===================================== 新增isInited() 方法 =====================================
/**
* 判断 ToastUtils 是否已初始化(供外部调用,如 CrashHandleNotifyUtils 中的复制提示)
* @return true已初始化可正常显示吐司false未初始化/已释放(无法正常显示)
*/
public static boolean isInited() {
ToastUtils instance = getInstance();
// 双重校验1. 未释放 2. 上下文已设置(确保初始化完成)
return !instance.isReleased && instance.mContext != null;
}
// ===================================== 新增结束 =====================================
/** /**
* 外部接口:显示短时长吐司 * 外部接口:显示短时长吐司
* @param message 吐司内容 * @param message 吐司内容
@@ -243,7 +255,6 @@ public class ToastUtils {
instance.mWorkerThread.join(1000); instance.mWorkerThread.join(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//LogUtils.e(TAG, "线程退出异常", e);
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
instance.mWorkerThread = null; instance.mWorkerThread = null;

View File

@@ -0,0 +1,264 @@
package cc.winboll.studio.libappbase.utils;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.CrashHandler;
import cc.winboll.studio.libappbase.GlobalCrashActivity;
import cc.winboll.studio.libappbase.LogUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/29 21:12
* @Describe 应用崩溃处理通知实用工具集(类库兼容版)
* 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志
* 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用
*/
public class CrashHandleNotifyUtils {
public static final String TAG = "CrashHandleNotifyUtils";
/** 通知渠道IDAndroid 8.0+ 必须) */
private static final String CRASH_NOTIFY_CHANNEL_ID = "crash_notify_channel";
/** 通知渠道名称(用户可见) */
private static final String CRASH_NOTIFY_CHANNEL_NAME = "应用崩溃通知";
/** 通知ID唯一 */
public static final int CRASH_NOTIFY_ID = 0x001;
/** Android 12 对应 API 版本号31 */
private static final int API_LEVEL_ANDROID_12 = 31;
/** PendingIntent.FLAG_IMMUTABLE 常量值API 31+ */
private static final int FLAG_IMMUTABLE = 0x00000040;
/** 通知内容最大行数控制在3行超出部分省略 */
private static final int NOTIFICATION_MAX_LINES = 3;
/**
* 处理未捕获异常(核心方法,类库入口)
* 改进点:新增宿主包名参数,移除类库对固定包名的依赖
* @param hostApp 宿主应用的 Application 实例(用于获取宿主上下文)
* @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity
* @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来)
*/
public static void handleUncaughtException(Application hostApp, String hostPackageName, String errorLog) {
// 1. 校验核心参数(类库场景必须严格校验,避免空指针)
if (hostApp == null || TextUtils.isEmpty(hostPackageName) || TextUtils.isEmpty(errorLog)) {
LogUtils.e(TAG, "发送崩溃通知失败参数为空hostApp=" + hostApp + ", hostPackageName=" + hostPackageName + ", errorLog=" + errorLog + "");
return;
}
// 2. 获取宿主应用名称(使用宿主上下文,避免类库包名混淆)
String hostAppName = getHostAppName(hostApp, hostPackageName);
// 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity
sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog);
}
/**
* 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大)
* 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式
* @param hostApp 宿主应用的 Application 实例
* @param intent 存储崩溃信息的意图extra 中携带崩溃日志)
*/
public static void handleUncaughtException(Application hostApp, Intent intent) {
// 从意图中提取宿主包名(优先使用意图中携带的包名,无则用宿主 Application 包名)
String hostPackageName = intent.getStringExtra("EXTRA_HOST_PACKAGE_NAME");
if (TextUtils.isEmpty(hostPackageName)) {
hostPackageName = hostApp.getPackageName();
LogUtils.w(TAG, "意图中未携带宿主包名,使用 Application 包名:" + hostPackageName);
}
// 从意图中提取崩溃日志(与 CrashHandler.EXTRA_CRASH_INFO 保持一致)
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
// 调用核心方法处理
handleUncaughtException(hostApp, hostPackageName, errorLog);
}
/**
* 获取宿主应用名称(类库场景适配:使用宿主包名获取,避免类库包名干扰)
* @param hostContext 宿主应用的上下文Application 实例)
* @param hostPackageName 宿主应用的包名
* @return 宿主应用名称(读取失败返回 "未知应用"
*/
private static String getHostAppName(Context hostContext, String hostPackageName) {
try {
// 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进)
return hostContext.getPackageManager().getApplicationLabel(
hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0)
).toString();
} catch (Exception e) {
LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + "", e);
return "未知应用";
}
}
/**
* 发送崩溃通知到宿主系统通知栏(类库兼容版)
* 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖
* @param hostContext 宿主应用的上下文Application 实例)
* @param hostPackageName 宿主应用的包名
* @param hostAppName 宿主应用的名称(用于通知标题)
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity
*/
private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog) {
// 1. 获取宿主的通知管理器(使用宿主上下文,确保通知归属宿主应用)
NotificationManager notificationManager = (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager == null) {
LogUtils.e(TAG, "获取宿主 NotificationManager 失败(包名:" + hostPackageName + "");
return;
}
// 2. 适配 Android 8.0+API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createCrashNotifyChannel(hostContext, notificationManager);
}
// 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity
PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog);
if (jumpIntent == null) {
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + "");
return;
}
// 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主)
Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
// 5. 发送通知(归属宿主应用,避免类库与宿主通知混淆)
notificationManager.notify(CRASH_NOTIFY_ID, notification);
LogUtils.d(TAG, "崩溃通知发送成功(宿主包名:" + hostPackageName + ",标题:" + hostAppName + ",日志长度:" + errorLog.length() + "字符)");
}
/**
* 创建宿主应用的崩溃通知渠道(类库场景:渠道归属宿主,避免类库与宿主渠道冲突)
* @param hostContext 宿主应用的上下文
* @param notificationManager 宿主的通知管理器
*/
private static void createCrashNotifyChannel(Context hostContext, NotificationManager notificationManager) {
// 仅 Android 8.0+ 执行(避免低版本报错)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 构建通知渠道(归属宿主应用,描述明确类库用途)
android.app.NotificationChannel channel = new android.app.NotificationChannel(
CRASH_NOTIFY_CHANNEL_ID,
CRASH_NOTIFY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
);
channel.setDescription("应用崩溃通知(由 WinBoLL Studio 类库提供,点击查看详情)");
// 注册渠道到宿主的通知管理器,确保渠道归属宿主
notificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "宿主崩溃通知渠道创建成功(宿主包名:" + hostContext.getPackageName() + "渠道ID" + CRASH_NOTIFY_CHANNEL_ID + "");
}
}
/**
* 核心改进:构建跳转宿主 GlobalCrashActivity 的意图(类库关键)
* 1. 绑定宿主包名,确保类库能正确启动宿主的 Activity
* 2. 传递崩溃日志,与宿主 GlobalCrashActivity 日志接收逻辑匹配;
* 3. 使用宿主上下文,避免类库上下文导致的适配问题。
* @param hostContext 宿主应用的上下文
* @param hostPackageName 宿主应用的包名
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity
* @return 跳转崩溃详情页的 PendingIntent
*/
private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog) {
try {
// 1. 构建跳转宿主 GlobalCrashActivity 的显式意图(类库场景必须显式指定宿主包名)
Intent crashIntent = new Intent(hostContext, GlobalCrashActivity.class);
// 关键:绑定宿主包名,确保意图能正确匹配宿主的 Activity避免类库包名干扰
crashIntent.setPackage(hostPackageName);
// 传递崩溃日志EXTRA_CRASH_INFO与宿主 GlobalCrashActivity 完全匹配)
crashIntent.putExtra(CrashHandler.EXTRA_CRASH_LOG, errorLog);
// 设置意图标志:确保在宿主应用中正常启动,避免重复创建和任务栈混乱
crashIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
// 2. 构建 PendingIntent使用宿主上下文适配高版本
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
flags |= FLAG_IMMUTABLE;
}
return PendingIntent.getActivity(
hostContext,
CRASH_NOTIFY_ID, // 用通知ID作为请求码确保唯一避免意图复用
crashIntent,
flags
);
} catch (Exception e) {
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + "", e);
return null;
}
}
/**
* 构建通知实例(类库兼容版)
* 改进点:使用宿主上下文加载资源,确保通知样式适配宿主应用
* @param hostContext 宿主应用的上下文
* @param hostAppName 宿主应用的名称(通知标题)
* @param errorLog 崩溃日志(通知内容)
* @param jumpIntent 通知点击跳转意图(跳转宿主的 GlobalCrashActivity
* @return 构建完成的 Notification 对象
*/
private static Notification buildNotification(Context hostContext, String hostAppName, String errorLog, PendingIntent jumpIntent) {
// 兼容 Android 8.0+指定宿主的通知渠道ID
Notification.Builder builder = new Notification.Builder(hostContext);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID);
}
// 核心用BigTextStyle控制“默认3行省略下拉显示完整”使用宿主上下文构建
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容");
bigTextStyle.bigText(errorLog);
bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); // 标题明确标识宿主和崩溃状态
builder.setStyle(bigTextStyle);
// 配置通知核心参数(全程使用宿主上下文,确保资源归属宿主)
builder
// 关键:使用宿主应用的小图标(避免类库图标显示异常)
.setSmallIcon(hostContext.getApplicationInfo().icon)
.setContentTitle(hostAppName + " 崩溃")
.setContentText(getShortContent(errorLog)) // 3行内缩略文本
.setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity
.setAutoCancel(true) // 点击后自动关闭
.setWhen(System.currentTimeMillis())
.setPriority(Notification.PRIORITY_DEFAULT);
// 适配 Android 4.1+:确保在宿主应用中正常显示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return builder.build();
} else {
return builder.getNotification();
}
}
/**
* 辅助方法截取日志文本确保显示在3行内通用逻辑无包名依赖
* @param content 完整崩溃日志
* @return 3行内的缩略文本
*/
private static String getShortContent(String content) {
if (content == null || content.isEmpty()) {
return "无崩溃日志";
}
int maxLength = 80; // 估算3行字符数可根据需求调整
return content.length() <= maxLength ? content : content.substring(0, maxLength) + "...";
}
/**
* 释放资源(类库场景:空实现,避免宿主调用时报错,预留扩展)
* @param hostContext 宿主应用的上下文(显式传入,避免类库上下文依赖)
*/
public static void release(Context hostContext) {
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(宿主包名:" + (hostContext != null ? hostContext.getPackageName() : "未知") + "");
}
}

View File

@@ -0,0 +1,129 @@
package cc.winboll.studio.libappbase.views;
/**
* @Author ZhanGSKen@AliYun.Com
* @Date 2025/03/12 12:29:01
* @Describe 水平布局的 ListView
*/
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ListView;
import android.widget.Scroller;
import cc.winboll.studio.libappbase.LogUtils;
public class HorizontalListView extends ListView {
public static final String TAG = "HorizontalListView";
private int verticalOffset = 0;
private Scroller scroller;
private int totalWidth;
public HorizontalListView(Context context) {
super(context);
init();
}
public HorizontalListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
scroller = new Scroller(getContext());
setHorizontalScrollBarEnabled(true);
setVerticalScrollBarEnabled(false);
}
public void setVerticalOffset(int verticalOffset) {
this.verticalOffset = verticalOffset;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
int childCount = getChildCount();
int left = getPaddingLeft();
int viewHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
totalWidth = left;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
child.layout(left, verticalOffset, left + width, verticalOffset + height);
left += width;
}
totalWidth = left + getPaddingRight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
public void smoothScrollTo(int x, int y) {
int dx = x - getScrollX();
int dy = y - getScrollY();
scroller.startScroll(getScrollX(), getScrollY(), dx, dy, 300); // 300ms平滑动画
invalidate();
}
@Override
public int computeHorizontalScrollRange() {
return totalWidth;
}
@Override
public int computeHorizontalScrollOffset() {
return getScrollX();
}
@Override
public int computeHorizontalScrollExtent() {
return getWidth();
}
public void scrollToItem(int position) {
if (position < 0 || position >= getChildCount()) {
LogUtils.d(TAG, "无效的position: " + position);
return;
}
View targetView = getChildAt(position);
int targetLeft = targetView.getLeft();
int scrollX = targetLeft - getPaddingLeft();
// 修正最大滚动范围计算
int maxScrollX = totalWidth;
scrollX = Math.max(0, Math.min(scrollX, maxScrollX));
// 强制重新布局和绘制
requestLayout();
invalidateViews();
smoothScrollTo(scrollX, 0);
LogUtils.d(TAG, String.format("滚动到position: %d, scrollX: %d computeHorizontalScrollRange() %d", position, scrollX, computeHorizontalScrollRange()));
}
public void resetScrollToStart() {
// 强制重新布局和绘制
requestLayout();
invalidateViews();
smoothScrollTo(0, 0);
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#ff000000"
android:pathData="M19,21H8V7H19M19,5H8A2,2 0,0 0,6 7V21A2,2 0,0 0,8 23H19A2,2 0,0 0,21 21V7A2,2 0,0 0,19 5M16,1H4A2,2 0,0 0,2 3V17H4V3H16V1Z"/>
</vector>

View File

@@ -99,7 +99,7 @@
android:layout_weight="1.0" android:layout_weight="1.0"
android:id="@+id/viewlogHorizontalScrollView1"> android:id="@+id/viewlogHorizontalScrollView1">
<cc.winboll.studio.libappbase.HorizontalListView <cc.winboll.studio.libappbase.views.HorizontalListView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/tags_listview"/> android:id="@+id/tags_listview"/>

View File

@@ -4,4 +4,5 @@
<color name="colorPrimaryDark">#FF005C12</color> <color name="colorPrimaryDark">#FF005C12</color>
<color name="colorAccent">#FF8DFFA2</color> <color name="colorAccent">#FF8DFFA2</color>
<color name="colorText">#FFFFFB8D</color> <color name="colorText">#FFFFFB8D</color>
<!-- 通知按钮颜色(启用/禁用) -->
</resources> </resources>