diff --git a/README.md b/README.md index 64db861..8edc455 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,22 @@ -# CrashX +![](https://github.com/TutorialsAndroid/crashx/blob/master/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png) + +# CrashX [![API](https://img.shields.io/badge/API-15%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=15) [![Known Vulnerabilities](https://snyk.io/test/github/TutorialsAndroid/CrashX/badge.svg?targetFile=library%2Fbuild.gradle)](https://snyk.io/test/github/TutorialsAndroid/CrashX?targetFile=library%2Fbuild.gradle) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-CrashX-red.svg?style=flat-square)](https://android-arsenal.com/details/1/7581) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/TutorialsAndroid/crashx) This library allows launching a crash activity when the app crashes, instead of showing the hated "Unfortunately, X has stopped" dialog. -[![API](https://img.shields.io/badge/API-15%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=15) +**Library Availbale at JitPack.io** + +[![](https://jitpack.io/v/TutorialsAndroid/crashx.svg)](https://jitpack.io/#TutorialsAndroid/crashx) -[![Known Vulnerabilities](https://snyk.io/test/github/TutorialsAndroid/CrashX/badge.svg?targetFile=library%2Fbuild.gradle)](https://snyk.io/test/github/TutorialsAndroid/CrashX?targetFile=library%2Fbuild.gradle) -**Library Availbale at JitPack.io** +## And Don't Forget To Follow Me On Instagram -[![](https://jitpack.io/v/TutorialsAndroid/CrashX.svg)](https://jitpack.io/#TutorialsAndroid/CrashX) +

Follow me on instagram to stay up-to-date https://instagram.com/a.masram444 + **Sample Screen** -![](https://github.com/TutorialsAndroid/CrashX/blob/master/images/device-2019-03-19-154405.png) +![](https://github.com/TutorialsAndroid/crashx/blob/master/images/device-2019-03-19-154405.png) ## How to use @@ -30,7 +34,7 @@ Add it in your root build.gradle at the end of repositories: Step 2. Add the dependency dependencies { - implementation 'com.github.TutorialsAndroid:CrashX:v0.1' + implementation 'com.github.TutorialsAndroid:crashx:v6.0.19' } ...and you are done! @@ -148,7 +152,7 @@ restartActivity(Class); ``` > This method sets the activity that must be launched by the error activity when the user presses the button to restart the app. > If you don't set it (or set it to null), the library will use the first activity on your manifest that has an intent-filter with action -> `com.kinda.crash.RESTART`, and if there is none, the default launchable activity on your app. +> `com.developer.crashx.RESTART`, and if there is none, the default launchable activity on your app. > If no launchable activity can be found and you didn't specify any, the "restart app" button will become a "close app" button, > even if `showRestartButton` is set to `true`. > @@ -156,7 +160,7 @@ restartActivity(Class); > ```xml > > -> +> > > ``` @@ -166,13 +170,13 @@ errorActivity(Class); > This method allows you to set a custom error activity to be launched, instead of the default one. > Use it if you need further customization that is not just strings, colors or themes (see below). > If you don't set it (or set it to null), the library will use the first activity on your manifest that has an intent-filter with action -> `com.kinda.crash.ERROR`, and if there is none, a default error activity from the library. +> `com.developer.crashx.ERROR`, and if there is none, a default error activity from the library. > If you use this, the activity **must** be declared in your `AndroidManifest.xml`, with `process` set to `:error_activity`. > > Example: > ```xml > android:name="com.kinda.crash.sample.CustomErrorActivity" +> android:name="com.developer.crashx.sample.CustomErrorActivity" > android:label="@string/error_title" > android:process=":error_activity" /> > ``` @@ -181,7 +185,7 @@ errorActivity(Class); > ```xml > > -> +> > > ``` @@ -204,7 +208,7 @@ If you want to specify a specific theme only for the error activity, you can do ```xml ``` diff --git a/build.gradle b/build.gradle index 5c2f118..000c19f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +1,11 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - buildscript { repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.android.tools.build:gradle:4.0.1' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files } } @@ -18,6 +13,10 @@ allprojects { repositories { google() jcenter() + mavenCentral() + maven { + url 'https://jitpack.io' + } } } diff --git a/gradle.properties b/gradle.properties index 5d08ba7..21ee4a5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,6 @@ # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +# org.gradle.parallel=true +android.enableJetifier=true +android.useAndroidX=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 905d23b..3cece4b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Nov 13 17:16:27 CET 2017 +#Sat Aug 29 18:42:59 IST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/images/device-2019-03-19-154405.png b/images/device-2019-03-19-154405.png index b7431b1..d6bac46 100644 Binary files a/images/device-2019-03-19-154405.png and b/images/device-2019-03-19-154405.png differ diff --git a/images/device-2019-03-19-154421.png b/images/device-2019-03-19-154421.png deleted file mode 100644 index b6cc52d..0000000 Binary files a/images/device-2019-03-19-154421.png and /dev/null differ diff --git a/library/build.gradle b/library/build.gradle index 47b01f8..7732a5b 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -4,14 +4,14 @@ apply plugin: 'com.github.dcendents.android-maven' group='com.github.TutorialsAndroid' android { - compileSdkVersion 28 + compileSdkVersion 29 resourcePrefix 'crash_' defaultConfig { minSdkVersion 15 - targetSdkVersion 28 - versionCode 1 - versionName "1.0-SNAPSHOT" + targetSdkVersion 29 + versionCode 8 + versionName "5.0.19" } buildTypes { release { @@ -19,10 +19,20 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - - implementation 'com.android.support:appcompat-v7:28.0.0' + + //ANDROIDX JETPACK LIBRARIES + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'com.google.android.material:material:1.2.0' + implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.1' } \ No newline at end of file diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index ec8921c..e34625e 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1,13 +1,13 @@ + package="com.developer.crashx"> diff --git a/library/src/main/java/com/kinda/crash/CrashActivity.java b/library/src/main/java/com/developer/crashx/CrashActivity.java similarity index 54% rename from library/src/main/java/com/kinda/crash/CrashActivity.java rename to library/src/main/java/com/developer/crashx/CrashActivity.java index dfab696..bf668eb 100644 --- a/library/src/main/java/com/kinda/crash/CrashActivity.java +++ b/library/src/main/java/com/developer/crashx/CrashActivity.java @@ -1,4 +1,4 @@ -package com.kinda.crash; +package com.developer.crashx; import android.annotation.SuppressLint; import android.app.Activity; @@ -11,11 +11,10 @@ import android.content.pm.ResolveInfo; import android.os.Build; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.RestrictTo; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import android.util.Log; - import java.io.PrintWriter; import java.io.Serializable; import java.io.StringWriter; @@ -27,25 +26,28 @@ import java.util.Deque; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import com.developer.crashx.activity.DefaultErrorActivity; +import com.developer.crashx.config.CrashConfig; -import com.kinda.crash.activity.DefaultErrorActivity; -import com.kinda.crash.config.CrashConfig; - +/** + * @author tkdco , TutorialsAndroid + */ public final class CrashActivity { private final static String TAG = "CrashActivity"; //Extras passed to the error activity - private static final String EXTRA_CONFIG = "com.kinda.crash.EXTRA_CONFIG"; - private static final String EXTRA_STACK_TRACE = "com.kinda.crash.EXTRA_STACK_TRACE"; - private static final String EXTRA_ACTIVITY_LOG = "com.kinda.crash.EXTRA_ACTIVITY_LOG"; + private static final String EXTRA_CONFIG = "com.developer.crashx.EXTRA_CONFIG"; + private static final String EXTRA_STACK_TRACE = "com.developer.crashx.EXTRA_STACK_TRACE"; + private static final String EXTRA_ACTIVITY_LOG = "com.developer.crashx.EXTRA_ACTIVITY_LOG"; //General constants - private static final String INTENT_ACTION_ERROR_ACTIVITY = "com.kinda.crash.ERROR"; - private static final String INTENT_ACTION_RESTART_ACTIVITY = "com.kinda.crash.RESTART"; - private static final String CRASH_HANDLER_PACKAGE_NAME = "com.kinda.crash."; + private static final String INTENT_ACTION_ERROR_ACTIVITY = "com.developer.crashx.ERROR"; + private static final String INTENT_ACTION_RESTART_ACTIVITY = "com.developer.crashx.RESTART"; + private static final String CRASH_HANDLER_PACKAGE_NAME = "com.developer.crashx."; private static final String DEFAULT_HANDLER_PACKAGE_NAME = "com.android.internal.os"; private static final int MAX_STACK_TRACE_SIZE = 131071; //128 KB - 1 private static final int MAX_ACTIVITIES_IN_LOG = 50; @@ -55,19 +57,12 @@ public final class CrashActivity { 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 CrashConfig config = new CrashConfig(); private static final Deque activityLog = new ArrayDeque<>(MAX_ACTIVITIES_IN_LOG); private static WeakReference lastActivityCreated = new WeakReference<>(null); private static boolean isInBackground = true; - - /** - * Installs CrashActivity 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 { @@ -86,92 +81,78 @@ public static void install(@Nullable final Context context) { 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(Thread thread, final Throwable throwable) { - if (config.isEnabled()) { - Log.e(TAG, "App has crashed, executing CrashActivity's UncaughtExceptionHandler", throwable); + Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { + if (config.isEnabled()) { + Log.e(TAG, "App has crashed, executing CrashActivity'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 errorActivityClass = config.getErrorActivityClass(); - 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 (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 { - setLastCrashTimestamp(application, new Date().getTime()); + } else if (config.getBackgroundMode() == CrashConfig.BACKGROUND_MODE_SHOW_CUSTOM || !isInBackground) { - Class errorActivityClass = config.getErrorActivityClass(); + final Intent intent = new Intent(application, errorActivityClass); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + String stackTraceString = sw.toString(); - if (errorActivityClass == null) { - errorActivityClass = guessErrorActivityClass(application); + 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 (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() == CrashConfig.BACKGROUND_MODE_SHOW_CUSTOM || !isInBackground) { - - 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.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)); - } + if (config.isShowRestartButton() && config.getRestartActivityClass() == null) { + 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() == CrashConfig.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 + 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() == CrashConfig.BACKGROUND_MODE_CRASH) { + if (oldHandler != null) { + oldHandler.uncaughtException(thread, throwable); + return; } - //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); } + final Activity lastActivity = lastActivityCreated.get(); + if (lastActivity != null) { + lastActivity.finish(); + lastActivityCreated.clear(); + } + killCurrentProcess(); + } else if (oldHandler != null) { + oldHandler.uncaughtException(thread, throwable); } }); application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { @@ -181,10 +162,6 @@ public void uncaughtException(Thread thread, final Throwable throwable) { @Override public void onActivityCreated(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); } if (config.isTrackActivities()) { @@ -241,26 +218,14 @@ public void onActivityDestroyed(Activity activity) { } } - /** - * 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(CrashActivity.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. - */ public static CrashConfig getConfigFromIntent(@NonNull Intent intent) { CrashConfig config = (CrashConfig) intent.getSerializableExtra(CrashActivity.EXTRA_CONFIG); - if (config.isLogErrorOnRestart()) { + if (Objects.requireNonNull(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)); @@ -270,35 +235,19 @@ public static CrashConfig getConfigFromIntent(@NonNull Intent 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 private static String getActivityLogFromIntent(@NonNull Intent intent) { return intent.getStringExtra(CrashActivity.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 = ""; @@ -308,9 +257,6 @@ public static String getAllErrorDetailsFromIntent(@NonNull Context context, @Non 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 \n"; errorDetails += "Stack trace: \n"; errorDetails += getStackTraceFromIntent(intent); @@ -324,17 +270,6 @@ public static String getAllErrorDetailsFromIntent(@NonNull Context context, @Non 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. - */ private static void restartApplicationWithIntent(@NonNull Activity activity, @NonNull Intent intent, @NonNull CrashConfig config) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); if (intent.getComponent() != null) { @@ -359,14 +294,6 @@ public static void restartApplication(@NonNull Activity activity, @NonNull Crash 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 CrashConfig config) { if (config.getEventListener() != null) { config.getEventListener().onCloseAppFromErrorActivity(); @@ -375,40 +302,17 @@ public static void closeApplication(@NonNull Activity activity, @NonNull CrashCo 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 CrashConfig.Builder.get(); - * - * @return the current configuration - */ @RestrictTo(RestrictTo.Scope.LIBRARY) @NonNull public static CrashConfig getConfig() { return config; } - /** - * INTERNAL method that sets the configuration of the library. - * You must not use this, use CrashConfig.Builder.apply() - * - * @param config the configuration to use - */ @RestrictTo(RestrictTo.Scope.LIBRARY) public static void setConfig(@NonNull CrashConfig config) { CrashActivity.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 error activity has crashed (activityClass is in the stack) - * - * @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 activityClass) { do { StackTraceElement[] stackTrace = throwable.getStackTrace(); @@ -421,13 +325,6 @@ private static boolean isStackTraceLikelyConflictive(@NonNull Throwable throwabl 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; @@ -452,12 +349,6 @@ private static String getBuildDateAsString(@NonNull Context context, @NonNull Da } } - /** - * 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 { @@ -468,12 +359,6 @@ private static String getVersionName(Context context) { } } - /** - * 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; @@ -485,12 +370,6 @@ private static String getDeviceModelName() { } } - /** - * 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) { @@ -504,23 +383,11 @@ private static String capitalize(@Nullable String s) { } } - /** - * 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 , - * 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 guessRestartActivityClass(@NonNull Context context) { Class resolvedActivityClass; - - //If action is defined, use that resolvedActivityClass = getRestartActivityClassWithIntentFilter(context); - //Else, get the default launcher activity if (resolvedActivityClass == null) { resolvedActivityClass = getLauncherActivity(context); } @@ -528,14 +395,6 @@ private static Class guessRestartActivityClass(@NonNull Cont return resolvedActivityClass; } - /** - * INTERNAL method used to get the first activity with an intent-filter , - * 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 getRestartActivityClassWithIntentFilter(@NonNull Context context) { Intent searchedIntent = new Intent().setAction(INTENT_ACTION_RESTART_ACTIVITY).setPackage(context.getPackageName()); @@ -555,14 +414,6 @@ private static Class getRestartActivityClassWithIntentFilter 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 getLauncherActivity(@NonNull Context context) { Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); @@ -578,22 +429,10 @@ private static Class getLauncherActivity(@NonNull Context co 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 , - * 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 guessErrorActivityClass(@NonNull Context context) { Class resolvedActivityClass; - - //If action is defined, use that resolvedActivityClass = getErrorActivityClassWithIntentFilter(context); - - //Else, get the default error activity if (resolvedActivityClass == null) { resolvedActivityClass = DefaultErrorActivity.class; } @@ -601,14 +440,6 @@ private static Class guessErrorActivityClass(@NonNull Contex return resolvedActivityClass; } - /** - * INTERNAL method used to get the first activity with an intent-filter , - * 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 getErrorActivityClassWithIntentFilter(@NonNull Context context) { Intent searchedIntent = new Intent().setAction(INTENT_ACTION_ERROR_ACTIVITY).setPackage(context.getPackageName()); @@ -620,7 +451,6 @@ private static Class getErrorActivityClassWithIntentFilter(@ try { return (Class) 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); } } @@ -628,40 +458,20 @@ private static Class getErrorActivityClassWithIntentFilter(@ 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(); @@ -669,10 +479,6 @@ private static boolean hasCrashedInTheLastSeconds(@NonNull Context context) { 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(); diff --git a/library/src/main/java/com/developer/crashx/activity/DefaultErrorActivity.java b/library/src/main/java/com/developer/crashx/activity/DefaultErrorActivity.java new file mode 100644 index 0000000..14d7ef8 --- /dev/null +++ b/library/src/main/java/com/developer/crashx/activity/DefaultErrorActivity.java @@ -0,0 +1,105 @@ +package com.developer.crashx.activity; + +import android.annotation.SuppressLint; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.res.TypedArray; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.core.content.res.ResourcesCompat; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import android.util.TypedValue; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.developer.crashx.CrashActivity; +import com.developer.crashx.R; +import com.developer.crashx.config.CrashConfig; + +/** + * @author tkdco , TutorialsAndroid + */ +public final class DefaultErrorActivity extends AppCompatActivity { + + @SuppressLint("PrivateResource") + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + + setTheme(R.style.Theme_AppCompat_Light_NoActionBar); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Window w = getWindow(); + w.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + + super.onCreate(savedInstanceState); + + TypedArray a = obtainStyledAttributes(R.styleable.AppCompatTheme); + if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) { + setTheme(R.style.Theme_AppCompat_Light_DarkActionBar); + } + a.recycle(); + + setContentView(R.layout.crash_default_error_activity); + Button restartButton = findViewById(R.id.crash_error_activity_restart_button); + + final CrashConfig config = CrashActivity.getConfigFromIntent(getIntent()); + + if (config == null) { + finish(); + return; + } + + if (config.isShowRestartButton() && config.getRestartActivityClass() != null) { + restartButton.setText(R.string.customactivityoncrash_error_activity_restart_app); + restartButton.setOnClickListener(v -> CrashActivity.restartApplication(DefaultErrorActivity.this, config)); + } else { + restartButton.setOnClickListener(v -> CrashActivity.closeApplication(DefaultErrorActivity.this, config)); + } + + Button moreInfoButton = findViewById(R.id.crash_error_activity_more_info_button); + + if (config.isShowErrorDetails()) { + moreInfoButton.setOnClickListener(v -> { + AlertDialog dialog = new AlertDialog.Builder(DefaultErrorActivity.this) + .setTitle(R.string.customactivityoncrash_error_activity_error_details_title) + .setMessage(CrashActivity.getAllErrorDetailsFromIntent(DefaultErrorActivity.this, getIntent())) + .setPositiveButton(R.string.customactivityoncrash_error_activity_error_details_close, null) + .setNeutralButton(R.string.customactivityoncrash_error_activity_error_details_copy, + (dialog1, which) -> copyErrorToClipboard()) + .show(); + TextView textView = dialog.findViewById(android.R.id.message); + if (textView != null) { + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.customactivityoncrash_error_activity_error_details_text_size)); + } + }); + } else { + moreInfoButton.setVisibility(View.GONE); + } + + Integer defaultErrorActivityDrawableId = config.getErrorDrawable(); + ImageView errorImageView = findViewById(R.id.crash_error_activity_image); + + if (defaultErrorActivityDrawableId != null) { + errorImageView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), defaultErrorActivityDrawableId, getTheme())); + } + } + + private void copyErrorToClipboard() { + String errorInformation = CrashActivity.getAllErrorDetailsFromIntent(DefaultErrorActivity.this, getIntent()); + + ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + if (clipboard != null) { + ClipData clip = ClipData.newPlainText(getString(R.string.customactivityoncrash_error_activity_error_details_clipboard_label), errorInformation); + clipboard.setPrimaryClip(clip); + Toast.makeText(DefaultErrorActivity.this, R.string.customactivityoncrash_error_activity_error_details_copied, Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/library/src/main/java/com/kinda/crash/config/CrashConfig.java b/library/src/main/java/com/developer/crashx/config/CrashConfig.java similarity index 64% rename from library/src/main/java/com/kinda/crash/config/CrashConfig.java rename to library/src/main/java/com/developer/crashx/config/CrashConfig.java index cc506c1..273dd9b 100644 --- a/library/src/main/java/com/kinda/crash/config/CrashConfig.java +++ b/library/src/main/java/com/developer/crashx/config/CrashConfig.java @@ -1,25 +1,26 @@ -package com.kinda.crash.config; +package com.developer.crashx.config; import android.app.Activity; -import android.support.annotation.DrawableRes; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.DrawableRes; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.io.Serializable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Modifier; -import com.kinda.crash.CrashActivity; +import com.developer.crashx.CrashActivity; +/** + * @author tkdco , TutorialsAndroid + */ public class CrashConfig implements Serializable { @IntDef({BACKGROUND_MODE_CRASH, BACKGROUND_MODE_SHOW_CUSTOM, BACKGROUND_MODE_SILENT}) @Retention(RetentionPolicy.SOURCE) - private @interface BackgroundMode { - //I hate empty blocks - } + private @interface BackgroundMode { } static final int BACKGROUND_MODE_SILENT = 0; public static final int BACKGROUND_MODE_SHOW_CUSTOM = 1; @@ -157,136 +158,66 @@ public static Builder create() { 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 CrashActivity crash interception mechanism is enabled. - * Set it to true if you want CrashActivity 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 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 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 CrashActivity.EventListener eventListener) { if (eventListener != null && eventListener.getClass().getEnclosingClass() != null && !Modifier.isStatic(eventListener.getClass().getModifiers())) { diff --git a/library/src/main/java/com/kinda/crash/provider/CrashInitProvider.java b/library/src/main/java/com/developer/crashx/provider/CrashInitProvider.java similarity index 84% rename from library/src/main/java/com/kinda/crash/provider/CrashInitProvider.java rename to library/src/main/java/com/developer/crashx/provider/CrashInitProvider.java index a97d3e3..313c158 100644 --- a/library/src/main/java/com/kinda/crash/provider/CrashInitProvider.java +++ b/library/src/main/java/com/developer/crashx/provider/CrashInitProvider.java @@ -1,14 +1,17 @@ -package com.kinda.crash.provider; +package com.developer.crashx.provider; import android.content.ContentProvider; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; -import com.kinda.crash.CrashActivity; +import com.developer.crashx.CrashActivity; +/** + * @author tkdco , TutorialsAndroid + */ public class CrashInitProvider extends ContentProvider { public boolean onCreate() { diff --git a/library/src/main/java/com/kinda/crash/activity/DefaultErrorActivity.java b/library/src/main/java/com/kinda/crash/activity/DefaultErrorActivity.java deleted file mode 100644 index 5ff0d3c..0000000 --- a/library/src/main/java/com/kinda/crash/activity/DefaultErrorActivity.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.kinda.crash.activity; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.DialogInterface; -import android.content.res.TypedArray; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.content.res.ResourcesCompat; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -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 com.kinda.crash.CrashActivity; -import com.kinda.crash.R; -import com.kinda.crash.config.CrashConfig; - -public final class DefaultErrorActivity extends AppCompatActivity { - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - //This is needed to avoid a crash if the developer has not specified - //an app-level theme that extends Theme.AppCompat - TypedArray a = obtainStyledAttributes(R.styleable.AppCompatTheme); - if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) { - setTheme(R.style.Theme_AppCompat_Light_DarkActionBar); - } - a.recycle(); - - setContentView(R.layout.crash_default_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. - Button restartButton = findViewById(R.id.crash_error_activity_restart_button); - - final CrashConfig config = CrashActivity.getConfigFromIntent(getIntent()); - - if (config == null) { - //This should never happen - Just finish the activity to avoid a recursive crash. - finish(); - return; - } - - if (config.isShowRestartButton() && config.getRestartActivityClass() != null) { - restartButton.setText(R.string.customactivityoncrash_error_activity_restart_app); - restartButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - CrashActivity.restartApplication(DefaultErrorActivity.this, config); - } - }); - } else { - restartButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - CrashActivity.closeApplication(DefaultErrorActivity.this, config); - } - }); - } - - Button moreInfoButton = findViewById(R.id.crash_error_activity_more_info_button); - - if (config.isShowErrorDetails()) { - moreInfoButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - //We retrieve all the error data and show it - - AlertDialog dialog = new AlertDialog.Builder(DefaultErrorActivity.this) - .setTitle(R.string.customactivityoncrash_error_activity_error_details_title) - .setMessage(CrashActivity.getAllErrorDetailsFromIntent(DefaultErrorActivity.this, getIntent())) - .setPositiveButton(R.string.customactivityoncrash_error_activity_error_details_close, null) - .setNeutralButton(R.string.customactivityoncrash_error_activity_error_details_copy, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - copyErrorToClipboard(); - } - }) - .show(); - TextView textView = dialog.findViewById(android.R.id.message); - if (textView != null) { - textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.customactivityoncrash_error_activity_error_details_text_size)); - } - } - }); - } else { - moreInfoButton.setVisibility(View.GONE); - } - - Integer defaultErrorActivityDrawableId = config.getErrorDrawable(); - ImageView errorImageView = findViewById(R.id.crash_error_activity_image); - - if (defaultErrorActivityDrawableId != null) { - errorImageView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), defaultErrorActivityDrawableId, getTheme())); - } - } - - private void copyErrorToClipboard() { - String errorInformation = CrashActivity.getAllErrorDetailsFromIntent(DefaultErrorActivity.this, getIntent()); - - ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - - //Are there any devices without clipboard...? - if (clipboard != null) { - ClipData clip = ClipData.newPlainText(getString(R.string.customactivityoncrash_error_activity_error_details_clipboard_label), errorInformation); - clipboard.setPrimaryClip(clip); - Toast.makeText(DefaultErrorActivity.this, R.string.customactivityoncrash_error_activity_error_details_copied, Toast.LENGTH_SHORT).show(); - } - } -} diff --git a/library/src/main/res/drawable-hdpi/buggy.png b/library/src/main/res/drawable-hdpi/buggy.png deleted file mode 100644 index f2b4958..0000000 Binary files a/library/src/main/res/drawable-hdpi/buggy.png and /dev/null differ diff --git a/library/src/main/res/drawable/crash_ic_bug_report.xml b/library/src/main/res/drawable/crash_ic_bug_report.xml new file mode 100644 index 0000000..37292e0 --- /dev/null +++ b/library/src/main/res/drawable/crash_ic_bug_report.xml @@ -0,0 +1,5 @@ + + + diff --git a/library/src/main/res/drawable/crash_ic_close.xml b/library/src/main/res/drawable/crash_ic_close.xml new file mode 100644 index 0000000..0c8775c --- /dev/null +++ b/library/src/main/res/drawable/crash_ic_close.xml @@ -0,0 +1,5 @@ + + + diff --git a/library/src/main/res/drawable/crash_ic_error.xml b/library/src/main/res/drawable/crash_ic_error.xml new file mode 100644 index 0000000..93db179 --- /dev/null +++ b/library/src/main/res/drawable/crash_ic_error.xml @@ -0,0 +1,5 @@ + + + diff --git a/library/src/main/res/layout/crash_default_error_activity.xml b/library/src/main/res/layout/crash_default_error_activity.xml index 107c11f..84ecc8d 100644 --- a/library/src/main/res/layout/crash_default_error_activity.xml +++ b/library/src/main/res/layout/crash_default_error_activity.xml @@ -1,31 +1,37 @@ + android:background="#D50000"> - + android:layout_centerInParent="true" + app:cardBackgroundColor="#FFFFFF" + app:cardCornerRadius="30dp" + app:cardElevation="0dp" + app:cardPreventCornerOverlap="true" + app:cardUseCompatPadding="true"> + android:paddingBottom="@dimen/customactivityoncrash_activity_vertical_margin"> + android:scaleType="fitXY" + android:src="@drawable/crash_ic_bug_report" /> + android:textColor="@android:color/white" + android:textStyle="bold" + android:background="#D50000" + android:text="@string/customactivityoncrash_error_activity_close_app" + android:drawableBottom="@drawable/crash_ic_close" />