Commit a621f762 authored by fanghaitong's avatar fanghaitong

增加崩溃重启

parent 240ca20c
...@@ -63,6 +63,7 @@ dependencies { ...@@ -63,6 +63,7 @@ dependencies {
implementation 'com.google.android.material:material:1.4.0' implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation project(path: ':printer') implementation project(path: ':printer')
implementation project(path: ':rebootoncrash')
// implementation 'com.android.tools.build:gradle:3.1.1' // implementation 'com.android.tools.build:gradle:3.1.1'
// implementation 'com.android.tools.lint:lint-gradle:26.1.1' // implementation 'com.android.tools.lint:lint-gradle:26.1.1'
implementation 'com.guolindev.permissionx:permissionx:1.5.1' implementation 'com.guolindev.permissionx:permissionx:1.5.1'
......
package com.qimai.android.widget package com.qimai.android.widget
import android.app.Application import android.app.Application
import com.qmai.rebootoncrash.config.CaocConfig
import zs.qimai.com.printer2.utils.PrintManagerUtils import zs.qimai.com.printer2.utils.PrintManagerUtils
class App : Application() { class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
PrintManagerUtils.getInstance().init(this) PrintManagerUtils.getInstance().init(this)
CaocConfig.Builder.create()
.errorDrawable(R.mipmap.ic_launcher)
.restartActivity(TopBarActivity::class.java)
.apply()
} }
} }
\ No newline at end of file
...@@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity ...@@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity
import com.qimai.android.widgetlib.popup.QmTipOverlayDrawable import com.qimai.android.widgetlib.popup.QmTipOverlayDrawable
import com.qimai.android.widgetlib.toast.HolderContext import com.qimai.android.widgetlib.toast.HolderContext
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import java.lang.RuntimeException
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
...@@ -57,6 +58,9 @@ class MainActivity : AppCompatActivity() { ...@@ -57,6 +58,9 @@ class MainActivity : AppCompatActivity() {
findViewById<Button>(R.id.btn_log).setOnClickListener { findViewById<Button>(R.id.btn_log).setOnClickListener {
startActivity(Intent(this, LogActivity::class.java)) startActivity(Intent(this, LogActivity::class.java))
} }
findViewById<Button>(R.id.btn_crash).setOnClickListener {
throw RuntimeException("主线程崩溃")
}
keyboard.btnText = "结账" keyboard.btnText = "结账"
keyboard.invalidate() keyboard.invalidate()
} }
......
...@@ -10,6 +10,7 @@ import android.os.Environment ...@@ -10,6 +10,7 @@ import android.os.Environment
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.Glide
import com.eclipsesource.v8.JavaVoidCallback import com.eclipsesource.v8.JavaVoidCallback
import com.eclipsesource.v8.V8 import com.eclipsesource.v8.V8
import com.eclipsesource.v8.V8Array import com.eclipsesource.v8.V8Array
......
...@@ -51,6 +51,11 @@ ...@@ -51,6 +51,11 @@
android:text="日志" android:text="日志"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" /> android:layout_height="50dp" />
<Button
android:id="@+id/btn_crash"
android:text="崩溃"
android:layout_width="match_parent"
android:layout_height="50dp" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
......
/build
\ No newline at end of file
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'com.whl.gradle-publish-plugin'
}
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
group 'com.qmai.android.rebootoncrash'
version '1.0.2'
gradlePublish {
sourceJarEnabled = true
javaDocEnabled = false
signEnabled = false
releaseRepository {
url = "https://hub.zmcms.cn/repository/maven-releases/"
userName = "wanglei1"
password = "woshiwanglei123"
}
snapshotRepository {
url = "https://hub.zmcms.cn/repository/maven-snapshots/"
userName = "wanglei1"
password = "woshiwanglei123"
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package com.qmai.rebootoncrash
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.qmai.rebootoncrash.test", appContext.packageName)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.qmai.rebootoncrash">
<application>
<activity
android:name=".activity.DefaultErrorActivity"
android:process=":error_activity" />
<provider
android:name=".provider.CaocInitProvider"
android:authorities="${applicationId}.customactivityoncrashinitprovider"
android:exported="false"
android:initOrder="101" />
</application>
</manifest>
\ No newline at end of file
/*
* Copyright 2014-2017 Eduard Ereza Martínez
*
* 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.qmai.rebootoncrash;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import com.qmai.rebootoncrash.activity.DefaultErrorActivity;
import com.qmai.rebootoncrash.config.CaocConfig;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.lang.ref.WeakReference;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayDeque;
import java.util.Date;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class CustomActivityOnCrash {
private final static String TAG = "CustomActivityOnCrash";
//Extras passed to the error activity
private static final String EXTRA_CONFIG = "cat.ereza.customactivityoncrash.EXTRA_CONFIG";
private static final String EXTRA_STACK_TRACE = "cat.ereza.customactivityoncrash.EXTRA_STACK_TRACE";
private static final String EXTRA_ACTIVITY_LOG = "cat.ereza.customactivityoncrash.EXTRA_ACTIVITY_LOG";
//General constants
private static final String INTENT_ACTION_ERROR_ACTIVITY = "cat.ereza.customactivityoncrash.ERROR";
private static final String INTENT_ACTION_RESTART_ACTIVITY = "cat.ereza.customactivityoncrash.RESTART";
private static final String CAOC_HANDLER_PACKAGE_NAME = "cat.ereza.customactivityoncrash";
private static final String DEFAULT_HANDLER_PACKAGE_NAME = "com.android.internal.os";
private static final int TIME_TO_CONSIDER_FOREGROUND_MS = 500;
private static final int MAX_STACK_TRACE_SIZE = 131071; //128 KB - 1
private static final int MAX_ACTIVITIES_IN_LOG = 50;
//Shared preferences
private static final String SHARED_PREFERENCES_FILE = "custom_activity_on_crash";
private static final String SHARED_PREFERENCES_FIELD_TIMESTAMP = "last_crash_timestamp";
//Internal variables
@SuppressLint("StaticFieldLeak") //This is an application-wide component
private static Application application;
private static CaocConfig config = new CaocConfig();
private static final Deque<String> activityLog = new ArrayDeque<>(MAX_ACTIVITIES_IN_LOG);
private static WeakReference<Activity> lastActivityCreated = new WeakReference<>(null);
private static long lastActivityCreatedTimestamp = 0L;
private static boolean isInBackground = true;
/**
* Installs CustomActivityOnCrash on the application using the default error activity.
*
* @param context Context to use for obtaining the ApplicationContext. Must not be null.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void install(@Nullable final Context context) {
try {
if (context == null) {
Log.e(TAG, "Install failed: context is null!");
} else {
//INSTALL!
final Thread.UncaughtExceptionHandler oldHandler = Thread.getDefaultUncaughtExceptionHandler();
if (oldHandler != null && oldHandler.getClass().getName().startsWith(CAOC_HANDLER_PACKAGE_NAME)) {
Log.e(TAG, "CustomActivityOnCrash was already installed, doing nothing!");
} else {
if (oldHandler != null && !oldHandler.getClass().getName().startsWith(DEFAULT_HANDLER_PACKAGE_NAME)) {
Log.e(TAG, "IMPORTANT WARNING! You already have an UncaughtExceptionHandler, are you sure this is correct? If you use a custom UncaughtExceptionHandler, you must initialize it AFTER CustomActivityOnCrash! Installing anyway, but your original handler will not be called.");
}
application = (Application) context.getApplicationContext();
//We define a default exception handler that does what we want so it can be called from Crashlytics/ACRA
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull final Throwable throwable) {
if (config.isEnabled()) {
Log.e(TAG, "App has crashed, executing CustomActivityOnCrash's UncaughtExceptionHandler", throwable);
if (hasCrashedInTheLastSeconds(application)) {
Log.e(TAG, "App already crashed recently, not starting custom error activity because we could enter a restart loop. Are you sure that your app does not crash directly on init?", throwable);
if (oldHandler != null) {
oldHandler.uncaughtException(thread, throwable);
return;
}
} else {
setLastCrashTimestamp(application, new Date().getTime());
Class<? extends Activity> errorActivityClass = config.getErrorActivityClass();
if (errorActivityClass == null) {
errorActivityClass = guessErrorActivityClass(application);
}
if (isStackTraceLikelyConflictive(throwable, errorActivityClass)) {
Log.e(TAG, "Your application class or your error activity have crashed, the custom activity will not be launched!");
if (oldHandler != null) {
oldHandler.uncaughtException(thread, throwable);
return;
}
} else if (config.getBackgroundMode() == CaocConfig.BACKGROUND_MODE_SHOW_CUSTOM || !isInBackground
|| (lastActivityCreatedTimestamp >= new Date().getTime() - TIME_TO_CONSIDER_FOREGROUND_MS)) {
final Intent intent = new Intent(application, errorActivityClass);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
String stackTraceString = sw.toString();
//Reduce data to 128KB so we don't get a TransactionTooLargeException when sending the intent.
//The limit is 1MB on Android but some devices seem to have it lower.
//See: http://developer.android.com/reference/android/os/TransactionTooLargeException.html
//And: http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception#comment46697371_12809171
if (stackTraceString.length() > MAX_STACK_TRACE_SIZE) {
String disclaimer = " [stack trace too large]";
stackTraceString = stackTraceString.substring(0, MAX_STACK_TRACE_SIZE - disclaimer.length()) + disclaimer;
}
intent.putExtra(EXTRA_STACK_TRACE, stackTraceString);
if (config.isTrackActivities()) {
StringBuilder activityLogStringBuilder = new StringBuilder();
while (!activityLog.isEmpty()) {
activityLogStringBuilder.append(activityLog.poll());
}
intent.putExtra(EXTRA_ACTIVITY_LOG, activityLogStringBuilder.toString());
}
if (config.isShowRestartButton() && config.getRestartActivityClass() == null) {
//We can set the restartActivityClass because the app will terminate right now,
//and when relaunched, will be null again by default.
config.setRestartActivityClass(guessRestartActivityClass(application));
}
intent.putExtra(EXTRA_CONFIG, config);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
if (config.getEventListener() != null) {
config.getEventListener().onLaunchErrorActivity();
}
application.startActivity(intent);
} else if (config.getBackgroundMode() == CaocConfig.BACKGROUND_MODE_CRASH) {
if (oldHandler != null) {
oldHandler.uncaughtException(thread, throwable);
return;
}
//If it is null (should not be), we let it continue and kill the process or it will be stuck
}
//Else (BACKGROUND_MODE_SILENT): do nothing and let the following code kill the process
}
final Activity lastActivity = lastActivityCreated.get();
if (lastActivity != null) {
//We finish the activity, this solves a bug which causes infinite recursion.
//See: https://github.com/ACRA/acra/issues/42
lastActivity.finish();
lastActivityCreated.clear();
}
killCurrentProcess();
} else if (oldHandler != null) {
oldHandler.uncaughtException(thread, throwable);
}
}
});
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
int currentlyStartedActivities = 0;
final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
@Override
public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {
if (activity.getClass() != config.getErrorActivityClass()) {
// Copied from ACRA:
// Ignore activityClass because we want the last
// application Activity that was started so that we can
// explicitly kill it off.
lastActivityCreated = new WeakReference<>(activity);
lastActivityCreatedTimestamp = new Date().getTime();
}
if (config.isTrackActivities()) {
activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " created\n");
}
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
currentlyStartedActivities++;
isInBackground = (currentlyStartedActivities == 0);
//Do nothing
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
if (config.isTrackActivities()) {
activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " resumed\n");
}
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
if (config.isTrackActivities()) {
activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " paused\n");
}
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
//Do nothing
currentlyStartedActivities--;
isInBackground = (currentlyStartedActivities == 0);
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
//Do nothing
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
if (config.isTrackActivities()) {
activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " destroyed\n");
}
}
});
}
Log.i(TAG, "CustomActivityOnCrash has been installed.");
}
} catch (Throwable t) {
Log.e(TAG, "An unknown error occurred while installing CustomActivityOnCrash, it may not have been properly initialized. Please report this as a bug if needed.", t);
}
}
/**
* Given an Intent, returns the stack trace extra from it.
*
* @param intent The Intent. Must not be null.
* @return The stacktrace, or null if not provided.
*/
@Nullable
public static String getStackTraceFromIntent(@NonNull Intent intent) {
return intent.getStringExtra(CustomActivityOnCrash.EXTRA_STACK_TRACE);
}
/**
* Given an Intent, returns the config extra from it.
*
* @param intent The Intent. Must not be null.
* @return The config, or null if not provided.
*/
@Nullable
public static CaocConfig getConfigFromIntent(@NonNull Intent intent) {
CaocConfig config = (CaocConfig) intent.getSerializableExtra(CustomActivityOnCrash.EXTRA_CONFIG);
if (config != null && config.isLogErrorOnRestart()) {
String stackTrace = getStackTraceFromIntent(intent);
if (stackTrace != null) {
Log.e(TAG, "The previous app process crashed. This is the stack trace of the crash:\n" + getStackTraceFromIntent(intent));
}
}
return config;
}
/**
* Given an Intent, returns the activity log extra from it.
*
* @param intent The Intent. Must not be null.
* @return The activity log, or null if not provided.
*/
@Nullable
public static String getActivityLogFromIntent(@NonNull Intent intent) {
return intent.getStringExtra(CustomActivityOnCrash.EXTRA_ACTIVITY_LOG);
}
/**
* Given an Intent, returns several error details including the stack trace extra from the intent.
*
* @param context A valid context. Must not be null.
* @param intent The Intent. Must not be null.
* @return The full error details.
*/
@NonNull
public static String getAllErrorDetailsFromIntent(@NonNull Context context, @NonNull Intent intent) {
//I don't think that this needs localization because it's a development string...
Date currentDate = new Date();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
//Get build date
String buildDateAsString = getBuildDateAsString(context, dateFormat);
//Get app version
String versionName = getVersionName(context);
String errorDetails = "";
errorDetails += "Build version: " + versionName + " \n";
if (buildDateAsString != null) {
errorDetails += "Build date: " + buildDateAsString + " \n";
}
errorDetails += "Current date: " + dateFormat.format(currentDate) + " \n";
//Added a space between line feeds to fix #18.
//Ideally, we should not use this method at all... It is only formatted this way because of coupling with the default error activity.
//We should move it to a method that returns a bean, and let anyone format it as they wish.
errorDetails += "Device: " + getDeviceModelName() + " \n";
errorDetails += "OS version: Android " + Build.VERSION.RELEASE + " (SDK " + Build.VERSION.SDK_INT + ") \n \n";
errorDetails += "Stack trace: \n";
errorDetails += getStackTraceFromIntent(intent);
String activityLog = getActivityLogFromIntent(intent);
if (activityLog != null) {
errorDetails += "\nUser actions: \n";
errorDetails += activityLog;
}
return errorDetails;
}
/**
* Given an Intent, restarts the app and launches a startActivity to that intent.
* The flags NEW_TASK and CLEAR_TASK are set if the Intent does not have them, to ensure
* the app stack is fully cleared.
* If an event listener is provided, the restart app event is invoked.
* Must only be used from your error activity.
*
* @param activity The current error activity. Must not be null.
* @param intent The Intent. Must not be null.
* @param config The config object as obtained by calling getConfigFromIntent.
*/
public static void restartApplicationWithIntent(@NonNull Activity activity, @NonNull Intent intent, @NonNull CaocConfig config) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
if (intent.getComponent() != null) {
//If the class name has been set, we force it to simulate a Launcher launch.
//If we don't do this, if you restart from the error activity, then press home,
//and then launch the activity from the launcher, the main activity appears twice on the backstack.
//This will most likely not have any detrimental effect because if you set the Intent component,
//if will always be launched regardless of the actions specified here.
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
}
if (config.getEventListener() != null) {
config.getEventListener().onRestartAppFromErrorActivity();
}
activity.finish();
activity.startActivity(intent);
killCurrentProcess();
}
public static void restartApplication(@NonNull Activity activity, @NonNull CaocConfig config) {
Intent intent = new Intent(activity, config.getRestartActivityClass());
restartApplicationWithIntent(activity, intent, config);
}
/**
* Closes the app.
* If an event listener is provided, the close app event is invoked.
* Must only be used from your error activity.
*
* @param activity The current error activity. Must not be null.
* @param config The config object as obtained by calling getConfigFromIntent.
*/
public static void closeApplication(@NonNull Activity activity, @NonNull CaocConfig config) {
if (config.getEventListener() != null) {
config.getEventListener().onCloseAppFromErrorActivity();
}
activity.finish();
killCurrentProcess();
}
/// INTERNAL METHODS NOT TO BE USED BY THIRD PARTIES
/**
* INTERNAL method that returns the current configuration of the library.
* If you want to check the config, use CaocConfig.Builder.get();
*
* @return the current configuration
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@NonNull
public static CaocConfig getConfig() {
return config;
}
/**
* INTERNAL method that sets the configuration of the library.
* You must not use this, use CaocConfig.Builder.apply()
*
* @param config the configuration to use
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void setConfig(@NonNull CaocConfig config) {
CustomActivityOnCrash.config = config;
}
/**
* INTERNAL method that checks if the stack trace that just crashed is conflictive. This is true in the following scenarios:
* - The application has crashed while initializing (handleBindApplication is in the stack)
* - The crash occurred inside the "error_activity" process
*
* @param throwable The throwable from which the stack trace will be checked
* @param activityClass The activity class to launch when the app crashes
* @return true if this stack trace is conflictive and the activity must not be launched, false otherwise
*/
private static boolean isStackTraceLikelyConflictive(@NonNull Throwable throwable, @NonNull Class<? extends Activity> activityClass) {
String process;
try {
BufferedReader br = new BufferedReader(new FileReader("/proc/self/cmdline"));
process = br.readLine().trim();
br.close();
} catch (IOException e) {
process = null;
}
if (process != null && process.endsWith(":error_activity")) {
//Error happened in the error activity process - conflictive, so use default handler
return true;
}
do {
StackTraceElement[] stackTrace = throwable.getStackTrace();
for (StackTraceElement element : stackTrace) {
if (element.getClassName().equals("android.app.ActivityThread") && element.getMethodName().equals("handleBindApplication")) {
return true;
}
}
} while ((throwable = throwable.getCause()) != null);
return false;
}
/**
* INTERNAL method that returns the build date of the current APK as a string, or null if unable to determine it.
*
* @param context A valid context. Must not be null.
* @param dateFormat DateFormat to use to convert from Date to String
* @return The formatted date, or "Unknown" if unable to determine it.
*/
@Nullable
private static String getBuildDateAsString(@NonNull Context context, @NonNull DateFormat dateFormat) {
long buildDate;
try {
ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
ZipFile zf = new ZipFile(ai.sourceDir);
//If this failed, try with the old zip method
ZipEntry ze = zf.getEntry("classes.dex");
buildDate = ze.getTime();
zf.close();
} catch (Exception e) {
buildDate = 0;
}
if (buildDate > 312764400000L) {
return dateFormat.format(new Date(buildDate));
} else {
return null;
}
}
/**
* INTERNAL method that returns the version name of the current app, or null if unable to determine it.
*
* @param context A valid context. Must not be null.
* @return The version name, or "Unknown if unable to determine it.
*/
@NonNull
private static String getVersionName(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionName;
} catch (Exception e) {
return "Unknown";
}
}
/**
* INTERNAL method that returns the device model name with correct capitalization.
* Taken from: http://stackoverflow.com/a/12707479/1254846
*
* @return The device model name (i.e., "LGE Nexus 5")
*/
@NonNull
private static String getDeviceModelName() {
String manufacturer = Build.MANUFACTURER;
String model = Build.MODEL;
if (model.startsWith(manufacturer)) {
return capitalize(model);
} else {
return capitalize(manufacturer) + " " + model;
}
}
/**
* INTERNAL method that capitalizes the first character of a string
*
* @param s The string to capitalize
* @return The capitalized string
*/
@NonNull
private static String capitalize(@Nullable String s) {
if (s == null || s.length() == 0) {
return "";
}
char first = s.charAt(0);
if (Character.isUpperCase(first)) {
return s;
} else {
return Character.toUpperCase(first) + s.substring(1);
}
}
/**
* INTERNAL method used to guess which activity must be called from the error activity to restart the app.
* It will first get activities from the AndroidManifest with intent filter <action android:name="cat.ereza.customactivityoncrash.RESTART" />,
* if it cannot find them, then it will get the default launcher.
* If there is no default launcher, this returns null.
*
* @param context A valid context. Must not be null.
* @return The guessed restart activity class, or null if no suitable one is found
*/
@Nullable
private static Class<? extends Activity> guessRestartActivityClass(@NonNull Context context) {
Class<? extends Activity> resolvedActivityClass;
//If action is defined, use that
resolvedActivityClass = getRestartActivityClassWithIntentFilter(context);
//Else, get the default launcher activity
if (resolvedActivityClass == null) {
resolvedActivityClass = getLauncherActivity(context);
}
return resolvedActivityClass;
}
/**
* INTERNAL method used to get the first activity with an intent-filter <action android:name="cat.ereza.customactivityoncrash.RESTART" />,
* If there is no activity with that intent filter, this returns null.
*
* @param context A valid context. Must not be null.
* @return A valid activity class, or null if no suitable one is found
*/
@SuppressWarnings("unchecked")
@Nullable
private static Class<? extends Activity> getRestartActivityClassWithIntentFilter(@NonNull Context context) {
Intent searchedIntent = new Intent().setAction(INTENT_ACTION_RESTART_ACTIVITY).setPackage(context.getPackageName());
List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(searchedIntent,
PackageManager.GET_RESOLVED_FILTER);
if (resolveInfos.size() > 0) {
ResolveInfo resolveInfo = resolveInfos.get(0);
try {
return (Class<? extends Activity>) Class.forName(resolveInfo.activityInfo.name);
} catch (ClassNotFoundException e) {
//Should not happen, print it to the log!
Log.e(TAG, "Failed when resolving the restart activity class via intent filter, stack trace follows!", e);
}
}
return null;
}
/**
* INTERNAL method used to get the default launcher activity for the app.
* If there is no launchable activity, this returns null.
*
* @param context A valid context. Must not be null.
* @return A valid activity class, or null if no suitable one is found
*/
@SuppressWarnings("unchecked")
@Nullable
private static Class<? extends Activity> getLauncherActivity(@NonNull Context context) {
Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
if (intent != null && intent.getComponent() != null) {
try {
return (Class<? extends Activity>) Class.forName(intent.getComponent().getClassName());
} catch (ClassNotFoundException e) {
//Should not happen, print it to the log!
Log.e(TAG, "Failed when resolving the restart activity class via getLaunchIntentForPackage, stack trace follows!", e);
}
}
return null;
}
/**
* INTERNAL method used to guess which error activity must be called when the app crashes.
* It will first get activities from the AndroidManifest with intent filter <action android:name="cat.ereza.customactivityoncrash.ERROR" />,
* if it cannot find them, then it will use the default error activity.
*
* @param context A valid context. Must not be null.
* @return The guessed error activity class, or the default error activity if not found
*/
@NonNull
private static Class<? extends Activity> guessErrorActivityClass(@NonNull Context context) {
Class<? extends Activity> resolvedActivityClass;
//If action is defined, use that
resolvedActivityClass = getErrorActivityClassWithIntentFilter(context);
//Else, get the default error activity
if (resolvedActivityClass == null) {
resolvedActivityClass = DefaultErrorActivity.class;
}
return resolvedActivityClass;
}
/**
* INTERNAL method used to get the first activity with an intent-filter <action android:name="cat.ereza.customactivityoncrash.ERROR" />,
* If there is no activity with that intent filter, this returns null.
*
* @param context A valid context. Must not be null.
* @return A valid activity class, or null if no suitable one is found
*/
@SuppressWarnings("unchecked")
@Nullable
private static Class<? extends Activity> getErrorActivityClassWithIntentFilter(@NonNull Context context) {
Intent searchedIntent = new Intent().setAction(INTENT_ACTION_ERROR_ACTIVITY).setPackage(context.getPackageName());
List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(searchedIntent,
PackageManager.GET_RESOLVED_FILTER);
if (resolveInfos.size() > 0) {
ResolveInfo resolveInfo = resolveInfos.get(0);
try {
return (Class<? extends Activity>) Class.forName(resolveInfo.activityInfo.name);
} catch (ClassNotFoundException e) {
//Should not happen, print it to the log!
Log.e(TAG, "Failed when resolving the error activity class via intent filter, stack trace follows!", e);
}
}
return null;
}
/**
* INTERNAL method that kills the current process.
* It is used after restarting or killing the app.
*/
private static void killCurrentProcess() {
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
}
/**
* INTERNAL method that stores the last crash timestamp
*
* @param timestamp The current timestamp.
*/
@SuppressLint("ApplySharedPref") //This must be done immediately since we are killing the app
private static void setLastCrashTimestamp(@NonNull Context context, long timestamp) {
context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).edit().putLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, timestamp).commit();
}
/**
* INTERNAL method that gets the last crash timestamp
*
* @return The last crash timestamp, or -1 if not set.
*/
private static long getLastCrashTimestamp(@NonNull Context context) {
return context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).getLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, -1);
}
/**
* INTERNAL method that tells if the app has crashed in the last seconds.
* This is used to avoid restart loops.
*
* @return true if the app has crashed in the last seconds, false otherwise.
*/
private static boolean hasCrashedInTheLastSeconds(@NonNull Context context) {
long lastTimestamp = getLastCrashTimestamp(context);
long currentTimestamp = new Date().getTime();
return (lastTimestamp <= currentTimestamp && currentTimestamp - lastTimestamp < config.getMinTimeBetweenCrashesMs());
}
/**
* Interface to be called when events occur, so they can be reported
* by the app as, for example, Google Analytics events.
*/
public interface EventListener extends Serializable {
void onLaunchErrorActivity();
void onRestartAppFromErrorActivity();
void onCloseAppFromErrorActivity();
}
}
package com.qmai.rebootoncrash.activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.ResourcesCompat
import com.qmai.rebootoncrash.CustomActivityOnCrash
import com.qmai.rebootoncrash.R
import com.qmai.rebootoncrash.config.CaocConfig
class DefaultErrorActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//This is needed to avoid a crash if the developer has not specified
//an app-level theme that extends Theme.AppCompat
//This is needed to avoid a crash if the developer has not specified
//an app-level theme that extends Theme.AppCompat
val a = obtainStyledAttributes(R.styleable.AppCompatTheme)
if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
setTheme(R.style.Theme_AppCompat_Light_DarkActionBar)
}
a.recycle()
setContentView(R.layout.activity_default_error)
//Close/restart button logic:
//If a class if set, use restart.
//Else, use close and just finish the app.
//It is recommended that you follow this logic if implementing a custom error activity.
//Close/restart button logic:
//If a class if set, use restart.
//Else, use close and just finish the app.
//It is recommended that you follow this logic if implementing a custom error activity.
val restartButton =
findViewById<Button>(R.id.customactivityoncrash_error_activity_restart_button)
val config: CaocConfig? = CustomActivityOnCrash.getConfigFromIntent(intent)
if (config == null) {
//This should never happen - Just finish the activity to avoid a recursive crash.
finish()
return
}
if (config.isShowRestartButton() && config.restartActivityClass != null) {
restartButton.setText(R.string.customactivityoncrash_error_activity_restart_app)
restartButton.setOnClickListener {
CustomActivityOnCrash.restartApplication(
this@DefaultErrorActivity,
config
)
}
} else {
restartButton.setOnClickListener {
CustomActivityOnCrash.closeApplication(
this@DefaultErrorActivity,
config
)
}
}
val moreInfoButton =
findViewById<Button>(R.id.customactivityoncrash_error_activity_more_info_button)
if (config.isShowErrorDetails()) {
moreInfoButton.setOnClickListener { //We retrieve all the error data and show it
val dialog = AlertDialog.Builder(this@DefaultErrorActivity)
.setTitle(R.string.customactivityoncrash_error_activity_error_details_title)
.setMessage(
CustomActivityOnCrash.getAllErrorDetailsFromIntent(
this@DefaultErrorActivity,
intent
)
)
.setPositiveButton(
R.string.customactivityoncrash_error_activity_error_details_close,
null
)
.setNeutralButton(
R.string.customactivityoncrash_error_activity_error_details_copy
) { _, _ -> copyErrorToClipboard() }
.show()
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(Color.BLACK);
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setTextColor(Color.BLACK);
val textView = dialog.findViewById<TextView>(android.R.id.message)
textView?.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
resources.getDimension(R.dimen.customactivityoncrash_error_activity_error_details_text_size)
)
}
} else {
moreInfoButton.visibility = View.GONE
}
val defaultErrorActivityDrawableId: Int? = config.errorDrawable
val errorImageView =
findViewById<ImageView>(R.id.customactivityoncrash_error_activity_image)
if (defaultErrorActivityDrawableId != null) {
errorImageView.setImageDrawable(
ResourcesCompat.getDrawable(
resources, defaultErrorActivityDrawableId,
theme
)
)
}
}
private fun copyErrorToClipboard() {
val errorInformation: String = CustomActivityOnCrash.getAllErrorDetailsFromIntent(
this@DefaultErrorActivity,
intent
)
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
//Are there any devices without clipboard...?
if (clipboard != null) {
val clip = ClipData.newPlainText(
getString(R.string.customactivityoncrash_error_activity_error_details_clipboard_label),
errorInformation
)
clipboard.setPrimaryClip(clip)
Toast.makeText(
this@DefaultErrorActivity,
R.string.customactivityoncrash_error_activity_error_details_copied,
Toast.LENGTH_SHORT
).show()
}
}
}
\ No newline at end of file
/*
* Copyright 2014-2017 Eduard Ereza Martínez
*
* 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.qmai.rebootoncrash.config;
import android.app.Activity;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.qmai.rebootoncrash.CustomActivityOnCrash;
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Modifier;
public class CaocConfig implements Serializable {
@IntDef({BACKGROUND_MODE_CRASH, BACKGROUND_MODE_SHOW_CUSTOM, BACKGROUND_MODE_SILENT})
@Retention(RetentionPolicy.SOURCE)
private @interface BackgroundMode {
//I hate empty blocks
}
public static final int BACKGROUND_MODE_SILENT = 0;
public static final int BACKGROUND_MODE_SHOW_CUSTOM = 1;
public static final int BACKGROUND_MODE_CRASH = 2;
private int backgroundMode = BACKGROUND_MODE_SHOW_CUSTOM;
private boolean enabled = true;
private boolean showErrorDetails = true;
private boolean showRestartButton = true;
private boolean logErrorOnRestart = true;
private boolean trackActivities = false;
private int minTimeBetweenCrashesMs = 3000;
private Integer errorDrawable = null;
private Class<? extends Activity> errorActivityClass = null;
private Class<? extends Activity> restartActivityClass = null;
private CustomActivityOnCrash.EventListener eventListener = null;
@BackgroundMode
public int getBackgroundMode() {
return backgroundMode;
}
public void setBackgroundMode(@BackgroundMode int backgroundMode) {
this.backgroundMode = backgroundMode;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isShowErrorDetails() {
return showErrorDetails;
}
public void setShowErrorDetails(boolean showErrorDetails) {
this.showErrorDetails = showErrorDetails;
}
public boolean isShowRestartButton() {
return showRestartButton;
}
public void setShowRestartButton(boolean showRestartButton) {
this.showRestartButton = showRestartButton;
}
public boolean isLogErrorOnRestart() {
return logErrorOnRestart;
}
public void setLogErrorOnRestart(boolean logErrorOnRestart) {
this.logErrorOnRestart = logErrorOnRestart;
}
public boolean isTrackActivities() {
return trackActivities;
}
public void setTrackActivities(boolean trackActivities) {
this.trackActivities = trackActivities;
}
public int getMinTimeBetweenCrashesMs() {
return minTimeBetweenCrashesMs;
}
public void setMinTimeBetweenCrashesMs(int minTimeBetweenCrashesMs) {
this.minTimeBetweenCrashesMs = minTimeBetweenCrashesMs;
}
@Nullable
@DrawableRes
public Integer getErrorDrawable() {
return errorDrawable;
}
public void setErrorDrawable(@Nullable @DrawableRes Integer errorDrawable) {
this.errorDrawable = errorDrawable;
}
@Nullable
public Class<? extends Activity> getErrorActivityClass() {
return errorActivityClass;
}
public void setErrorActivityClass(@Nullable Class<? extends Activity> errorActivityClass) {
this.errorActivityClass = errorActivityClass;
}
@Nullable
public Class<? extends Activity> getRestartActivityClass() {
return restartActivityClass;
}
public void setRestartActivityClass(@Nullable Class<? extends Activity> restartActivityClass) {
this.restartActivityClass = restartActivityClass;
}
@Nullable
public CustomActivityOnCrash.EventListener getEventListener() {
return eventListener;
}
public void setEventListener(@Nullable CustomActivityOnCrash.EventListener eventListener) {
this.eventListener = eventListener;
}
public static class Builder {
private CaocConfig config;
@NonNull
public static Builder create() {
Builder builder = new Builder();
CaocConfig currentConfig = CustomActivityOnCrash.getConfig();
CaocConfig config = new CaocConfig();
config.backgroundMode = currentConfig.backgroundMode;
config.enabled = currentConfig.enabled;
config.showErrorDetails = currentConfig.showErrorDetails;
config.showRestartButton = currentConfig.showRestartButton;
config.logErrorOnRestart = currentConfig.logErrorOnRestart;
config.trackActivities = currentConfig.trackActivities;
config.minTimeBetweenCrashesMs = currentConfig.minTimeBetweenCrashesMs;
config.errorDrawable = currentConfig.errorDrawable;
config.errorActivityClass = currentConfig.errorActivityClass;
config.restartActivityClass = currentConfig.restartActivityClass;
config.eventListener = currentConfig.eventListener;
builder.config = config;
return builder;
}
/**
* Defines if the error activity must be launched when the app is on background.
* BackgroundMode.BACKGROUND_MODE_SHOW_CUSTOM: launch the error activity when the app is in background,
* BackgroundMode.BACKGROUND_MODE_CRASH: launch the default system error when the app is in background,
* BackgroundMode.BACKGROUND_MODE_SILENT: crash silently when the app is in background,
* The default is BackgroundMode.BACKGROUND_MODE_SHOW_CUSTOM (the app will be brought to front when a crash occurs).
*/
@NonNull
public Builder backgroundMode(@BackgroundMode int backgroundMode) {
config.backgroundMode = backgroundMode;
return this;
}
/**
* Defines if CustomActivityOnCrash crash interception mechanism is enabled.
* Set it to true if you want CustomActivityOnCrash to intercept crashes,
* false if you want them to be treated as if the library was not installed.
* The default is true.
*/
@NonNull
public Builder enabled(boolean enabled) {
config.enabled = enabled;
return this;
}
/**
* Defines if the error activity must shown the error details button.
* Set it to true if you want to show the full stack trace and device info,
* false if you want it to be hidden.
* The default is true.
*/
@NonNull
public Builder showErrorDetails(boolean showErrorDetails) {
config.showErrorDetails = showErrorDetails;
return this;
}
/**
* Defines if the error activity should show a restart button.
* Set it to true if you want to show a restart button,
* false if you want to show a close button.
* Note that even if restart is enabled but you app does not have any launcher activities,
* a close button will still be used by the default error activity.
* The default is true.
*/
@NonNull
public Builder showRestartButton(boolean showRestartButton) {
config.showRestartButton = showRestartButton;
return this;
}
/**
* Defines if the stack trace must be logged again once the custom activity is shown.
* Set it to true if you want to log the stack trace again,
* false if you don't want the extra logging.
* This option exists because the default Android Studio logcat view only shows the output
* of the current process, and since the error activity runs on a new process,
* you can't see the previous output easily.
* Internally, it's logged when getConfigFromIntent() is called.
* The default is true.
*/
@NonNull
public Builder logErrorOnRestart(boolean logErrorOnRestart) {
config.logErrorOnRestart = logErrorOnRestart;
return this;
}
/**
* Defines if the activities visited by the user should be tracked
* so they are reported when an error occurs.
* The default is false.
*/
@NonNull
public Builder trackActivities(boolean trackActivities) {
config.trackActivities = trackActivities;
return this;
}
/**
* Defines the time that must pass between app crashes to determine that we are not
* in a crash loop. If a crash has occurred less that this time ago,
* the error activity will not be launched and the system crash screen will be invoked.
* The default is 3000.
*/
@NonNull
public Builder minTimeBetweenCrashesMs(int minTimeBetweenCrashesMs) {
config.minTimeBetweenCrashesMs = minTimeBetweenCrashesMs;
return this;
}
/**
* Defines which drawable to use in the default error activity image.
* Set this if you want to use an image other than the default one.
* The default is R.drawable.customactivityoncrash_error_image (a cute upside-down bug).
*/
@NonNull
public Builder errorDrawable(@Nullable @DrawableRes Integer errorDrawable) {
config.errorDrawable = errorDrawable;
return this;
}
/**
* Sets the error activity class to launch when a crash occurs.
* If null, the default error activity will be used.
*/
@NonNull
public Builder errorActivity(@Nullable Class<? extends Activity> errorActivityClass) {
config.errorActivityClass = errorActivityClass;
return this;
}
/**
* Sets the main activity class that the error activity must launch when a crash occurs.
* If not set or set to null, the default launch activity will be used.
* If your app has no launch activities and this is not set, the default error activity will close instead.
*/
@NonNull
public Builder restartActivity(@Nullable Class<? extends Activity> restartActivityClass) {
config.restartActivityClass = restartActivityClass;
return this;
}
/**
* Sets an event listener to be called when events occur, so they can be reported
* by the app as, for example, Google Analytics events.
* If not set or set to null, no events will be reported.
*
* @param eventListener The event listener.
* @throws IllegalArgumentException if the eventListener is an inner or anonymous class
*/
@NonNull
public Builder eventListener(@Nullable CustomActivityOnCrash.EventListener eventListener) {
if (eventListener != null && eventListener.getClass().getEnclosingClass() != null && !Modifier.isStatic(eventListener.getClass().getModifiers())) {
throw new IllegalArgumentException("The event listener cannot be an inner or anonymous class, because it will need to be serialized. Change it to a class of its own, or make it a static inner class.");
} else {
config.eventListener = eventListener;
}
return this;
}
@NonNull
public CaocConfig get() {
return config;
}
public void apply() {
CustomActivityOnCrash.setConfig(config);
}
}
}
package com.qmai.rebootoncrash.provider
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import com.qmai.rebootoncrash.CustomActivityOnCrash
/**
*
* @author: tongzi
* @date: 2021/12/28
*
*/
class CaocInitProvider : ContentProvider() {
override fun onCreate(): Boolean {
CustomActivityOnCrash.install(context)
return false
}
override fun query(
uri: Uri,
projection: Array<String?>?,
selection: String?,
selectionArgs: Array<String?>?,
sortOrder: String?
): Cursor? {
return null
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String?>?): Int {
return 0
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String?>?
): Int {
return 0
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="UselessParent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="@dimen/customactivityoncrash_activity_vertical_margin"
android:paddingLeft="@dimen/customactivityoncrash_activity_horizontal_margin"
android:paddingRight="@dimen/customactivityoncrash_activity_horizontal_margin"
android:paddingTop="@dimen/customactivityoncrash_activity_vertical_margin">
<ImageView
android:id="@+id/customactivityoncrash_error_activity_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/customactivityoncrash_activity_vertical_margin"
android:gravity="center"
android:text="@string/customactivityoncrash_error_activity_error_occurred_explanation"
android:textSize="18sp"
android:textStyle="bold" />
<Button
android:id="@+id/customactivityoncrash_error_activity_restart_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/customactivityoncrash_activity_vertical_margin"
android:text="@string/customactivityoncrash_error_activity_close_app" />
<Button
android:id="@+id/customactivityoncrash_error_activity_more_info_button"
style="?borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/customactivityoncrash_error_activity_error_details"
android:textColor="?colorPrimary" />
</LinearLayout>
</ScrollView>
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="customactivityoncrash_activity_horizontal_margin">16dp</dimen>
<dimen name="customactivityoncrash_activity_vertical_margin">16dp</dimen>
<dimen name="customactivityoncrash_error_activity_error_details_text_size">12sp</dimen>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="customactivityoncrash_error_activity_error_occurred_explanation">出小差啦~</string>
<string name="customactivityoncrash_error_activity_restart_app">重启 app</string>
<string name="customactivityoncrash_error_activity_close_app">关闭 app</string>
<string name="customactivityoncrash_error_activity_error_details">错误详情</string>
<string name="customactivityoncrash_error_activity_error_details_title">错误详情</string>
<string name="customactivityoncrash_error_activity_error_details_close">关闭</string>
<string name="customactivityoncrash_error_activity_error_details_copy">复制到粘贴板</string>
<string name="customactivityoncrash_error_activity_error_details_copied">已复制到粘贴板</string>
<string name="customactivityoncrash_error_activity_error_details_clipboard_label">错误详情</string>
</resources>
package com.qmai.rebootoncrash
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
\ No newline at end of file
...@@ -7,3 +7,4 @@ include ':wheelview' ...@@ -7,3 +7,4 @@ include ':wheelview'
include ':pickerview' include ':pickerview'
include ':dialog' include ':dialog'
include ':websocket' include ':websocket'
include ':rebootoncrash'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment