mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-11 18:29:05 +01:00
Merge branch '1720-no-acra' into 'master'
Remove ACRA and implement the few bits we need ourselves Closes #1114, #1720, and #1793 See merge request briar/briar!1319
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -68,14 +68,21 @@
|
||||
android:exported="false"></service>
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.reporting.DevReportActivity"
|
||||
android:name="org.briarproject.briar.android.reporting.CrashReportActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:label="@string/crash_report_title"
|
||||
android:launchMode="singleInstance"
|
||||
android:process=":briar_error_handler"
|
||||
android:theme="@style/BriarTheme.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden"></activity>
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.reporting.FeedbackActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/feedback_title"
|
||||
android:theme="@style/BriarTheme.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.splash.ExpiredActivity"
|
||||
|
||||
@@ -33,7 +33,6 @@ import org.briarproject.briar.BriarCoreModule;
|
||||
import org.briarproject.briar.android.attachment.AttachmentModule;
|
||||
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
|
||||
import org.briarproject.briar.android.login.SignInReminderReceiver;
|
||||
import org.briarproject.briar.android.reporting.BriarReportSender;
|
||||
import org.briarproject.briar.android.view.EmojiTextInputView;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.android.DozeWatchdog;
|
||||
@@ -173,8 +172,6 @@ public interface AndroidComponent
|
||||
|
||||
void inject(BriarService briarService);
|
||||
|
||||
void inject(BriarReportSender briarReportSender);
|
||||
|
||||
void inject(NotificationCleanupService notificationCleanupService);
|
||||
|
||||
void inject(EmojiTextInputView textInputView);
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.briarproject.briar.android.account.LockManagerImpl;
|
||||
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
|
||||
import org.briarproject.briar.android.login.LoginModule;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
|
||||
import org.briarproject.briar.android.reporting.DevReportModule;
|
||||
import org.briarproject.briar.android.viewmodel.ViewModelModule;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.android.DozeWatchdog;
|
||||
@@ -63,7 +64,8 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
ContactExchangeModule.class,
|
||||
LoginModule.class,
|
||||
NavDrawerModule.class,
|
||||
ViewModelModule.class
|
||||
ViewModelModule.class,
|
||||
DevReportModule.class
|
||||
})
|
||||
public class AppModule {
|
||||
|
||||
|
||||
@@ -14,19 +14,13 @@ import android.preference.PreferenceManager;
|
||||
import com.vanniktech.emoji.EmojiManager;
|
||||
import com.vanniktech.emoji.google.GoogleEmojiProvider;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.ReportingInteractionMode;
|
||||
import org.acra.annotation.ReportsCrashes;
|
||||
import org.briarproject.bramble.BrambleAndroidEagerSingletons;
|
||||
import org.briarproject.bramble.BrambleAppComponent;
|
||||
import org.briarproject.bramble.BrambleCoreEagerSingletons;
|
||||
import org.briarproject.briar.BriarCoreEagerSingletons;
|
||||
import org.briarproject.briar.BuildConfig;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.logging.CachingLogHandler;
|
||||
import org.briarproject.briar.android.reporting.BriarReportPrimer;
|
||||
import org.briarproject.briar.android.reporting.BriarReportSenderFactory;
|
||||
import org.briarproject.briar.android.reporting.DevReportActivity;
|
||||
import org.briarproject.briar.android.reporting.BriarExceptionHandler;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -34,50 +28,14 @@ import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
|
||||
import static java.util.logging.Level.FINE;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
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.BRAND;
|
||||
import static org.acra.ReportField.BUILD_CONFIG;
|
||||
import static org.acra.ReportField.CRASH_CONFIGURATION;
|
||||
import static org.acra.ReportField.CUSTOM_DATA;
|
||||
import static org.acra.ReportField.DEVICE_FEATURES;
|
||||
import static org.acra.ReportField.DISPLAY;
|
||||
import static org.acra.ReportField.INITIAL_CONFIGURATION;
|
||||
import static org.acra.ReportField.PACKAGE_NAME;
|
||||
import static org.acra.ReportField.PHONE_MODEL;
|
||||
import static org.acra.ReportField.PRODUCT;
|
||||
import static org.acra.ReportField.REPORT_ID;
|
||||
import static org.acra.ReportField.STACK_TRACE;
|
||||
import static org.acra.ReportField.USER_APP_START_DATE;
|
||||
import static org.acra.ReportField.USER_CRASH_DATE;
|
||||
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
|
||||
@ReportsCrashes(
|
||||
reportPrimerClass = BriarReportPrimer.class,
|
||||
logcatArguments = {"-d", "-v", "time", "*:I"},
|
||||
reportSenderFactoryClasses = {BriarReportSenderFactory.class},
|
||||
mode = ReportingInteractionMode.DIALOG,
|
||||
reportDialogClass = DevReportActivity.class,
|
||||
resDialogOkToast = R.string.dev_report_saved,
|
||||
deleteOldUnsentReportsOnApplicationStart = false,
|
||||
buildConfigClass = BuildConfig.class,
|
||||
customReportContent = {
|
||||
REPORT_ID,
|
||||
APP_VERSION_CODE, APP_VERSION_NAME, PACKAGE_NAME,
|
||||
PHONE_MODEL, ANDROID_VERSION, BRAND, PRODUCT,
|
||||
BUILD_CONFIG,
|
||||
CUSTOM_DATA,
|
||||
STACK_TRACE,
|
||||
INITIAL_CONFIGURATION, CRASH_CONFIGURATION,
|
||||
DISPLAY, DEVICE_FEATURES,
|
||||
USER_APP_START_DATE, USER_CRASH_DATE
|
||||
}
|
||||
)
|
||||
public class BriarApplicationImpl extends Application
|
||||
implements BriarApplication {
|
||||
|
||||
@@ -85,12 +43,15 @@ public class BriarApplicationImpl extends Application
|
||||
getLogger(BriarApplicationImpl.class.getName());
|
||||
|
||||
private final CachingLogHandler logHandler = new CachingLogHandler();
|
||||
private final BriarExceptionHandler exceptionHandler =
|
||||
new BriarExceptionHandler(this);
|
||||
|
||||
private AndroidComponent applicationComponent;
|
||||
private volatile SharedPreferences prefs;
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
|
||||
if (prefs == null)
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(base);
|
||||
// Loading the language needs to be done here.
|
||||
@@ -98,7 +59,6 @@ public class BriarApplicationImpl extends Application
|
||||
super.attachBaseContext(
|
||||
Localizer.getInstance().setLocale(base));
|
||||
setTheme(base, prefs);
|
||||
ACRA.init(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,7 +104,7 @@ public class BriarApplicationImpl extends Application
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
Localizer.getInstance().setLocale(this);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ import org.briarproject.briar.android.privategroup.memberlist.GroupMemberModule;
|
||||
import org.briarproject.briar.android.privategroup.reveal.GroupRevealModule;
|
||||
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
|
||||
import org.briarproject.briar.android.privategroup.reveal.RevealContactsFragment;
|
||||
import org.briarproject.briar.android.reporting.CrashFragment;
|
||||
import org.briarproject.briar.android.reporting.CrashReportActivity;
|
||||
import org.briarproject.briar.android.reporting.ReportFormFragment;
|
||||
import org.briarproject.briar.android.settings.SettingsActivity;
|
||||
import org.briarproject.briar.android.settings.SettingsFragment;
|
||||
import org.briarproject.briar.android.sharing.BlogInvitationActivity;
|
||||
@@ -184,6 +187,8 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(PendingContactListActivity activity);
|
||||
|
||||
void inject(CrashReportActivity crashReportActivity);
|
||||
|
||||
// Fragments
|
||||
|
||||
void inject(AuthorNameFragment fragment);
|
||||
@@ -234,4 +239,8 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(ImageFragment imageFragment);
|
||||
|
||||
void inject(ReportFormFragment reportFormFragment);
|
||||
|
||||
void inject(CrashFragment crashFragment);
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import org.briarproject.briar.android.controller.ActivityLifecycleController;
|
||||
import org.briarproject.briar.android.forum.ForumModule;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
|
||||
import org.briarproject.briar.android.reporting.DevReportActivity;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
import org.briarproject.briar.android.widget.TapSafeFrameLayout;
|
||||
import org.briarproject.briar.android.widget.TapSafeFrameLayout.OnTapFilteredListener;
|
||||
@@ -50,7 +49,6 @@ import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
|
||||
|
||||
/**
|
||||
* Warning: Some activities don't extend {@link BaseActivity}.
|
||||
* E.g. {@link DevReportActivity}
|
||||
*/
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
@@ -123,6 +121,7 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
return new ActivityModule(this);
|
||||
}
|
||||
|
||||
// TODO use a test module where this is used in tests
|
||||
protected ForumModule getForumModule() {
|
||||
return new ForumModule();
|
||||
}
|
||||
|
||||
@@ -10,14 +10,11 @@ import android.widget.TextView;
|
||||
|
||||
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.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
@@ -41,9 +38,6 @@ public class ContactExchangeErrorFragment extends BaseFragment {
|
||||
return f;
|
||||
}
|
||||
|
||||
@Inject
|
||||
AndroidExecutor androidExecutor;
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
@@ -88,8 +82,8 @@ public class ContactExchangeErrorFragment extends BaseFragment {
|
||||
}
|
||||
|
||||
private void triggerFeedback() {
|
||||
UiUtils.triggerFeedback(requireContext());
|
||||
finish();
|
||||
UiUtils.triggerFeedback(androidExecutor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Process;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
|
||||
import static org.briarproject.briar.android.util.UiUtils.startDevReportActivity;
|
||||
|
||||
@NotNullByDefault
|
||||
public class BriarExceptionHandler implements UncaughtExceptionHandler {
|
||||
|
||||
private final Context ctx;
|
||||
private final long appStartTime;
|
||||
|
||||
public BriarExceptionHandler(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
this.appStartTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
// activity runs in its own process, so we can kill the old one
|
||||
startDevReportActivity(ctx, CrashReportActivity.class, e, appStartTime);
|
||||
Process.killProcess(Process.myPid());
|
||||
System.exit(10);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
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.content.pm.PackageManager.NameNotFoundException;
|
||||
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.bramble.api.nullsafety.NotNullByDefault;
|
||||
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 org.briarproject.briar.android.reporting.ReportData.SingleReportInfo;
|
||||
|
||||
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.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
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.Locale.US;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.TimeZone.getTimeZone;
|
||||
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;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class BriarReportCollector {
|
||||
|
||||
private final Context ctx;
|
||||
|
||||
BriarReportCollector(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
public ReportData collectReportData(@Nullable Throwable t,
|
||||
long appStartTime) {
|
||||
ReportData reportData = new ReportData()
|
||||
.add(getBasicInfo(t))
|
||||
.add(getDeviceInfo());
|
||||
if (t != null) reportData.add(getStacktrace(t));
|
||||
return reportData
|
||||
.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;
|
||||
int versionCode;
|
||||
try {
|
||||
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
|
||||
versionName = packageInfo.versionName;
|
||||
versionCode = packageInfo.versionCode;
|
||||
} catch (NameNotFoundException e) {
|
||||
versionName = e.toString();
|
||||
versionCode = 0;
|
||||
}
|
||||
MultiReportInfo basicInfo = new MultiReportInfo()
|
||||
.add("PackageName", packageName)
|
||||
.add("VersionName", versionName)
|
||||
.add("VersionCode", versionCode)
|
||||
.add("IsCrashReport", t != null);
|
||||
return new ReportItem("BasicInfo", R.string.dev_report_basic_info,
|
||||
basicInfo, false);
|
||||
}
|
||||
|
||||
private ReportItem getDeviceInfo() {
|
||||
MultiReportInfo deviceInfo = new MultiReportInfo()
|
||||
.add("AndroidVersion", Build.VERSION.RELEASE)
|
||||
.add("AndroidApi", SDK_INT)
|
||||
.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 getStacktrace(Throwable t) {
|
||||
final Writer sw = new StringWriter();
|
||||
final PrintWriter printWriter = new PrintWriter(sw);
|
||||
if (!isNullOrEmpty(t.getMessage())) {
|
||||
printWriter.println(t.getMessage());
|
||||
}
|
||||
t.printStackTrace(printWriter);
|
||||
SingleReportInfo stacktrace = new SingleReportInfo(sw.toString());
|
||||
return new ReportItem("Stacktrace", R.string.dev_report_stacktrace,
|
||||
stacktrace);
|
||||
}
|
||||
|
||||
private ReportItem getTimeInfo(long startTime) {
|
||||
MultiReportInfo timeInfo = new MultiReportInfo()
|
||||
.add("ReportTime", formatTime(System.currentTimeMillis()));
|
||||
if (startTime > -1) {
|
||||
timeInfo.add("AppStartTime", formatTime(startTime));
|
||||
}
|
||||
return new ReportItem("TimeInfo", R.string.dev_report_time_info,
|
||||
timeInfo);
|
||||
}
|
||||
|
||||
private String formatTime(long time) {
|
||||
SimpleDateFormat format =
|
||||
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", US);
|
||||
format.setTimeZone(getTimeZone("UTC"));
|
||||
return format.format(new Date(time));
|
||||
}
|
||||
|
||||
private ReportItem getMemory() {
|
||||
MultiReportInfo memInfo = new MultiReportInfo();
|
||||
|
||||
// System memory
|
||||
ActivityManager am = getSystemService(ctx, ActivityManager.class);
|
||||
ActivityManager.MemoryInfo mem = new ActivityManager.MemoryInfo();
|
||||
requireNonNull(am).getMemoryInfo(mem);
|
||||
memInfo.add("SystemMemoryTotal", mem.totalMem);
|
||||
memInfo.add("SystemMemoryFree", mem.availMem);
|
||||
memInfo.add("SystemMemoryThreshold", mem.threshold);
|
||||
|
||||
// Virtual machine memory
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
memInfo.add("VirtualMachineMemoryAllocated", runtime.totalMemory());
|
||||
memInfo.add("VirtualMachineMemoryFree", runtime.freeMemory());
|
||||
memInfo.add("VirtualMachineMemoryMaximum", runtime.maxMemory());
|
||||
|
||||
return new ReportItem("Memory", R.string.dev_report_memory, memInfo);
|
||||
}
|
||||
|
||||
private ReportItem getStorage() {
|
||||
MultiReportInfo storageInfo = new MultiReportInfo();
|
||||
|
||||
// Internal storage
|
||||
File root = Environment.getRootDirectory();
|
||||
storageInfo.add("InternalStorageTotal", root.getTotalSpace());
|
||||
storageInfo.add("InternalStorageFree", root.getFreeSpace());
|
||||
|
||||
// External storage (SD card)
|
||||
File sd = Environment.getExternalStorageDirectory();
|
||||
storageInfo.add("ExternalStorageTotal", sd.getTotalSpace());
|
||||
storageInfo.add("ExternalStorageFree", sd.getFreeSpace());
|
||||
|
||||
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();
|
||||
connectivityInfo.add("MobileDataAvailable", mobileAvailable);
|
||||
|
||||
// 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) requireNonNull(method.invoke(cm));
|
||||
} catch (ClassNotFoundException
|
||||
| NoSuchMethodException
|
||||
| IllegalArgumentException
|
||||
| InvocationTargetException
|
||||
| IllegalAccessException e) {
|
||||
connectivityInfo
|
||||
.add("MobileDataReflectionException", e.toString());
|
||||
}
|
||||
connectivityInfo.add("MobileDataEnabled", mobileEnabled);
|
||||
|
||||
// Is mobile data connected ?
|
||||
boolean mobileConnected = mobile != null && mobile.isConnected();
|
||||
connectivityInfo.add("MobileDataConnected", mobileConnected);
|
||||
|
||||
// Is wifi available?
|
||||
NetworkInfo wifi = cm.getNetworkInfo(TYPE_WIFI);
|
||||
boolean wifiAvailable = wifi != null && wifi.isAvailable();
|
||||
connectivityInfo.add("WifiAvailable", wifiAvailable);
|
||||
|
||||
// Is wifi enabled?
|
||||
WifiManager wm = getSystemService(ctx, WifiManager.class);
|
||||
boolean wifiEnabled = wm != null &&
|
||||
wm.getWifiState() == WIFI_STATE_ENABLED;
|
||||
connectivityInfo.add("WifiEnabled", wifiEnabled);
|
||||
|
||||
// Is wifi connected?
|
||||
boolean wifiConnected = wifi != null && wifi.isConnected();
|
||||
connectivityInfo.add("WifiConnected", wifiConnected);
|
||||
|
||||
// Is wifi direct supported?
|
||||
boolean wifiDirect = ctx.getSystemService(WIFI_P2P_SERVICE) != null;
|
||||
connectivityInfo.add("WiFiDirectSupported", wifiDirect);
|
||||
|
||||
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("WiFiAddress",
|
||||
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("BluetoothAvailable", false);
|
||||
} else {
|
||||
connectivityInfo.add("BluetoothAvailable", true);
|
||||
|
||||
// Is Bluetooth enabled?
|
||||
@SuppressLint("HardwareIds")
|
||||
boolean btEnabled = bt.isEnabled()
|
||||
&& !isNullOrEmpty(bt.getAddress());
|
||||
connectivityInfo.add("BluetoothEnabled", btEnabled);
|
||||
|
||||
// Is Bluetooth connectable?
|
||||
int scanMode = bt.getScanMode();
|
||||
boolean btConnectable = scanMode == SCAN_MODE_CONNECTABLE ||
|
||||
scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
connectivityInfo.add("BluetoothConnectable", btConnectable);
|
||||
|
||||
// Is Bluetooth discoverable?
|
||||
boolean btDiscoverable =
|
||||
scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
connectivityInfo.add("BluetoothDiscoverable", btDiscoverable);
|
||||
|
||||
if (SDK_INT >= 21) {
|
||||
// Is Bluetooth LE scanning and advertising supported?
|
||||
boolean btLeScan = bt.getBluetoothLeScanner() != null;
|
||||
connectivityInfo.add("BluetoothLeScanningSupported", btLeScan);
|
||||
boolean btLeAdvertise =
|
||||
bt.getBluetoothLeAdvertiser() != null;
|
||||
connectivityInfo.add("BluetoothLeAdvertisingSupported",
|
||||
btLeAdvertise);
|
||||
}
|
||||
|
||||
Pair<String, String> p = getBluetoothAddressAndMethod(ctx, bt);
|
||||
String address = p.getFirst();
|
||||
String method = p.getSecond();
|
||||
connectivityInfo.add("BluetoothAddress", scrubMacAddress(address));
|
||||
connectivityInfo.add("BluetoothAddressMethod", method);
|
||||
}
|
||||
return new ReportItem("Connectivity", R.string.dev_report_connectivity,
|
||||
connectivityInfo);
|
||||
}
|
||||
|
||||
private ReportItem getBuildConfig() {
|
||||
MultiReportInfo buildConfig = new MultiReportInfo()
|
||||
.add("GitHash", BuildConfig.GitHash)
|
||||
.add("BuildType", BuildConfig.BUILD_TYPE)
|
||||
.add("Flavor", BuildConfig.FLAVOR)
|
||||
.add("Debug", BuildConfig.DEBUG)
|
||||
.add("BuildTimestamp", formatTime(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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<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<>();
|
||||
|
||||
// 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<String, String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,36 @@ 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.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class CrashFragment extends Fragment {
|
||||
public class CrashFragment extends BaseFragment {
|
||||
|
||||
public final static String TAG = CrashFragment.class.getName();
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
private ReportViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||
.get(ReportViewModel.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -25,15 +48,16 @@ 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();
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
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.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
||||
import org.briarproject.briar.android.logout.HideUiActivity;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
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.widget.Toast.LENGTH_LONG;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class CrashReportActivity extends BaseActivity
|
||||
implements BaseFragmentListener {
|
||||
|
||||
public static final String EXTRA_THROWABLE = "throwable";
|
||||
public static final String EXTRA_APP_START_TIME = "appStartTime";
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private ReportViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_dev_report);
|
||||
|
||||
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, -1);
|
||||
viewModel.init(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
|
||||
public void runOnDbThread(Runnable runnable) {
|
||||
throw new AssertionError("deprecated!!!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
exit();
|
||||
}
|
||||
|
||||
void displayFragment(boolean showReportForm) {
|
||||
BaseFragment f;
|
||||
if (showReportForm) {
|
||||
f = new ReportFormFragment();
|
||||
requireNonNull(getSupportActionBar()).show();
|
||||
} else {
|
||||
f = new CrashFragment();
|
||||
requireNonNull(getSupportActionBar()).hide();
|
||||
}
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
|
||||
.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);
|
||||
// crash reports run in their own process that we should kill now
|
||||
// otherwise it keeps running and e.g. doesn't pick up theme changes
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
Process.killProcess(Process.myPid());
|
||||
// kill the process with some delay to keep the Toast visible
|
||||
}, 5000);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
@NotThreadSafe
|
||||
@NotNullByDefault
|
||||
class ReportData {
|
||||
|
||||
private final ArrayList<ReportItem> items = new ArrayList<>();
|
||||
|
||||
ReportData add(ReportItem item) {
|
||||
items.add(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
List<ReportItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
static class MultiReportInfo implements ReportInfo {
|
||||
private final Map<String, Object> map = new TreeMap<>();
|
||||
|
||||
MultiReportInfo add(String key, @Nullable Object value) {
|
||||
map.put(key, value == null ? "null" : value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
||||
sb
|
||||
.append(entry.getKey())
|
||||
.append(": ")
|
||||
.append(entry.getValue())
|
||||
.append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toJson() {
|
||||
return new JSONObject(map);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ReportDataAdapter.ReportDataViewHolder> {
|
||||
|
||||
private final List<ReportItem> items;
|
||||
|
||||
ReportDataAdapter(List<ReportItem> 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,58 @@ 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.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
|
||||
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.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 BaseFragment {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ReportFormFragment.class.getName());
|
||||
private static final String IS_FEEDBACK = "isFeedback";
|
||||
private static final Set<ReportField> requiredFields = new HashSet<>();
|
||||
private static final Set<ReportField> excludedFields = new HashSet<>();
|
||||
static {
|
||||
requiredFields.add(REPORT_ID);
|
||||
requiredFields.add(APP_VERSION_CODE);
|
||||
requiredFields.add(APP_VERSION_NAME);
|
||||
requiredFields.add(PACKAGE_NAME);
|
||||
requiredFields.add(ANDROID_VERSION);
|
||||
requiredFields.add(STACK_TRACE);
|
||||
}
|
||||
public final static String TAG = ReportFormFragment.class.getName();
|
||||
|
||||
private boolean isFeedback;
|
||||
private File reportFile;
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
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 injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||
.get(ReportViewModel.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -110,15 +75,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 +89,73 @@ 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);
|
||||
}
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
report.setVisibility(INVISIBLE);
|
||||
progress.setVisibility(VISIBLE);
|
||||
report.removeAllViews();
|
||||
new AsyncTask<Void, Void, CrashReportData>() {
|
||||
|
||||
@Override
|
||||
protected CrashReportData doInBackground(Void... args) {
|
||||
CrashReportPersister persister = new CrashReportPersister();
|
||||
try {
|
||||
return persister.load(reportFile);
|
||||
} catch (IOException | JSONException e) {
|
||||
LOG.log(WARNING, "Could not load report file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(CrashReportData crashData) {
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
if (crashData != null) {
|
||||
for (Map.Entry<ReportField, Element> e : crashData
|
||||
.entrySet()) {
|
||||
ReportField field = e.getKey();
|
||||
StringBuilder valueBuilder = new StringBuilder();
|
||||
for (String pair : e.getValue().flatten()) {
|
||||
valueBuilder.append(pair).append("\n");
|
||||
}
|
||||
String value = valueBuilder.toString();
|
||||
boolean required = requiredFields.contains(field);
|
||||
boolean excluded = excludedFields.contains(field);
|
||||
View v = inflater.inflate(R.layout.list_item_crash,
|
||||
report, false);
|
||||
CheckBox cb = v.findViewById(R.id.include_in_report);
|
||||
cb.setTag(field);
|
||||
cb.setChecked(required || !excluded);
|
||||
cb.setEnabled(!required);
|
||||
cb.setOnCheckedChangeListener(ReportFormFragment.this);
|
||||
cb.setText(field.toString());
|
||||
TextView content = v.findViewById(R.id.content);
|
||||
content.setText(value);
|
||||
report.addView(v);
|
||||
}
|
||||
} else {
|
||||
View v = inflater.inflate(
|
||||
android.R.layout.simple_list_item_1, report, false);
|
||||
TextView error = v.findViewById(android.R.id.text1);
|
||||
error.setText(R.string.could_not_load_report_data);
|
||||
report.addView(v);
|
||||
}
|
||||
report.setVisibility(VISIBLE);
|
||||
progress.setVisibility(GONE);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private void processReport() {
|
||||
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<Void, Void, Boolean>() {
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... args) {
|
||||
CrashReportPersister persister = new CrashReportPersister();
|
||||
try {
|
||||
CrashReportData data = persister.load(reportFile);
|
||||
if (includeReport) {
|
||||
for (ReportField field : excludedFields) {
|
||||
LOG.info("Removing field " + field.name());
|
||||
data.remove(field);
|
||||
}
|
||||
} else {
|
||||
Iterator<Map.Entry<ReportField, Element>> iter =
|
||||
data.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
Map.Entry<ReportField, Element> e = iter.next();
|
||||
if (!requiredFields.contains(e.getKey())) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
persister.store(data, reportFile);
|
||||
return true;
|
||||
} catch (IOException | JSONException e) {
|
||||
LOG.log(WARNING, "Error processing report file", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
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.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.Nullable;
|
||||
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;
|
||||
|
||||
@NotNullByDefault
|
||||
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<Boolean> showReport =
|
||||
new MutableLiveEvent<>();
|
||||
private final MutableLiveData<Boolean> showReportData =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<ReportData> reportData =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveEvent<Integer> 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(@Nullable Throwable t, long appStartTime) {
|
||||
isFeedback = t == null;
|
||||
if (reportData.getValue() == null) new SingleShotAndroidExecutor(() -> {
|
||||
ReportData data = collector.collectReportData(t, appStartTime);
|
||||
reportData.postValue(data);
|
||||
}).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<Boolean> 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<Boolean> getShowReportData() {
|
||||
return showReportData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The content of the report
|
||||
* that will be loaded after {@link #init(Throwable, long)} was called.
|
||||
*/
|
||||
LiveData<ReportData> 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<Integer> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,7 +26,6 @@ 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;
|
||||
@@ -72,6 +71,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;
|
||||
@@ -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,12 @@ 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 -> {
|
||||
triggerFeedback(requireContext());
|
||||
return true;
|
||||
});
|
||||
|
||||
if (SDK_INT < 27) {
|
||||
// remove System Default Theme option from preference entries
|
||||
@@ -245,17 +243,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);
|
||||
}
|
||||
|
||||
@@ -27,14 +27,13 @@ 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.reporting.FeedbackActivity;
|
||||
import org.briarproject.briar.android.view.ArticleMovementMethod;
|
||||
import org.briarproject.briar.android.widget.LinkDialogFragment;
|
||||
|
||||
@@ -51,6 +50,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
@@ -92,6 +92,8 @@ import static java.util.Objects.requireNonNull;
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
|
||||
import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_START_TIME;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
@@ -345,10 +347,18 @@ public class UiUtils {
|
||||
return fm.hasEnrolledFingerprints() && fm.isHardwareDetected();
|
||||
}
|
||||
|
||||
public static void triggerFeedback(AndroidExecutor androidExecutor) {
|
||||
androidExecutor.runOnBackgroundThread(
|
||||
() -> ACRA.getErrorReporter()
|
||||
.handleException(new UserFeedback(), false));
|
||||
public static void triggerFeedback(Context ctx) {
|
||||
startDevReportActivity(ctx, FeedbackActivity.class, null, null);
|
||||
}
|
||||
|
||||
public static void startDevReportActivity(Context ctx,
|
||||
Class<? extends FragmentActivity> activity, @Nullable Throwable t,
|
||||
@Nullable Long appStartTime) {
|
||||
final Intent dialogIntent = new Intent(ctx, activity);
|
||||
dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
dialogIntent.putExtra(EXTRA_THROWABLE, t);
|
||||
dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime);
|
||||
ctx.startActivity(dialogIntent);
|
||||
}
|
||||
|
||||
public static boolean enterPressed(int actionId,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.briarproject.briar.android.util;
|
||||
|
||||
public class UserFeedback extends Exception {
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
@@ -79,19 +79,20 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/user_email_layout" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/report_content"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/margin_large"
|
||||
android:paddingTop="@dimen/margin_small"
|
||||
android:paddingEnd="@dimen/margin_large"
|
||||
android:paddingBottom="@dimen/listitem_height_one_line_avatar"
|
||||
android:paddingBottom="@dimen/margin_large"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/include_debug_report"
|
||||
tools:listitem="@layout/list_item_crash"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
@@ -99,8 +100,7 @@
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -109,4 +109,4 @@
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -567,10 +567,22 @@
|
||||
<string name="optional_contact_email">Your email address (optional)</string>
|
||||
<string name="include_debug_report_crash">Include anonymous data about the crash</string>
|
||||
<string name="include_debug_report_feedback">Include anonymous data about this device</string>
|
||||
<string name="could_not_load_report_data">Could not load report data.</string>
|
||||
<string name="dev_report_basic_info">Basic information</string>
|
||||
<string name="dev_report_device_info">Device information</string>
|
||||
<string name="dev_report_stacktrace">Stacktrace</string>
|
||||
<string name="dev_report_time_info">Time information</string>
|
||||
<string name="dev_report_memory">Memory</string>
|
||||
<string name="dev_report_storage">Storage</string>
|
||||
<string name="dev_report_connectivity">Connectivity</string>
|
||||
<string name="dev_report_build_config">Build configuration</string>
|
||||
<string name="dev_report_logcat">App log</string>
|
||||
<string name="dev_report_device_features">Device Features</string>
|
||||
<string name="send_report">Send report</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="dev_report_sending">Sending feedback…</string>
|
||||
<string name="dev_report_sent">Feedback sent</string>
|
||||
<string name="dev_report_saved">Report saved. It will be sent the next time you log into Briar.</string>
|
||||
<string name="dev_report_error">Error: Sending report failed</string>
|
||||
|
||||
<!-- Sign Out -->
|
||||
<string name="progress_title_logout">Signing out of Briar…</string>
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package org.briarproject.briar.android;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.vanniktech.emoji.EmojiManager;
|
||||
import com.vanniktech.emoji.google.GoogleEmojiProvider;
|
||||
|
||||
import org.briarproject.bramble.BrambleAndroidEagerSingletons;
|
||||
import org.briarproject.bramble.BrambleAppComponent;
|
||||
import org.briarproject.bramble.BrambleCoreEagerSingletons;
|
||||
import org.briarproject.briar.BriarCoreEagerSingletons;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
/**
|
||||
* This class only exists to avoid static initialisation of ACRA
|
||||
*/
|
||||
public class TestBriarApplication extends Application
|
||||
implements BriarApplication {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(TestBriarApplication.class.getName());
|
||||
|
||||
private AndroidComponent applicationComponent;
|
||||
private volatile SharedPreferences prefs;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LOG.info("Created");
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
Localizer.initialize(prefs);
|
||||
applicationComponent = DaggerAndroidComponent.builder()
|
||||
.appModule(new AppModule(this))
|
||||
.build();
|
||||
|
||||
// We need to load the eager singletons directly after making the
|
||||
// dependency graphs
|
||||
BrambleCoreEagerSingletons.Helper
|
||||
.injectEagerSingletons(applicationComponent);
|
||||
BrambleAndroidEagerSingletons.Helper
|
||||
.injectEagerSingletons(applicationComponent);
|
||||
BriarCoreEagerSingletons.Helper
|
||||
.injectEagerSingletons(applicationComponent);
|
||||
AndroidEagerSingletons.Helper
|
||||
.injectEagerSingletons(applicationComponent);
|
||||
EmojiManager.install(new GoogleEmojiProvider());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrambleAppComponent getBrambleAppComponent() {
|
||||
return applicationComponent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<LogRecord> getRecentLogRecords() {
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidComponent getApplicationComponent() {
|
||||
return applicationComponent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SharedPreferences getDefaultSharedPreferences() {
|
||||
return prefs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunningInBackground() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package org.briarproject.briar.android.account;
|
||||
import android.view.View;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.TestBriarApplication;
|
||||
import org.briarproject.briar.android.login.StrengthMeter;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
@@ -42,7 +41,7 @@ import static org.briarproject.briar.android.login.StrengthMeter.YELLOW;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@Config(sdk = 21, application = TestBriarApplication.class)
|
||||
@Config(sdk = 21)
|
||||
public class SetupActivityTest {
|
||||
|
||||
@Rule
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.AuthorInfo;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.android.TestBriarApplication;
|
||||
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
|
||||
import org.briarproject.briar.android.threaded.ThreadItemList;
|
||||
@@ -36,7 +35,7 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 21, application = TestBriarApplication.class)
|
||||
@Config(sdk = 21)
|
||||
public class ForumActivityTest {
|
||||
|
||||
private final static MessageId[] MESSAGE_IDS = new MessageId[6];
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.DecryptionResult;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.TestBriarApplication;
|
||||
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
@@ -36,7 +35,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 21, application = TestBriarApplication.class)
|
||||
@Config(sdk = 21)
|
||||
public class ChangePasswordActivityTest {
|
||||
|
||||
private ChangePasswordActivity changePasswordActivity;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user