Commit 34583e6d authored by akwizgran's avatar akwizgran

Merge branch '1054-crash-scroll' into 'master'

Improve crash screen and reporter

Closes #1426, #1061, #1390, #1012, and #1054

See merge request !1049
parents d210215b ea5a8622
Pipeline #3597 passed with stage
in 8 minutes and 50 seconds
......@@ -20,6 +20,7 @@ import org.acra.annotation.ReportsCrashes;
import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.BuildConfig;
import org.briarproject.briar.R;
import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.reporting.BriarReportPrimer;
......@@ -64,6 +65,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
reportDialogClass = DevReportActivity.class,
resDialogOkToast = R.string.dev_report_saved,
deleteOldUnsentReportsOnApplicationStart = false,
buildConfigClass = BuildConfig.class,
customReportContent = {
REPORT_ID,
APP_VERSION_CODE, APP_VERSION_NAME, PACKAGE_NAME,
......
package org.briarproject.briar.android.reporting;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import static java.util.Objects.requireNonNull;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class CrashFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater
.inflate(R.layout.fragment_crash, container, false);
v.findViewById(R.id.acceptButton).setOnClickListener(view ->
getDevReportActivity().displayFragment(true));
v.findViewById(R.id.declineButton).setOnClickListener(view ->
getDevReportActivity().closeReport());
return v;
}
private DevReportActivity getDevReportActivity() {
return (DevReportActivity) requireNonNull(getActivity());
}
}
package org.briarproject.briar.android.reporting;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.acra.ReportField;
import org.acra.collector.CrashReportData;
import org.acra.file.CrashReportPersister;
import org.acra.model.Element;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import static android.view.MenuItem.SHOW_AS_ACTION_ALWAYS;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.acra.ACRAConstants.EXTRA_REPORT_FILE;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.PACKAGE_NAME;
import static org.acra.ReportField.REPORT_ID;
import static org.acra.ReportField.STACK_TRACE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ReportFormFragment extends Fragment
implements OnCheckedChangeListener {
private static final Logger LOG =
getLogger(ReportFormFragment.class.getName());
private static final String IS_FEEDBACK = "isFeedback";
private static final Set<ReportField> requiredFields = new HashSet<>();
private static final Set<ReportField> excludedFields = new HashSet<>();
static {
requiredFields.add(REPORT_ID);
requiredFields.add(APP_VERSION_CODE);
requiredFields.add(APP_VERSION_NAME);
requiredFields.add(PACKAGE_NAME);
requiredFields.add(ANDROID_VERSION);
requiredFields.add(STACK_TRACE);
}
private boolean isFeedback;
private File reportFile;
private EditText userCommentView;
private EditText userEmailView;
private CheckBox includeDebugReport;
private Button chevron;
private LinearLayout report;
private View progress;
@Nullable
private MenuItem sendReport;
static ReportFormFragment newInstance(boolean isFeedback,
File reportFile) {
ReportFormFragment f = new ReportFormFragment();
Bundle args = new Bundle();
args.putBoolean(IS_FEEDBACK, isFeedback);
args.putSerializable(EXTRA_REPORT_FILE, reportFile);
f.setArguments(args);
return f;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_report_form, container,
false);
userCommentView = v.findViewById(R.id.user_comment);
userEmailView = v.findViewById(R.id.user_email);
includeDebugReport = v.findViewById(R.id.include_debug_report);
chevron = v.findViewById(R.id.chevron);
report = v.findViewById(R.id.report_content);
progress = v.findViewById(R.id.progress_wheel);
Bundle args = requireNonNull(getArguments());
isFeedback = args.getBoolean(IS_FEEDBACK);
reportFile =
(File) requireNonNull(args.getSerializable(EXTRA_REPORT_FILE));
if (isFeedback) {
includeDebugReport
.setText(getString(R.string.include_debug_report_feedback));
userCommentView.setHint(R.string.enter_feedback);
} else {
includeDebugReport.setChecked(true);
userCommentView.setHint(R.string.describe_crash);
}
chevron.setOnClickListener(view -> {
boolean show = chevron.getText().equals(getString(R.string.show));
if (show) {
chevron.setText(R.string.hide);
refresh();
} else {
chevron.setText(R.string.show);
report.setVisibility(GONE);
}
});
return v;
}
@Override
public void onStart() {
super.onStart();
if (chevron.isSelected()) refresh();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.dev_report_actions, menu);
sendReport = menu.findItem(R.id.action_send_report);
// calling setShowAsAction() shouldn't be needed, but for some reason is
sendReport.setShowAsAction(SHOW_AS_ACTION_ALWAYS);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_send_report) {
processReport();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
ReportField field = (ReportField) buttonView.getTag();
if (field != null) {
if (isChecked) excludedFields.remove(field);
else excludedFields.add(field);
}
}
private void refresh() {
report.setVisibility(INVISIBLE);
progress.setVisibility(VISIBLE);
report.removeAllViews();
new AsyncTask<Void, Void, CrashReportData>() {
@Override
protected CrashReportData doInBackground(Void... args) {
CrashReportPersister persister = new CrashReportPersister();
try {
return persister.load(reportFile);
} catch (IOException | JSONException e) {
LOG.log(WARNING, "Could not load report file", e);
return null;
}
}
@Override
protected void onPostExecute(CrashReportData crashData) {
LayoutInflater inflater = getLayoutInflater();
if (crashData != null) {
for (Map.Entry<ReportField, Element> e : crashData
.entrySet()) {
ReportField field = e.getKey();
StringBuilder valueBuilder = new StringBuilder();
for (String pair : e.getValue().flatten()) {
valueBuilder.append(pair).append("\n");
}
String value = valueBuilder.toString();
boolean required = requiredFields.contains(field);
boolean excluded = excludedFields.contains(field);
View v = inflater.inflate(R.layout.list_item_crash,
report, false);
CheckBox cb = v.findViewById(R.id.include_in_report);
cb.setTag(field);
cb.setChecked(required || !excluded);
cb.setEnabled(!required);
cb.setOnCheckedChangeListener(ReportFormFragment.this);
cb.setText(field.toString());
TextView content = v.findViewById(R.id.content);
content.setText(value);
report.addView(v);
}
} else {
View v = inflater.inflate(
android.R.layout.simple_list_item_1, report, false);
TextView error = v.findViewById(android.R.id.text1);
error.setText(R.string.could_not_load_report_data);
report.addView(v);
}
report.setVisibility(VISIBLE);
progress.setVisibility(GONE);
}
}.execute();
}
private void processReport() {
userCommentView.setEnabled(false);
userEmailView.setEnabled(false);
requireNonNull(sendReport).setEnabled(false);
progress.setVisibility(VISIBLE);
boolean includeReport = !isFeedback || includeDebugReport.isChecked();
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... args) {
CrashReportPersister persister = new CrashReportPersister();
try {
CrashReportData data = persister.load(reportFile);
if (includeReport) {
for (ReportField field : excludedFields) {
LOG.info("Removing field " + field.name());
data.remove(field);
}
} else {
Iterator<Map.Entry<ReportField, Element>> iter =
data.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<ReportField, Element> e = iter.next();
if (!requiredFields.contains(e.getKey())) {
iter.remove();
}
}
}
persister.store(data, reportFile);
return true;
} catch (IOException | JSONException e) {
LOG.log(WARNING, "Error processing report file", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
if (success) {
// Retrieve user's comment and email address, if any
String comment = "";
if (userCommentView != null)
comment = userCommentView.getText().toString();
String email = "";
if (userEmailView != null) {
email = userEmailView.getText().toString();
}
getDevReportActivity().sendCrashReport(comment, email);
}
if (getActivity() != null) getDevReportActivity().exit();
}
}.execute();
}
private DevReportActivity getDevReportActivity() {
return (DevReportActivity) requireNonNull(getActivity());
}
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M11.99,2C6.47,2 2,6.47 2,12s4.47,10 9.99,10S22,17.53 22,12 17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM16.18,7.76l-1.06,1.06 -1.06,-1.06L13,8.82l1.06,1.06L13,10.94 14.06,12l1.06,-1.06L16.18,12l1.06,-1.06 -1.06,-1.06 1.06,-1.06zM7.82,12l1.06,-1.06L9.94,12 11,10.94 9.94,9.88 11,8.82 9.94,7.76 8.88,8.82 7.82,7.76 6.76,8.82l1.06,1.06 -1.06,1.06zM12,14c-2.33,0 -4.31,1.46 -5.11,3.5h10.22c-0.8,-2.04 -2.78,-3.5 -5.11,-3.5z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<LinearLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.constraint.ConstraintLayout
android:id="@+id/report_form"
<include
android:id="@+id/appBar"
layout="@layout/toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
tools:context=".android.reporting.DevReportActivity"
tools:visibility="invisible">
android:layout_height="wrap_content"/>
<include
android:id="@+id/appBar"
layout="@layout/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<android.support.design.widget.TextInputLayout
android:id="@+id/user_comment_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_large"
android:layout_marginLeft="@dimen/margin_large"
android:layout_marginRight="@dimen/margin_large"
android:layout_marginStart="@dimen/margin_large"
android:layout_marginTop="@dimen/margin_large"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appBar">
<android.support.design.widget.TextInputEditText
android:id="@+id/user_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine|textCapSentences"
android:maxLines="5"
tools:hint="@string/describe_crash"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/user_email_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_large"
android:layout_marginLeft="@dimen/margin_large"
android:layout_marginRight="@dimen/margin_large"
android:layout_marginStart="@dimen/margin_large"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_comment_layout">
<android.support.design.widget.TextInputEditText
android:id="@+id/user_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/optional_contact_email"
android:inputType="textEmailAddress"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>
<CheckBox
android:id="@+id/include_debug_report"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_large"
android:layout_marginStart="@dimen/margin_large"
android:checked="false"
android:text="@string/include_debug_report_crash"
app:layout_constraintBottom_toBottomOf="@+id/chevron"
app:layout_constraintEnd_toStartOf="@+id/chevron"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/chevron"/>
<Button
android:id="@+id/chevron"
style="@style/BriarButtonFlat.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/show"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_email_layout"/>
<ScrollView
android:id="@+id/report_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/include_debug_report">
<LinearLayout
android:id="@+id/report_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="@dimen/listitem_height_one_line_avatar"
android:paddingEnd="@dimen/margin_large"
android:paddingStart="@dimen/margin_large"
android:paddingTop="@dimen/margin_small"
android:visibility="gone"
tools:visibility="visible"/>
</ScrollView>
<ProgressBar
android:id="@+id/progress_wheel"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/include_debug_report"
tools:visibility="visible"/>
</android.support.constraint.ConstraintLayout>
<android.support.constraint.ConstraintLayout
android:id="@+id/request_report"
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/margin_large"
android:visibility="invisible"
tools:visibility="visible">
<TextView
android:id="@+id/crashed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/briar_crashed"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/fault"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:layout_editor_absoluteY="8dp"/>
<TextView
android:id="@+id/fault"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/not_your_fault"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/pleaseSend"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/crashed"/>
<TextView
android:id="@+id/pleaseSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/please_send_report"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/encrypted"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fault"/>
<TextView
android:id="@+id/encrypted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/report_is_encrypted"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/acceptButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pleaseSend"/>
<Button
android:id="@+id/declineButton"
style="@style/BriarButtonFlat.Negative"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/close"
app:layout_constraintBottom_toBottomOf="@+id/acceptButton"
app:layout_constraintEnd_toStartOf="@+id/acceptButton"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/acceptButton"/>
<Button
android:id="@+id/acceptButton"
style="@style/BriarButtonFlat.Positive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:text="@string/send_report"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@+id/declineButton"
app:layout_constraintTop_toBottomOf="@+id/encrypted"/>
</android.support.constraint.ConstraintLayout>
android:layout_height="match_parent"/>
</FrameLayout>
\ No newline at end of file
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toTopOf="@+id/acceptButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_large">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/errorIcon"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="8dp"
android:src="@drawable/ic_crash"
app:layout_constraintBottom_toTopOf="@+id/crashed"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="128dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:tint="?attr/colorControlNormal"
tools:ignore="ContentDescription"/>
<TextView
android:id="@+id/crashed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/briar_crashed"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/fault"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorIcon"
tools:layout_editor_absoluteY="8dp"/>
<TextView
android:id="@+id/fault"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/not_your_fault"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/pleaseSend"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/crashed"/>
<TextView
android:id="@+id/pleaseSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/please_send_report"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/encrypted"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fault"/>
<TextView
android:id="@+id/encrypted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/report_is_encrypted"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"