From 719e3c6138ec01b37669a5cf8f7d51da0bae8cdb Mon Sep 17 00:00:00 2001 From: akwizgran Date: Wed, 22 Sep 2021 18:12:16 +0100 Subject: [PATCH] Save encrypted logs to disk on debug builds. --- .../account/AndroidAccountManager.java | 36 +++- .../bramble/util/AndroidUtils.java | 6 +- .../account/AndroidAccountManagerTest.java | 19 +- .../bramble/api/FeatureFlags.java | 2 + .../briarproject/bramble/api/StringMap.java | 23 +++ .../api/logging/PersistentLogManager.java | 46 +++++ .../bramble/api/reporting/DevConfig.java | 15 +- .../briarproject/bramble/util/LogUtils.java | 12 ++ .../bramble/BrambleCoreModule.java | 2 + .../bramble}/logging/BriefLogFormatter.java | 13 +- .../logging/FlushingStreamHandler.java | 44 +++++ .../bramble/logging/LoggingModule.java | 29 +++ .../logging/PersistentLogManagerImpl.java | 176 ++++++++++++++++++ .../bramble/test/TestFeatureFlagModule.java | 5 + .../bramble/account/BriarAccountManager.java | 14 +- .../briar/android/AndroidComponent.java | 6 + .../briarproject/briar/android/AppModule.java | 10 +- .../briar/android/BriarApplicationImpl.java | 17 ++ .../briar/android/logging/LogDecrypter.java | 5 +- .../android/logging/LogDecrypterImpl.java | 2 +- .../briar/android/logging/LogEncrypter.java | 2 +- .../android/logging/LogEncrypterImpl.java | 9 +- .../reporting/BriarReportCollector.java | 9 +- .../android/reporting/ReportViewModel.java | 67 +++++-- .../android/settings/SettingsFragment.java | 35 +++- .../android/settings/SettingsViewModel.java | 45 ++++- briar-android/src/main/res/xml/settings.xml | 8 + .../logging/LogEncryptionDecryptionTest.java | 3 +- .../android/logging/LoggingTestModule.java | 2 +- .../briar/headless/HeadlessModule.kt | 1 + 30 files changed, 610 insertions(+), 53 deletions(-) create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/logging/PersistentLogManager.java rename {briar-android/src/main/java/org/briarproject/briar/android => bramble-core/src/main/java/org/briarproject/bramble}/logging/BriefLogFormatter.java (81%) create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/logging/FlushingStreamHandler.java create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/logging/LoggingModule.java create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/logging/PersistentLogManagerImpl.java diff --git a/bramble-android/src/main/java/org/briarproject/bramble/account/AndroidAccountManager.java b/bramble-android/src/main/java/org/briarproject/bramble/account/AndroidAccountManager.java index 37460cc9b..009d237c7 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/account/AndroidAccountManager.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/account/AndroidAccountManager.java @@ -5,12 +5,15 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import org.briarproject.bramble.api.FeatureFlags; import org.briarproject.bramble.api.account.AccountManager; import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.logging.PersistentLogManager; import java.io.File; +import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -23,14 +26,18 @@ import javax.inject.Inject; import static android.os.Build.VERSION.SDK_INT; import static java.util.Arrays.asList; import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.AndroidUtils.getPersistentLogDir; import static org.briarproject.bramble.util.IoUtils.deleteFileOrDir; +import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logFileOrDir; class AndroidAccountManager extends AccountManagerImpl implements AccountManager { private static final Logger LOG = - Logger.getLogger(AndroidAccountManager.class.getName()); + getLogger(AndroidAccountManager.class.getName()); /** * Directories that shouldn't be deleted when deleting the user's account. @@ -40,13 +47,22 @@ class AndroidAccountManager extends AccountManagerImpl protected final Context appContext; private final SharedPreferences prefs; + private final PersistentLogManager logManager; + private final FeatureFlags featureFlags; @Inject - AndroidAccountManager(DatabaseConfig databaseConfig, - CryptoComponent crypto, IdentityManager identityManager, - SharedPreferences prefs, Application app) { + AndroidAccountManager( + DatabaseConfig databaseConfig, + CryptoComponent crypto, + IdentityManager identityManager, + SharedPreferences prefs, + PersistentLogManager logManager, + FeatureFlags featureFlags, + Application app) { super(databaseConfig, crypto, identityManager); this.prefs = prefs; + this.logManager = logManager; + this.featureFlags = featureFlags; appContext = app.getApplicationContext(); } @@ -74,6 +90,9 @@ class AndroidAccountManager extends AccountManagerImpl LOG.info("Contents of account directory after deleting:"); logFileOrDir(LOG, INFO, getDataDir()); } + if (featureFlags.shouldEnablePersistentLogs()) { + replacePersistentLogger(); + } } } @@ -134,4 +153,13 @@ class AndroidAccountManager extends AccountManagerImpl private void addIfNotNull(Set files, @Nullable File file) { if (file != null) files.add(file); } + + private void replacePersistentLogger() { + File logDir = getPersistentLogDir(appContext); + try { + logManager.addLogHandler(logDir, getLogger("")); + } catch (IOException e) { + logException(LOG, WARNING, e); + } + } } diff --git a/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java b/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java index bde4b9a23..674c8ece3 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java @@ -111,10 +111,14 @@ public class AndroidUtils { return ctx.getDir(STORED_REPORTS, MODE_PRIVATE); } - public static File getLogcatFile(Context ctx) { + public static File getTemporaryLogFile(Context ctx) { return new File(ctx.getFilesDir(), STORED_LOGCAT); } + public static File getPersistentLogDir(Context ctx) { + return ctx.getDir("log", MODE_PRIVATE); + } + /** * Returns an array of supported content types for image attachments. */ diff --git a/bramble-android/src/test/java/org/briarproject/bramble/account/AndroidAccountManagerTest.java b/bramble-android/src/test/java/org/briarproject/bramble/account/AndroidAccountManagerTest.java index 2b7b8bb88..7b04ece02 100644 --- a/bramble-android/src/test/java/org/briarproject/bramble/account/AndroidAccountManagerTest.java +++ b/bramble-android/src/test/java/org/briarproject/bramble/account/AndroidAccountManagerTest.java @@ -4,9 +4,11 @@ import android.app.Application; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; +import org.briarproject.bramble.api.FeatureFlags; import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.logging.PersistentLogManager; import org.briarproject.bramble.test.BrambleMockTestCase; import org.jmock.Expectations; import org.jmock.lib.legacy.ClassImposteriser; @@ -15,7 +17,9 @@ import org.junit.Before; import org.junit.Test; import java.io.File; +import java.util.logging.Logger; +import static android.content.Context.MODE_PRIVATE; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; @@ -27,6 +31,10 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase { context.mock(SharedPreferences.class, "prefs"); private final SharedPreferences defaultPrefs = context.mock(SharedPreferences.class, "defaultPrefs"); + private final PersistentLogManager logManager = + context.mock(PersistentLogManager.class); + private final FeatureFlags featureFlags = + context.mock(FeatureFlags.class); private final DatabaseConfig databaseConfig = context.mock(DatabaseConfig.class); private final CryptoComponent crypto = context.mock(CryptoComponent.class); @@ -40,6 +48,7 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase { private final File testDir = getTestDirectory(); private final File keyDir = new File(testDir, "key"); private final File dbDir = new File(testDir, "db"); + private final File logDir = new File(testDir, "log"); private AndroidAccountManager accountManager; @@ -61,7 +70,7 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase { will(returnValue(app)); }}); accountManager = new AndroidAccountManager(databaseConfig, crypto, - identityManager, prefs, app) { + identityManager, prefs, logManager, featureFlags, app) { @Override SharedPreferences getDefaultSharedPreferences() { return defaultPrefs; @@ -109,10 +118,17 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase { will(returnValue(cacheDir)); oneOf(app).getExternalCacheDir(); will(returnValue(externalCacheDir)); + oneOf(featureFlags).shouldEnablePersistentLogs(); + will(returnValue(true)); + oneOf(app).getDir("log", MODE_PRIVATE); + will(returnValue(logDir)); + oneOf(logManager).addLogHandler(with(logDir), + with(any(Logger.class))); }}); assertTrue(dbDir.mkdirs()); assertTrue(keyDir.mkdirs()); + assertTrue(logDir.mkdirs()); assertTrue(codeCacheDir.mkdirs()); assertTrue(codeCacheFile.createNewFile()); assertTrue(libDir.mkdirs()); @@ -130,6 +146,7 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase { assertFalse(dbDir.exists()); assertFalse(keyDir.exists()); + assertFalse(logDir.exists()); assertTrue(codeCacheDir.exists()); assertTrue(codeCacheFile.exists()); assertTrue(libDir.exists()); diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java index f0acbd8e1..e0771e23d 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java @@ -10,4 +10,6 @@ public interface FeatureFlags { boolean shouldEnableProfilePictures(); boolean shouldEnableDisappearingMessages(); + + boolean shouldEnablePersistentLogs(); } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/StringMap.java b/bramble-api/src/main/java/org/briarproject/bramble/api/StringMap.java index bda2a94d2..978b83e4b 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/StringMap.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/StringMap.java @@ -1,8 +1,16 @@ package org.briarproject.bramble.api; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + import java.util.Hashtable; import java.util.Map; +import javax.annotation.Nullable; + +import static org.briarproject.bramble.util.StringUtils.fromHexString; +import static org.briarproject.bramble.util.StringUtils.toHexString; + +@NotNullByDefault public abstract class StringMap extends Hashtable { protected StringMap(Map m) { @@ -52,4 +60,19 @@ public abstract class StringMap extends Hashtable { public void putLong(String key, long value) { put(key, String.valueOf(value)); } + + @Nullable + public byte[] getBytes(String key) { + String s = get(key); + if (s == null) return null; + try { + return fromHexString(s); + } catch (IllegalArgumentException e) { + return null; + } + } + + public void putBytes(String key, byte[] value) { + put(key, toHexString(value)); + } } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/logging/PersistentLogManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/logging/PersistentLogManager.java new file mode 100644 index 000000000..9134b2dff --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/logging/PersistentLogManager.java @@ -0,0 +1,46 @@ +package org.briarproject.bramble.api.logging; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.settings.Settings; + +import java.io.File; +import java.io.IOException; +import java.util.Scanner; +import java.util.logging.Handler; +import java.util.logging.Logger; + +@NotNullByDefault +public interface PersistentLogManager { + + /** + * The namespace of the (@link Settings) where the log key is stored. + */ + String LOG_SETTINGS_NAMESPACE = "log"; + + /** + * The {@link Settings} key under which the log key is stored. + */ + String LOG_KEY_KEY = "logKey"; + + /** + * Creates and returns a persistent log handler that stores its logs in + * the given directory. + */ + Handler createLogHandler(File dir) throws IOException; + + /** + * Creates a persistent log handler that stores its logs in the given + * directory and adds the handler to the given logger, replacing any + * existing persistent log handler. + */ + void addLogHandler(File dir, Logger logger) throws IOException; + + /** + * Returns a {@link Scanner} for reading the persistent log entries stored + * in the given directory. + * + * @param old True if the previous session's log should be loaded, or false + * if the current session's log should be loaded + */ + Scanner getPersistentLog(File dir, boolean old) throws IOException; +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/reporting/DevConfig.java b/bramble-api/src/main/java/org/briarproject/bramble/api/reporting/DevConfig.java index 8c7c89000..f544db6e8 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/reporting/DevConfig.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/reporting/DevConfig.java @@ -8,11 +8,24 @@ import java.io.File; @NotNullByDefault public interface DevConfig { + /** + * Returns the public key for encrypting feedback and crash reports. + */ PublicKey getDevPublicKey(); + /** + * Returns the onion address for submitting feedback and crash reports. + */ String getDevOnionAddress(); + /** + * Returns the directory for storing unsent feedback and crash reports. + */ File getReportDir(); - File getLogcatFile(); + /** + * Returns the temporary file for passing the encrypted app log from the + * main process to the crash reporter process. + */ + File getTemporaryLogFile(); } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/util/LogUtils.java b/bramble-api/src/main/java/org/briarproject/bramble/util/LogUtils.java index c98ef09f6..bbdf913ac 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/util/LogUtils.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/util/LogUtils.java @@ -1,7 +1,10 @@ package org.briarproject.bramble.util; import java.io.File; +import java.util.Collection; +import java.util.logging.Formatter; import java.util.logging.Level; +import java.util.logging.LogRecord; import java.util.logging.Logger; import static java.util.logging.Level.FINE; @@ -57,4 +60,13 @@ public class LogUtils { String type) { logger.log(level, type + " " + f.getAbsolutePath() + " " + f.length()); } + + public static String formatLog(Formatter formatter, + Collection logRecords) { + StringBuilder sb = new StringBuilder(); + for (LogRecord record : logRecords) { + sb.append(formatter.format(record)).append('\n'); + } + return sb.toString(); + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java index 85ec0372a..84bc92aef 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java @@ -14,6 +14,7 @@ import org.briarproject.bramble.identity.IdentityModule; import org.briarproject.bramble.io.IoModule; import org.briarproject.bramble.keyagreement.KeyAgreementModule; import org.briarproject.bramble.lifecycle.LifecycleModule; +import org.briarproject.bramble.logging.LoggingModule; import org.briarproject.bramble.mailbox.MailboxModule; import org.briarproject.bramble.plugin.PluginModule; import org.briarproject.bramble.properties.PropertiesModule; @@ -44,6 +45,7 @@ import dagger.Module; IoModule.class, KeyAgreementModule.class, LifecycleModule.class, + LoggingModule.class, MailboxModule.class, PluginModule.class, PropertiesModule.class, diff --git a/briar-android/src/main/java/org/briarproject/briar/android/logging/BriefLogFormatter.java b/bramble-core/src/main/java/org/briarproject/bramble/logging/BriefLogFormatter.java similarity index 81% rename from briar-android/src/main/java/org/briarproject/briar/android/logging/BriefLogFormatter.java rename to bramble-core/src/main/java/org/briarproject/bramble/logging/BriefLogFormatter.java index 0256cf1b4..309a29021 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/logging/BriefLogFormatter.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/logging/BriefLogFormatter.java @@ -1,10 +1,9 @@ -package org.briarproject.briar.android.logging; +package org.briarproject.bramble.logging; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.Collection; import java.util.Date; import java.util.TimeZone; import java.util.logging.Formatter; @@ -18,16 +17,6 @@ import static java.util.Locale.US; @NotNullByDefault public class BriefLogFormatter extends Formatter { - public static String formatLog(Formatter formatter, - Collection logRecords) { - StringBuilder sb = new StringBuilder(); - for (LogRecord record : logRecords) { - String formatted = formatter.format(record); - sb.append(formatted).append('\n'); - } - return sb.toString(); - } - private final Object lock = new Object(); private final DateFormat dateFormat; // Locking: lock private final Date date; // Locking: lock diff --git a/bramble-core/src/main/java/org/briarproject/bramble/logging/FlushingStreamHandler.java b/bramble-core/src/main/java/org/briarproject/bramble/logging/FlushingStreamHandler.java new file mode 100644 index 000000000..1376f68b3 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/logging/FlushingStreamHandler.java @@ -0,0 +1,44 @@ +package org.briarproject.bramble.logging; + +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.system.TaskScheduler; + +import java.io.OutputStream; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; +import java.util.logging.StreamHandler; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +class FlushingStreamHandler extends StreamHandler { + + private static final int FLUSH_DELAY_MS = 5_000; + + private final TaskScheduler scheduler; + private final Executor ioExecutor; + private final AtomicBoolean flushScheduled = new AtomicBoolean(false); + + FlushingStreamHandler(TaskScheduler scheduler, + Executor ioExecutor, OutputStream out, Formatter formatter) { + super(out, formatter); + this.scheduler = scheduler; + this.ioExecutor = ioExecutor; + } + + @Override + public void publish(LogRecord record) { + super.publish(record); + if (!flushScheduled.getAndSet(true)) { + scheduler.schedule(this::scheduledFlush, ioExecutor, + FLUSH_DELAY_MS, MILLISECONDS); + } + } + + @IoExecutor + private void scheduledFlush() { + flushScheduled.set(false); + flush(); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/logging/LoggingModule.java b/bramble-core/src/main/java/org/briarproject/bramble/logging/LoggingModule.java new file mode 100644 index 000000000..33799a522 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/logging/LoggingModule.java @@ -0,0 +1,29 @@ +package org.briarproject.bramble.logging; + +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.logging.PersistentLogManager; + +import java.util.logging.Formatter; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class LoggingModule { + + @Provides + Formatter provideFormatter() { + return new BriefLogFormatter(); + } + + @Provides + @Singleton + PersistentLogManager providePersistentLogManager( + LifecycleManager lifecycleManager, + PersistentLogManagerImpl persistentLogManager) { + lifecycleManager.registerOpenDatabaseHook(persistentLogManager); + return persistentLogManager; + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/logging/PersistentLogManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/logging/PersistentLogManagerImpl.java new file mode 100644 index 000000000..317a433cc --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/logging/PersistentLogManagerImpl.java @@ -0,0 +1,176 @@ +package org.briarproject.bramble.logging; + +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook; +import org.briarproject.bramble.api.lifecycle.ShutdownManager; +import org.briarproject.bramble.api.logging.PersistentLogManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.settings.Settings; +import org.briarproject.bramble.api.system.TaskScheduler; +import org.briarproject.bramble.api.transport.StreamReaderFactory; +import org.briarproject.bramble.api.transport.StreamWriter; +import org.briarproject.bramble.api.transport.StreamWriterFactory; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Scanner; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Logger; +import java.util.logging.StreamHandler; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; + +@ThreadSafe +@NotNullByDefault +class PersistentLogManagerImpl implements PersistentLogManager, + OpenDatabaseHook { + + private static final Logger LOG = + getLogger(PersistentLogManagerImpl.class.getName()); + + private static final String LOG_FILE = "briar.log"; + private static final String OLD_LOG_FILE = "briar.log.old"; + + private final TaskScheduler scheduler; + private final Executor ioExecutor; + private final ShutdownManager shutdownManager; + private final DatabaseComponent db; + private final StreamReaderFactory streamReaderFactory; + private final StreamWriterFactory streamWriterFactory; + private final Formatter formatter; + private final SecretKey logKey; + private final AtomicReference shutdownHookHandle = + new AtomicReference<>(); + + @Nullable + private volatile SecretKey oldLogKey = null; + + @Inject + PersistentLogManagerImpl( + TaskScheduler scheduler, + @IoExecutor Executor ioExecutor, + ShutdownManager shutdownManager, + DatabaseComponent db, + StreamReaderFactory streamReaderFactory, + StreamWriterFactory streamWriterFactory, + Formatter formatter, + CryptoComponent crypto) { + this.scheduler = scheduler; + this.ioExecutor = ioExecutor; + this.shutdownManager = shutdownManager; + this.db = db; + this.streamReaderFactory = streamReaderFactory; + this.streamWriterFactory = streamWriterFactory; + this.formatter = formatter; + logKey = crypto.generateSecretKey(); + } + + @Override + public void onDatabaseOpened(Transaction txn) throws DbException { + Settings s = db.getSettings(txn, LOG_SETTINGS_NAMESPACE); + // Load the old log key, if any + byte[] oldKeyBytes = s.getBytes(LOG_KEY_KEY); + if (oldKeyBytes != null && oldKeyBytes.length == SecretKey.LENGTH) { + LOG.info("Loaded old log key"); + oldLogKey = new SecretKey(oldKeyBytes); + } + // Store the current log key + s.putBytes(LOG_KEY_KEY, logKey.getBytes()); + db.mergeSettings(txn, s, LOG_SETTINGS_NAMESPACE); + } + + @Override + public Handler createLogHandler(File dir) throws IOException { + File logFile = new File(dir, LOG_FILE); + File oldLogFile = new File(dir, OLD_LOG_FILE); + if (oldLogFile.exists() && !oldLogFile.delete()) + LOG.warning("Failed to delete old log file"); + if (logFile.exists() && !logFile.renameTo(oldLogFile)) + LOG.warning("Failed to rename log file"); + try { + OutputStream out = new FileOutputStream(logFile); + StreamWriter writer = + streamWriterFactory.createLogStreamWriter(out, logKey); + StreamHandler handler = new FlushingStreamHandler(scheduler, + ioExecutor, writer.getOutputStream(), formatter); + // Flush the log and terminate the stream at shutdown + Runnable shutdownHook = () -> { + LOG.info("Shutting down"); + handler.flush(); + try { + writer.sendEndOfStream(); + } catch (IOException e) { + logException(LOG, WARNING, e); + } + }; + int handle = shutdownManager.addShutdownHook(shutdownHook); + // If a previous handler registered a shutdown hook, remove it + Integer oldHandle = shutdownHookHandle.getAndSet(handle); + if (oldHandle != null) { + shutdownManager.removeShutdownHook(oldHandle); + } + return handler; + } catch (SecurityException e) { + throw new IOException(e); + } + } + + @Override + public void addLogHandler(File dir, Logger logger) throws IOException { + for (Handler h : logger.getHandlers()) { + if (h instanceof FlushingStreamHandler) logger.removeHandler(h); + } + logger.addHandler(createLogHandler(dir)); + } + + @Override + public Scanner getPersistentLog(File dir, boolean old) + throws IOException { + if (old) { + SecretKey oldLogKey = this.oldLogKey; + if (oldLogKey == null) { + LOG.info("Old log key has not been loaded"); + return emptyScanner(); + } + return getPersistentLog(new File(dir, OLD_LOG_FILE), oldLogKey); + } else { + return getPersistentLog(new File(dir, LOG_FILE), logKey); + } + } + + private Scanner getPersistentLog(File logFile, SecretKey key) + throws IOException { + if (logFile.exists()) { + LOG.info("Reading log file"); + InputStream in = new FileInputStream(logFile); + return new Scanner(streamReaderFactory.createLogStreamReader(in, + key)); + } else { + LOG.info("Log file does not exist"); + return emptyScanner(); + } + } + + private Scanner emptyScanner() { + return new Scanner(new ByteArrayInputStream(new byte[0])); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java index 77f485e9b..e9561dc42 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java @@ -24,6 +24,11 @@ public class TestFeatureFlagModule { public boolean shouldEnableDisappearingMessages() { return true; } + + @Override + public boolean shouldEnablePersistentLogs() { + return true; + } }; } } diff --git a/briar-android/src/main/java/org/briarproject/bramble/account/BriarAccountManager.java b/briar-android/src/main/java/org/briarproject/bramble/account/BriarAccountManager.java index eb1e2faaa..117b94e45 100644 --- a/briar-android/src/main/java/org/briarproject/bramble/account/BriarAccountManager.java +++ b/briar-android/src/main/java/org/briarproject/bramble/account/BriarAccountManager.java @@ -3,9 +3,11 @@ package org.briarproject.bramble.account; import android.app.Application; import android.content.SharedPreferences; +import org.briarproject.bramble.api.FeatureFlags; import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.logging.PersistentLogManager; import org.briarproject.briar.R; import org.briarproject.briar.android.Localizer; import org.briarproject.briar.android.util.UiUtils; @@ -15,10 +17,16 @@ import javax.inject.Inject; class BriarAccountManager extends AndroidAccountManager { @Inject - BriarAccountManager(DatabaseConfig databaseConfig, CryptoComponent crypto, - IdentityManager identityManager, SharedPreferences prefs, + BriarAccountManager( + DatabaseConfig databaseConfig, + CryptoComponent crypto, + IdentityManager identityManager, + SharedPreferences prefs, + PersistentLogManager logManager, + FeatureFlags featureFlags, Application app) { - super(databaseConfig, crypto, identityManager, prefs, app); + super(databaseConfig, crypto, identityManager, prefs, logManager, + featureFlags, app); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java index 42d6570e2..f4212acd1 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java @@ -22,6 +22,7 @@ import org.briarproject.bramble.api.keyagreement.PayloadEncoder; import org.briarproject.bramble.api.keyagreement.PayloadParser; import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.logging.PersistentLogManager; import org.briarproject.bramble.api.plugin.PluginManager; import org.briarproject.bramble.api.settings.SettingsManager; import org.briarproject.bramble.api.system.AndroidExecutor; @@ -78,6 +79,7 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager import org.briarproject.briar.api.test.TestDataCreator; import java.util.concurrent.Executor; +import java.util.logging.Formatter; import javax.inject.Singleton; @@ -204,6 +206,10 @@ public interface AndroidComponent AutoDeleteManager autoDeleteManager(); + PersistentLogManager persistentLogManager(); + + Formatter formatter(); + void inject(SignInReminderReceiver briarService); void inject(BriarService briarService); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index 1d37010ef..b5bcc4ef3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -245,8 +245,9 @@ public class AppModule { } @Override - public File getLogcatFile() { - return AndroidUtils.getLogcatFile(app.getApplicationContext()); + public File getTemporaryLogFile() { + return AndroidUtils + .getTemporaryLogFile(app.getApplicationContext()); } }; return devConfig; @@ -337,6 +338,11 @@ public class AppModule { public boolean shouldEnableDisappearingMessages() { return true; } + + @Override + public boolean shouldEnablePersistentLogs() { + return IS_DEBUG_BUILD; + } }; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java index 3026d8302..3c3a9cd4d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java @@ -17,11 +17,14 @@ import com.vanniktech.emoji.google.GoogleEmojiProvider; import org.briarproject.bramble.BrambleAndroidEagerSingletons; import org.briarproject.bramble.BrambleAppComponent; import org.briarproject.bramble.BrambleCoreEagerSingletons; +import org.briarproject.bramble.api.logging.PersistentLogManager; import org.briarproject.briar.BriarCoreEagerSingletons; import org.briarproject.briar.R; import org.briarproject.briar.android.logging.CachingLogHandler; import org.briarproject.briar.android.util.UiUtils; +import java.io.File; +import java.io.IOException; import java.lang.Thread.UncaughtExceptionHandler; import java.util.logging.Handler; import java.util.logging.Logger; @@ -31,7 +34,10 @@ 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.Level.WARNING; import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.AndroidUtils.getPersistentLogDir; +import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; public class BriarApplicationImpl extends Application @@ -81,6 +87,17 @@ public class BriarApplicationImpl extends Application rootLogger.addHandler(logHandler); rootLogger.setLevel(IS_DEBUG_BUILD ? FINE : INFO); + if (applicationComponent.featureFlags().shouldEnablePersistentLogs()) { + PersistentLogManager logManager = + applicationComponent.persistentLogManager(); + File logDir = getPersistentLogDir(this); + try { + rootLogger.addHandler(logManager.createLogHandler(logDir)); + } catch (IOException e) { + logException(LOG, WARNING, e); + } + } + LOG.info("Created"); EmojiManager.install(new GoogleEmojiProvider()); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/logging/LogDecrypter.java b/briar-android/src/main/java/org/briarproject/briar/android/logging/LogDecrypter.java index 2295838de..a4327cef3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/logging/LogDecrypter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/logging/LogDecrypter.java @@ -8,8 +8,9 @@ import androidx.annotation.Nullable; @NotNullByDefault public interface LogDecrypter { /** - * Returns decrypted log records from {@link AndroidUtils#getLogcatFile} - * or null if there was an error reading the logs. + * Returns decrypted log records from + * {@link AndroidUtils#getTemporaryLogFile} or null if there was an error + * reading the logs. */ @Nullable String decryptLogs(@Nullable byte[] logKey); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/logging/LogDecrypterImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/logging/LogDecrypterImpl.java index 61cfaa136..f87cd3cd8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/logging/LogDecrypterImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/logging/LogDecrypterImpl.java @@ -41,7 +41,7 @@ class LogDecrypterImpl implements LogDecrypter { public String decryptLogs(@Nullable byte[] logKey) { if (logKey == null) return null; SecretKey key = new SecretKey(logKey); - File logFile = devConfig.getLogcatFile(); + File logFile = devConfig.getTemporaryLogFile(); try (InputStream in = new FileInputStream(logFile)) { InputStream reader = streamReaderFactory.createLogStreamReader(in, key); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/logging/LogEncrypter.java b/briar-android/src/main/java/org/briarproject/briar/android/logging/LogEncrypter.java index 354e22ecc..1c1ecd268 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/logging/LogEncrypter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/logging/LogEncrypter.java @@ -8,7 +8,7 @@ import androidx.annotation.Nullable; @NotNullByDefault public interface LogEncrypter { /** - * Writes encrypted log records to {@link AndroidUtils#getLogcatFile} + * Writes encrypted log records to {@link AndroidUtils#getTemporaryLogFile} * and returns the encryption key if everything went fine. */ @Nullable diff --git a/briar-android/src/main/java/org/briarproject/briar/android/logging/LogEncrypterImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/logging/LogEncrypterImpl.java index f868f6ed3..15ab611d2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/logging/LogEncrypterImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/logging/LogEncrypterImpl.java @@ -33,16 +33,19 @@ class LogEncrypterImpl implements LogEncrypter { private final DevConfig devConfig; private final CachingLogHandler logHandler; + private final Formatter formatter; private final CryptoComponent crypto; private final StreamWriterFactory streamWriterFactory; @Inject LogEncrypterImpl(DevConfig devConfig, CachingLogHandler logHandler, + Formatter formatter, CryptoComponent crypto, StreamWriterFactory streamWriterFactory) { this.devConfig = devConfig; this.logHandler = logHandler; + this.formatter = formatter; this.crypto = crypto; this.streamWriterFactory = streamWriterFactory; } @@ -51,7 +54,7 @@ class LogEncrypterImpl implements LogEncrypter { @Override public byte[] encryptLogs() { SecretKey logKey = crypto.generateSecretKey(); - File logFile = devConfig.getLogcatFile(); + File logFile = devConfig.getTemporaryLogFile(); try (OutputStream out = new FileOutputStream(logFile)) { StreamWriter streamWriter = streamWriterFactory.createLogStreamWriter(out, logKey); @@ -67,10 +70,8 @@ class LogEncrypterImpl implements LogEncrypter { } private void writeLogString(Writer writer) throws IOException { - Formatter formatter = new BriefLogFormatter(); for (LogRecord record : logHandler.getRecentLogRecords()) { - String formatted = formatter.format(record); - writer.append(formatted).append('\n'); + writer.append(formatter.format(record)).append('\n'); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java index 723edf10a..49c7ca977 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java @@ -26,6 +26,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.BuildConfig; import org.briarproject.briar.R; import org.briarproject.briar.android.reporting.ReportData.MultiReportInfo; +import org.briarproject.briar.android.reporting.ReportData.ReportInfo; import org.briarproject.briar.android.reporting.ReportData.ReportItem; import org.briarproject.briar.android.reporting.ReportData.SingleReportInfo; @@ -71,7 +72,7 @@ class BriarReportCollector { } ReportData collectReportData(@Nullable Throwable t, long appStartTime, - String logs) { + ReportInfo logs) { ReportData reportData = new ReportData() .add(getBasicInfo(t)) .add(getDeviceInfo()); @@ -82,7 +83,7 @@ class BriarReportCollector { .add(getStorage()) .add(getConnectivity()) .add(getBuildConfig()) - .add(getLogcat(logs)) + .add(getLogs(logs)) .add(getDeviceFeatures()); } @@ -309,8 +310,8 @@ class BriarReportCollector { buildConfig); } - private ReportItem getLogcat(String logs) { - return new ReportItem("Logcat", R.string.dev_report_logcat, logs); + private ReportItem getLogs(ReportInfo logs) { + return new ReportItem("Logs", R.string.dev_report_logcat, logs); } private ReportItem getDeviceFeatures() { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportViewModel.java index 6e0a898b6..c0331038f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportViewModel.java @@ -4,6 +4,8 @@ import android.app.Application; import android.os.Handler; import android.os.Looper; +import org.briarproject.bramble.api.FeatureFlags; +import org.briarproject.bramble.api.logging.PersistentLogManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.Plugin; import org.briarproject.bramble.api.plugin.PluginManager; @@ -11,7 +13,6 @@ 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.logging.BriefLogFormatter; import org.briarproject.briar.android.logging.CachingLogHandler; import org.briarproject.briar.android.logging.LogDecrypter; import org.briarproject.briar.android.reporting.ReportData.MultiReportInfo; @@ -22,6 +23,9 @@ import org.json.JSONException; import java.io.File; import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.LinkedList; +import java.util.Scanner; import java.util.UUID; import java.util.logging.Formatter; import java.util.logging.Logger; @@ -39,18 +43,24 @@ 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.AndroidUtils.getPersistentLogDir; +import static org.briarproject.bramble.util.LogUtils.formatLog; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; -import static org.briarproject.briar.android.logging.BriefLogFormatter.formatLog; @NotNullByDefault class ReportViewModel extends AndroidViewModel { + private static final int MAX_PERSISTENT_LOG_LINES = 1000; + private static final Logger LOG = getLogger(ReportViewModel.class.getName()); private final CachingLogHandler logHandler; private final LogDecrypter logDecrypter; + private final Formatter formatter; + private final PersistentLogManager logManager; + private final FeatureFlags featureFlags; private final BriarReportCollector collector; private final DevReporter reporter; private final PluginManager pluginManager; @@ -71,12 +81,18 @@ class ReportViewModel extends AndroidViewModel { ReportViewModel(@NonNull Application application, CachingLogHandler logHandler, LogDecrypter logDecrypter, + Formatter formatter, + PersistentLogManager logManager, + FeatureFlags featureFlags, DevReporter reporter, PluginManager pluginManager) { super(application); collector = new BriarReportCollector(application); this.logHandler = logHandler; this.logDecrypter = logDecrypter; + this.formatter = formatter; + this.logManager = logManager; + this.featureFlags = featureFlags; this.reporter = reporter; this.pluginManager = pluginManager; } @@ -86,22 +102,30 @@ class ReportViewModel extends AndroidViewModel { this.initialComment = initialComment; isFeedback = t == null; if (reportData.getValue() == null) new SingleShotAndroidExecutor(() -> { - String decryptedLogs; + String currentLog; if (isFeedback) { - Formatter formatter = new BriefLogFormatter(); - decryptedLogs = - formatLog(formatter, logHandler.getRecentLogRecords()); + // We're in the main process, so get the log for this process + currentLog = formatLog(formatter, + logHandler.getRecentLogRecords()); } else { - decryptedLogs = logDecrypter.decryptLogs(logKey); - if (decryptedLogs == null) { + // We're in the crash reporter process, so try to load + // the encrypted log that was saved by the main process + currentLog = logDecrypter.decryptLogs(logKey); + if (currentLog == null) { // error decrypting logs, get logs from this process - Formatter formatter = new BriefLogFormatter(); - decryptedLogs = formatLog(formatter, + currentLog = formatLog(formatter, logHandler.getRecentLogRecords()); } } + MultiReportInfo logs = new MultiReportInfo(); + logs.add("Current", currentLog); + if (isFeedback && featureFlags.shouldEnablePersistentLogs()) { + // Add persistent logs for the current and previous processes + logs.add("Persistent", getPersistentLog(false)); + logs.add("PersistentOld", getPersistentLog(true)); + } ReportData data = - collector.collectReportData(t, appStartTime, decryptedLogs); + collector.collectReportData(t, appStartTime, logs); reportData.postValue(data); }).start(); } @@ -226,6 +250,27 @@ class ReportViewModel extends AndroidViewModel { return closeReport; } + private String getPersistentLog(boolean old) { + File logDir = getPersistentLogDir(getApplication()); + StringBuilder sb = new StringBuilder(); + try { + Scanner scanner = logManager.getPersistentLog(logDir, old); + LinkedList lines = new LinkedList<>(); + int numLines = 0; + while (scanner.hasNextLine()) { + lines.add(scanner.nextLine()); + // If there are too many lines, return the most recent ones + if (numLines == MAX_PERSISTENT_LOG_LINES) lines.pollFirst(); + else numLines++; + } + scanner.close(); + for (String line : lines) sb.append(line).append('\n'); + } catch (IOException e) { + sb.append("Could not recover persistent log: ").append(e); + } + return sb.toString(); + } + // Used for a new thread as the Android executor thread may have died private static class SingleShotAndroidExecutor extends Thread { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java index c4d368db5..c942ba3a3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java @@ -8,6 +8,7 @@ import android.view.View; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.util.ActivityLaunchers.CreateDocumentAdvanced; import org.briarproject.briar.android.util.ActivityLaunchers.GetImageAdvanced; import javax.inject.Inject; @@ -37,6 +38,10 @@ public class SettingsFragment extends PreferenceFragmentCompat { private static final String PREF_KEY_DEV = "pref_key_dev"; private static final String PREF_KEY_EXPLODE = "pref_key_explode"; private static final String PREF_KEY_SHARE_APP = "pref_key_share_app"; + private static final String PREF_KEY_EXPORT_LOG = "pref_key_export_log"; + private static final String PREF_EXPORT_OLD_LOG = "pref_key_export_old_log"; + + private static final String LOG_EXPORT_FILENAME = "briar-log.txt"; @Inject ViewModelProvider.Factory viewModelFactory; @@ -44,10 +49,18 @@ public class SettingsFragment extends PreferenceFragmentCompat { private SettingsViewModel viewModel; private AvatarPreference prefAvatar; - private final ActivityResultLauncher launcher = + private final ActivityResultLauncher imageLauncher = registerForActivityResult(new GetImageAdvanced(), this::onImageSelected); + private final ActivityResultLauncher logLauncher = + registerForActivityResult(new CreateDocumentAdvanced(), + uri -> onLogFileSelected(false, uri)); + + private final ActivityResultLauncher oldLogLauncher = + registerForActivityResult(new CreateDocumentAdvanced(), + uri -> onLogFileSelected(true, uri)); + @Override public void onAttach(@NonNull Context context) { super.onAttach(context); @@ -63,7 +76,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { prefAvatar = requireNonNull(findPreference(PREF_KEY_AVATAR)); if (viewModel.shouldEnableProfilePictures()) { prefAvatar.setOnPreferenceClickListener(preference -> { - launcher.launch("image/*"); + imageLauncher.launch("image/*"); return true; }); } else { @@ -77,11 +90,24 @@ public class SettingsFragment extends PreferenceFragmentCompat { return true; }); - Preference explode = requireNonNull(findPreference(PREF_KEY_EXPLODE)); if (IS_DEBUG_BUILD) { + Preference explode = + requireNonNull(findPreference(PREF_KEY_EXPLODE)); explode.setOnPreferenceClickListener(preference -> { throw new RuntimeException("Boom!"); }); + Preference exportLog = + requireNonNull(findPreference(PREF_KEY_EXPORT_LOG)); + exportLog.setOnPreferenceClickListener(preference -> { + logLauncher.launch(LOG_EXPORT_FILENAME); + return true; + }); + Preference exportOldLog = + requireNonNull(findPreference(PREF_EXPORT_OLD_LOG)); + exportOldLog.setOnPreferenceClickListener(preference -> { + oldLogLauncher.launch(LOG_EXPORT_FILENAME); + return true; + }); } else { PreferenceGroup dev = requireNonNull(findPreference(PREF_KEY_DEV)); dev.setVisible(false); @@ -111,4 +137,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { ConfirmAvatarDialogFragment.TAG); } + private void onLogFileSelected(boolean old, @Nullable Uri uri) { + if (uri != null) viewModel.exportPersistentLog(old, uri); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java index ba92042f0..3249436a1 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java @@ -16,6 +16,7 @@ import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.logging.PersistentLogManager; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.plugin.BluetoothConstants; @@ -35,8 +36,12 @@ import org.briarproject.briar.api.avatar.AvatarManager; import org.briarproject.briar.api.identity.AuthorInfo; import org.briarproject.briar.api.identity.AuthorManager; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.Scanner; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -50,6 +55,7 @@ import static android.widget.Toast.LENGTH_LONG; import static java.util.Arrays.asList; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.AndroidUtils.getPersistentLogDir; import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes; import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; @@ -78,6 +84,7 @@ class SettingsViewModel extends DbViewModel implements EventListener { private final ImageCompressor imageCompressor; private final Executor ioExecutor; private final FeatureFlags featureFlags; + private final PersistentLogManager logManager; final SettingsStore settingsStore; final TorSummaryProvider torSummaryProvider; @@ -108,7 +115,8 @@ class SettingsViewModel extends DbViewModel implements EventListener { LocationUtils locationUtils, CircumventionProvider circumventionProvider, @IoExecutor Executor ioExecutor, - FeatureFlags featureFlags) { + FeatureFlags featureFlags, + PersistentLogManager logManager) { super(application, dbExecutor, lifecycleManager, db, androidExecutor); this.settingsManager = settingsManager; this.identityManager = identityManager; @@ -118,6 +126,7 @@ class SettingsViewModel extends DbViewModel implements EventListener { this.authorManager = authorManager; this.ioExecutor = ioExecutor; this.featureFlags = featureFlags; + this.logManager = logManager; settingsStore = new SettingsStore(settingsManager, dbExecutor, SETTINGS_NAMESPACE); torSummaryProvider = new TorSummaryProvider(getApplication(), @@ -262,4 +271,38 @@ class SettingsViewModel extends DbViewModel implements EventListener { return screenLockTimeout; } + void exportPersistentLog(boolean old, Uri uri) { + // We can use untranslated strings here, as this method is only called + // in debug builds + ioExecutor.execute(() -> { + Application app = getApplication(); + try { + OutputStream os = + app.getContentResolver().openOutputStream(uri); + if (os == null) throw new IOException(); + File logDir = getPersistentLogDir(app); + Scanner scanner = logManager.getPersistentLog(logDir, old); + if (!scanner.hasNextLine()) { + scanner.close(); + androidExecutor.runOnUiThread(() -> + Toast.makeText(app, "Log is empty", + LENGTH_LONG).show()); + return; + } + PrintWriter w = new PrintWriter(os); + while (scanner.hasNextLine()) w.println(scanner.nextLine()); + w.flush(); + w.close(); + scanner.close(); + androidExecutor.runOnUiThread(() -> + Toast.makeText(app, "Log exported", + LENGTH_LONG).show()); + } catch (IOException e) { + logException(LOG, WARNING, e); + androidExecutor.runOnUiThread(() -> + Toast.makeText(app, "Failed to export log", + LENGTH_LONG).show()); + } + }); + } } diff --git a/briar-android/src/main/res/xml/settings.xml b/briar-android/src/main/res/xml/settings.xml index 3bd775506..f2088f1eb 100644 --- a/briar-android/src/main/res/xml/settings.xml +++ b/briar-android/src/main/res/xml/settings.xml @@ -58,6 +58,14 @@ android:targetPackage="@string/app_package" /> + + + + diff --git a/briar-android/src/test/java/org/briarproject/briar/android/logging/LogEncryptionDecryptionTest.java b/briar-android/src/test/java/org/briarproject/briar/android/logging/LogEncryptionDecryptionTest.java index f08426579..9f22bc3e6 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/logging/LogEncryptionDecryptionTest.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/logging/LogEncryptionDecryptionTest.java @@ -1,5 +1,6 @@ package org.briarproject.briar.android.logging; +import org.briarproject.bramble.logging.BriefLogFormatter; import org.briarproject.bramble.test.BrambleMockTestCase; import org.junit.ClassRule; import org.junit.Test; @@ -15,8 +16,8 @@ import static java.util.logging.Level.FINE; import static java.util.logging.Level.INFO; import static java.util.logging.Level.SEVERE; import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.util.LogUtils.formatLog; import static org.briarproject.bramble.util.StringUtils.getRandomString; -import static org.briarproject.briar.android.logging.BriefLogFormatter.formatLog; import static org.junit.Assert.assertEquals; public class LogEncryptionDecryptionTest extends BrambleMockTestCase { diff --git a/briar-android/src/test/java/org/briarproject/briar/android/logging/LoggingTestModule.java b/briar-android/src/test/java/org/briarproject/briar/android/logging/LoggingTestModule.java index a4bbb6165..b928c6968 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/logging/LoggingTestModule.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/logging/LoggingTestModule.java @@ -42,7 +42,7 @@ class LoggingTestModule { } @Override - public File getLogcatFile() { + public File getTemporaryLogFile() { return logFile; } }; diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt index ac7879906..e5d0119d8 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt @@ -107,5 +107,6 @@ internal class HeadlessModule(private val appDir: File) { override fun shouldEnableImageAttachments() = false override fun shouldEnableProfilePictures() = false override fun shouldEnableDisappearingMessages() = false + override fun shouldEnablePersistentLogs() = false } }