diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/reporting/DevReporter.java b/bramble-api/src/main/java/org/briarproject/bramble/api/reporting/DevReporter.java index 9ef25b5f5..392886a2f 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/reporting/DevReporter.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/reporting/DevReporter.java @@ -23,6 +23,8 @@ public interface DevReporter { /** * Sends any reports previously stored on disk. + * + * @return The number of reports that were sent. */ - void sendReports(); + int sendReports(); } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/reporting/DevReporterImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/reporting/DevReporterImpl.java index 228569fa2..e32f4757c 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/reporting/DevReporterImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/reporting/DevReporterImpl.java @@ -29,6 +29,7 @@ import javax.annotation.concurrent.Immutable; import javax.inject.Inject; import javax.net.SocketFactory; +import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.bramble.util.IoUtils.tryToClose; @@ -100,11 +101,12 @@ class DevReporterImpl implements DevReporter, EventListener { } @Override - public void sendReports() { + public int sendReports() { File reportDir = devConfig.getReportDir(); File[] reports = reportDir.listFiles(); + int reportsSent = 0; if (reports == null || reports.length == 0) - return; // No reports to send + return reportsSent; // No reports to send LOG.info("Sending reports to developers"); for (File f : reports) { @@ -116,13 +118,15 @@ class DevReporterImpl implements DevReporter, EventListener { in = new FileInputStream(f); IoUtils.copyAndClose(in, out); f.delete(); + reportsSent++; } catch (IOException e) { LOG.log(WARNING, "Failed to send reports", e); tryToClose(out, LOG, WARNING); tryToClose(in, LOG, WARNING); - return; + return reportsSent; } } - LOG.info("Reports sent"); + if (LOG.isLoggable(INFO)) LOG.info(reportsSent + " report(s) sent"); + return reportsSent; } } diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 489c5a248..6d6a46b88 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -100,7 +100,6 @@ dependencies { implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.recyclerview:recyclerview-selection:1.1.0-rc03' - implementation 'ch.acra:acra:4.11' implementation 'info.guardianproject.panic:panic:1.0' implementation 'info.guardianproject.trustedintents:trustedintents:0.2' implementation 'de.hdodenhof:circleimageview:3.0.1' diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index a54a99b34..5d54ab2e1 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -68,14 +68,24 @@ android:exported="false"> + android:windowSoftInputMode="adjustResize|stateHidden" /> + activity, Throwable e) { + final Intent dialogIntent = new Intent(ctx, activity); + dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK); + dialogIntent.putExtra(EXTRA_THROWABLE, e); + dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime); + ctx.startActivity(dialogIntent); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java new file mode 100644 index 000000000..5687c8915 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java @@ -0,0 +1,350 @@ +/* + Some of the code in this file was copied from or inspired by ACRA + which is licenced under Apache 2.0 and authored by F43nd1r. + https://github.com/ACRA/acra/blob/3b9034/acra-core/src/main/java/org/acra/collector/ + */ + +package org.briarproject.briar.android.reporting; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.pm.FeatureInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +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 org.briarproject.bramble.api.Pair; +import org.briarproject.briar.BuildConfig; +import org.briarproject.briar.R; +import org.briarproject.briar.android.BriarApplication; +import org.briarproject.briar.android.logging.BriefLogFormatter; +import org.briarproject.briar.android.reporting.ReportData.MultiReportInfo; +import org.briarproject.briar.android.reporting.ReportData.ReportItem; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Date; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +import androidx.annotation.Nullable; + +import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE; +import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE; +import static android.content.Context.WIFI_P2P_SERVICE; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED; +import static android.os.Build.VERSION.SDK_INT; +import static androidx.core.content.ContextCompat.getSystemService; +import static java.util.Objects.requireNonNull; +import static org.briarproject.bramble.util.AndroidUtils.getBluetoothAddressAndMethod; +import static org.briarproject.bramble.util.PrivacyUtils.scrubInetAddress; +import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress; +import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; + +class BriarReportCollector { + + private final Context ctx; + + BriarReportCollector(Context ctx) { + this.ctx = ctx; + } + + public ReportData collectReportData(@Nullable Throwable t, + long appStartTime) { + return new ReportData() + .add(getBasicInfo(t)) + .add(getDeviceInfo()) + .add(getTimeInfo(appStartTime)) + .add(getMemory()) + .add(getStorage()) + .add(getConnectivity()) + .add(getBuildConfig()) + .add(getLogcat()) + .add(getDeviceFeatures()); + } + + private ReportItem getBasicInfo(@Nullable Throwable t) { + String packageName = ctx.getPackageName(); + PackageManager pm = ctx.getPackageManager(); + String versionName, versionCode; + try { + PackageInfo packageInfo = pm.getPackageInfo(packageName, 0); + versionName = packageInfo.versionName; + versionCode = String.valueOf(packageInfo.versionCode); + } catch (PackageManager.NameNotFoundException e) { + versionName = e.toString(); + versionCode = "?"; + } + MultiReportInfo basicInfo = new MultiReportInfo() + .add("Package name", packageName) + .add("Version name", versionName) + .add("Version code", versionCode) + .add("Android version", Build.VERSION.RELEASE) + .add("Android SDK API", String.valueOf(SDK_INT)); + // print stacktrace of Throwable if this is not feedback + if (t != null) { + final Writer sw = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(sw); + if (!isNullOrEmpty(t.getMessage())) { + printWriter.println(t.getMessage()); + } + t.printStackTrace(printWriter); + basicInfo.add("stracktrace", sw.toString()); + } + return new ReportItem("BasicInfo", R.string.dev_report_basic_info, + basicInfo, false); + } + + private ReportItem getDeviceInfo() { + MultiReportInfo deviceInfo = new MultiReportInfo() + .add("Product", Build.PRODUCT) + .add("Model", Build.MODEL) + .add("Brand", Build.BRAND); + return new ReportItem("DeviceInfo", R.string.dev_report_device_info, + deviceInfo); + } + + private ReportItem getTimeInfo(long startTime) { + MultiReportInfo timeInfo = new MultiReportInfo() + .add("App start time", formatTime(startTime)) + .add("Crash time", formatTime(System.currentTimeMillis())); + return new ReportItem("DeviceInfo", R.string.dev_report_time_info, + timeInfo); + } + + private String formatTime(long time) { + return new Date(time) + " (" + time + ")"; + } + + private ReportItem getMemory() { + // System memory + ActivityManager am = getSystemService(ctx, ActivityManager.class); + ActivityManager.MemoryInfo mem = new ActivityManager.MemoryInfo(); + requireNonNull(am).getMemoryInfo(mem); + String systemMemory; + systemMemory = (mem.totalMem / 1024 / 1024) + " MiB total, " + + (mem.availMem / 1024 / 1204) + " MiB free, " + + (mem.threshold / 1024 / 1024) + " MiB threshold"; + + // 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"; + + MultiReportInfo memInfo = new MultiReportInfo() + .add("System memory", systemMemory) + .add("Virtual machine memory", vmMemory); + return new ReportItem("Memory", R.string.dev_report_memory, memInfo); + } + + private ReportItem getStorage() { + // 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"; + + // 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"; + + MultiReportInfo storageInfo = new MultiReportInfo() + .add("Internal storage", internal) + .add("External storage", external); + return new ReportItem("Storage", R.string.dev_report_storage, + storageInfo); + } + + + private ReportItem getConnectivity() { + MultiReportInfo connectivityInfo = new MultiReportInfo(); + + // Is mobile data available? + ConnectivityManager cm = requireNonNull( + getSystemService(ctx, ConnectivityManager.class)); + 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); + //noinspection ConstantConditions + mobileEnabled = (Boolean) method.invoke(cm); + } catch (ClassNotFoundException + | NoSuchMethodException + | IllegalArgumentException + | InvocationTargetException + | IllegalAccessException e) { + connectivityInfo + .add("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"; + connectivityInfo.add("Mobile data status", mobileStatus); + + // Is wifi available? + NetworkInfo wifi = cm.getNetworkInfo(TYPE_WIFI); + boolean wifiAvailable = wifi != null && wifi.isAvailable(); + // Is wifi enabled? + WifiManager wm = getSystemService(ctx, WifiManager.class); + 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"; + connectivityInfo.add("Wi-Fi status", wifiStatus); + + // Is wifi direct supported? + String wifiDirectStatus = "Supported"; + if (ctx.getSystemService(WIFI_P2P_SERVICE) == null) + wifiDirectStatus = "Not supported"; + connectivityInfo.add("Wi-Fi Direct", wifiDirectStatus); + + if (wm != null) { + WifiInfo wifiInfo = wm.getConnectionInfo(); + if (wifiInfo != null) { + int ip = wifiInfo.getIpAddress(); // Nice API, Google + byte[] ipBytes = new byte[4]; + ipBytes[0] = (byte) (ip & 0xFF); + ipBytes[1] = (byte) ((ip >> 8) & 0xFF); + ipBytes[2] = (byte) ((ip >> 16) & 0xFF); + ipBytes[3] = (byte) ((ip >> 24) & 0xFF); + try { + InetAddress address = InetAddress.getByAddress(ipBytes); + connectivityInfo + .add("Wi-Fi address", scrubInetAddress(address)); + } catch (UnknownHostException ignored) { + // Should only be thrown if address has illegal length + } + } + } + + // Is Bluetooth available? + BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); + if (bt == null) { + connectivityInfo.add("Bluetooth status", "Not available"); + } else { + // Is Bluetooth enabled? + @SuppressLint("HardwareIds") + boolean btEnabled = bt.isEnabled() + && !isNullOrEmpty(bt.getAddress()); + // Is Bluetooth connectable? + int scanMode = bt.getScanMode(); + boolean btConnectable = scanMode == SCAN_MODE_CONNECTABLE || + scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE; + // Is Bluetooth discoverable? + boolean btDiscoverable = + scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE; + + String btStatus; + if (btEnabled) btStatus = "Available, enabled, "; + else btStatus = "Available, not enabled, "; + if (btConnectable) btStatus += "connectable, "; + else btStatus += "not connectable, "; + if (btDiscoverable) btStatus += "discoverable"; + else btStatus += "not discoverable"; + connectivityInfo.add("Bluetooth status", btStatus); + + if (SDK_INT >= 21) { + // Is Bluetooth LE scanning and advertising supported? + boolean btLeScan = bt.getBluetoothLeScanner() != null; + boolean btLeAdvertise = + bt.getBluetoothLeAdvertiser() != null; + String btLeStatus; + if (btLeScan) btLeStatus = "Scanning, "; + else btLeStatus = "No scanning, "; + if (btLeAdvertise) btLeStatus += "advertising"; + else btLeStatus += "no advertising"; + connectivityInfo.add("Bluetooth LE status", btLeStatus); + } + + Pair p = getBluetoothAddressAndMethod(ctx, bt); + String address = p.getFirst(); + String method = p.getSecond(); + connectivityInfo.add("Bluetooth address", scrubMacAddress(address)); + connectivityInfo.add("Bluetooth address method", method); + } + return new ReportItem("Connectivity", R.string.dev_report_connectivity, + connectivityInfo); + } + + private ReportItem getBuildConfig() { + MultiReportInfo buildConfig = new MultiReportInfo() + .add("GitHash", BuildConfig.GitHash) + .add("BUILD_TYPE", BuildConfig.BUILD_TYPE) + .add("FLAVOR", BuildConfig.FLAVOR) + .add("DEBUG", String.valueOf(BuildConfig.DEBUG)) + .add("BuildTimestamp", + String.valueOf(BuildConfig.BuildTimestamp)); + return new ReportItem("BuildConfig", R.string.dev_report_build_config, + buildConfig); + } + + private ReportItem getLogcat() { + BriarApplication app = (BriarApplication) ctx.getApplicationContext(); + StringBuilder sb = new StringBuilder(); + Formatter formatter = new BriefLogFormatter(); + for (LogRecord record : app.getRecentLogRecords()) { + sb.append(formatter.format(record)).append('\n'); + } + return new ReportItem("Logcat", R.string.dev_report_logcat, + sb.toString()); + } + + private ReportItem getDeviceFeatures() { + PackageManager pm = ctx.getPackageManager(); + FeatureInfo[] features = pm.getSystemAvailableFeatures(); + MultiReportInfo deviceFeatures = new MultiReportInfo(); + for (FeatureInfo feature : features) { + String featureName = feature.name; + if (featureName != null) { + deviceFeatures.add(featureName, "true"); + } else { + deviceFeatures.add("glEsVersion", feature.getGlEsVersion()); + } + } + return new ReportItem("DeviceFeatures", + R.string.dev_report_device_features, deviceFeatures); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportPrimer.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportPrimer.java deleted file mode 100644 index 8c5e3e6e3..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportPrimer.java +++ /dev/null @@ -1,277 +0,0 @@ -package org.briarproject.briar.android.reporting; - -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.Environment; -import android.os.Handler; -import android.os.Looper; - -import org.acra.builder.ReportBuilder; -import org.acra.builder.ReportPrimer; -import org.briarproject.bramble.api.Pair; -import org.briarproject.briar.BuildConfig; -import org.briarproject.briar.android.BriarApplication; -import org.briarproject.briar.android.logging.BriefLogFormatter; - -import java.io.File; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; -import java.util.logging.Formatter; -import java.util.logging.LogRecord; - -import androidx.annotation.NonNull; - -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_P2P_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; -import static android.os.Build.VERSION.SDK_INT; -import static java.util.Collections.unmodifiableMap; -import static org.briarproject.bramble.util.AndroidUtils.getBluetoothAddressAndMethod; -import static org.briarproject.bramble.util.PrivacyUtils.scrubInetAddress; -import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress; -import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; - -public class BriarReportPrimer implements ReportPrimer { - - @Override - public void primeReport(@NonNull Context ctx, - @NonNull ReportBuilder builder) { - CustomDataTask task = new CustomDataTask(ctx); - FutureTask> 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> { - - private final Context ctx; - - private CustomDataTask(Context ctx) { - this.ctx = ctx; - } - - @Override - public Map call() { - Map customData = new LinkedHashMap<>(); - - // Log - BriarApplication app = - (BriarApplication) ctx.getApplicationContext(); - StringBuilder sb = new StringBuilder(); - Formatter formatter = new BriefLogFormatter(); - for (LogRecord record : app.getRecentLogRecords()) { - sb.append(formatter.format(record)).append('\n'); - } - customData.put("Log", sb.toString()); - - // System memory - Object o = ctx.getSystemService(ACTIVITY_SERVICE); - ActivityManager am = (ActivityManager) o; - ActivityManager.MemoryInfo mem = new ActivityManager.MemoryInfo(); - am.getMemoryInfo(mem); - String systemMemory; - systemMemory = (mem.totalMem / 1024 / 1024) + " MiB total, " - + (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.getApplicationContext().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); - - // Is wifi direct supported? - String wifiDirectStatus = "Supported"; - if (ctx.getSystemService(WIFI_P2P_SERVICE) == null) - wifiDirectStatus = "Not supported"; - customData.put("Wi-Fi Direct", wifiDirectStatus); - - if (wm != null) { - WifiInfo wifiInfo = wm.getConnectionInfo(); - if (wifiInfo != null) { - int ip = wifiInfo.getIpAddress(); // Nice API, Google - byte[] ipBytes = new byte[4]; - ipBytes[0] = (byte) (ip & 0xFF); - ipBytes[1] = (byte) ((ip >> 8) & 0xFF); - ipBytes[2] = (byte) ((ip >> 16) & 0xFF); - ipBytes[3] = (byte) ((ip >> 24) & 0xFF); - try { - InetAddress address = InetAddress.getByAddress(ipBytes); - customData.put("Wi-Fi address", - scrubInetAddress(address)); - } catch (UnknownHostException ignored) { - // Should only be thrown if address has illegal length - } - } - } - - // Is Bluetooth available? - BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); - if (bt == null) { - customData.put("Bluetooth status", "Not available"); - } else { - // Is Bluetooth enabled? - boolean btEnabled = bt.isEnabled() - && !isNullOrEmpty(bt.getAddress()); - // Is Bluetooth connectable? - int scanMode = bt.getScanMode(); - boolean btConnectable = scanMode == SCAN_MODE_CONNECTABLE || - scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE; - // Is Bluetooth discoverable? - boolean btDiscoverable = - scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE; - - String btStatus; - if (btEnabled) btStatus = "Available, enabled, "; - else btStatus = "Available, not enabled, "; - if (btConnectable) btStatus += "connectable, "; - else btStatus += "not connectable, "; - if (btDiscoverable) btStatus += "discoverable"; - else btStatus += "not discoverable"; - customData.put("Bluetooth status", btStatus); - - if (SDK_INT >= 21) { - // Is Bluetooth LE scanning and advertising supported? - boolean btLeScan = bt.getBluetoothLeScanner() != null; - boolean btLeAdvertise = - bt.getBluetoothLeAdvertiser() != null; - String btLeStatus; - if (btLeScan) btLeStatus = "Scanning, "; - else btLeStatus = "No scanning, "; - if (btLeAdvertise) btLeStatus += "advertising"; - else btLeStatus += "no advertising"; - customData.put("Bluetooth LE status", btLeStatus); - } - - Pair p = getBluetoothAddressAndMethod(ctx, bt); - String address = p.getFirst(); - String method = p.getSecond(); - customData.put("Bluetooth address", scrubMacAddress(address)); - customData.put("Bluetooth address method", method); - } - - // Git commit ID - customData.put("Commit ID", BuildConfig.GitHash); - - return 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(() -> { - Looper looper = Looper.myLooper(); - if (looper != null) looper.quit(); - }); - Looper.loop(); - } - } -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportSender.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportSender.java deleted file mode 100644 index 312ab6b51..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportSender.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.briarproject.briar.android.reporting; - -import android.content.Context; - -import org.acra.collector.CrashReportData; -import org.acra.sender.ReportSender; -import org.acra.sender.ReportSenderException; -import org.briarproject.bramble.api.reporting.DevReporter; -import org.briarproject.bramble.util.AndroidUtils; -import org.briarproject.briar.android.AndroidComponent; - -import java.io.File; -import java.io.FileNotFoundException; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; - -import static org.acra.ReportField.REPORT_ID; - -public class BriarReportSender implements ReportSender { - - private final AndroidComponent component; - - @Inject - DevReporter reporter; - - BriarReportSender(AndroidComponent component) { - this.component = component; - } - - @Override - public void send(@NonNull Context ctx, - @NonNull CrashReportData errorContent) - throws ReportSenderException { - component.inject(this); - String crashReport = errorContent.toJSON().toString(); - 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); - } - } -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportSenderFactory.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportSenderFactory.java deleted file mode 100644 index 365948952..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportSenderFactory.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.briarproject.briar.android.reporting; - -import android.content.Context; - -import org.acra.config.ACRAConfiguration; -import org.acra.sender.ReportSender; -import org.acra.sender.ReportSenderFactory; -import org.briarproject.briar.android.BriarApplication; - -import androidx.annotation.NonNull; - -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()); - } -} 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 index 892daf0b8..00ab9961d 100644 --- 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 @@ -8,14 +8,37 @@ import android.view.ViewGroup; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.AndroidComponent; +import org.briarproject.briar.android.BriarApplication; + +import javax.inject.Inject; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; @MethodsNotNullByDefault @ParametersNotNullByDefault public class CrashFragment extends Fragment { + @Inject + ViewModelProvider.Factory viewModelFactory; + + private ReportViewModel viewModel; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FragmentActivity a = requireActivity(); + BriarApplication app = + (BriarApplication) a.getApplicationContext(); + AndroidComponent androidComponent = app.getApplicationComponent(); + androidComponent.inject(this); + viewModel = new ViewModelProvider(a, viewModelFactory) + .get(ReportViewModel.class); + } + @Nullable @Override public View onCreateView(LayoutInflater inflater, @@ -25,15 +48,11 @@ public class CrashFragment extends Fragment { .inflate(R.layout.fragment_crash, container, false); v.findViewById(R.id.acceptButton).setOnClickListener(view -> - getDevReportActivity().displayFragment(true)); + viewModel.showReport()); v.findViewById(R.id.declineButton).setOnClickListener(view -> - getDevReportActivity().closeReport()); + viewModel.closeReport()); return v; } - private DevReportActivity getDevReportActivity() { - return (DevReportActivity) requireActivity(); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashReportActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashReportActivity.java new file mode 100644 index 000000000..bd17ea3ab --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashReportActivity.java @@ -0,0 +1,111 @@ +package org.briarproject.briar.android.reporting; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Toast; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.AndroidComponent; +import org.briarproject.briar.android.BriarApplication; +import org.briarproject.briar.android.Localizer; +import org.briarproject.briar.android.logout.HideUiActivity; + +import javax.inject.Inject; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION; +import static android.view.WindowManager.LayoutParams.FLAG_SECURE; +import static android.widget.Toast.LENGTH_LONG; +import static java.util.Objects.requireNonNull; +import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class CrashReportActivity extends AppCompatActivity { + + static final String EXTRA_THROWABLE = "throwable"; + static final String EXTRA_APP_START_TIME = "appStartTime"; + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private ReportViewModel viewModel; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE); + setContentView(R.layout.activity_dev_report); + + AndroidComponent androidComponent = + ((BriarApplication) getApplication()).getApplicationComponent(); + androidComponent.inject(this); + + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(ReportViewModel.class); + Intent intent = getIntent(); + Throwable t = (Throwable) intent.getSerializableExtra(EXTRA_THROWABLE); + long appStartTime = intent.getLongExtra(EXTRA_APP_START_TIME, 0); + viewModel.init(requireNonNull(t), appStartTime); + viewModel.getShowReport().observeEvent(this, show -> { + if (show) displayFragment(true); + }); + viewModel.getCloseReport().observeEvent(this, res -> { + if (res != 0) { + Toast.makeText(this, res, LENGTH_LONG).show(); + } + exit(); + }); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + if (savedInstanceState == null) displayFragment(viewModel.isFeedback()); + } + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(Localizer.getInstance().setLocale(base)); + } + + @Override + public void onBackPressed() { + exit(); + } + + void displayFragment(boolean showReportForm) { + Fragment f; + if (showReportForm) { + f = new ReportFormFragment(); + requireNonNull(getSupportActionBar()).show(); + } else { + f = new CrashFragment(); + requireNonNull(getSupportActionBar()).hide(); + } + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragmentContainer, f, f.getTag()) + .commit(); + } + + void exit() { + if (!viewModel.isFeedback()) { + Intent i = new Intent(this, HideUiActivity.class); + i.addFlags(FLAG_ACTIVITY_NEW_TASK + | FLAG_ACTIVITY_NO_ANIMATION + | FLAG_ACTIVITY_CLEAR_TASK); + startActivity(i); + } + finish(); + } + +} 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 deleted file mode 100644 index 4861bdb50..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/DevReportActivity.java +++ /dev/null @@ -1,188 +0,0 @@ -package org.briarproject.briar.android.reporting; - -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; - -import org.acra.dialog.BaseCrashReportDialog; -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.logout.HideUiActivity; -import org.briarproject.briar.android.util.UserFeedback; - -import java.io.File; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.Fragment; - -import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; -import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; -import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION; -import static android.os.Build.VERSION.SDK_INT; -import static android.view.WindowManager.LayoutParams.FLAG_SECURE; -import static java.util.Objects.requireNonNull; -import static org.acra.ACRAConstants.EXTRA_REPORT_FILE; -import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS; - -@MethodsNotNullByDefault -@ParametersNotNullByDefault -public class DevReportActivity extends BaseCrashReportDialog { - - private AppCompatDelegate delegate; - - private AppCompatDelegate getDelegate() { - if (delegate == null) { - delegate = AppCompatDelegate.create(this, null); - } - return delegate; - } - - @Override - protected void preInit(@Nullable Bundle savedInstanceState) { - super.preInit(savedInstanceState); - getDelegate().installViewFactory(); - getDelegate().onCreate(savedInstanceState); - getDelegate().applyDayNight(); - // We always need to re-apply the theme - // for day/night the changes to take effect. - // On API 23+, we should bypass setTheme(), which will no-op - // if the theme ID is identical to the current theme ID. - int theme = R.style.BriarTheme_NoActionBar; - if (SDK_INT >= 23) { - onApplyThemeResource(getTheme(), theme, false); - } else { - setTheme(theme); - } - } - - @Override - public void init(@Nullable Bundle state) { - super.init(state); - - if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE); - - getDelegate().setContentView(R.layout.activity_dev_report); - - Toolbar toolbar = findViewById(R.id.toolbar); - getDelegate().setSupportActionBar(toolbar); - - String title = getString(isFeedback() ? R.string.feedback_title : - R.string.crash_report_title); - requireNonNull(getDelegate().getSupportActionBar()).setTitle(title); - - if (state == null) displayFragment(isFeedback()); - } - - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext( - Localizer.getInstance().setLocale(base)); - } - - @Override - public void onPostCreate(@Nullable Bundle state) { - super.onPostCreate(state); - getDelegate().onPostCreate(state); - } - - @Override - protected void onStart() { - super.onStart(); - getDelegate().onStart(); - } - - @Override - protected void onPostResume() { - super.onPostResume(); - getDelegate().onPostResume(); - } - - @Override - public void onTitleChanged(CharSequence title, int color) { - super.onTitleChanged(title, color); - getDelegate().setTitle(title); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - getDelegate().onConfigurationChanged(newConfig); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - getDelegate().onSaveInstanceState(outState); - } - - @Override - public void onStop() { - super.onStop(); - getDelegate().onStop(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - getDelegate().onDestroy(); - } - - @Override - public void onBackPressed() { - closeReport(); - } - - void sendCrashReport(String comment, String email) { - sendCrash(comment, email); - } - - private boolean isFeedback() { - return getException() instanceof UserFeedback; - } - - void displayFragment(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, f.getTag()) - .commit(); - - } - - @Override - public void invalidateOptionsMenu() { - super.invalidateOptionsMenu(); - getDelegate().invalidateOptionsMenu(); - } - - void closeReport() { - cancelReports(); - exit(); - } - - void exit() { - if (!isFeedback()) { - Intent i = new Intent(this, HideUiActivity.class); - i.addFlags(FLAG_ACTIVITY_NEW_TASK - | FLAG_ACTIVITY_NO_ANIMATION - | FLAG_ACTIVITY_CLEAR_TASK); - startActivity(i); - } - finish(); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/DevReportModule.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/DevReportModule.java new file mode 100644 index 000000000..440879e86 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/DevReportModule.java @@ -0,0 +1,18 @@ +package org.briarproject.briar.android.reporting; + +import org.briarproject.briar.android.viewmodel.ViewModelKey; + +import androidx.lifecycle.ViewModel; +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.IntoMap; + +@Module +public abstract class DevReportModule { + + @Binds + @IntoMap + @ViewModelKey(ReportViewModel.class) + abstract ViewModel bindReportViewModel(ReportViewModel reportViewModel); + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/FeedbackActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/FeedbackActivity.java new file mode 100644 index 000000000..4564edf29 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/FeedbackActivity.java @@ -0,0 +1,6 @@ +package org.briarproject.briar.android.reporting; + +public class FeedbackActivity extends CrashReportActivity { + // this is used so it can run in the app process, + // while CrashReportActivity runs in a dedicated process +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportData.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportData.java new file mode 100644 index 000000000..507ca57f1 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportData.java @@ -0,0 +1,123 @@ +package org.briarproject.briar.android.reporting; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.annotation.concurrent.Immutable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +class ReportData { + + private final ArrayList items = new ArrayList<>(); + + ReportData add(ReportItem item) { + items.add(item); + return this; + } + + List getItems() { + return items; + } + + @NonNull + public JSONObject toJson(boolean includeReport) throws JSONException { + JSONObject json = new JSONObject(); + for (ReportItem item : items) { + // only include required items when report not added + if (!includeReport && item.isOptional) continue; + // only include what should be included + if (!item.isIncluded) continue; + json.put(item.name, item.info.toJson()); + } + return json; + } + + @NotNullByDefault + static class ReportItem { + final String name; + @StringRes + final int nameRes; + final ReportInfo info; + final boolean isOptional; + volatile boolean isIncluded = true; + + ReportItem(String name, int nameRes, ReportInfo info) { + this(name, nameRes, info, true); + } + + ReportItem(String name, int nameRes, String info) { + this(name, nameRes, new SingleReportInfo(info), true); + } + + ReportItem(String name, int nameRes, ReportInfo info, + boolean isOptional) { + this.name = name; + this.nameRes = nameRes; + this.info = info; + this.isOptional = isOptional; + } + } + + interface ReportInfo { + Object toJson(); + } + + @Immutable + @NotNullByDefault + static class SingleReportInfo implements ReportInfo { + private final String string; + + SingleReportInfo(String string) { + this.string = string; + } + + @Override + public String toString() { + return string; + } + + @Override + public Object toJson() { + return string; + } + } + + @Immutable + @NotNullByDefault + static class MultiReportInfo implements ReportInfo { + private final Map map = new TreeMap<>(); + + MultiReportInfo add(String key, @Nullable String value) { + map.put(key, value == null ? "null" : value); + return this; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + sb + .append(entry.getKey()) + .append("=") + .append(entry.getValue()) + .append("\n"); + } + return sb.toString(); + } + + @Override + public Object toJson() { + return new JSONObject(map); + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportDataAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportDataAdapter.java new file mode 100644 index 000000000..a49e67b20 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportDataAdapter.java @@ -0,0 +1,67 @@ +package org.briarproject.briar.android.reporting; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.reporting.ReportData.ReportItem; + +import java.util.List; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.Adapter; + +@NotNullByDefault +class ReportDataAdapter + extends Adapter { + + private final List items; + + ReportDataAdapter(List items) { + this.items = items; + } + + @Override + public ReportDataViewHolder onCreateViewHolder(ViewGroup parent, + int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_item_crash, parent, false); + return new ReportDataViewHolder(v); + } + + @Override + public void onBindViewHolder(ReportDataViewHolder holder, int position) { + holder.bind(items.get(position)); + } + + @Override + public int getItemCount() { + return items.size(); + } + + static class ReportDataViewHolder extends RecyclerView.ViewHolder { + private final CheckBox cb; + private final TextView content; + + private ReportDataViewHolder(View v) { + super(v); + cb = v.findViewById(R.id.include_in_report); + content = v.findViewById(R.id.content); + } + + public void bind(ReportItem item) { + cb.setChecked(!item.isOptional || item.isIncluded); + cb.setEnabled(item.isOptional); + cb.setOnCheckedChangeListener((buttonView, isChecked) -> + item.isIncluded = isChecked + ); + cb.setText(item.nameRes); + content.setText(item.info.toString()); + } + } + +} 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 index 4dd3f9d2f..dee217a99 100644 --- 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 @@ -1,6 +1,5 @@ package org.briarproject.briar.android.reporting; -import android.os.AsyncTask; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; @@ -10,92 +9,57 @@ 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 android.widget.Toast; -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 org.briarproject.briar.android.AndroidComponent; +import org.briarproject.briar.android.BriarApplication; -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 javax.inject.Inject; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.RecyclerView; -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 android.widget.Toast.LENGTH_SHORT; 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 { +public class ReportFormFragment extends Fragment { - 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); - } + @Inject + ViewModelProvider.Factory viewModelFactory; - private boolean isFeedback; - private File reportFile; + private ReportViewModel viewModel; private EditText userCommentView; private EditText userEmailView; private CheckBox includeDebugReport; private Button chevron; - private LinearLayout report; + private RecyclerView list; 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); + FragmentActivity a = requireActivity(); + BriarApplication app = (BriarApplication) a.getApplicationContext(); + AndroidComponent androidComponent = app.getApplicationComponent(); + androidComponent.inject(this); + viewModel = new ViewModelProvider(a, viewModelFactory) + .get(ReportViewModel.class); } @Nullable @@ -110,15 +74,10 @@ public class ReportFormFragment extends Fragment 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); + list = v.findViewById(R.id.list); progress = v.findViewById(R.id.progress_wheel); - Bundle args = requireArguments(); - isFeedback = args.getBoolean(IS_FEEDBACK); - reportFile = - (File) requireNonNull(args.getSerializable(EXTRA_REPORT_FILE)); - - if (isFeedback) { + if (viewModel.isFeedback()) { includeDebugReport .setText(getString(R.string.include_debug_report_feedback)); userCommentView.setHint(R.string.enter_feedback); @@ -129,163 +88,68 @@ public class ReportFormFragment extends Fragment 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); - } + viewModel.showReportData(show); }); + viewModel.getShowReportData().observe(getViewLifecycleOwner(), show -> { + if (show) { + chevron.setText(R.string.hide); + list.setVisibility(VISIBLE); + if (list.getAdapter() == null) { + progress.setVisibility(VISIBLE); + } else { + progress.setVisibility(INVISIBLE); + } + } else { + chevron.setText(R.string.show); + list.setVisibility(GONE); + progress.setVisibility(INVISIBLE); + } + }); 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); + sendReport.setEnabled(false); + viewModel.getReportData().observe(getViewLifecycleOwner(), data -> { + list.setAdapter(new ReportDataAdapter(data.getItems())); + sendReport.setEnabled(true); + progress.setVisibility(INVISIBLE); + }); super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.action_send_report) { - processReport(); + sendReport(); 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(); - 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() { + private void sendReport() { userCommentView.setEnabled(false); userEmailView.setEnabled(false); requireNonNull(sendReport).setEnabled(false); + list.setVisibility(GONE); // ensures that progress fits on screen 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; - } - } + // Retrieve user's comment and email address, if any + String comment = userCommentView.getText().toString(); + String email = userEmailView.getText().toString(); - @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(); - } + boolean includeReport = includeDebugReport.isChecked(); - private DevReportActivity getDevReportActivity() { - return (DevReportActivity) requireActivity(); + // Send report (now or after next sign-in) + if (viewModel.sendReport(comment, email, includeReport)) { + // trying to send now + Toast.makeText(requireContext(), R.string.dev_report_sending, + LENGTH_SHORT).show(); + } } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportViewModel.java new file mode 100644 index 000000000..719d4ae4e --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportViewModel.java @@ -0,0 +1,215 @@ +package org.briarproject.briar.android.reporting; + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.plugin.Plugin; +import org.briarproject.bramble.api.plugin.PluginManager; +import org.briarproject.bramble.api.plugin.TorConstants; +import org.briarproject.bramble.api.reporting.DevReporter; +import org.briarproject.bramble.util.AndroidUtils; +import org.briarproject.briar.R; +import org.briarproject.briar.android.reporting.ReportData.MultiReportInfo; +import org.briarproject.briar.android.util.UserFeedback; +import org.briarproject.briar.android.viewmodel.LiveEvent; +import org.briarproject.briar.android.viewmodel.MutableLiveEvent; +import org.json.JSONException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.UUID; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.UiThread; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static java.util.Objects.requireNonNull; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; +import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class ReportViewModel extends AndroidViewModel { + + private static final Logger LOG = + getLogger(ReportViewModel.class.getName()); + + private final BriarReportCollector collector; + private final DevReporter reporter; + private final PluginManager pluginManager; + + private final MutableLiveEvent showReport = + new MutableLiveEvent<>(); + private final MutableLiveData showReportData = + new MutableLiveData<>(); + private final MutableLiveData reportData = + new MutableLiveData<>(); + private final MutableLiveEvent closeReport = + new MutableLiveEvent<>(); + private boolean isFeedback; + + @Inject + public ReportViewModel(@NonNull Application application, + DevReporter reporter, PluginManager pluginManager) { + super(application); + this.collector = new BriarReportCollector(application); + this.reporter = reporter; + this.pluginManager = pluginManager; + } + + void init(Throwable throwable, long appStartTime) { + isFeedback = throwable instanceof UserFeedback; + if (reportData.getValue() == null) new SingleShotAndroidExecutor(() -> { + Throwable t = isFeedback ? null : throwable; + reportData.postValue(collector.collectReportData(t, appStartTime)); + }).start(); + } + + boolean isFeedback() { + return isFeedback; + } + + /** + * Call this from the crash screen, if the user wants to report a crash. + */ + @UiThread + void showReport() { + showReport.setEvent(true); + } + + /** + * Will be set to true when the user wants to report a crash. + */ + LiveEvent getShowReport() { + return showReport; + } + + /** + * The report data will be made visible in the UI when visible is true, + * otherwise hidden. + */ + @UiThread + void showReportData(boolean visible) { + showReportData.setValue(visible); + } + + /** + * Will be set to true when the user wants to see report data. + */ + LiveData getShowReportData() { + return showReportData; + } + + /** + * The content of the report + * that will be loaded after {@link #init(Throwable, long)} was called. + */ + LiveData getReportData() { + return reportData; + } + + /** + * Sends reports and returns now if reports are being sent now + * or false, if reports will be sent next time TorPlugin becomes active. + */ + @UiThread + boolean sendReport(String comment, String email, boolean includeReport) { + ReportData data = requireNonNull(reportData.getValue()); + if (!isNullOrEmpty(comment) || isNullOrEmpty(email)) { + MultiReportInfo userInfo = new MultiReportInfo(); + if (!isNullOrEmpty(comment)) userInfo.add("comment", comment); + if (!isNullOrEmpty(email)) userInfo.add("email", email); + data.add(new ReportData.ReportItem("UserInfo", 0, userInfo, false)); + } + + // check the state of the TorPlugin, if this is feedback + boolean sendFeedbackNow; + if (isFeedback) { + Plugin plugin = pluginManager.getPlugin(TorConstants.ID); + sendFeedbackNow = plugin != null && plugin.getState() == ACTIVE; + } else { + sendFeedbackNow = false; + } + + Runnable reportSender = + getReportSender(includeReport, data, sendFeedbackNow); + new SingleShotAndroidExecutor(reportSender).start(); + return sendFeedbackNow; + } + + private Runnable getReportSender(boolean includeReport, ReportData data, + boolean sendFeedbackNow) { + return () -> { + boolean error = false; + try { + File reportDir = AndroidUtils.getReportDir(getApplication()); + String reportId = UUID.randomUUID().toString(); + String report = data.toJson(includeReport).toString(); + reporter.encryptReportToFile(reportDir, reportId, report); + } catch (FileNotFoundException | JSONException e) { + logException(LOG, WARNING, e); + error = true; + } + + int stringRes; + if (error) { + stringRes = R.string.dev_report_error; + } else if (sendFeedbackNow) { + boolean sent = reporter.sendReports() > 0; + stringRes = sent ? + R.string.dev_report_sent : R.string.dev_report_saved; + } else { + stringRes = R.string.dev_report_saved; + } + closeReport.postEvent(stringRes); + }; + } + + @UiThread + void closeReport() { + closeReport.setEvent(0); + } + + /** + * An integer representing a string resource + * informing about the outcome of the report + * or 0 if no information is required, such as when back button was pressed. + */ + LiveEvent getCloseReport() { + return closeReport; + } + + // Used for a new thread as the Android executor thread may have died + 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(() -> { + Looper looper = Looper.myLooper(); + if (looper != null) looper.quit(); + }); + Looper.loop(); + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java index c09f91190..1dd20ca53 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java @@ -26,11 +26,11 @@ import org.briarproject.bramble.api.plugin.TorConstants; import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.SettingsManager; import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent; -import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.plugin.tor.CircumventionProvider; import org.briarproject.bramble.util.StringUtils; import org.briarproject.briar.R; +import org.briarproject.briar.android.BriarApplication; import org.briarproject.briar.android.Localizer; import org.briarproject.briar.android.util.UiUtils; @@ -72,6 +72,7 @@ import static android.provider.Settings.EXTRA_CHANNEL_ID; import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI; import static android.widget.Toast.LENGTH_SHORT; import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR; +import static java.util.Objects.requireNonNull; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE; @@ -92,7 +93,6 @@ import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGT import static org.briarproject.briar.android.navdrawer.NavDrawerActivity.SIGN_OUT_URI; import static org.briarproject.briar.android.util.UiUtils.getCountryDisplayName; import static org.briarproject.briar.android.util.UiUtils.hasScreenLock; -import static org.briarproject.briar.android.util.UiUtils.triggerFeedback; import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID; import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID; import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID; @@ -166,9 +166,6 @@ public class SettingsFragment extends PreferenceFragmentCompat @Inject CircumventionProvider circumventionProvider; - @Inject - AndroidExecutor androidExecutor; - @Override public void onAttach(Context context) { super.onAttach(context); @@ -226,11 +223,14 @@ public class SettingsFragment extends PreferenceFragmentCompat screenLock.setOnPreferenceChangeListener(this); screenLockTimeout.setOnPreferenceChangeListener(this); - findPreference("pref_key_send_feedback").setOnPreferenceClickListener( - preference -> { - triggerFeedback(androidExecutor); - return true; - }); + Preference prefFeedback = + requireNonNull(findPreference("pref_key_send_feedback")); + prefFeedback.setOnPreferenceClickListener(preference -> { + BriarApplication app = + (BriarApplication) requireContext().getApplicationContext(); + app.triggerFeedback(); + return true; + }); if (SDK_INT < 27) { // remove System Default Theme option from preference entries @@ -245,17 +245,15 @@ public class SettingsFragment extends PreferenceFragmentCompat values.remove(getString(R.string.pref_theme_system_value)); theme.setEntryValues(values.toArray(new CharSequence[0])); } + Preference explode = requireNonNull(findPreference("pref_key_explode")); if (IS_DEBUG_BUILD) { - findPreference("pref_key_explode").setOnPreferenceClickListener( - preference -> { - throw new RuntimeException("Boom!"); - } - ); + explode.setOnPreferenceClickListener(preference -> { + throw new RuntimeException("Boom!"); + }); } else { - findPreference("pref_key_explode").setVisible(false); + explode.setVisible(false); findPreference("pref_key_test_data").setVisible(false); - PreferenceGroup testing = - findPreference("pref_key_explode").getParent(); + PreferenceGroup testing = explode.getParent(); if (testing == null) throw new AssertionError(); testing.setVisible(false); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index c22a012ca..d71cdd865 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -27,13 +27,11 @@ import android.widget.TextView; import com.google.android.material.textfield.TextInputLayout; -import org.acra.ACRA; import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.briar.R; import org.briarproject.briar.android.view.ArticleMovementMethod; import org.briarproject.briar.android.widget.LinkDialogFragment; @@ -345,12 +343,6 @@ public class UiUtils { return fm.hasEnrolledFingerprints() && fm.isHardwareDetected(); } - public static void triggerFeedback(AndroidExecutor androidExecutor) { - androidExecutor.runOnBackgroundThread( - () -> ACRA.getErrorReporter() - .handleException(new UserFeedback(), false)); - } - public static boolean enterPressed(int actionId, @Nullable KeyEvent keyEvent) { return actionId == IME_NULL && diff --git a/briar-android/src/main/res/layout/fragment_report_form.xml b/briar-android/src/main/res/layout/fragment_report_form.xml index cbff42b61..7ffc1b9b9 100644 --- a/briar-android/src/main/res/layout/fragment_report_form.xml +++ b/briar-android/src/main/res/layout/fragment_report_form.xml @@ -1,5 +1,5 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 66cd51313..0d9f7f14e 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -568,9 +568,21 @@ Include anonymous data about the crash Include anonymous data about this device Could not load report data. + Basic information + Device information + Time information + Memory + Storage + Connectivity + Build configuration + App log + Device Features Send report Close + Trying to send feedback now… + Feedback sent. Report saved. It will be sent the next time you log into Briar. + Error: Sending report failed. Signing out of Briar… diff --git a/briar-android/src/test/java/org/briarproject/briar/android/TestBriarApplication.java b/briar-android/src/test/java/org/briarproject/briar/android/TestBriarApplication.java index 7535d5cc4..e8288eb9e 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/TestBriarApplication.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/TestBriarApplication.java @@ -74,6 +74,10 @@ public class TestBriarApplication extends Application return prefs; } + @Override + public void triggerFeedback() { + } + @Override public boolean isRunningInBackground() { return false; diff --git a/briar-android/witness.gradle b/briar-android/witness.gradle index 3e76e3f65..dc2a9f174 100644 --- a/briar-android/witness.gradle +++ b/briar-android/witness.gradle @@ -64,7 +64,6 @@ dependencyVerification { 'androidx.viewpager:viewpager:1.0.0:viewpager-1.0.0.aar:147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682', 'backport-util-concurrent:backport-util-concurrent:3.1:backport-util-concurrent-3.1.jar:f5759b7fcdfc83a525a036deedcbd32e5b536b625ebc282426f16ca137eb5902', 'cglib:cglib:3.2.0:cglib-3.2.0.jar:adb13bab79712ad6bdf1bd59f2a3918018a8016e722e8a357065afb9e6690861', - 'ch.acra:acra:4.11:acra-4.11.aar:21ca06be074749c9aaf3f7df67fcbe3695e633b92e691f025af55cabde22e551', 'classworlds:classworlds:1.1-alpha-2:classworlds-1.1-alpha-2.jar:2bf4e59f3acd106fea6145a9a88fe8956509f8b9c0fdd11eb96fee757269e3f3', 'com.almworks.sqlite4java:sqlite4java:0.282:sqlite4java-0.282.jar:9e1d8dd83ca6003f841e3af878ce2dc7c22497493a7bb6d1b62ec1b0d0a83c05', 'com.android.tools.analytics-library:protos:27.1.1:protos-27.1.1.jar:13f77e73762e58ab372d140b3a6be6903aea9775b62dd14fbc62d4cc7069c9a4',