Compare commits

...

19 Commits

Author SHA1 Message Date
akwizgran
9ea4463cbc Avoid creating an in-memory copy of the log where possible.
This helps to avoid OOMs on low-memory devices.
2020-07-20 16:46:31 +01:00
akwizgran
a83682d4b6 Test that persistent log directory is deleted. 2020-07-14 11:29:14 +01:00
akwizgran
b1ec344bdb Update account manager test. 2020-07-14 11:28:33 +01:00
akwizgran
821327d62e Replace persistent logger after deleting account. 2020-07-14 11:19:40 +01:00
akwizgran
cda722a8b2 Refactor persistent logging to bramble-core. 2020-07-14 11:16:36 +01:00
akwizgran
ca5c18ece3 Show toast if log file is empty. 2020-07-13 14:49:54 +01:00
akwizgran
81ed5978d6 Add buttons for exporting persisted logs to SD card. 2020-07-13 14:49:54 +01:00
akwizgran
a63619ab47 Load the current and previous session's logs. 2020-07-13 14:49:53 +01:00
akwizgran
2d88819e80 Limit the number of lines we try to hold in memory. 2020-07-13 14:49:53 +01:00
akwizgran
fef19c1329 Flush the log stream whenever there are records to flush. 2020-07-13 14:49:53 +01:00
akwizgran
7ce91066f5 Check that createLogHandler() isn't called more than once.
This would result in multiple log files encrypted with the same key.
2020-07-13 14:49:53 +01:00
akwizgran
dfb581ef12 Remove try-with-resources to appease Animal Sniffer. 2020-07-13 14:49:53 +01:00
akwizgran
d9b4c013bb Encrypt logs on disk, store encryption key in DB. 2020-07-13 14:49:53 +01:00
akwizgran
61407c3e06 Save logs to disk. 2020-07-13 14:49:46 +01:00
Torsten Grote
ab682c82a3 Merge branch 'translation-md' into 'master'
Add TRANSLATION.md file

See merge request briar/briar!1261
2020-07-10 13:31:24 +00:00
akwizgran
375a7276ad Add link to LocLab wiki. 2020-07-10 14:22:06 +01:00
akwizgran
b7084b2486 Add TRANSLATION.md file.
This was recommended by Translate House.
2020-07-10 11:19:57 +01:00
akwizgran
aa152a80d1 Merge branch 'headless-connected' into 'master'
Expose contact connected state to REST API

See merge request briar/briar!1260
2020-07-06 14:50:57 +00:00
Torsten Grote
3f0d9233d9 [headless] expose contact connected state to REST API 2020-07-06 08:21:07 -03:00
31 changed files with 775 additions and 39 deletions

9
TRANSLATION.md Normal file
View File

@@ -0,0 +1,9 @@
Translations for this project are managed through Transifex:
https://transifex.com/otf/briar
If you'd like to volunteer as a translator, please create a Transifex account and request to be
added to the project's translation team. The Localization Lab has some instructions and advice for
translators here:
https://wiki.localizationlab.org/index.php/Briar

View File

@@ -9,8 +9,10 @@ import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.logging.PersistentLogManager;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -20,17 +22,21 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject; import javax.inject.Inject;
import static android.content.Context.MODE_PRIVATE;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.logging.Level.INFO; 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.IoUtils.deleteFileOrDir; import static org.briarproject.bramble.util.IoUtils.deleteFileOrDir;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.logFileOrDir; import static org.briarproject.bramble.util.LogUtils.logFileOrDir;
class AndroidAccountManager extends AccountManagerImpl class AndroidAccountManager extends AccountManagerImpl
implements AccountManager { implements AccountManager {
private static final Logger LOG = 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. * Directories that shouldn't be deleted when deleting the user's account.
@@ -40,13 +46,16 @@ class AndroidAccountManager extends AccountManagerImpl
protected final Context appContext; protected final Context appContext;
private final SharedPreferences prefs; private final SharedPreferences prefs;
private final PersistentLogManager logManager;
@Inject @Inject
AndroidAccountManager(DatabaseConfig databaseConfig, AndroidAccountManager(DatabaseConfig databaseConfig,
CryptoComponent crypto, IdentityManager identityManager, CryptoComponent crypto, IdentityManager identityManager,
SharedPreferences prefs, Application app) { SharedPreferences prefs, PersistentLogManager logManager,
Application app) {
super(databaseConfig, crypto, identityManager); super(databaseConfig, crypto, identityManager);
this.prefs = prefs; this.prefs = prefs;
this.logManager = logManager;
appContext = app.getApplicationContext(); appContext = app.getApplicationContext();
} }
@@ -74,6 +83,7 @@ class AndroidAccountManager extends AccountManagerImpl
LOG.info("Contents of account directory after deleting:"); LOG.info("Contents of account directory after deleting:");
logFileOrDir(LOG, INFO, getDataDir()); logFileOrDir(LOG, INFO, getDataDir());
} }
replacePersistentLogger();
} }
} }
@@ -134,4 +144,13 @@ class AndroidAccountManager extends AccountManagerImpl
private void addIfNotNull(Set<File> files, @Nullable File file) { private void addIfNotNull(Set<File> files, @Nullable File file) {
if (file != null) files.add(file); if (file != null) files.add(file);
} }
private void replacePersistentLogger() {
File logDir = appContext.getDir("log", MODE_PRIVATE);
try {
logManager.addLogHandler(logDir, getLogger(""));
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
} }

View File

@@ -7,6 +7,7 @@ import android.content.pm.ApplicationInfo;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.logging.PersistentLogManager;
import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.jmock.lib.legacy.ClassImposteriser; import org.jmock.lib.legacy.ClassImposteriser;
@@ -15,7 +16,9 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.io.File; 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.assertFalse;
import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.assertTrue;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
@@ -27,6 +30,8 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
context.mock(SharedPreferences.class, "prefs"); context.mock(SharedPreferences.class, "prefs");
private final SharedPreferences defaultPrefs = private final SharedPreferences defaultPrefs =
context.mock(SharedPreferences.class, "defaultPrefs"); context.mock(SharedPreferences.class, "defaultPrefs");
private final PersistentLogManager logManager =
context.mock(PersistentLogManager.class);
private final DatabaseConfig databaseConfig = private final DatabaseConfig databaseConfig =
context.mock(DatabaseConfig.class); context.mock(DatabaseConfig.class);
private final CryptoComponent crypto = context.mock(CryptoComponent.class); private final CryptoComponent crypto = context.mock(CryptoComponent.class);
@@ -40,6 +45,7 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
private final File testDir = getTestDirectory(); private final File testDir = getTestDirectory();
private final File keyDir = new File(testDir, "key"); private final File keyDir = new File(testDir, "key");
private final File dbDir = new File(testDir, "db"); private final File dbDir = new File(testDir, "db");
private final File logDir = new File(testDir, "log");
private AndroidAccountManager accountManager; private AndroidAccountManager accountManager;
@@ -61,7 +67,7 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
will(returnValue(app)); will(returnValue(app));
}}); }});
accountManager = new AndroidAccountManager(databaseConfig, crypto, accountManager = new AndroidAccountManager(databaseConfig, crypto,
identityManager, prefs, app) { identityManager, prefs, logManager, app) {
@Override @Override
SharedPreferences getDefaultSharedPreferences() { SharedPreferences getDefaultSharedPreferences() {
return defaultPrefs; return defaultPrefs;
@@ -109,10 +115,15 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
will(returnValue(cacheDir)); will(returnValue(cacheDir));
oneOf(app).getExternalCacheDir(); oneOf(app).getExternalCacheDir();
will(returnValue(externalCacheDir)); will(returnValue(externalCacheDir));
oneOf(app).getDir("log", MODE_PRIVATE);
will(returnValue(logDir));
oneOf(logManager).addLogHandler(with(logDir),
with(any(Logger.class)));
}}); }});
assertTrue(dbDir.mkdirs()); assertTrue(dbDir.mkdirs());
assertTrue(keyDir.mkdirs()); assertTrue(keyDir.mkdirs());
assertTrue(logDir.mkdirs());
assertTrue(codeCacheDir.mkdirs()); assertTrue(codeCacheDir.mkdirs());
assertTrue(codeCacheFile.createNewFile()); assertTrue(codeCacheFile.createNewFile());
assertTrue(libDir.mkdirs()); assertTrue(libDir.mkdirs());
@@ -130,6 +141,7 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
assertFalse(dbDir.exists()); assertFalse(dbDir.exists());
assertFalse(keyDir.exists()); assertFalse(keyDir.exists());
assertFalse(logDir.exists());
assertTrue(codeCacheDir.exists()); assertTrue(codeCacheDir.exists());
assertTrue(codeCacheFile.exists()); assertTrue(codeCacheFile.exists());
assertTrue(libDir.exists()); assertTrue(libDir.exists());

View File

@@ -1,8 +1,16 @@
package org.briarproject.bramble.api; package org.briarproject.bramble.api;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.Map; 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> { public abstract class StringMap extends Hashtable<String, String> {
protected StringMap(Map<String, String> m) { 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) { public void putLong(String key, long value) {
put(key, String.valueOf(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

@@ -19,4 +19,10 @@ public interface StreamDecrypterFactory {
*/ */
StreamDecrypter createContactExchangeStreamDecrypter(InputStream in, StreamDecrypter createContactExchangeStreamDecrypter(InputStream in,
SecretKey headerKey); SecretKey headerKey);
/**
* Creates a {@link StreamDecrypter} for decrypting a log stream.
*/
StreamDecrypter createLogStreamDecrypter(InputStream in,
SecretKey headerKey);
} }

View File

@@ -17,6 +17,12 @@ public interface StreamEncrypterFactory {
* Creates a {@link StreamEncrypter} for encrypting a contact exchange * Creates a {@link StreamEncrypter} for encrypting a contact exchange
* stream. * stream.
*/ */
StreamEncrypter createContactExchangeStreamDecrypter(OutputStream out, StreamEncrypter createContactExchangeStreamEncrypter(OutputStream out,
SecretKey headerKey);
/**
* Creates a {@link StreamEncrypter} for encrypting a log stream.
*/
StreamEncrypter createLogStreamEncrypter(OutputStream out,
SecretKey headerKey); SecretKey headerKey);
} }

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 getPersistedLog(File dir, boolean old) throws IOException;
}

View File

@@ -16,8 +16,13 @@ public interface StreamReaderFactory {
/** /**
* Creates an {@link InputStream InputStream} for reading from a contact * Creates an {@link InputStream InputStream} for reading from a contact
* exchangestream. * exchange stream.
*/ */
InputStream createContactExchangeStreamReader(InputStream in, InputStream createContactExchangeStreamReader(InputStream in,
SecretKey headerKey); SecretKey headerKey);
/**
* Creates an {@link InputStream} for reading from a log stream.
*/
InputStream createLogStreamReader(InputStream in, SecretKey headerKey);
} }

View File

@@ -9,15 +9,18 @@ import java.io.OutputStream;
public interface StreamWriterFactory { public interface StreamWriterFactory {
/** /**
* Creates an {@link OutputStream OutputStream} for writing to a * Creates a {@link StreamWriter} for writing to a transport stream.
* transport stream
*/ */
StreamWriter createStreamWriter(OutputStream out, StreamContext ctx); StreamWriter createStreamWriter(OutputStream out, StreamContext ctx);
/** /**
* Creates an {@link OutputStream OutputStream} for writing to a contact * Creates a {@link StreamWriter} for writing to a contact exchange stream.
* exchange stream.
*/ */
StreamWriter createContactExchangeStreamWriter(OutputStream out, StreamWriter createContactExchangeStreamWriter(OutputStream out,
SecretKey headerKey); SecretKey headerKey);
/**
* Creates a {@link StreamWriter} for writing to a log stream.
*/
StreamWriter createLogStreamWriter(OutputStream out, SecretKey headerKey);
} }

View File

@@ -13,6 +13,7 @@ import org.briarproject.bramble.identity.IdentityModule;
import org.briarproject.bramble.io.IoModule; import org.briarproject.bramble.io.IoModule;
import org.briarproject.bramble.keyagreement.KeyAgreementModule; import org.briarproject.bramble.keyagreement.KeyAgreementModule;
import org.briarproject.bramble.lifecycle.LifecycleModule; import org.briarproject.bramble.lifecycle.LifecycleModule;
import org.briarproject.bramble.logging.LoggingModule;
import org.briarproject.bramble.plugin.PluginModule; import org.briarproject.bramble.plugin.PluginModule;
import org.briarproject.bramble.properties.PropertiesModule; import org.briarproject.bramble.properties.PropertiesModule;
import org.briarproject.bramble.record.RecordModule; import org.briarproject.bramble.record.RecordModule;
@@ -41,6 +42,7 @@ import dagger.Module;
IoModule.class, IoModule.class,
KeyAgreementModule.class, KeyAgreementModule.class,
LifecycleModule.class, LifecycleModule.class,
LoggingModule.class,
PluginModule.class, PluginModule.class,
PropertiesModule.class, PropertiesModule.class,
RecordModule.class, RecordModule.class,

View File

@@ -36,4 +36,10 @@ class StreamDecrypterFactoryImpl implements StreamDecrypterFactory {
SecretKey headerKey) { SecretKey headerKey) {
return new StreamDecrypterImpl(in, cipherProvider.get(), 0, headerKey); return new StreamDecrypterImpl(in, cipherProvider.get(), 0, headerKey);
} }
@Override
public StreamDecrypter createLogStreamDecrypter(InputStream in,
SecretKey headerKey) {
return createContactExchangeStreamDecrypter(in, headerKey);
}
} }

View File

@@ -51,7 +51,7 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
} }
@Override @Override
public StreamEncrypter createContactExchangeStreamDecrypter( public StreamEncrypter createContactExchangeStreamEncrypter(
OutputStream out, SecretKey headerKey) { OutputStream out, SecretKey headerKey) {
AuthenticatedCipher cipher = cipherProvider.get(); AuthenticatedCipher cipher = cipherProvider.get();
byte[] streamHeaderNonce = new byte[STREAM_HEADER_NONCE_LENGTH]; byte[] streamHeaderNonce = new byte[STREAM_HEADER_NONCE_LENGTH];
@@ -60,4 +60,10 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
return new StreamEncrypterImpl(out, cipher, 0, null, streamHeaderNonce, return new StreamEncrypterImpl(out, cipher, 0, null, streamHeaderNonce,
headerKey, frameKey); headerKey, frameKey);
} }
@Override
public StreamEncrypter createLogStreamEncrypter(OutputStream out,
SecretKey headerKey) {
return createContactExchangeStreamEncrypter(out, headerKey);
}
} }

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.logging; package org.briarproject.bramble.logging;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -47,6 +47,7 @@ public class BriefLogFormatter extends Formatter {
sb.append('\n'); sb.append('\n');
appendThrowable(sb, t); appendThrowable(sb, t);
} }
sb.append('\n');
return sb.toString(); return sb.toString();
} }

View File

@@ -0,0 +1,43 @@
package org.briarproject.bramble.logging;
import java.io.OutputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
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 ScheduledExecutorService scheduler;
private final Executor ioExecutor;
private final AtomicBoolean flushScheduled = new AtomicBoolean(false);
FlushingStreamHandler(ScheduledExecutorService 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,
FLUSH_DELAY_MS, MILLISECONDS);
}
}
private void scheduledFlush() {
ioExecutor.execute(() -> {
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,177 @@
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.Scheduler;
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.ScheduledExecutorService;
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 ScheduledExecutorService 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(
@Scheduler ScheduledExecutorService 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 getPersistedLog(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 getPersistedLog(new File(dir, OLD_LOG_FILE), oldLogKey);
} else {
return getPersistedLog(new File(dir, LOG_FILE), logKey);
}
}
private Scanner getPersistedLog(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,15 +24,21 @@ class StreamReaderFactoryImpl implements StreamReaderFactory {
@Override @Override
public InputStream createStreamReader(InputStream in, StreamContext ctx) { public InputStream createStreamReader(InputStream in, StreamContext ctx) {
return new StreamReaderImpl( return new StreamReaderImpl(streamDecrypterFactory
streamDecrypterFactory.createStreamDecrypter(in, ctx)); .createStreamDecrypter(in, ctx));
} }
@Override @Override
public InputStream createContactExchangeStreamReader(InputStream in, public InputStream createContactExchangeStreamReader(InputStream in,
SecretKey headerKey) { SecretKey headerKey) {
return new StreamReaderImpl( return new StreamReaderImpl(streamDecrypterFactory
streamDecrypterFactory.createContactExchangeStreamDecrypter(in, .createContactExchangeStreamDecrypter(in, headerKey));
headerKey)); }
@Override
public InputStream createLogStreamReader(InputStream in,
SecretKey headerKey) {
return new StreamReaderImpl(streamDecrypterFactory
.createLogStreamDecrypter(in, headerKey));
} }
} }

View File

@@ -26,15 +26,21 @@ class StreamWriterFactoryImpl implements StreamWriterFactory {
@Override @Override
public StreamWriter createStreamWriter(OutputStream out, public StreamWriter createStreamWriter(OutputStream out,
StreamContext ctx) { StreamContext ctx) {
return new StreamWriterImpl( return new StreamWriterImpl(streamEncrypterFactory
streamEncrypterFactory.createStreamEncrypter(out, ctx)); .createStreamEncrypter(out, ctx));
} }
@Override @Override
public StreamWriter createContactExchangeStreamWriter(OutputStream out, public StreamWriter createContactExchangeStreamWriter(OutputStream out,
SecretKey headerKey) { SecretKey headerKey) {
return new StreamWriterImpl( return new StreamWriterImpl(streamEncrypterFactory
streamEncrypterFactory.createContactExchangeStreamDecrypter(out, .createContactExchangeStreamEncrypter(out, headerKey));
headerKey)); }
@Override
public StreamWriter createLogStreamWriter(OutputStream out,
SecretKey headerKey) {
return new StreamWriterImpl(streamEncrypterFactory
.createLogStreamEncrypter(out, headerKey));
} }
} }

View File

@@ -6,6 +6,7 @@ import android.content.SharedPreferences;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.logging.PersistentLogManager;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.Localizer; import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.util.UiUtils;
@@ -17,8 +18,8 @@ class BriarAccountManager extends AndroidAccountManager {
@Inject @Inject
BriarAccountManager(DatabaseConfig databaseConfig, CryptoComponent crypto, BriarAccountManager(DatabaseConfig databaseConfig, CryptoComponent crypto,
IdentityManager identityManager, SharedPreferences prefs, IdentityManager identityManager, SharedPreferences prefs,
Application app) { PersistentLogManager logManager, Application app) {
super(databaseConfig, crypto, identityManager, prefs, app); super(databaseConfig, crypto, identityManager, prefs, logManager, app);
} }
@Override @Override

View File

@@ -20,6 +20,7 @@ import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
import org.briarproject.bramble.api.keyagreement.PayloadParser; import org.briarproject.bramble.api.keyagreement.PayloadParser;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; 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.plugin.PluginManager;
import org.briarproject.bramble.api.settings.SettingsManager; import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.api.system.AndroidExecutor;
@@ -56,6 +57,7 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager
import org.briarproject.briar.api.test.TestDataCreator; import org.briarproject.briar.api.test.TestDataCreator;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Formatter;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -165,6 +167,10 @@ public interface AndroidComponent
FeatureFlags featureFlags(); FeatureFlags featureFlags();
PersistentLogManager persistentLogManager();
Formatter formatter();
void inject(SignInReminderReceiver briarService); void inject(SignInReminderReceiver briarService);
void inject(BriarService briarService); void inject(BriarService briarService);

View File

@@ -19,6 +19,7 @@ import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes; import org.acra.annotation.ReportsCrashes;
import org.briarproject.bramble.BrambleAndroidEagerSingletons; import org.briarproject.bramble.BrambleAndroidEagerSingletons;
import org.briarproject.bramble.BrambleCoreEagerSingletons; import org.briarproject.bramble.BrambleCoreEagerSingletons;
import org.briarproject.bramble.api.logging.PersistentLogManager;
import org.briarproject.briar.BriarCoreEagerSingletons; import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BuildConfig; import org.briarproject.briar.BuildConfig;
import org.briarproject.briar.R; import org.briarproject.briar.R;
@@ -28,6 +29,8 @@ import org.briarproject.briar.android.reporting.BriarReportSenderFactory;
import org.briarproject.briar.android.reporting.DevReportActivity; import org.briarproject.briar.android.reporting.DevReportActivity;
import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.util.UiUtils;
import java.io.File;
import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.logging.Handler; import java.util.logging.Handler;
import java.util.logging.LogRecord; import java.util.logging.LogRecord;
@@ -36,6 +39,7 @@ import java.util.logging.Logger;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static java.util.logging.Level.FINE; import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.acra.ReportField.ANDROID_VERSION; import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE; import static org.acra.ReportField.APP_VERSION_CODE;
@@ -54,6 +58,7 @@ import static org.acra.ReportField.REPORT_ID;
import static org.acra.ReportField.STACK_TRACE; import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_APP_START_DATE; import static org.acra.ReportField.USER_APP_START_DATE;
import static org.acra.ReportField.USER_CRASH_DATE; import static org.acra.ReportField.USER_CRASH_DATE;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@ReportsCrashes( @ReportsCrashes(
@@ -120,9 +125,19 @@ public class BriarApplicationImpl extends Application
rootLogger.addHandler(logHandler); rootLogger.addHandler(logHandler);
rootLogger.setLevel(IS_DEBUG_BUILD ? FINE : INFO); rootLogger.setLevel(IS_DEBUG_BUILD ? FINE : INFO);
applicationComponent = createApplicationComponent();
PersistentLogManager logManager =
applicationComponent.persistentLogManager();
File logDir = getDir("log", MODE_PRIVATE);
try {
rootLogger.addHandler(logManager.createLogHandler(logDir));
} catch (IOException e) {
logException(LOG, WARNING, e);
}
LOG.info("Created"); LOG.info("Created");
applicationComponent = createApplicationComponent();
EmojiManager.install(new GoogleEmojiProvider()); EmojiManager.install(new GoogleEmojiProvider());
} }

View File

@@ -16,5 +16,7 @@ public interface RequestCodes {
int REQUEST_KEYGUARD_UNLOCK = 12; int REQUEST_KEYGUARD_UNLOCK = 12;
int REQUEST_ATTACH_IMAGE = 13; int REQUEST_ATTACH_IMAGE = 13;
int REQUEST_SAVE_ATTACHMENT = 14; int REQUEST_SAVE_ATTACHMENT = 14;
int REQUEST_EXPORT_LOG = 15;
int REQUEST_EXPORT_OLD_LOG = 16;
} }

View File

@@ -14,17 +14,21 @@ import android.os.Looper;
import org.acra.builder.ReportBuilder; import org.acra.builder.ReportBuilder;
import org.acra.builder.ReportPrimer; import org.acra.builder.ReportPrimer;
import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.logging.PersistentLogManager;
import org.briarproject.briar.BuildConfig; import org.briarproject.briar.BuildConfig;
import org.briarproject.briar.android.AndroidComponent;
import org.briarproject.briar.android.BriarApplication; import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.logging.BriefLogFormatter;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map; import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
@@ -37,6 +41,7 @@ import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE; import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.content.Context.ACTIVITY_SERVICE; import static android.content.Context.ACTIVITY_SERVICE;
import static android.content.Context.CONNECTIVITY_SERVICE; import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Context.MODE_PRIVATE;
import static android.content.Context.WIFI_P2P_SERVICE; import static android.content.Context.WIFI_P2P_SERVICE;
import static android.content.Context.WIFI_SERVICE; import static android.content.Context.WIFI_SERVICE;
import static android.net.ConnectivityManager.TYPE_MOBILE; import static android.net.ConnectivityManager.TYPE_MOBILE;
@@ -51,6 +56,8 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
public class BriarReportPrimer implements ReportPrimer { public class BriarReportPrimer implements ReportPrimer {
private static final int MAX_PERSISTED_LOG_LINES = 1_000;
@Override @Override
public void primeReport(@NonNull Context ctx, public void primeReport(@NonNull Context ctx,
@NonNull ReportBuilder builder) { @NonNull ReportBuilder builder) {
@@ -81,13 +88,23 @@ public class BriarReportPrimer implements ReportPrimer {
// Log // Log
BriarApplication app = BriarApplication app =
(BriarApplication) ctx.getApplicationContext(); (BriarApplication) ctx.getApplicationContext();
AndroidComponent appComponent = app.getApplicationComponent();
PersistentLogManager logManager =
appComponent.persistentLogManager();
Formatter formatter = appComponent.formatter();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
Formatter formatter = new BriefLogFormatter();
for (LogRecord record : app.getRecentLogRecords()) { for (LogRecord record : app.getRecentLogRecords()) {
sb.append(formatter.format(record)).append('\n'); sb.append(formatter.format(record));
} }
customData.put("Log", sb.toString()); customData.put("Log", sb.toString());
customData.put("Persisted log",
getPersistedLog(ctx, logManager, false));
customData.put("Previous persisted log",
getPersistedLog(ctx, logManager, true));
// System memory // System memory
Object o = ctx.getSystemService(ACTIVITY_SERVICE); Object o = ctx.getSystemService(ACTIVITY_SERVICE);
ActivityManager am = (ActivityManager) o; ActivityManager am = (ActivityManager) o;
@@ -252,6 +269,27 @@ public class BriarReportPrimer implements ReportPrimer {
return unmodifiableMap(customData); return unmodifiableMap(customData);
} }
private String getPersistedLog(Context ctx,
PersistentLogManager logManager, boolean old) {
File logDir = ctx.getDir("log", MODE_PRIVATE);
StringBuilder sb = new StringBuilder();
try {
Scanner scanner = logManager.getPersistedLog(logDir, old);
LinkedList<String> lines = new LinkedList<>();
int numLines = 0;
while (scanner.hasNextLine()) {
lines.add(scanner.nextLine());
if (numLines == MAX_PERSISTED_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 persisted log: ").append(e);
}
return sb.toString();
}
} }
private static class SingleShotAndroidExecutor extends Thread { private static class SingleShotAndroidExecutor extends Thread {

View File

@@ -1,18 +1,65 @@
package org.briarproject.briar.android.settings; package org.briarproject.briar.android.settings;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.MenuItem; import android.view.MenuItem;
import android.widget.Toast;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.logging.PersistentLogManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import static android.content.Intent.ACTION_CREATE_DOCUMENT;
import static android.content.Intent.CATEGORY_OPENABLE;
import static android.content.Intent.EXTRA_TITLE;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Environment.DIRECTORY_DOWNLOADS;
import static android.os.Environment.getExternalStoragePublicDirectory;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_EXPORT_LOG;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_EXPORT_OLD_LOG;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SettingsActivity extends BriarActivity { public class SettingsActivity extends BriarActivity {
private static final Logger LOG =
getLogger(SettingsActivity.class.getName());
private static final String LOG_EXPORT_FILENAME = "briar-log.txt";
@Inject
@IoExecutor
Executor ioExecutor;
@Inject
PersistentLogManager logManager;
@Override @Override
public void onCreate(Bundle bundle) { public void onCreate(@Nullable Bundle bundle) {
super.onCreate(bundle); super.onCreate(bundle);
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
@@ -37,4 +84,96 @@ public class SettingsActivity extends BriarActivity {
} }
return false; return false;
} }
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_EXPORT_LOG && result == RESULT_OK &&
data != null && data.getData() != null) {
exportLog(false, data.getData());
} else if (request == REQUEST_EXPORT_OLD_LOG && result == RESULT_OK &&
data != null && data.getData() != null) {
exportLog(true, data.getData());
}
}
private void exportLog(boolean old, Uri uri) {
copyLog(old, () -> getOutputStream(uri));
}
private void copyLog(boolean old, OutputStreamProvider osp) {
ioExecutor.execute(() -> {
try {
File logDir = getApplication().getDir("log", MODE_PRIVATE);
Scanner scanner = logManager.getPersistedLog(logDir, old);
if (!scanner.hasNextLine()) {
scanner.close();
runOnUiThreadUnlessDestroyed(() ->
Toast.makeText(getApplication(), "Log is empty",
LENGTH_LONG).show());
return;
}
PrintWriter w = new PrintWriter(osp.getOutputStream());
while (scanner.hasNextLine()) w.println(scanner.nextLine());
w.flush();
w.close();
scanner.close();
runOnUiThreadUnlessDestroyed(() ->
Toast.makeText(getApplication(), "Log exported",
LENGTH_LONG).show());
} catch (IOException e) {
logException(LOG, WARNING, e);
runOnUiThreadUnlessDestroyed(() ->
Toast.makeText(getApplication(), "Failed to export log",
LENGTH_LONG).show());
}
});
}
void onExportLogClick(boolean old) {
if (SDK_INT >= 19) {
Intent intent = getExportLogIntent();
int request = old ? REQUEST_EXPORT_OLD_LOG : REQUEST_EXPORT_LOG;
startActivityForResult(intent, request);
} else {
exportLog(old);
}
}
@RequiresApi(api = 19)
private Intent getExportLogIntent() {
Intent intent = new Intent(ACTION_CREATE_DOCUMENT);
intent.addCategory(CATEGORY_OPENABLE);
intent.setType("text/plain");
intent.putExtra(EXTRA_TITLE, LOG_EXPORT_FILENAME);
return intent;
}
private void exportLog(boolean old) {
File file = getLogOutputFile();
copyLog(old, () -> getOutputStream(file));
}
private File getLogOutputFile() {
File path = getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS);
//noinspection ResultOfMethodCallIgnored
path.mkdirs();
return new File(path, LOG_EXPORT_FILENAME);
}
private OutputStream getOutputStream(File file) throws IOException {
return new FileOutputStream(file);
}
private OutputStream getOutputStream(Uri uri) throws IOException {
OutputStream os =
getApplication().getContentResolver().openOutputStream(uri);
if (os == null) throw new IOException();
return os;
}
private interface OutputStreamProvider {
OutputStream getOutputStream() throws IOException;
}
} }

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.settings; package org.briarproject.briar.android.settings;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
@@ -74,6 +75,7 @@ import static android.widget.Toast.LENGTH_SHORT;
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR; import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE; import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_MOBILE; import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_NETWORK; import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_NETWORK;
@@ -133,7 +135,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
"pref_key_tor_only_when_charging"; "pref_key_tor_only_when_charging";
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(SettingsFragment.class.getName()); getLogger(SettingsFragment.class.getName());
private SettingsActivity listener; private SettingsActivity listener;
private ListPreference language; private ListPreference language;
@@ -251,8 +253,25 @@ public class SettingsFragment extends PreferenceFragmentCompat
throw new RuntimeException("Boom!"); throw new RuntimeException("Boom!");
} }
); );
findPreference("pref_key_export_log").setOnPreferenceClickListener(
preference -> {
((SettingsActivity) requireActivity())
.onExportLogClick(false);
return true;
}
);
findPreference("pref_key_export_old_log")
.setOnPreferenceClickListener(
preference -> {
((SettingsActivity) requireActivity())
.onExportLogClick(true);
return true;
}
);
} else { } else {
findPreference("pref_key_explode").setVisible(false); findPreference("pref_key_explode").setVisible(false);
findPreference("pref_key_export_log").setVisible(false);
findPreference("pref_key_export_old_log").setVisible(false);
findPreference("pref_key_test_data").setVisible(false); findPreference("pref_key_test_data").setVisible(false);
PreferenceGroup testing = PreferenceGroup testing =
findPreference("pref_key_explode").getParent(); findPreference("pref_key_explode").getParent();
@@ -331,6 +350,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
return direction == LAYOUT_DIRECTION_LTR; return direction == LAYOUT_DIRECTION_LTR;
} }
@SuppressLint("StringFormatInvalid")
private void setTorNetworkSummary(int torNetworkSetting) { private void setTorNetworkSummary(int torNetworkSetting) {
if (torNetworkSetting != PREF_TOR_NETWORK_AUTOMATIC) { if (torNetworkSetting != PREF_TOR_NETWORK_AUTOMATIC) {
torNetwork.setSummary("%s"); // use setting value torNetwork.setSummary("%s"); // use setting value

View File

@@ -227,6 +227,16 @@
android:targetPackage="@string/app_package"/> android:targetPackage="@string/app_package"/>
</Preference> </Preference>
<Preference
android:key="pref_key_export_log"
android:title="Export Current Log to SD Card"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_key_export_old_log"
android:title="Export Previous Log to SD Card"
app:iconSpaceReserved="false"/>
<Preference <Preference
android:key="pref_key_explode" android:key="pref_key_explode"
android:title="Crash" android:title="Crash"

View File

@@ -68,7 +68,8 @@ Returns a JSON array of contacts:
"alias" : "A local nickname", "alias" : "A local nickname",
"handshakePublicKey": "XnYRd7a7E4CTqgAvh4hCxh/YZ0EPscxknB9ZcEOpSzY=", "handshakePublicKey": "XnYRd7a7E4CTqgAvh4hCxh/YZ0EPscxknB9ZcEOpSzY=",
"verified": true, "verified": true,
"lastChatActivity": 1557838312175 "lastChatActivity": 1557838312175,
"connected": false
} }
``` ```
@@ -392,3 +393,19 @@ will no longer work on making this `pendingContact` become `contact`.
"type": "event" "type": "event"
} }
``` ```
### A contact connected or disconnected
When Briar establishes a connection to a contact (the contact comes online),
it sends a `ContactConnectedEvent`.
When the last connection is lost (the contact goes offline), it sends a `ContactDisconnectedEvent`.
```json
{
"data": {
"contactId": 1
},
"name": "ContactConnectedEvent",
"type": "event"
}
```

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import io.javalin.http.BadRequestResponse import io.javalin.http.BadRequestResponse
import io.javalin.http.Context import io.javalin.http.Context
import io.javalin.http.NotFoundResponse import io.javalin.http.NotFoundResponse
import org.briarproject.bramble.api.connection.ConnectionRegistry
import org.briarproject.bramble.api.contact.ContactManager import org.briarproject.bramble.api.contact.ContactManager
import org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_REGEX import org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_REGEX
import org.briarproject.bramble.api.contact.PendingContactId import org.briarproject.bramble.api.contact.PendingContactId
@@ -16,6 +17,8 @@ import org.briarproject.bramble.api.db.NoSuchPendingContactException
import org.briarproject.bramble.api.event.Event import org.briarproject.bramble.api.event.Event
import org.briarproject.bramble.api.event.EventListener import org.briarproject.bramble.api.event.EventListener
import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent
import org.briarproject.bramble.util.StringUtils.toUtf8 import org.briarproject.bramble.util.StringUtils.toUtf8
import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.api.conversation.ConversationManager
import org.briarproject.briar.headless.event.WebSocketController import org.briarproject.briar.headless.event.WebSocketController
@@ -32,6 +35,8 @@ internal const val EVENT_CONTACT_ADDED = "ContactAddedEvent"
internal const val EVENT_PENDING_CONTACT_STATE_CHANGED = "PendingContactStateChangedEvent" internal const val EVENT_PENDING_CONTACT_STATE_CHANGED = "PendingContactStateChangedEvent"
internal const val EVENT_PENDING_CONTACT_ADDED = "PendingContactAddedEvent" internal const val EVENT_PENDING_CONTACT_ADDED = "PendingContactAddedEvent"
internal const val EVENT_PENDING_CONTACT_REMOVED = "PendingContactRemovedEvent" internal const val EVENT_PENDING_CONTACT_REMOVED = "PendingContactRemovedEvent"
internal const val EVENT_CONTACT_CONNECTED = "ContactConnectedEvent"
internal const val EVENT_CONTACT_DISCONNECTED = "ContactDisconnectedEvent"
@Immutable @Immutable
@Singleton @Singleton
@@ -41,7 +46,8 @@ constructor(
private val contactManager: ContactManager, private val contactManager: ContactManager,
private val conversationManager: ConversationManager, private val conversationManager: ConversationManager,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val webSocket: WebSocketController private val webSocket: WebSocketController,
private val connectionRegistry: ConnectionRegistry
) : ContactController, EventListener { ) : ContactController, EventListener {
override fun eventOccurred(e: Event) = when (e) { override fun eventOccurred(e: Event) = when (e) {
@@ -57,6 +63,12 @@ constructor(
is PendingContactRemovedEvent -> { is PendingContactRemovedEvent -> {
webSocket.sendEvent(EVENT_PENDING_CONTACT_REMOVED, e.output()) webSocket.sendEvent(EVENT_PENDING_CONTACT_REMOVED, e.output())
} }
is ContactConnectedEvent -> {
webSocket.sendEvent(EVENT_CONTACT_CONNECTED, e.output())
}
is ContactDisconnectedEvent -> {
webSocket.sendEvent(EVENT_CONTACT_DISCONNECTED, e.output())
}
else -> { else -> {
} }
} }
@@ -64,7 +76,8 @@ constructor(
override fun list(ctx: Context): Context { override fun list(ctx: Context): Context {
val contacts = contactManager.contacts.map { contact -> val contacts = contactManager.contacts.map { contact ->
val latestMsgTime = conversationManager.getGroupCount(contact.id).latestMsgTime val latestMsgTime = conversationManager.getGroupCount(contact.id).latestMsgTime
contact.output(latestMsgTime) val connected = connectionRegistry.isConnected(contact.id)
contact.output(latestMsgTime, connected)
} }
return ctx.json(contacts) return ctx.json(contacts)
} }

View File

@@ -2,15 +2,17 @@ package org.briarproject.briar.headless.contact
import org.briarproject.bramble.api.contact.Contact import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.api.contact.event.ContactAddedEvent import org.briarproject.bramble.api.contact.event.ContactAddedEvent
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent
import org.briarproject.bramble.identity.output import org.briarproject.bramble.identity.output
import org.briarproject.briar.api.conversation.ConversationManager
import org.briarproject.briar.headless.json.JsonDict import org.briarproject.briar.headless.json.JsonDict
internal fun Contact.output(latestMsgTime: Long) = JsonDict( internal fun Contact.output(latestMsgTime: Long, connected: Boolean) = JsonDict(
"contactId" to id.int, "contactId" to id.int,
"author" to author.output(), "author" to author.output(),
"verified" to isVerified, "verified" to isVerified,
"lastChatActivity" to latestMsgTime "lastChatActivity" to latestMsgTime,
"connected" to connected
).apply { ).apply {
alias?.let { put("alias", it) } alias?.let { put("alias", it) }
handshakePublicKey?.let { put("handshakePublicKey", it.encoded) } handshakePublicKey?.let { put("handshakePublicKey", it.encoded) }
@@ -20,3 +22,11 @@ internal fun ContactAddedEvent.output() = JsonDict(
"contactId" to contactId.int, "contactId" to contactId.int,
"verified" to isVerified "verified" to isVerified
) )
internal fun ContactConnectedEvent.output() = JsonDict(
"contactId" to contactId.int
)
internal fun ContactDisconnectedEvent.output() = JsonDict(
"contactId" to contactId.int
)

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import io.javalin.http.Context import io.javalin.http.Context
import io.javalin.http.util.ContextUtil import io.javalin.http.util.ContextUtil
import io.mockk.mockk import io.mockk.mockk
import org.briarproject.bramble.api.connection.ConnectionRegistry
import org.briarproject.bramble.api.contact.Contact import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.api.contact.ContactManager import org.briarproject.bramble.api.contact.ContactManager
import org.briarproject.bramble.api.identity.Author import org.briarproject.bramble.api.identity.Author
@@ -26,6 +27,7 @@ abstract class ControllerTest {
protected val contactManager = mockk<ContactManager>() protected val contactManager = mockk<ContactManager>()
protected val conversationManager = mockk<ConversationManager>() protected val conversationManager = mockk<ConversationManager>()
protected val identityManager = mockk<IdentityManager>() protected val identityManager = mockk<IdentityManager>()
protected val connectionRegistry = mockk<ConnectionRegistry>()
protected val clock = mockk<Clock>() protected val clock = mockk<Clock>()
protected val ctx = mockk<Context>() protected val ctx = mockk<Context>()

View File

@@ -20,6 +20,8 @@ import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEven
import org.briarproject.bramble.api.db.NoSuchContactException import org.briarproject.bramble.api.db.NoSuchContactException
import org.briarproject.bramble.api.db.NoSuchPendingContactException import org.briarproject.bramble.api.db.NoSuchPendingContactException
import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent
import org.briarproject.bramble.identity.output import org.briarproject.bramble.identity.output
import org.briarproject.bramble.test.TestUtils.getPendingContact import org.briarproject.bramble.test.TestUtils.getPendingContact
import org.briarproject.bramble.test.TestUtils.getRandomBytes import org.briarproject.bramble.test.TestUtils.getRandomBytes
@@ -29,6 +31,7 @@ import org.briarproject.briar.headless.json.JsonDict
import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import kotlin.random.Random
internal class ContactControllerTest : ControllerTest() { internal class ContactControllerTest : ControllerTest() {
@@ -38,7 +41,8 @@ internal class ContactControllerTest : ControllerTest() {
contactManager, contactManager,
conversationManager, conversationManager,
objectMapper, objectMapper,
webSocketController webSocketController,
connectionRegistry
) )
@Test @Test
@@ -50,9 +54,11 @@ internal class ContactControllerTest : ControllerTest() {
@Test @Test
fun testList() { fun testList() {
val connected = Random.nextBoolean()
every { contactManager.contacts } returns listOf(contact) every { contactManager.contacts } returns listOf(contact)
every { conversationManager.getGroupCount(contact.id).latestMsgTime } returns timestamp every { conversationManager.getGroupCount(contact.id).latestMsgTime } returns timestamp
every { ctx.json(listOf(contact.output(timestamp))) } returns ctx every { connectionRegistry.isConnected(contact.id) } returns connected
every { ctx.json(listOf(contact.output(timestamp, connected))) } returns ctx
controller.list(ctx) controller.list(ctx)
} }
@@ -267,8 +273,37 @@ internal class ContactControllerTest : ControllerTest() {
controller.eventOccurred(event) controller.eventOccurred(event)
} }
@Test
fun testContactConnectedEvent() {
val event = ContactConnectedEvent(contact.id)
every {
webSocketController.sendEvent(
EVENT_CONTACT_CONNECTED,
event.output()
)
} just runs
controller.eventOccurred(event)
}
@Test
fun testContactDisconnectedEvent() {
val event = ContactDisconnectedEvent(contact.id)
every {
webSocketController.sendEvent(
EVENT_CONTACT_DISCONNECTED,
event.output()
)
} just runs
controller.eventOccurred(event)
}
@Test @Test
fun testOutputContact() { fun testOutputContact() {
val connected = Random.nextBoolean()
assertNotNull(contact.handshakePublicKey) assertNotNull(contact.handshakePublicKey)
val json = """ val json = """
{ {
@@ -277,10 +312,11 @@ internal class ContactControllerTest : ControllerTest() {
"alias" : "${contact.alias}", "alias" : "${contact.alias}",
"handshakePublicKey": ${toJson(contact.handshakePublicKey!!.encoded)}, "handshakePublicKey": ${toJson(contact.handshakePublicKey!!.encoded)},
"verified": ${contact.isVerified}, "verified": ${contact.isVerified},
"lastChatActivity": $timestamp "lastChatActivity": $timestamp,
"connected": $connected
} }
""" """
assertJsonEquals(json, contact.output(timestamp)) assertJsonEquals(json, contact.output(timestamp, connected))
} }
@Test @Test
@@ -358,4 +394,26 @@ internal class ContactControllerTest : ControllerTest() {
assertJsonEquals(json, event.output()) assertJsonEquals(json, event.output())
} }
@Test
fun testOutputContactConnectedEvent() {
val event = ContactConnectedEvent(contact.id)
val json = """
{
"contactId": ${contact.id.int}
}
"""
assertJsonEquals(json, event.output())
}
@Test
fun testOutputContactDisconnectedEvent() {
val event = ContactDisconnectedEvent(contact.id)
val json = """
{
"contactId": ${contact.id.int}
}
"""
assertJsonEquals(json, event.output())
}
} }