Compare commits

...

2 Commits

Author SHA1 Message Date
akwizgran
5b9955d71f Hide menu items on API < 19. 2021-11-30 13:43:51 +00:00
akwizgran
719e3c6138 Save encrypted logs to disk on debug builds. 2021-11-30 13:43:44 +00:00
30 changed files with 619 additions and 53 deletions

View File

@@ -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<File> 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);
}
}
}

View File

@@ -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.
*/

View File

@@ -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());

View File

@@ -10,4 +10,6 @@ public interface FeatureFlags {
boolean shouldEnableProfilePictures();
boolean shouldEnableDisappearingMessages();
boolean shouldEnablePersistentLogs();
}

View File

@@ -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<String, String> {
protected StringMap(Map<String, String> m) {
@@ -52,4 +60,19 @@ public abstract class StringMap extends Hashtable<String, String> {
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));
}
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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<LogRecord> logRecords) {
StringBuilder sb = new StringBuilder();
for (LogRecord record : logRecords) {
sb.append(formatter.format(record)).append('\n');
}
return sb.toString();
}
}

View File

@@ -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,

View File

@@ -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<LogRecord> 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

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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<Integer> 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]));
}
}

View File

@@ -24,6 +24,11 @@ public class TestFeatureFlagModule {
public boolean shouldEnableDisappearingMessages() {
return true;
}
@Override
public boolean shouldEnablePersistentLogs() {
return true;
}
};
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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;
}
};
}
}

View File

@@ -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());

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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');
}
}

View File

@@ -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() {

View File

@@ -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<String> 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 {

View File

@@ -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;
@@ -21,6 +22,7 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceGroup;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@@ -37,6 +39,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 +50,18 @@ public class SettingsFragment extends PreferenceFragmentCompat {
private SettingsViewModel viewModel;
private AvatarPreference prefAvatar;
private final ActivityResultLauncher<String> launcher =
private final ActivityResultLauncher<String> imageLauncher =
registerForActivityResult(new GetImageAdvanced(),
this::onImageSelected);
private final ActivityResultLauncher<String> logLauncher =
registerForActivityResult(new CreateDocumentAdvanced(),
uri -> onLogFileSelected(false, uri));
private final ActivityResultLauncher<String> oldLogLauncher =
registerForActivityResult(new CreateDocumentAdvanced(),
uri -> onLogFileSelected(true, uri));
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
@@ -63,7 +77,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 +91,32 @@ 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));
if (SDK_INT >= 19) {
exportLog.setOnPreferenceClickListener(preference -> {
logLauncher.launch(LOG_EXPORT_FILENAME);
return true;
});
} else {
exportLog.setVisible(false);
}
Preference exportOldLog =
requireNonNull(findPreference(PREF_EXPORT_OLD_LOG));
if (SDK_INT >= 19) {
exportOldLog.setOnPreferenceClickListener(preference -> {
oldLogLauncher.launch(LOG_EXPORT_FILENAME);
return true;
});
} else {
exportOldLog.setVisible(false);
}
} else {
PreferenceGroup dev = requireNonNull(findPreference(PREF_KEY_DEV));
dev.setVisible(false);
@@ -111,4 +146,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
ConfirmAvatarDialogFragment.TAG);
}
private void onLogFileSelected(boolean old, @Nullable Uri uri) {
if (uri != null) viewModel.exportPersistentLog(old, uri);
}
}

View File

@@ -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());
}
});
}
}

View File

@@ -58,6 +58,14 @@
android:targetPackage="@string/app_package" />
</Preference>
<Preference
android:key="pref_key_export_log"
android:title="Export current log to SD card" />
<Preference
android:key="pref_key_export_old_log"
android:title="Export previous log to SD card" />
<Preference
android:key="pref_key_explode"
android:title="Crash" />

View File

@@ -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 {

View File

@@ -42,7 +42,7 @@ class LoggingTestModule {
}
@Override
public File getLogcatFile() {
public File getTemporaryLogFile() {
return logFile;
}
};

View File

@@ -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
}
}