diff --git a/app/src/main/java/com/hippo/ehviewer/AppConfig.java b/app/src/main/java/com/hippo/ehviewer/AppConfig.java
index edd5ddf0c..03a9f30fe 100644
--- a/app/src/main/java/com/hippo/ehviewer/AppConfig.java
+++ b/app/src/main/java/com/hippo/ehviewer/AppConfig.java
@@ -38,6 +38,7 @@ public class AppConfig {
private static final String PARSE_ERROR = "parse_error";
private static final String LOGCAT = "logcat";
private static final String DATA = "data";
+ private static final String CRASH = "crash";
private static Context sContext;
@@ -107,6 +108,11 @@ public class AppConfig {
return getDirInExternalAppDir(DATA);
}
+ @Nullable
+ public static File getExternalCrashDir() {
+ return getDirInExternalAppDir(CRASH);
+ }
+
@Nullable
public static File createExternalTempFile() {
return FileUtils.createTempFile(getExternalTempDir(), null);
diff --git a/app/src/main/java/com/hippo/ehviewer/Crash.java b/app/src/main/java/com/hippo/ehviewer/Crash.java
new file mode 100644
index 000000000..ea486294a
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/Crash.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2019 Hippo Seven
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Debug;
+import androidx.annotation.NonNull;
+import com.hippo.scene.StageActivity;
+import com.hippo.util.PackageUtils;
+import com.hippo.util.ReadableTime;
+import com.hippo.yorozuya.FileUtils;
+import com.hippo.yorozuya.IOUtils;
+import com.hippo.yorozuya.OSUtils;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+public final class Crash {
+ private Crash() {}
+
+ @NonNull
+ private static String avoidNull(String str) {
+ return null != str ? str : "null";
+ }
+
+ private static void collectInfo(Context context, FileWriter fw) throws IOException {
+ try {
+ PackageManager pm = context.getPackageManager();
+ PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
+ if (pi != null) {
+ String versionName = pi.versionName == null ? "null" : pi.versionName;
+ String versionCode = String.valueOf(pi.versionCode);
+ fw.write("======== PackageInfo ========\r\n");
+ fw.write("PackageName=");fw.write(pi.packageName);fw.write("\r\n");
+ fw.write("VersionName=");fw.write(versionName);fw.write("\r\n");
+ fw.write("VersionCode=");fw.write(versionCode);fw.write("\r\n");
+ String signature = PackageUtils.getSignature(context, pi.packageName);
+ fw.write("Signature=");fw.write(null != signature ? signature : "null");fw.write("\r\n");
+ fw.write("\r\n");
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ fw.write("======== PackageInfo ========\r\n");
+ fw.write("Can't get package information\r\n");
+ fw.write("\r\n");
+ }
+
+ // Runtime
+ String topActivityClazzName = "null";
+ String topSceneClazzName = "null";
+ try {
+ Activity topActivity = ((EhApplication) context.getApplicationContext()).getTopActivity();
+ if (null != topActivity) {
+ topActivityClazzName = topActivity.getClass().getName();
+ if (topActivity instanceof StageActivity) {
+ Class> clazz = ((StageActivity) topActivity).getTopSceneClass();
+ if (clazz != null) {
+ topSceneClazzName = clazz.getName();
+ }
+ }
+ }
+ } catch (Throwable e) {
+ // Ignore
+ }
+ fw.write("======== Runtime ========\r\n");
+ fw.write("TopActivity=");fw.write(avoidNull(topActivityClazzName));fw.write("\r\n");
+ fw.write("TopScene=");fw.write(avoidNull(topSceneClazzName));fw.write("\r\n");
+ fw.write("\r\n");
+
+ // Device info
+ fw.write("======== DeviceInfo ========\r\n");
+ fw.write("BOARD=");fw.write(Build.BOARD);fw.write("\r\n");
+ fw.write("BOOTLOADER=");fw.write(Build.BOOTLOADER);fw.write("\r\n");
+ fw.write("CPU_ABI=");fw.write(Build.CPU_ABI);fw.write("\r\n");
+ fw.write("CPU_ABI2=");fw.write(Build.CPU_ABI2);fw.write("\r\n");
+ fw.write("DEVICE=");fw.write(Build.DEVICE);fw.write("\r\n");
+ fw.write("DISPLAY=");fw.write(Build.DISPLAY);fw.write("\r\n");
+ fw.write("FINGERPRINT=");fw.write(Build.FINGERPRINT);fw.write("\r\n");
+ fw.write("HARDWARE=");fw.write(Build.HARDWARE);fw.write("\r\n");
+ fw.write("HOST=");fw.write(Build.HOST);fw.write("\r\n");
+ fw.write("ID=");fw.write(Build.ID);fw.write("\r\n");
+ fw.write("MANUFACTURER=");fw.write(Build.MANUFACTURER);fw.write("\r\n");
+ fw.write("MODEL=");fw.write(Build.MODEL);fw.write("\r\n");
+ fw.write("PRODUCT=");fw.write(Build.PRODUCT);fw.write("\r\n");
+ fw.write("RADIO=");fw.write(Build.getRadioVersion());fw.write("\r\n");
+ fw.write("SERIAL=");fw.write(Build.SERIAL);fw.write("\r\n");
+ fw.write("TAGS=");fw.write(Build.TAGS);fw.write("\r\n");
+ fw.write("TYPE=");fw.write(Build.TYPE);fw.write("\r\n");
+ fw.write("USER=");fw.write(Build.USER);fw.write("\r\n");
+ fw.write("CODENAME=");fw.write(Build.VERSION.CODENAME);fw.write("\r\n");
+ fw.write("INCREMENTAL=");fw.write(Build.VERSION.INCREMENTAL);fw.write("\r\n");
+ fw.write("RELEASE=");fw.write(Build.VERSION.RELEASE);fw.write("\r\n");
+ fw.write("SDK=");fw.write(Integer.toString(Build.VERSION.SDK_INT));fw.write("\r\n");
+ fw.write("MEMORY=");fw.write(
+ FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false));fw.write("\r\n");
+ fw.write("MEMORY_NATIVE=");fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false));fw.write("\r\n");
+ fw.write("MEMORY_MAX=");fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false));fw.write("\r\n");
+ fw.write("MEMORY_TOTAL=");fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false));fw.write("\r\n");
+ fw.write("\r\n");
+ }
+
+ private static void getThrowableInfo(Throwable t, FileWriter fw) {
+ PrintWriter printWriter = new PrintWriter(fw);
+ t.printStackTrace(printWriter);
+ Throwable cause = t.getCause();
+ while (cause != null) {
+ cause.printStackTrace(printWriter);
+ cause = cause.getCause();
+ }
+ }
+
+ public static void saveCrashLog(Context context, Throwable t) {
+ File dir = AppConfig.getExternalCrashDir();
+ if (dir == null) {
+ return;
+ }
+
+ String nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis());
+ String fileName = "crash-" + nowString + ".log";
+ File file = new File(dir, fileName);
+
+ FileWriter fw = null;
+ try {
+ fw = new FileWriter(file);
+ fw.write("TIME=");fw.write(nowString);fw.write("\r\n");
+ fw.write("\r\n");
+ collectInfo(context, fw);
+ fw.write("======== CrashInfo ========\r\n");
+ getThrowableInfo(t, fw);
+ fw.write("\r\n");
+ fw.flush();
+ } catch (Exception e) {
+ file.delete();
+ } finally {
+ IOUtils.closeQuietly(fw);
+ }
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/EhApplication.java b/app/src/main/java/com/hippo/ehviewer/EhApplication.java
index 70a3a9d89..799f43cc0 100644
--- a/app/src/main/java/com/hippo/ehviewer/EhApplication.java
+++ b/app/src/main/java/com/hippo/ehviewer/EhApplication.java
@@ -104,6 +104,19 @@ public class EhApplication extends RecordingApplication {
public void onCreate() {
instance = this;
+ Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
+ Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
+ try {
+ if (Settings.getSaveCrashLog()) {
+ Crash.saveCrashLog(instance, e);
+ }
+ } catch (Throwable ignored) { }
+
+ if (handler != null) {
+ handler.uncaughtException(t, e);
+ }
+ });
+
super.onCreate();
GetText.initialize(this);
diff --git a/app/src/main/java/com/hippo/ehviewer/Settings.java b/app/src/main/java/com/hippo/ehviewer/Settings.java
index ddc97a981..ba9579689 100755
--- a/app/src/main/java/com/hippo/ehviewer/Settings.java
+++ b/app/src/main/java/com/hippo/ehviewer/Settings.java
@@ -950,6 +950,13 @@ public class Settings {
putBoolean(KEY_SAVE_PARSE_ERROR_BODY, value);
}
+ private static final String KEY_SAVE_CRASH_LOG = "save_crash_log";
+ private static final boolean DEFAULT_SAVE_CRASH_LOG = false;
+
+ public static boolean getSaveCrashLog() {
+ return getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG);
+ }
+
public static final String KEY_SECURITY = "security";
public static final String DEFAULT_SECURITY = "";
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index d1726b9df..f348f1f5b 100755
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -503,6 +503,8 @@
高级
解析失败时保存页面内容
页面内容可能含有隐私敏感信息
+ 应用崩溃时保存错误日志
+ 错误日志可以帮助开发者修正问题
导出日志
保存日志至外置存储器
导出日志失败
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 2d99e782b..6a799c8c2 100755
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -502,6 +502,8 @@
高級
解析失敗時保存頁面內容
頁面內容可能含有隱私敏感信息
+ 應用崩潰時保存錯誤日誌
+ 錯誤日誌可以幫助開發者修正問題
導出日誌
保存日誌至外置存儲器
導出日誌失敗
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 772f877af..4cb64cb2e 100755
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -502,6 +502,8 @@
進階
解析失敗時儲存網頁的內容
網頁內容可能含有敏感的隱私資料
+ 應用崩潰時儲存錯誤日誌
+ 錯誤日誌可以幫助開發者修正問題
傾印log
儲存log紀錄至外接儲存裝置中
無法傾印log紀錄
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a75696f01..3a198f7e0 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -546,6 +546,8 @@
Advanced
Save html content when parsing error
Html content may be privacy-sensitive
+ Save crash log when app crashes
+ Crash logs help developers fix bugs
Dump logcat
Save logcat to external storage
Dump logcat failed
diff --git a/app/src/main/res/xml/advanced_settings.xml b/app/src/main/res/xml/advanced_settings.xml
index 1d028d918..839337f5f 100644
--- a/app/src/main/res/xml/advanced_settings.xml
+++ b/app/src/main/res/xml/advanced_settings.xml
@@ -24,6 +24,12 @@
android:summary="@string/settings_advanced_save_parse_error_body_summary"
android:defaultValue="@bool/beta"/>
+
+