Reorganize and group modules
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,45 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
namespace = "de.mm20.launcher2.crashreporter"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.materialcomponents.core)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
|
||||
|
||||
implementation(project(":core:base"))
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.
|
||||
#
|
||||
# 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
|
||||
@@ -0,0 +1,11 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<application>
|
||||
<provider
|
||||
android:name="com.balsikandar.crashreporter.CrashReporterInitProvider"
|
||||
android:authorities="${applicationId}.CrashReporterInitProvider"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.balsikandar.crashreporter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import com.balsikandar.crashreporter.utils.CrashReporterNotInitializedException;
|
||||
import com.balsikandar.crashreporter.utils.CrashReporterExceptionHandler;
|
||||
import com.balsikandar.crashreporter.utils.CrashUtil;
|
||||
|
||||
public class CrashReporter {
|
||||
|
||||
private static Context applicationContext;
|
||||
|
||||
private static String crashReportPath;
|
||||
|
||||
private static boolean isNotificationEnabled = true;
|
||||
|
||||
private CrashReporter() {
|
||||
// This class in not publicly instantiable
|
||||
}
|
||||
|
||||
public static void initialize(Context context) {
|
||||
applicationContext = context;
|
||||
setUpExceptionHandler();
|
||||
}
|
||||
|
||||
public static void initialize(Context context, String crashReportSavePath) {
|
||||
applicationContext = context;
|
||||
crashReportPath = crashReportSavePath;
|
||||
setUpExceptionHandler();
|
||||
}
|
||||
|
||||
private static void setUpExceptionHandler() {
|
||||
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashReporterExceptionHandler)) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(new CrashReporterExceptionHandler());
|
||||
}
|
||||
}
|
||||
|
||||
public static Context getContext() {
|
||||
if (applicationContext == null) {
|
||||
try {
|
||||
throw new CrashReporterNotInitializedException("Initialize CrashReporter : call CrashReporter.initialize(context, crashReportPath)");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return applicationContext;
|
||||
}
|
||||
|
||||
public static String getCrashReportPath() {
|
||||
return crashReportPath;
|
||||
}
|
||||
|
||||
public static boolean isNotificationEnabled() {
|
||||
return isNotificationEnabled;
|
||||
}
|
||||
|
||||
//LOG Exception APIs
|
||||
public static void logException(Exception exception) {
|
||||
CrashUtil.logException(exception);
|
||||
}
|
||||
|
||||
public static void disableNotification() {
|
||||
isNotificationEnabled = false;
|
||||
}
|
||||
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.balsikandar.crashreporter;
|
||||
|
||||
/**
|
||||
* Created by bali on 02/08/17.
|
||||
*/
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ProviderInfo;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* Created by amitshekhar on 16/11/16.
|
||||
*/
|
||||
|
||||
public class CrashReporterInitProvider extends ContentProvider {
|
||||
|
||||
|
||||
public CrashReporterInitProvider() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
CrashReporter.initialize(getContext());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachInfo(Context context, ProviderInfo providerInfo) {
|
||||
if (providerInfo == null) {
|
||||
throw new NullPointerException("CrashReporterInitProvider ProviderInfo cannot be null.");
|
||||
}
|
||||
// So if the authorities equal the library internal ones, the developer forgot to set his applicationId
|
||||
if ("com.balsikandar.crashreporter.CrashReporterInitProvider".equals(providerInfo.authority)) {
|
||||
throw new IllegalStateException("Incorrect provider authority in manifest. Most likely due to a "
|
||||
+ "missing applicationId variable in application\'s build.gradle.kts.kts.");
|
||||
}
|
||||
super.attachInfo(context, providerInfo);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.balsikandar.crashreporter.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.TimeZone;
|
||||
|
||||
import static com.balsikandar.crashreporter.utils.AppUtilsKt.getAppSignature;
|
||||
|
||||
/**
|
||||
* Created by bali on 12/08/17.
|
||||
*/
|
||||
|
||||
public class AppUtils {
|
||||
private static String getCurrentLauncherApp(Context context) {
|
||||
String str = "";
|
||||
PackageManager localPackageManager = context.getPackageManager();
|
||||
Intent intent = new Intent("android.intent.action.MAIN");
|
||||
intent.addCategory("android.intent.category.HOME");
|
||||
try {
|
||||
ResolveInfo resolveInfo = localPackageManager.resolveActivity(intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY);
|
||||
if (resolveInfo != null && resolveInfo.activityInfo != null) {
|
||||
str = resolveInfo.activityInfo.packageName;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("AppUtils", "Exception : " + e.getMessage());
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
public static String getDeviceDetails(Context context) {
|
||||
|
||||
return "APP.VERSION : " + getAppVersion(context)
|
||||
+ "\nAPP.VERSIONCODE : " + getAppVersionCode(context)
|
||||
+ "\nAPP.SIGNATURE : " + getAppSignature(context)
|
||||
+ "\nLAUNCHER.APP : " + getCurrentLauncherApp(context)
|
||||
+ "\nTIMEZONE : " + timeZone()
|
||||
+ "\nVERSION.RELEASE : " + Build.VERSION.RELEASE
|
||||
+ "\nVERSION.INCREMENTAL : " + Build.VERSION.INCREMENTAL
|
||||
+ "\nVERSION.SDK.NUMBER : " + Build.VERSION.SDK_INT
|
||||
+ "\nBOARD : " + Build.BOARD
|
||||
+ "\nBOOTLOADER : " + Build.BOOTLOADER
|
||||
+ "\nBRAND : " + Build.BRAND
|
||||
+ "\nCPU_ABI : " + Build.CPU_ABI
|
||||
+ "\nCPU_ABI2 : " + Build.CPU_ABI2
|
||||
+ "\nDISPLAY : " + Build.DISPLAY
|
||||
+ "\nFINGERPRINT : " + Build.FINGERPRINT
|
||||
+ "\nHARDWARE : " + Build.HARDWARE
|
||||
+ "\nHOST : " + Build.HOST
|
||||
+ "\nID : " + Build.ID
|
||||
+ "\nMANUFACTURER : " + Build.MANUFACTURER
|
||||
+ "\nMODEL : " + Build.MODEL
|
||||
+ "\nPRODUCT : " + Build.PRODUCT
|
||||
+ "\nTAGS : " + Build.TAGS
|
||||
+ "\nTIME : " + Build.TIME
|
||||
+ "\nTYPE : " + Build.TYPE;
|
||||
}
|
||||
|
||||
private static String timeZone() {
|
||||
TimeZone tz = TimeZone.getDefault();
|
||||
return tz.getID();
|
||||
}
|
||||
|
||||
private static int getAppVersionCode(Context context) {
|
||||
try {
|
||||
PackageInfo packageInfo = context.getPackageManager()
|
||||
.getPackageInfo(context.getPackageName(), 0);
|
||||
return packageInfo.versionCode;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new RuntimeException("Could not get package name: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getAppVersion(Context context) {
|
||||
try {
|
||||
PackageInfo packageInfo = context.getPackageManager()
|
||||
.getPackageInfo(context.getPackageName(), 0);
|
||||
return packageInfo.versionName;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new RuntimeException("Could not get package name: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.balsikandar.crashreporter.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import java.security.MessageDigest
|
||||
|
||||
internal fun getAppSignature(context: Context): String {
|
||||
val signature = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val pi = context.packageManager.getPackageInfo(
|
||||
context.packageName,
|
||||
PackageManager.GET_SIGNING_CERTIFICATES
|
||||
)
|
||||
pi.signingInfo.apkContentsSigners.firstOrNull()
|
||||
} else {
|
||||
val pi = context.packageManager.getPackageInfo(
|
||||
context.packageName,
|
||||
PackageManager.GET_SIGNATURES
|
||||
)
|
||||
pi.signatures.firstOrNull()
|
||||
}
|
||||
return if (signature != null) {
|
||||
val digest = MessageDigest.getInstance("SHA")
|
||||
digest.update(signature.toByteArray())
|
||||
Base64.encodeToString(digest.digest(), Base64.NO_WRAP)
|
||||
} else "null"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.balsikandar.crashreporter.utils;
|
||||
|
||||
/**
|
||||
* Created by bali on 15/08/17.
|
||||
*/
|
||||
|
||||
public class Constants {
|
||||
public static final String EXCEPTION_SUFFIX = "_exception";
|
||||
public static final String CRASH_SUFFIX = "_crash";
|
||||
public static final String FILE_EXTENSION = ".txt";
|
||||
public static final String CRASH_REPORT_DIR = "crashReports";
|
||||
public static final int NOTIFICATION_ID = 1;
|
||||
public static final String CHANNEL_NOTIFICATION_ID = "crashreporter_channel_id";
|
||||
public static final String LANDING = "landing";
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.balsikandar.crashreporter.utils;
|
||||
|
||||
/**
|
||||
* Created by bali on 02/08/17.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents an error condition specific to the Crash Reporter for Android.
|
||||
*/
|
||||
public class CrashReporterException extends RuntimeException {
|
||||
static final long serialVersionUID = 1;
|
||||
|
||||
/**
|
||||
* Constructs a new CrashReporterException.
|
||||
*/
|
||||
public CrashReporterException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CrashReporterException.
|
||||
*
|
||||
* @param message the detail message of this exception
|
||||
*/
|
||||
public CrashReporterException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CrashReporterException.
|
||||
*
|
||||
* @param format the format string (see {@link java.util.Formatter#format})
|
||||
* @param args the list of arguments passed to the formatter.
|
||||
*/
|
||||
public CrashReporterException(String format, Object... args) {
|
||||
this(String.format(format, args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CrashReporterException.
|
||||
*
|
||||
* @param message the detail message of this exception
|
||||
* @param throwable the cause of this exception
|
||||
*/
|
||||
public CrashReporterException(String message, Throwable throwable) {
|
||||
super(message, throwable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CrashReporterException.
|
||||
*
|
||||
* @param throwable the cause of this exception
|
||||
*/
|
||||
public CrashReporterException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Throwable.toString() returns "CrashReporterException:{message}". Returning just "{message}"
|
||||
// should be fine here.
|
||||
return getMessage();
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.balsikandar.crashreporter.utils;
|
||||
|
||||
public class CrashReporterExceptionHandler implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
private Thread.UncaughtExceptionHandler exceptionHandler;
|
||||
|
||||
public CrashReporterExceptionHandler() {
|
||||
this.exceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread thread, Throwable throwable) {
|
||||
|
||||
CrashUtil.saveCrashReport(throwable);
|
||||
|
||||
exceptionHandler.uncaughtException(thread, throwable);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.balsikandar.crashreporter.utils;
|
||||
|
||||
/**
|
||||
* Created by bali on 02/08/17.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An Exception indicating that the Crash Reporter has not been correctly initialized.
|
||||
*/
|
||||
public class CrashReporterNotInitializedException extends CrashReporterException {
|
||||
static final long serialVersionUID = 1;
|
||||
|
||||
/**
|
||||
* Constructs a CrashReporterNotInitializedException with no additional information.
|
||||
*/
|
||||
public CrashReporterNotInitializedException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CrashReporterNotInitializedException with a message.
|
||||
*
|
||||
* @param message A String to be returned from getMessage.
|
||||
*/
|
||||
public CrashReporterNotInitializedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CrashReporterNotInitializedException with a message and inner error.
|
||||
*
|
||||
* @param message A String to be returned from getMessage.
|
||||
* @param throwable A Throwable to be returned from getCause.
|
||||
*/
|
||||
public CrashReporterNotInitializedException(String message, Throwable throwable) {
|
||||
super(message, throwable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CrashReporterNotInitializedException with an inner error.
|
||||
*
|
||||
* @param throwable A Throwable to be returned from getCause.
|
||||
*/
|
||||
public CrashReporterNotInitializedException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.balsikandar.crashreporter.utils;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.balsikandar.crashreporter.CrashReporter;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.Writer;
|
||||
import java.net.URLEncoder;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import de.mm20.launcher2.crashreporter.R;
|
||||
|
||||
import static android.content.Context.NOTIFICATION_SERVICE;
|
||||
import static com.balsikandar.crashreporter.utils.Constants.CHANNEL_NOTIFICATION_ID;
|
||||
|
||||
public class CrashUtil {
|
||||
|
||||
private static final String TAG = CrashUtil.class.getSimpleName();
|
||||
|
||||
private CrashUtil() {
|
||||
//this class is not publicly instantiable
|
||||
}
|
||||
|
||||
private static String getCrashLogTime() {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||
return dateFormat.format(new Date());
|
||||
}
|
||||
|
||||
public static void saveCrashReport(final Throwable throwable) {
|
||||
|
||||
String crashReportPath = CrashReporter.getCrashReportPath();
|
||||
String filename = getCrashLogTime() + Constants.CRASH_SUFFIX + Constants.FILE_EXTENSION;
|
||||
writeToFile(crashReportPath, filename, getStackTrace(throwable));
|
||||
|
||||
//if (crashReportPath.isEmpty()) crashReportPath = getDefaultPath();
|
||||
|
||||
try {
|
||||
showNotification(throwable.getLocalizedMessage(), filename);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void logException(final Exception exception) {
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
String crashReportPath = CrashReporter.getCrashReportPath();
|
||||
final String filename = getCrashLogTime() + Constants.EXCEPTION_SUFFIX + Constants.FILE_EXTENSION;
|
||||
writeToFile(crashReportPath, filename, getStackTrace(exception));
|
||||
|
||||
//showNotification(exception.getLocalizedMessage(), false);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static void writeToFile(String crashReportPath, String filename, String crashLog) {
|
||||
|
||||
if (TextUtils.isEmpty(crashReportPath)) {
|
||||
crashReportPath = getDefaultPath();
|
||||
}
|
||||
|
||||
File crashDir = new File(crashReportPath);
|
||||
if (!crashDir.exists() || !crashDir.isDirectory()) {
|
||||
crashReportPath = getDefaultPath();
|
||||
Log.e(TAG, "Path provided doesn't exists : " + crashDir + "\nSaving crash report at : " + getDefaultPath());
|
||||
}
|
||||
|
||||
BufferedWriter bufferedWriter;
|
||||
try {
|
||||
bufferedWriter = new BufferedWriter(new FileWriter(
|
||||
crashReportPath + File.separator + filename));
|
||||
|
||||
bufferedWriter.write(crashLog);
|
||||
bufferedWriter.flush();
|
||||
bufferedWriter.close();
|
||||
Log.d(TAG, "crash report saved in : " + crashReportPath);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static void showNotification(String localisedMsg, String fileName) throws UnsupportedEncodingException {
|
||||
|
||||
if (CrashReporter.isNotificationEnabled()) {
|
||||
Context context = CrashReporter.getContext();
|
||||
if (Build.VERSION.SDK_INT >= 33 && context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
return;
|
||||
}
|
||||
NotificationManager notificationManager = (NotificationManager) context.
|
||||
getSystemService(NOTIFICATION_SERVICE);
|
||||
createNotificationChannel(notificationManager, context);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_NOTIFICATION_ID);
|
||||
builder.setSmallIcon(R.drawable.ic_warning_black_24dp);
|
||||
|
||||
String filePath = new File(getDefaultPath(), fileName).getAbsolutePath();
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setComponent(new ComponentName(context.getPackageName(), "de.mm20.launcher2.ui.settings.SettingsActivity"));
|
||||
intent.putExtra("de.mm20.launcher2.settings.ROUTE", "settings/debug/crashreporter/report?fileName=" + URLEncoder.encode(filePath, "utf8"));
|
||||
intent.setAction(Long.toString(System.currentTimeMillis()));
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||
builder.setContentIntent(pendingIntent);
|
||||
|
||||
builder.setContentTitle(context.getString(R.string.view_crash_report));
|
||||
|
||||
if (TextUtils.isEmpty(localisedMsg)) {
|
||||
builder.setContentText(context.getString(R.string.check_your_message_here));
|
||||
} else {
|
||||
builder.setContentText(localisedMsg);
|
||||
}
|
||||
|
||||
builder.setAutoCancel(true);
|
||||
builder.setColor(ContextCompat.getColor(context, R.color.colorAccent_CrashReporter));
|
||||
|
||||
notificationManager.notify(Constants.NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
private static void createNotificationChannel(NotificationManager notificationManager, Context context) {
|
||||
CharSequence name = context.getString(R.string.notification_crash_report_title);
|
||||
String description = "";
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_NOTIFICATION_ID, name, NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.setDescription(description);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
private static String getStackTrace(Throwable e) {
|
||||
final Writer result = new StringWriter();
|
||||
final PrintWriter printWriter = new PrintWriter(result);
|
||||
|
||||
e.printStackTrace(printWriter);
|
||||
String crashLog = result.toString();
|
||||
printWriter.close();
|
||||
return crashLog;
|
||||
}
|
||||
|
||||
public static String getDefaultPath() {
|
||||
String defaultPath = CrashReporter.getContext().getExternalFilesDir(null).getAbsolutePath()
|
||||
+ File.separator + Constants.CRASH_REPORT_DIR;
|
||||
|
||||
File file = new File(defaultPath);
|
||||
file.mkdirs();
|
||||
return defaultPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.balsikandar.crashreporter.utils;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by bali on 10/08/17.
|
||||
*/
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
public static final String TAG = FileUtils.class.getSimpleName();
|
||||
|
||||
private FileUtils() {
|
||||
//this class is not publicly instantiable
|
||||
}
|
||||
|
||||
public static boolean delete(String absPath) {
|
||||
if (TextUtils.isEmpty(absPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File file = new File(absPath);
|
||||
return delete(file);
|
||||
}
|
||||
|
||||
public static boolean delete(File file) {
|
||||
if (!exists(file)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (file.isFile()) {
|
||||
return file.delete();
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
File files[] = file.listFiles();
|
||||
if (files == null) return false;
|
||||
for (int index = 0; index < files.length; index++) {
|
||||
result |= delete(files[index]);
|
||||
}
|
||||
result |= file.delete();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean exists(File file) {
|
||||
return file != null && file.exists();
|
||||
}
|
||||
|
||||
public static String cleanPath(String absPath) {
|
||||
if (TextUtils.isEmpty(absPath)) {
|
||||
return absPath;
|
||||
}
|
||||
try {
|
||||
File file = new File(absPath);
|
||||
absPath = file.getCanonicalPath();
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
return absPath;
|
||||
}
|
||||
|
||||
public final static String getParent(File file) {
|
||||
return file == null ? null : file.getParent();
|
||||
}
|
||||
|
||||
public final static String getParent(String absPath) {
|
||||
if (TextUtils.isEmpty(absPath)) {
|
||||
return null;
|
||||
}
|
||||
absPath = cleanPath(absPath);
|
||||
File file = new File(absPath);
|
||||
return getParent(file);
|
||||
}
|
||||
|
||||
public static boolean deleteFiles(String directoryPath) {
|
||||
String directoryToDelete;
|
||||
if (!TextUtils.isEmpty(directoryPath)) {
|
||||
directoryToDelete = directoryPath;
|
||||
} else {
|
||||
directoryToDelete = CrashUtil.getDefaultPath();
|
||||
}
|
||||
|
||||
return delete(directoryToDelete);
|
||||
}
|
||||
|
||||
public static String readFirstLineFromFile(File file) {
|
||||
String line = "";
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||
line = reader.readLine();
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
public static String readFromFile(File file) {
|
||||
StringBuilder crash = new StringBuilder();
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
crash.append(line);
|
||||
crash.append('\n');
|
||||
}
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return crash.toString();
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.balsikandar.crashreporter.utils;
|
||||
|
||||
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
/**
|
||||
* Created by bali on 11/08/17.
|
||||
*/
|
||||
|
||||
public abstract class SimplePageChangeListener implements ViewPager.OnPageChangeListener {
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
||||
@Override
|
||||
public abstract void onPageSelected(int position);
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.mm20.launcher2.crashreporter
|
||||
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.icu.util.TimeZone
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class CrashReport(
|
||||
val type: CrashReportType,
|
||||
val time: Date,
|
||||
val summary: String,
|
||||
val stacktrace: String?,
|
||||
val filePath: String
|
||||
) {
|
||||
companion object {
|
||||
suspend fun fromFile(file: File, loadStackTrace: Boolean): CrashReport {
|
||||
val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||
val time = df.parse(file.name.replace("[a-zA-Z_.]", ""))
|
||||
val content = if (loadStackTrace) {
|
||||
withContext(Dispatchers.IO) {
|
||||
file.inputStream().bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
}
|
||||
} else null
|
||||
val summary = content?.substringBefore("\n")
|
||||
?: withContext(Dispatchers.IO) {
|
||||
file.inputStream().bufferedReader().use {
|
||||
it.readLine()
|
||||
}
|
||||
}
|
||||
return CrashReport(
|
||||
type = if (file.name.endsWith("_crash.txt")) CrashReportType.Crash else CrashReportType.Exception,
|
||||
time = time,
|
||||
summary = summary,
|
||||
stacktrace = content,
|
||||
filePath = file.absolutePath
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CrashReportType {
|
||||
Exception,
|
||||
Crash
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.mm20.launcher2.crashreporter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.balsikandar.crashreporter.CrashReporter
|
||||
import com.balsikandar.crashreporter.utils.AppUtils
|
||||
import com.balsikandar.crashreporter.utils.CrashUtil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
object CrashReporter {
|
||||
fun logException(e: Exception) {
|
||||
if (e !is CancellationException) {
|
||||
com.balsikandar.crashreporter.CrashReporter.logException(e)
|
||||
}
|
||||
Log.e("MM20", Log.getStackTraceString(e))
|
||||
}
|
||||
|
||||
suspend fun getCrashReports(): List<CrashReport> {
|
||||
val files = withContext(Dispatchers.IO) {
|
||||
val now = System.currentTimeMillis()
|
||||
val path = CrashReporter.getCrashReportPath()?.takeIf { it.isEmpty() } ?: CrashUtil.getDefaultPath()
|
||||
File(path).listFiles { f ->
|
||||
f.lastModified() > now - 7 * 24 * 60 * 60 * 1000L
|
||||
}?.sortedByDescending { it.lastModified() }
|
||||
}
|
||||
return files?.map { CrashReport.fromFile(it, false) } ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun getCrashReport(filePath: String): CrashReport {
|
||||
val path = CrashReporter.getCrashReportPath()?.takeIf { it.isEmpty() } ?: CrashUtil.getDefaultPath()
|
||||
return CrashReport.fromFile(File(filePath), true)
|
||||
}
|
||||
|
||||
fun getDeviceInformation(context: Context): String {
|
||||
return AppUtils.getDeviceDetails(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/text_color_secondary"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/text_color_secondary"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/text_color_secondary"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/delete_log"
|
||||
android:icon="@drawable/ic_menu_delete_white_24dp"
|
||||
android:title="Item"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/share_crash_log"
|
||||
android:icon="@drawable/ic_menu_share_white_24dp"
|
||||
android:title="Item"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/delete_crash_logs"
|
||||
android:icon="@drawable/ic_menu_delete_white_24dp"
|
||||
app:showAsAction="always"
|
||||
android:title="Item" />
|
||||
<!--<item-->
|
||||
<!--android:id="@+id/app_bar_search"-->
|
||||
<!--android:actionViewClass="android.widget.SearchView"-->
|
||||
<!--android:icon="@drawable/ic_search_white_24dp"-->
|
||||
<!--app:showAsAction="always"-->
|
||||
<!--android:title="Search" />-->
|
||||
</menu>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary_CrashReporter">#ff4081</color>
|
||||
<color name="colorPrimaryDark_CrashReporter">#f50057</color>
|
||||
<color name="colorAccent_CrashReporter">#cf1162</color>
|
||||
<color name="black">#000000</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<resources>
|
||||
<string name="crash_reporter">CrashReporter</string>
|
||||
<string name="crashes">Crashes</string>
|
||||
<string name="exceptions">Exceptions</string>
|
||||
<string name="view_crash_report">View Crash Report</string>
|
||||
<string name="notification_crash_report_title">Crash Reporter notifications</string>
|
||||
<string name="check_your_message_here">Check your crashes and exceptions here.</string>
|
||||
<string name="delete_confirmation_msg">Are you sure to delete all the crash logs</string>
|
||||
<string name="cancel">CANCEL</string>
|
||||
<string name="ok">OK</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="CrashReporter.Theme" parent="SettingsTheme">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user