diff --git a/appbase/build.properties b/appbase/build.properties index 76fc542..77816e1 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon May 11 19:10:52 HKT 2026 +#Mon May 11 20:19:17 CST 2026 stageCount=8 libraryProject=libappbase baseVersion=15.20 publishVersion=15.20.7 -buildCount=0 +buildCount=8 baseBetaVersion=15.20.8 diff --git a/libappbase/build.properties b/libappbase/build.properties index a5d544b..77816e1 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon May 11 19:10:34 HKT 2026 +#Mon May 11 20:19:17 CST 2026 stageCount=8 libraryProject=libappbase baseVersion=15.20 publishVersion=15.20.7 -buildCount=0 +buildCount=8 baseBetaVersion=15.20.8 diff --git a/libappbase/src/main/AndroidManifest.xml b/libappbase/src/main/AndroidManifest.xml index f2838e6..bbfd3f3 100644 --- a/libappbase/src/main/AndroidManifest.xml +++ b/libappbase/src/main/AndroidManifest.xml @@ -47,6 +47,13 @@ + + \ No newline at end of file diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java index 2b8832b..06f9d73 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java @@ -1,11 +1,11 @@ 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.net.Uri; import android.os.Build; import android.text.TextUtils; @@ -13,13 +13,19 @@ import cc.winboll.studio.libappbase.CrashHandler; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.R; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; + /** * 应用崩溃处理通知实用工具集(类库兼容版) * 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志 * 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用 * @Author 豆包&ZhanGSKen * @CreateTime 2025/11/29 21:12:00 - * @EditTime 2026/05/11 15:38:21 + * @EditTime 2026/05/11 21:55:00 */ public class CrashHandleNotifyUtils { @@ -35,17 +41,18 @@ public class CrashHandleNotifyUtils { 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; /** 通知摘要最大长度 */ private static final int SUMMARY_MAX_LENGTH = 200; - /** 展开按钮广播Action */ - private static final String ACTION_EXPAND = "cc.winboll.studio.libappbase.ACTION_EXPAND_CRASH"; + /** 分享日志请求码 */ + private static final int REQUEST_CODE_SHARE_LOG = 0x002; + /** 缓存崩溃日志子目录 */ + private static final String CRASH_LOG_CACHE_SUBDIR = "crashnotify"; + /** 缓存崩溃日志文件名 */ + private static final String CRASH_LOG_CACHE_FILENAME = "crash_log.txt"; - private static Context sHostContext = null; + // ====================== 静态成员 ====================== private static String sHostPackageName = ""; - private static String sHostAppName = ""; - private static Class sReportCrashActivity = null; + private static String sCrashLogCacheFilePath = ""; // ====================== 正则表达式定义 ====================== private static final String REGEX_EXCEPTION_TYPE = "([\\w.]+Exception|[\\w.]+Error)"; @@ -61,18 +68,29 @@ public class CrashHandleNotifyUtils { * @param errorLog 崩溃日志内容 * @param reportCrashActivity 崩溃详情跳转Activity类 */ - public static void handleUncaughtException(final Application hostApp, - final String hostPackageName, - final String errorLog, - final Class reportCrashActivity) { + public static void handleUncaughtException(final android.app.Application hostApp, + final String hostPackageName, + final String errorLog, + final Class reportCrashActivity) { LogUtils.d(TAG, "handleUncaughtException 进入方法"); - // 校验入参 if (hostApp == null || TextUtils.isEmpty(hostPackageName) || TextUtils.isEmpty(errorLog)) { LogUtils.e(TAG, "handleUncaughtException 参数为空校验不通过"); return; } + sHostPackageName = hostPackageName; final String hostAppName = getHostAppName(hostApp, hostPackageName); - sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog, reportCrashActivity); + final String crashLogFilePath = saveCrashLogToCache(hostApp, errorLog); + if (TextUtils.isEmpty(crashLogFilePath)) { + LogUtils.e(TAG, "保存崩溃日志到缓存文件失败"); + return; + } + sCrashLogCacheFilePath = crashLogFilePath; + final Intent shareIntent = new Intent(hostApp, ShareLogActivity.class); + shareIntent.putExtra(ShareLogActivity.EXTRA_CRASH_LOG_FILEPATH, crashLogFilePath); + shareIntent.putExtra(ShareLogActivity.EXTRA_CRASH_LOG_SUBJECT, "崩溃日志"); + shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final PendingIntent sharePendingIntent = createSharePendingIntent(hostApp, shareIntent); + sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog, reportCrashActivity, sharePendingIntent); } /** @@ -81,9 +99,9 @@ public class CrashHandleNotifyUtils { * @param intent 携带崩溃信息Intent * @param reportCrashActivity 崩溃详情Activity */ - public static void handleUncaughtException(final Application hostApp, - final Intent intent, - final Class reportCrashActivity) { + public static void handleUncaughtException(final android.app.Application hostApp, + final Intent intent, + final Class reportCrashActivity) { LogUtils.d(TAG, "handleUncaughtException 重载方法进入"); String hostPackageName = intent.getStringExtra("EXTRA_HOST_PACKAGE_NAME"); if (TextUtils.isEmpty(hostPackageName)) { @@ -94,6 +112,16 @@ public class CrashHandleNotifyUtils { handleUncaughtException(hostApp, hostPackageName, errorLog, reportCrashActivity); } + /** + * 资源释放 + * @param hostContext 宿主上下文 + */ + public static void release(final Context hostContext) { + LogUtils.d(TAG, "release 执行资源释放"); + sHostPackageName = ""; + sCrashLogCacheFilePath = ""; + } + // ====================== 内部工具方法 ====================== /** * 获取宿主应用名称 @@ -104,14 +132,87 @@ public class CrashHandleNotifyUtils { private static String getHostAppName(final Context hostContext, final String hostPackageName) { try { return hostContext.getPackageManager() - .getApplicationLabel(hostContext.getPackageManager() - .getApplicationInfo(hostPackageName, 0)).toString(); + .getApplicationLabel(hostContext.getPackageManager() + .getApplicationInfo(hostPackageName, 0)).toString(); } catch (Exception e) { LogUtils.e(TAG, "获取宿主应用名称失败", e); return "未知应用"; } } + /** + * 保存崩溃日志到缓存文件 + * @param hostContext 宿主上下文 + * @param crashLog 崩溃日志内容 + * @return 缓存文件路径,失败返回空字符串 + */ + private static String saveCrashLogToCache(final Context hostContext, final String crashLog) { + if (hostContext == null || TextUtils.isEmpty(crashLog)) { + return ""; + } + BufferedReader reader = null; + FileOutputStream fos = null; + try { + final File cacheDir = new File(hostContext.getCacheDir(), CRASH_LOG_CACHE_SUBDIR); + if (!cacheDir.exists()) { + cacheDir.mkdirs(); + } + final File cacheFile = new File(cacheDir, CRASH_LOG_CACHE_FILENAME); + if (cacheFile.exists()) { + cacheFile.delete(); + } + cacheFile.createNewFile(); + fos = new FileOutputStream(cacheFile); + fos.write(crashLog.getBytes("UTF-8")); + fos.flush(); + LogUtils.d(TAG, "saveCrashLogToCache 保存崩溃日志到缓存: " + cacheFile.getAbsolutePath()); + return cacheFile.getAbsolutePath(); + } catch (Exception e) { + LogUtils.e(TAG, "saveCrashLogToCache 异常", e); + return ""; + } finally { + if (reader != null) { + try { reader.close(); } catch (Exception e) {} + } + if (fos != null) { + try { fos.close(); } catch (Exception e) {} + } + } + } + + /** + * 读取缓存的崩溃日志文件内容 + * @param hostContext 宿主上下文 + * @return 日志内容,失败返回空字符串 + */ + private static String readCachedCrashLog(final Context hostContext) { + if (hostContext == null || TextUtils.isEmpty(sCrashLogCacheFilePath)) { + return ""; + } + BufferedReader reader = null; + try { + final File cacheFile = new File(sCrashLogCacheFilePath); + if (!cacheFile.exists()) { + LogUtils.w(TAG, "readCachedCrashLog 缓存文件不存在"); + return ""; + } + reader = new BufferedReader(new InputStreamReader(new FileInputStream(cacheFile), "UTF-8")); + final StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + return sb.toString(); + } catch (Exception e) { + LogUtils.e(TAG, "readCachedCrashLog 异常", e); + return ""; + } finally { + if (reader != null) { + try { reader.close(); } catch (Exception e) {} + } + } + } + /** * 发送崩溃系统通知 * @param hostContext 宿主上下文 @@ -119,45 +220,65 @@ public class CrashHandleNotifyUtils { * @param hostAppName 宿主应用名 * @param errorLog 崩溃日志 * @param reportCrashActivity 跳转Activity + * @param sharePendingIntent 分享日志PendingIntent */ -private static void sendCrashNotification(final Context hostContext, - final String hostPackageName, - final String hostAppName, - final String errorLog, - final Class reportCrashActivity) { + private static void sendCrashNotification(final Context hostContext, + final String hostPackageName, + final String hostAppName, + final String errorLog, + final Class reportCrashActivity, + final PendingIntent sharePendingIntent) { final NotificationManager notificationManager = - (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE); + (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE); if (notificationManager == null) { LogUtils.e(TAG, "获取NotificationManager失败"); return; } - // 8.0以上创建通知渠道 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createCrashNotifyChannel(hostContext, notificationManager); } final PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, - hostPackageName, errorLog, reportCrashActivity); + hostPackageName, errorLog, reportCrashActivity); if (jumpIntent == null) { LogUtils.e(TAG, "构建跳转PendingIntent失败"); return; } - final Notification notification = buildNotification(hostContext, hostPackageName, hostAppName, errorLog, jumpIntent); + final Notification notification = buildNotification(hostContext, hostPackageName, hostAppName, errorLog, jumpIntent, sharePendingIntent); notificationManager.notify(CRASH_NOTIFY_ID, notification); LogUtils.d(TAG, "崩溃通知发送成功,宿主包名:" + hostPackageName); } + /** + * 创建分享日志PendingIntent + * @param hostContext 宿主上下文 + * @param shareIntent 分享意图 + * @return PendingIntent实例 + */ + private static PendingIntent createSharePendingIntent(final Context hostContext, final Intent shareIntent) { + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) { + flags |= FLAG_IMMUTABLE; + } + return PendingIntent.getActivity( + hostContext, + REQUEST_CODE_SHARE_LOG, + shareIntent, + flags + ); + } + /** * 创建通知渠道(适配Android O及以上) * @param hostContext 宿主上下文 * @param notificationManager 通知管理器 */ private static void createCrashNotifyChannel(final Context hostContext, - final NotificationManager notificationManager) { + final NotificationManager notificationManager) { 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 + CRASH_NOTIFY_CHANNEL_ID, + CRASH_NOTIFY_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT ); channel.setDescription("应用崩溃通知(由 WinBoLL Studio 类库提供,点击查看详情)"); notificationManager.createNotificationChannel(channel); @@ -174,9 +295,9 @@ private static void sendCrashNotification(final Context hostContext, * @return PendingIntent实例 */ private static PendingIntent getGlobalCrashPendingIntent(final Context hostContext, - final String hostPackageName, - final String errorLog, - final Class reportCrashActivity) { + final String hostPackageName, + final String errorLog, + final Class reportCrashActivity) { try { final Intent crashIntent = new Intent(hostContext, reportCrashActivity); crashIntent.setPackage(hostPackageName); @@ -187,11 +308,11 @@ private static void sendCrashNotification(final Context hostContext, if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) { flags |= FLAG_IMMUTABLE; } -return PendingIntent.getActivity( - hostContext, - CRASH_NOTIFY_ID, - crashIntent, - flags + return PendingIntent.getActivity( + hostContext, + CRASH_NOTIFY_ID, + crashIntent, + flags ); } catch (Exception e) { LogUtils.e(TAG, "构建跳转Intent异常", e); @@ -206,32 +327,36 @@ return PendingIntent.getActivity( * @param hostAppName 宿主应用名 * @param errorLog 崩溃日志 * @param jumpIntent 点击跳转意图 + * @param shareIntent 分享日志意图 * @return 构建好的Notification */ @SuppressWarnings("deprecation") private static Notification buildNotification(final Context hostContext, - final String hostPackageName, - final String hostAppName, - final String errorLog, - final PendingIntent jumpIntent) { + final String hostPackageName, + final String hostAppName, + final String errorLog, + final PendingIntent jumpIntent, + final PendingIntent shareIntent) { Notification.Builder builder = new Notification.Builder(hostContext); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID); } - String briefInfo = extractBriefInfo(errorLog); - Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle(); + final String briefInfo = extractBriefInfo(errorLog); + final Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle(); bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); bigTextStyle.bigText(briefInfo); bigTextStyle.setSummaryText("点击查看详情"); builder.setStyle(bigTextStyle); builder.setSmallIcon(hostContext.getApplicationInfo().icon) - .setContentTitle(hostAppName + " 崩溃") - .setContentText(briefInfo.split("\n")[0]) - .setContentIntent(jumpIntent) - .setAutoCancel(true) - .setWhen(System.currentTimeMillis()) - .setPriority(Notification.PRIORITY_DEFAULT); - + .setContentTitle(hostAppName + " 崩溃") + .setContentText(briefInfo.split("\n")[0]) + .setContentIntent(jumpIntent) + .setAutoCancel(true) + .setWhen(System.currentTimeMillis()) + .setPriority(Notification.PRIORITY_DEFAULT); + if (shareIntent != null) { + builder.addAction(android.R.drawable.ic_menu_send, "分享日志", shareIntent); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return builder.build(); } else { @@ -248,10 +373,10 @@ return PendingIntent.getActivity( if (errorLog == null || errorLog.isEmpty()) { return "无崩溃日志"; } - String brief = extractBriefInfo(errorLog); - String firstLine = brief.split("\n")[0]; + final String brief = extractBriefInfo(errorLog); + final String firstLine = brief.split("\n")[0]; if (firstLine.length() > 80) { - firstLine = firstLine.substring(0, 80) + "..."; + return firstLine.substring(0, 80) + "..."; } return firstLine; } @@ -265,7 +390,7 @@ return PendingIntent.getActivity( if (crashLog == null || crashLog.isEmpty()) { return "无崩溃日志"; } - StringBuilder brief = new StringBuilder(); + final StringBuilder brief = new StringBuilder(); try { java.util.regex.Pattern exceptionPattern = java.util.regex.Pattern.compile(REGEX_EXCEPTION_TYPE); java.util.regex.Matcher exceptionMatcher = exceptionPattern.matcher(crashLog); @@ -315,13 +440,4 @@ return PendingIntent.getActivity( } return brief.toString(); } - - /** - * 资源释放预留方法 - * @param hostContext 宿主上下文 - */ - public static void release(final Context hostContext) { - LogUtils.d(TAG, "CrashHandleNotifyUtils 执行资源释放"); - } -} - +} \ No newline at end of file diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ShareLogActivity.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ShareLogActivity.java new file mode 100644 index 0000000..a9edd48 --- /dev/null +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ShareLogActivity.java @@ -0,0 +1,91 @@ +package cc.winboll.studio.libappbase.utils; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; + +/** + * 分享崩溃日志窗口类 + * @Author ZhanGSKen + * @CreateTime 2026/05/11 22:30:00 + */ +public class ShareLogActivity extends Activity { + + public static final String TAG = "ShareLogActivity"; + public static final String EXTRA_CRASH_LOG_FILEPATH = "crash_log_filepath"; + public static final String EXTRA_CRASH_LOG_SUBJECT = "crash_log_subject"; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d(TAG, "onCreate 进入方法"); + + final Intent intent = getIntent(); + if (intent == null) { + Log.e(TAG, "onCreate intent 为空"); + finish(); + return; + } + + final String crashLogFilePath = intent.getStringExtra(EXTRA_CRASH_LOG_FILEPATH); + if (crashLogFilePath == null || crashLogFilePath.isEmpty()) { + Log.e(TAG, "onCreate crashLogFilePath 为空"); + Toast.makeText(this, "日志文件路径无效", Toast.LENGTH_SHORT).show(); + finish(); + return; + } + + final String subject = intent.getStringExtra(EXTRA_CRASH_LOG_SUBJECT); + handleShareCrashLog(crashLogFilePath, subject); + } + + private void handleShareCrashLog(final String crashLogFilePath, final String subject) { + Log.d(TAG, "handleShareCrashLog crashLogFilePath = " + crashLogFilePath); + + final File crashLogFile = new File(crashLogFilePath); + if (!crashLogFile.exists()) { + Log.e(TAG, "handleShareCrashLog 文件不存在"); + Toast.makeText(this, "日志文件不存在", Toast.LENGTH_SHORT).show(); + finish(); + return; + } + + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(crashLogFile), "UTF-8")); + final StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + final String logContent = sb.toString(); + + final Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, logContent); + if (subject != null && !subject.isEmpty()) { + shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + } else { + shareIntent.putExtra(Intent.EXTRA_SUBJECT, "崩溃日志"); + } + + startActivity(Intent.createChooser(shareIntent, "分享日志到")); + Log.d(TAG, "handleShareCrashLog 分享成功"); + } catch (Exception e) { + Log.e(TAG, "handleShareCrashLog 异常", e); + Toast.makeText(this, "分享失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } finally { + if (reader != null) { + try { reader.close(); } catch (Exception e) {} + } + finish(); + } + } +} \ No newline at end of file