From 197800de8b0a128c5655e6aed388dfec49fd91d4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 27 Feb 2019 12:28:41 -0300 Subject: [PATCH] [android] split crash report screen into two fragments --- .../android/reporting/CrashFragment.java | 40 +++ .../android/reporting/DevReportActivity.java | 302 ++---------------- .../android/reporting/ReportFormFragment.java | 286 +++++++++++++++++ .../main/res/layout/activity_dev_report.xml | 247 +------------- .../src/main/res/layout/fragment_crash.xml | 119 +++++++ .../main/res/layout/fragment_report_form.xml | 113 +++++++ 6 files changed, 602 insertions(+), 505 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashFragment.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportFormFragment.java create mode 100644 briar-android/src/main/res/layout/fragment_crash.xml create mode 100644 briar-android/src/main/res/layout/fragment_report_form.xml diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashFragment.java new file mode 100644 index 000000000..ece37d728 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashFragment.java @@ -0,0 +1,40 @@ +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().showReportForm(true)); + v.findViewById(R.id.declineButton).setOnClickListener(view -> + getDevReportActivity().closeReport()); + + return v; + } + + private DevReportActivity getDevReportActivity() { + return (DevReportActivity) requireNonNull(getActivity()); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/DevReportActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/DevReportActivity.java index d20ef8573..1388016c7 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/DevReportActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/DevReportActivity.java @@ -2,86 +2,32 @@ package org.briarproject.briar.android.reporting; import android.content.Context; import android.content.res.Configuration; -import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatDelegate; import android.support.v7.widget.Toolbar; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; -import org.acra.ReportField; -import org.acra.collector.CrashReportData; import org.acra.dialog.BaseCrashReportDialog; -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.briarproject.briar.android.Localizer; import org.briarproject.briar.android.util.UserFeedback; -import org.json.JSONException; import java.io.File; -import java.io.IOException; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map.Entry; -import java.util.Set; -import java.util.logging.Logger; import static android.os.Build.VERSION.SDK_INT; -import static android.view.View.GONE; -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; -import static android.view.inputmethod.InputMethodManager.SHOW_FORCED; -import static java.util.logging.Level.WARNING; +import static java.util.Objects.requireNonNull; 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; import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS; -public class DevReportActivity extends BaseCrashReportDialog - implements CompoundButton.OnCheckedChangeListener { - - private static final Logger LOG = - Logger.getLogger(DevReportActivity.class.getName()); - - private static final String STATE_REVIEWING = "reviewing"; - private static final Set requiredFields = 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); - } +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class DevReportActivity extends BaseCrashReportDialog { private AppCompatDelegate delegate; - private Set excludedFields = new HashSet<>(); - private EditText userCommentView = null; - private EditText userEmailView = null; - private CheckBox includeDebugReport = null; - private Button chevron = null; - private LinearLayout report = null; - private View progress = null; - private MenuItem sendReport = null; - private boolean reviewing = false; private AppCompatDelegate getDelegate() { if (delegate == null) { @@ -110,68 +56,21 @@ public class DevReportActivity extends BaseCrashReportDialog } @Override - public void init(Bundle state) { + public void init(@Nullable Bundle state) { super.init(state); if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE); getDelegate().setContentView(R.layout.activity_dev_report); - Toolbar tb = findViewById(R.id.toolbar); - getDelegate().setSupportActionBar(tb); + Toolbar toolbar = findViewById(R.id.toolbar); + getDelegate().setSupportActionBar(toolbar); - View requestReport = findViewById(R.id.request_report); - View reportForm = findViewById(R.id.report_form); - userCommentView = findViewById(R.id.user_comment); - userEmailView = findViewById(R.id.user_email); - includeDebugReport = findViewById(R.id.include_debug_report); - chevron = findViewById(R.id.chevron); - report = findViewById(R.id.report_content); - progress = findViewById(R.id.progress_wheel); + String title = getString(isFeedback() ? R.string.feedback_title : + R.string.crash_report_title); + requireNonNull(getDelegate().getSupportActionBar()).setTitle(title); - //noinspection ConstantConditions - getDelegate().getSupportActionBar().setTitle( - isFeedback() ? R.string.feedback_title : - R.string.crash_report_title); - userCommentView.setHint(isFeedback() ? R.string.enter_feedback : - R.string.describe_crash); - - if (isFeedback()) { - includeDebugReport - .setText(getString(R.string.include_debug_report_feedback)); - reportForm.setVisibility(VISIBLE); - requestReport.setVisibility(INVISIBLE); - } else { - includeDebugReport.setChecked(true); - reportForm.setVisibility(INVISIBLE); - requestReport.setVisibility(VISIBLE); - } - - findViewById(R.id.acceptButton).setOnClickListener(v -> { - reviewing = true; - reportForm.setVisibility(VISIBLE); - requestReport.setVisibility(INVISIBLE); - ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)) - .showSoftInput(userCommentView, SHOW_FORCED); - }); - findViewById(R.id.declineButton).setOnClickListener(v -> closeReport()); - chevron.setOnClickListener(v -> { - 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); - } - }); - - if (state != null) - reviewing = state.getBoolean(STATE_REVIEWING, isFeedback()); - - if (!isFeedback() && !reviewing) - requestReport.setVisibility(VISIBLE); + if (state == null) showReportForm(isFeedback()); } @Override @@ -181,47 +80,17 @@ public class DevReportActivity extends BaseCrashReportDialog } @Override - public void onPostCreate(Bundle state) { + public void onPostCreate(@Nullable Bundle state) { super.onPostCreate(state); getDelegate().onPostCreate(state); } - @Override - public void onStart() { - super.onStart(); - if (chevron.isSelected()) refresh(); - } - @Override protected void onPostResume() { super.onPostResume(); getDelegate().onPostResume(); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu items for use in the action bar - MenuInflater inflater = getDelegate().getMenuInflater(); - inflater.inflate(R.menu.dev_report_actions, menu); - sendReport = menu.findItem(R.id.action_send_report); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle presses on the action bar items - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - case R.id.action_send_report: - processReport(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - @Override public void onTitleChanged(CharSequence title, int color) { super.onTitleChanged(title, color); @@ -234,12 +103,6 @@ public class DevReportActivity extends BaseCrashReportDialog getDelegate().onConfigurationChanged(newConfig); } - @Override - public void onSaveInstanceState(Bundle state) { - super.onSaveInstanceState(state); - state.putBoolean(STATE_REVIEWING, reviewing); - } - @Override public void onStop() { super.onStop(); @@ -257,132 +120,33 @@ public class DevReportActivity extends BaseCrashReportDialog closeReport(); } - @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); - } + void sendCrashReport(String comment, String email) { + sendCrash(comment, email); } - @SuppressWarnings("ThrowableResultOfMethodCallIgnored") private boolean isFeedback() { return getException() instanceof UserFeedback; } - private void refresh() { - report.setVisibility(INVISIBLE); - progress.setVisibility(VISIBLE); - report.removeAllViews(); - new AsyncTask() { - - @Override - protected CrashReportData doInBackground(Void... args) { - File reportFile = (File) getIntent().getSerializableExtra( - EXTRA_REPORT_FILE); - 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 (Entry e : crashData.entrySet()) { - ReportField field = e.getKey(); - String value = e.getValue().toString() - .replaceAll("\\\\n", "\n"); - 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(DevReportActivity.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(); + void showReportForm(boolean showReportForm) { + Fragment f; + if (showReportForm) { + File file = + (File) getIntent().getSerializableExtra(EXTRA_REPORT_FILE); + f = ReportFormFragment.newInstance(isFeedback(), file); + requireNonNull(getDelegate().getSupportActionBar()).show(); + } else { + f = new CrashFragment(); + requireNonNull(getDelegate().getSupportActionBar()).hide(); + } + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragmentContainer, f) + .commit(); } - private void processReport() { - userCommentView.setEnabled(false); - userEmailView.setEnabled(false); - sendReport.setEnabled(false); - progress.setVisibility(VISIBLE); - boolean includeReport = !isFeedback() || includeDebugReport.isChecked(); - new AsyncTask() { - - @Override - protected Boolean doInBackground(Void... args) { - File reportFile = (File) getIntent().getSerializableExtra( - EXTRA_REPORT_FILE); - 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> iter = - data.entrySet().iterator(); - while (iter.hasNext()) { - Entry 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(); - } - sendCrash(comment, email); - } - finish(); - } - }.execute(); - } - - private void closeReport() { + void closeReport() { cancelReports(); finish(); } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportFormFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportFormFragment.java new file mode 100644 index 000000000..a90e72012 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportFormFragment.java @@ -0,0 +1,286 @@ +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.support.v7.widget.Toolbar; +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.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 CompoundButton.OnCheckedChangeListener { + + private static final Logger LOG = + getLogger(ReportFormFragment.class.getName()); + private static final String IS_FEEDBACK = "isFeedback"; + private static final Set requiredFields = new HashSet<>(); + private static final Set 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(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() { + + @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 e : crashData.entrySet()) { + ReportField field = e.getKey(); + String value = e.getValue().toString() + .replaceAll("\\\\n", "\n"); + 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() { + + @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> iter = + data.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry 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) getActivity().finish(); + } + }.execute(); + } + + private DevReportActivity getDevReportActivity() { + return (DevReportActivity) requireNonNull(getActivity()); + } + +} diff --git a/briar-android/src/main/res/layout/activity_dev_report.xml b/briar-android/src/main/res/layout/activity_dev_report.xml index 5f7271d55..c06205faf 100644 --- a/briar-android/src/main/res/layout/activity_dev_report.xml +++ b/briar-android/src/main/res/layout/activity_dev_report.xml @@ -1,244 +1,19 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> - + android:layout_height="wrap_content"/> - - - - - - - - - - - - - - - - -