forked from upstream-mirrors/OpenTracks
Add ExceptionHandler and ShowErrorActivity
This commit is contained in:
@@ -364,6 +364,14 @@ limitations under the License.
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ShowErrorActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:finishOnTaskLaunch="true"
|
||||||
|
android:launchMode="singleInstance"
|
||||||
|
android:process=":crash"/>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".data.ShareContentProvider"
|
android:name=".data.ShareContentProvider"
|
||||||
android:authorities="${applicationId}.content"
|
android:authorities="${applicationId}.content"
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.databinding.ActivityShowErrorBinding;
|
||||||
|
import de.dennisguse.opentracks.databinding.TrackListBinding;
|
||||||
|
|
||||||
|
public class ShowErrorActivity extends AbstractActivity {
|
||||||
|
|
||||||
|
public static final String EXTRA_ERROR_TEXT = "error";
|
||||||
|
|
||||||
|
private ActivityShowErrorBinding viewBinding;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
viewBinding.textViewError.setText(getIntent().getStringExtra(EXTRA_ERROR_TEXT));
|
||||||
|
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(createErrorTitle());
|
||||||
|
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View getRootView() {
|
||||||
|
viewBinding = ActivityShowErrorBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createErrorTitle() {
|
||||||
|
return String.format(getString(R.string.error_crash_title), getString(R.string.app_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportBug() {
|
||||||
|
Uri uriUrl;
|
||||||
|
try {
|
||||||
|
uriUrl = Uri.parse(
|
||||||
|
String.format(
|
||||||
|
getString(R.string.report_issue_link),
|
||||||
|
URLEncoder.encode(viewBinding.textViewError.getText().toString(), StandardCharsets.UTF_8.toString())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (final UnsupportedEncodingException ignored) {
|
||||||
|
// can't happen as UTF-8 is always available
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, uriUrl);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.show_error, menu);
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
|
if (item.getItemId() == R.id.error_share) {
|
||||||
|
onClickedShare();
|
||||||
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.error_report) {
|
||||||
|
reportBug();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClickedShare() {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
|
intent.putExtra(Intent.EXTRA_SUBJECT, createErrorTitle());
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, viewBinding.textViewError.getText());
|
||||||
|
intent.setType("text/plain");
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
package de.dennisguse.opentracks;
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.StrictMode;
|
import android.os.StrictMode;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
|
import de.dennisguse.opentracks.util.ExceptionHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Code that is executed when the application starts.
|
* Code that is executed when the application starts.
|
||||||
@@ -37,6 +43,36 @@ public class Startup extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attachBaseContext(final Context base) {
|
||||||
|
super.attachBaseContext(base);
|
||||||
|
|
||||||
|
// handle crashes only outside the crash reporter activity/process
|
||||||
|
if (!isCrashReportingProcess()) {
|
||||||
|
Thread.UncaughtExceptionHandler defaultPlatformHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||||
|
ExceptionHandler crashReporter = new ExceptionHandler(this, defaultPlatformHandler);
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(crashReporter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCrashReportingProcess() {
|
||||||
|
String processName = "";
|
||||||
|
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
// Using the same technique as Application.getProcessName() for older devices
|
||||||
|
// Using reflection since ActivityThread is an internal API
|
||||||
|
try {
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
|
Class<?> activityThread = Class.forName("android.app.ActivityThread");
|
||||||
|
@SuppressLint("DiscouragedPrivateApi") Method getProcessName = activityThread.getDeclaredMethod("currentProcessName");
|
||||||
|
processName = (String) getProcessName.invoke(null);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
processName = Application.getProcessName();
|
||||||
|
}
|
||||||
|
return processName != null && processName.endsWith(":crash");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the database used by SQLiteOpenHelper.
|
* Returns the name of the database used by SQLiteOpenHelper.
|
||||||
* See {@link android.database.sqlite.SQLiteOpenHelper} for details.
|
* See {@link android.database.sqlite.SQLiteOpenHelper} for details.
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package de.dennisguse.opentracks.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.io.Writer;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.BuildConfig;
|
||||||
|
import de.dennisguse.opentracks.ShowErrorActivity;
|
||||||
|
|
||||||
|
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final Thread.UncaughtExceptionHandler defaultExceptionHandler;
|
||||||
|
|
||||||
|
public ExceptionHandler(final Context context, final Thread.UncaughtExceptionHandler defaultExceptionHandler) {
|
||||||
|
this.context = context;
|
||||||
|
this.defaultExceptionHandler = defaultExceptionHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void uncaughtException(@NonNull final Thread thread, @NonNull final Throwable exception) {
|
||||||
|
try {
|
||||||
|
String errorReport = generateErrorReport(formatException(thread, exception));
|
||||||
|
Intent intent = new Intent(context, ShowErrorActivity.class);
|
||||||
|
intent.putExtra(ShowErrorActivity.EXTRA_ERROR_TEXT, errorReport);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
context.startActivity(intent);
|
||||||
|
// Pass exception to OS for graceful handling - OS will report it via ADB
|
||||||
|
// and close all activities and services.
|
||||||
|
defaultExceptionHandler.uncaughtException(thread, exception);
|
||||||
|
} catch (final Exception fatalException) {
|
||||||
|
// do not recurse into custom handler if exception is thrown during
|
||||||
|
// exception handling. Pass this ultimate fatal exception to OS
|
||||||
|
defaultExceptionHandler.uncaughtException(thread, fatalException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatException(final Thread thread, final Throwable exception) {
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
stringBuilder.append(String.format("Exception in thread \"%s\": ", thread.getName()));
|
||||||
|
|
||||||
|
// print available stacktrace
|
||||||
|
Writer writer = new StringWriter();
|
||||||
|
exception.printStackTrace(new PrintWriter(writer));
|
||||||
|
stringBuilder.append(writer);
|
||||||
|
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateErrorReport(final String stackTrace) {
|
||||||
|
return
|
||||||
|
"### App information\n" +
|
||||||
|
"* ID: " + BuildConfig.APPLICATION_ID + "\n" +
|
||||||
|
"* Version: " + BuildConfig.VERSION_CODE + " " + BuildConfig.VERSION_NAME + "\n" +
|
||||||
|
"\n" +
|
||||||
|
"### Device information\n" +
|
||||||
|
"* Brand: " + Build.BRAND + "\n" +
|
||||||
|
"* Device: " + Build.DEVICE + "\n" +
|
||||||
|
"* Model: " + Build.MODEL + "\n" +
|
||||||
|
"* Id: " + Build.ID + "\n" +
|
||||||
|
"* Product: " + Build.PRODUCT + "\n" +
|
||||||
|
"\n" +
|
||||||
|
"### Firmware\n" +
|
||||||
|
"* SDK: " + Build.VERSION.SDK_INT + "\n" +
|
||||||
|
"* Release: " + Build.VERSION.RELEASE + "\n" +
|
||||||
|
"* Incremental: " + Build.VERSION.INCREMENTAL + "\n" +
|
||||||
|
"\n" +
|
||||||
|
"### Cause of error\n" +
|
||||||
|
"```java\n" +
|
||||||
|
stackTrace +
|
||||||
|
"```\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<vector android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportHeight="48" android:viewportWidth="48"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M24,42C19.871,42.036 15.525,39.899 13.8,36L8,36v-3h4.6c-0.498,-1.726 -0.314,-3.53 -0.35,-5.3h-4.3v-3h4.3c-0.169,-1.827 0.064,-4.169 0.293,-5.667L8,19v-3h6c0.913,-1.868 2.567,-3.276 4.4,-4.2L14.55,8l2,-2 4.7,4.7c2.672,-0.682 3.902,-0.511 5.855,-0.205L31.6,6l2,2 -3.8,3.8c1.847,0.901 3.312,2.447 4.35,4.2h5.9v3L35.3,19c0.482,0.673 0.569,4.047 0.689,5.7L40.05,24.7v3L35.7,27.7c0.127,2.006 -0.004,4.014 -0.242,5.3h4.592v3h-5.8c-1.661,4.014 -6.109,6.018 -10.25,6zM24,39c4.625,0.162 8.864,-4.009 8.7,-8.65 -0.034,-3.149 0.073,-6.304 -0.062,-9.449 -0.439,-4.459 -4.853,-8.007 -9.304,-7.529 -4.488,0.196 -8.327,4.419 -8.034,8.917 0.034,3.053 -0.072,6.111 0.062,9.159C15.79,35.668 19.764,39.128 24,39ZM20,32h8v-3h-8zM20,23.35h8v-3h-8zM24,26.2c0.015,0 0.026,0 0,0z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/error_page_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_view_error"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="72dp"
|
||||||
|
android:textIsSelectable="true" />
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<include android:id="@+id/bottom_app_bar_layout" layout="@layout/bottomappbar" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:ignore="AppCompatResource">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/error_share"
|
||||||
|
android:icon="@drawable/ic_share_24dp"
|
||||||
|
android:showAsAction="ifRoom"
|
||||||
|
android:title="@string/share_crash_report"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/error_report"
|
||||||
|
android:icon="@drawable/ic_bug_report_24dp"
|
||||||
|
android:showAsAction="ifRoom"
|
||||||
|
android:title="@string/report_bug"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
@@ -44,6 +44,7 @@ limitations under the License.
|
|||||||
<string name="sensor_unit_power" translatable="false">w</string>
|
<string name="sensor_unit_power" translatable="false">w</string>
|
||||||
|
|
||||||
<string name="value_unknown" translatable="false">-</string>
|
<string name="value_unknown" translatable="false">-</string>
|
||||||
|
<string name="report_issue_link" translatable="false">https://github.com/OpenTracksApp/OpenTracks/issues/new?labels=bug&body=%1$s</string>
|
||||||
|
|
||||||
<string-array name="marker_types">
|
<string-array name="marker_types">
|
||||||
<item>@string/waypoint_type_atm</item>
|
<item>@string/waypoint_type_atm</item>
|
||||||
|
|||||||
@@ -666,4 +666,7 @@ limitations under the License.
|
|||||||
<string name="field_set_secondary">Secondary</string>
|
<string name="field_set_secondary">Secondary</string>
|
||||||
<string name="field_remove_from_layout">Remove</string>
|
<string name="field_remove_from_layout">Remove</string>
|
||||||
<string name="no_compatible_file_manager_installed">No compatible file manager installed</string>
|
<string name="no_compatible_file_manager_installed">No compatible file manager installed</string>
|
||||||
|
<string name="error_crash_title">%1$s crashed</string>
|
||||||
|
<string name="share_crash_report">Share crash report</string>
|
||||||
|
<string name="report_bug">Report bug</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user