Collect dev report data on a thread with a looper. #328

This commit is contained in:
akwizgran
2016-05-03 11:24:45 +01:00
parent 6873dbc493
commit 2f11f81582
8 changed files with 270 additions and 216 deletions

View File

@@ -0,0 +1,240 @@
package org.briarproject.android.report;
import android.app.ActivityManager;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.support.annotation.NonNull;
import org.acra.builder.ReportBuilder;
import org.acra.builder.ReportPrimer;
import org.briarproject.util.StringUtils;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.content.Context.ACTIVITY_SERVICE;
import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED;
public class BriarReportPrimer implements ReportPrimer {
@Override
public void primeReport(@NonNull Context ctx,
@NonNull ReportBuilder builder) {
CustomDataTask task = new CustomDataTask(ctx);
FutureTask<Map<String, String>> futureTask = new FutureTask<>(task);
// Use a new thread as the Android executor thread may have died
new SingleShotAndroidExecutor(futureTask).start();
try {
builder.customData(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
builder.customData("Custom data exception", e.toString());
}
}
private static class CustomDataTask
implements Callable<Map<String, String>> {
private final Context ctx;
private CustomDataTask(Context ctx) {
this.ctx = ctx;
}
@Override
public Map<String, String> call() {
Map<String, String> customData = new LinkedHashMap<>();
// System memory
Object o = ctx.getSystemService(ACTIVITY_SERVICE);
ActivityManager am = (ActivityManager) o;
ActivityManager.MemoryInfo mem = new ActivityManager.MemoryInfo();
am.getMemoryInfo(mem);
String systemMemory;
if (Build.VERSION.SDK_INT >= 16) {
systemMemory = (mem.totalMem / 1024 / 1024) + " MiB total, "
+ (mem.availMem / 1024 / 1204) + " MiB free, "
+ (mem.threshold / 1024 / 1024) + " MiB threshold";
} else {
systemMemory = (mem.availMem / 1024 / 1204) + " MiB free, "
+ (mem.threshold / 1024 / 1024) + " MiB threshold";
}
customData.put("System memory", systemMemory);
// Virtual machine memory
Runtime runtime = Runtime.getRuntime();
long heap = runtime.totalMemory();
long heapFree = runtime.freeMemory();
long heapMax = runtime.maxMemory();
String vmMemory = (heap / 1024 / 1024) + " MiB allocated, "
+ (heapFree / 1024 / 1024) + " MiB free, "
+ (heapMax / 1024 / 1024) + " MiB maximum";
customData.put("Virtual machine memory", vmMemory);
// Internal storage
File root = Environment.getRootDirectory();
long rootTotal = root.getTotalSpace();
long rootFree = root.getFreeSpace();
String internal = (rootTotal / 1024 / 1024) + " MiB total, "
+ (rootFree / 1024 / 1024) + " MiB free";
customData.put("Internal storage", internal);
// External storage (SD card)
File sd = Environment.getExternalStorageDirectory();
long sdTotal = sd.getTotalSpace();
long sdFree = sd.getFreeSpace();
String external = (sdTotal / 1024 / 1024) + " MiB total, "
+ (sdFree / 1024 / 1024) + " MiB free";
customData.put("External storage", external);
// Is mobile data available?
o = ctx.getSystemService(CONNECTIVITY_SERVICE);
ConnectivityManager cm = (ConnectivityManager) o;
NetworkInfo mobile = cm.getNetworkInfo(TYPE_MOBILE);
boolean mobileAvailable = mobile != null && mobile.isAvailable();
// Is mobile data enabled?
boolean mobileEnabled = false;
try {
Class<?> clazz = Class.forName(cm.getClass().getName());
Method method = clazz.getDeclaredMethod("getMobileDataEnabled");
method.setAccessible(true);
mobileEnabled = (Boolean) method.invoke(cm);
} catch (ClassNotFoundException
| NoSuchMethodException
| IllegalArgumentException
| InvocationTargetException
| IllegalAccessException e) {
customData.put("Mobile data reflection exception",
e.toString());
}
// Is mobile data connected ?
boolean mobileConnected = mobile != null && mobile.isConnected();
String mobileStatus;
if (mobileAvailable) mobileStatus = "Available, ";
else mobileStatus = "Not available, ";
if (mobileEnabled) mobileStatus += "enabled, ";
else mobileStatus += "not enabled, ";
if (mobileConnected) mobileStatus += "connected";
else mobileStatus += "not connected";
customData.put("Mobile data status", mobileStatus);
// Is wifi available?
NetworkInfo wifi = cm.getNetworkInfo(TYPE_WIFI);
boolean wifiAvailable = wifi != null && wifi.isAvailable();
// Is wifi enabled?
o = ctx.getSystemService(WIFI_SERVICE);
WifiManager wm = (WifiManager) o;
boolean wifiEnabled = wm != null &&
wm.getWifiState() == WIFI_STATE_ENABLED;
// Is wifi connected?
boolean wifiConnected = wifi != null && wifi.isConnected();
String wifiStatus;
if (wifiAvailable) wifiStatus = "Available, ";
else wifiStatus = "Not available, ";
if (wifiEnabled) wifiStatus += "enabled, ";
else wifiStatus += "not enabled, ";
if (wifiConnected) wifiStatus += "connected";
else wifiStatus += "not connected";
customData.put("Wi-Fi status", wifiStatus);
if (wm != null) {
WifiInfo wifiInfo = wm.getConnectionInfo();
if (wifiInfo != null) {
int ip = wifiInfo.getIpAddress(); // Nice API, Google
int ip1 = ip & 0xFF;
int ip2 = (ip >> 8) & 0xFF;
int ip3 = (ip >> 16) & 0xFF;
int ip4 = (ip >> 24) & 0xFF;
String address = ip1 + "." + ip2 + "." + ip3 + "." + ip4;
customData.put("Wi-Fi address", address);
}
}
// Is Bluetooth available?
BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
boolean btAvailable = bt != null;
// Is Bluetooth enabled?
boolean btEnabled = bt != null && bt.isEnabled() &&
!StringUtils.isNullOrEmpty(bt.getAddress());
// Is Bluetooth connectable?
boolean btConnectable = bt != null &&
(bt.getScanMode() == SCAN_MODE_CONNECTABLE ||
bt.getScanMode() ==
SCAN_MODE_CONNECTABLE_DISCOVERABLE);
// Is Bluetooth discoverable?
boolean btDiscoverable = bt != null &&
bt.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
String btStatus;
if (btAvailable) btStatus = "Available, ";
else btStatus = "Not available, ";
if (btEnabled) btStatus += "enabled, ";
else btStatus += "not enabled, ";
if (btConnectable) btStatus += "connectable, ";
else btStatus += "not connectable, ";
if (btDiscoverable) btStatus += "discoverable";
else btStatus += "not discoverable";
customData.put("Bluetooth status", btStatus);
if (bt != null)
customData.put("Bluetooth address", bt.getAddress());
String btSettingsAddr;
try {
btSettingsAddr = Settings.Secure.getString(
ctx.getContentResolver(), "bluetooth_address");
} catch (SecurityException e) {
btSettingsAddr = "Could not get address from settings";
}
customData.put("Bluetooth address from settings", btSettingsAddr);
return Collections.unmodifiableMap(customData);
}
}
private static class SingleShotAndroidExecutor extends Thread {
private final Runnable runnable;
private SingleShotAndroidExecutor(Runnable runnable) {
this.runnable = runnable;
}
@Override
public void run() {
Looper.prepare();
Handler handler = new Handler();
handler.post(runnable);
handler.post(new Runnable() {
@Override
public void run() {
Looper looper = Looper.myLooper();
if (looper != null) looper.quit();
}
});
Looper.loop();
}
}
}

View File

@@ -0,0 +1,51 @@
package org.briarproject.android.report;
import android.content.Context;
import android.support.annotation.NonNull;
import org.acra.collector.CrashReportData;
import org.acra.sender.ReportSender;
import org.acra.sender.ReportSenderException;
import org.acra.util.JSONReportBuilder;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.util.AndroidUtils;
import org.briarproject.api.reporting.DevReporter;
import java.io.File;
import java.io.FileNotFoundException;
import javax.inject.Inject;
import static org.acra.ReportField.REPORT_ID;
public class BriarReportSender implements ReportSender {
private final AndroidComponent component;
@Inject
protected DevReporter reporter;
public BriarReportSender(AndroidComponent component) {
this.component = component;
}
@Override
public void send(@NonNull Context ctx,
@NonNull CrashReportData errorContent)
throws ReportSenderException {
component.inject(this);
String crashReport;
try {
crashReport = errorContent.toJSON().toString();
} catch (JSONReportBuilder.JSONReportException e) {
throw new ReportSenderException("Couldn't create JSON", e);
}
try {
File reportDir = AndroidUtils.getReportDir(ctx);
String reportId = errorContent.getProperty(REPORT_ID);
reporter.encryptReportToFile(reportDir, reportId, crashReport);
} catch (FileNotFoundException e) {
throw new ReportSenderException("Failed to encrypt report", e);
}
}
}

View File

@@ -0,0 +1,21 @@
package org.briarproject.android.report;
import android.content.Context;
import android.support.annotation.NonNull;
import org.acra.config.ACRAConfiguration;
import org.acra.sender.ReportSender;
import org.acra.sender.ReportSenderFactory;
import org.briarproject.android.BriarApplication;
public class BriarReportSenderFactory implements ReportSenderFactory {
@NonNull
@Override
public ReportSender create(@NonNull Context ctx,
@NonNull ACRAConfiguration config) {
// ACRA passes in the Application as context
BriarApplication app = (BriarApplication) ctx;
return new BriarReportSender(app.getApplicationComponent());
}
}

View File

@@ -0,0 +1,321 @@
package org.briarproject.android.report;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.acra.ACRA;
import org.acra.ReportField;
import org.acra.collector.CrashReportData;
import org.acra.dialog.BaseCrashReportDialog;
import org.acra.file.CrashReportPersister;
import org.acra.prefs.SharedPreferencesFactory;
import org.briarproject.R;
import org.briarproject.android.util.UserFeedback;
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.content.DialogInterface.BUTTON_POSITIVE;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static java.util.logging.Level.WARNING;
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;
public class DevReportActivity extends BaseCrashReportDialog
implements DialogInterface.OnClickListener,
DialogInterface.OnCancelListener,
CompoundButton.OnCheckedChangeListener {
private static final Logger LOG =
Logger.getLogger(DevReportActivity.class.getName());
private static final String PREF_EXCLUDED_FIELDS = "excludedReportFields";
private static final String STATE_REVIEWING = "reviewing";
private static final Set<ReportField> 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);
}
private SharedPreferencesFactory sharedPreferencesFactory;
private Set<ReportField> excludedFields;
private EditText userCommentView = null;
private EditText userEmailView = null;
private CheckBox includeDebugReport = null;
private LinearLayout report = null;
private View progress = null;
private View share = null;
private boolean reviewing = false;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_dev_report);
sharedPreferencesFactory = new SharedPreferencesFactory(
getApplicationContext(), getConfig());
SharedPreferences prefs = sharedPreferencesFactory.create();
excludedFields = new HashSet<>();
if (Build.VERSION.SDK_INT >= 11) {
for (String name : prefs.getStringSet(PREF_EXCLUDED_FIELDS,
new HashSet<String>())) {
excludedFields.add(ReportField.valueOf(name));
}
}
TextView title = (TextView) findViewById(R.id.title);
userCommentView = (EditText) findViewById(R.id.user_comment);
userEmailView = (EditText) findViewById(R.id.user_email);
includeDebugReport = (CheckBox) findViewById(R.id.include_debug_report);
report = (LinearLayout) findViewById(R.id.report_content);
progress = findViewById(R.id.progress_wheel);
share = findViewById(R.id.share_dev_report);
title.setText(isFeedback() ? R.string.feedback_title :
R.string.crash_report_title);
userCommentView.setHint(isFeedback() ? R.string.enter_feedback :
R.string.describe_crash);
includeDebugReport.setVisibility(isFeedback() ? VISIBLE : GONE);
report.setVisibility(isFeedback() ? GONE : VISIBLE);
includeDebugReport.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (includeDebugReport.isChecked())
refresh();
else
report.setVisibility(GONE);
}
});
share.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
processReport();
}
});
String userEmail = prefs.getString(ACRA.PREF_USER_EMAIL_ADDRESS, "");
userEmailView.setText(userEmail);
if (state != null)
reviewing = state.getBoolean(STATE_REVIEWING, false);
}
@Override
public void onResume() {
super.onResume();
if (!isFeedback() && !reviewing) showCrashDialog();
if (!isFeedback() || includeDebugReport.isChecked()) refresh();
}
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
state.putBoolean(STATE_REVIEWING, reviewing);
}
@Override
public void onBackPressed() {
closeReport();
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == BUTTON_POSITIVE) dialog.dismiss();
else dialog.cancel();
}
@Override
public void onCancel(DialogInterface dialog) {
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);
}
}
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
private boolean isFeedback() {
return getException() instanceof UserFeedback;
}
private void showCrashDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this,
R.style.BriarDialogTheme);
builder.setTitle(R.string.dialog_title_share_crash_report)
.setIcon(R.drawable.ic_warning_black_24dp)
.setMessage(R.string.dialog_message_share_crash_report)
.setPositiveButton(R.string.dialog_button_ok, this)
.setNegativeButton(R.string.cancel_button, this);
AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
dialog.setOnCancelListener(this);
dialog.show();
}
private void refresh() {
report.setVisibility(INVISIBLE);
progress.setVisibility(VISIBLE);
report.removeAllViews();
new AsyncTask<Void, Void, CrashReportData>() {
@Override
protected CrashReportData doInBackground(Void... args) {
File reportFile = (File) getIntent().getSerializableExtra(
EXTRA_REPORT_FILE);
final CrashReportPersister persister =
new CrashReportPersister();
try {
return persister.load(reportFile);
} catch (IOException 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<ReportField, String> e : crashData.entrySet()) {
ReportField field = e.getKey();
boolean required = requiredFields.contains(field);
boolean excluded = excludedFields.contains(field);
View v = inflater.inflate(R.layout.list_item_crash,
report, false);
CheckBox cb = (CheckBox) v
.findViewById(R.id.include_in_report);
cb.setTag(field);
cb.setChecked(required || !excluded);
cb.setEnabled(!required);
cb.setOnCheckedChangeListener(DevReportActivity.this);
((TextView) v.findViewById(R.id.title))
.setText(e.getKey().toString());
((TextView) v.findViewById(R.id.content))
.setText(e.getValue());
report.addView(v);
}
} else {
View v = inflater.inflate(
android.R.layout.simple_list_item_1, report, false);
((TextView) v.findViewById(android.R.id.text1))
.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);
share.setEnabled(false);
progress.setVisibility(VISIBLE);
final boolean includeReport =
!isFeedback() || includeDebugReport.isChecked();
new AsyncTask<Void, Void, Boolean>() {
@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<Entry<ReportField, String>> iter =
data.entrySet().iterator();
while (iter.hasNext()) {
Entry<ReportField, String> e = iter.next();
if (!requiredFields.contains(e.getKey())) {
iter.remove();
}
}
}
persister.store(data, reportFile);
return true;
} catch (IOException e) {
LOG.log(WARNING, "Error processing report file", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
final SharedPreferences prefs =
sharedPreferencesFactory.create();
if (Build.VERSION.SDK_INT >= 11) {
final SharedPreferences.Editor prefEditor =
prefs.edit();
Set<String> fields = new HashSet<>();
for (ReportField field : excludedFields) {
fields.add(field.name());
}
prefEditor.putStringSet(PREF_EXCLUDED_FIELDS, fields);
prefEditor.commit();
}
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() {
cancelReports();
finish();
}
}