mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 02:39:05 +01:00
Compare commits
70 Commits
beta-0.16.
...
beta-0.16.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef2286ab53 | ||
|
|
47b25f3221 | ||
|
|
c30bfa12ce | ||
|
|
d0fc04251d | ||
|
|
dcbb41eb7a | ||
|
|
999bdf8866 | ||
|
|
911c0c0fd9 | ||
|
|
99d8cc64a6 | ||
|
|
ba727d7568 | ||
|
|
ed01048f9f | ||
|
|
043ee3c58e | ||
|
|
6e0af7deda | ||
|
|
9591db2097 | ||
|
|
329a4c64f6 | ||
|
|
79015bc5ae | ||
|
|
27422ab9f9 | ||
|
|
abcb682498 | ||
|
|
5044127c46 | ||
|
|
0e4b8ca62e | ||
|
|
822017c69c | ||
|
|
eb6561b93d | ||
|
|
d24b1884a2 | ||
|
|
078534889e | ||
|
|
e92713006a | ||
|
|
18f43f3bc1 | ||
|
|
a4118b40e1 | ||
|
|
de29fbc324 | ||
|
|
3197dcf9b5 | ||
|
|
35aad409fd | ||
|
|
08ce6a7331 | ||
|
|
33a0099065 | ||
|
|
34d20fafda | ||
|
|
aafddcd0f0 | ||
|
|
0d6983b4ef | ||
|
|
69bfb72171 | ||
|
|
1aa33ec9b2 | ||
|
|
6702df1e22 | ||
|
|
c1748c9a86 | ||
|
|
9df624c62a | ||
|
|
0ee6197d7f | ||
|
|
b03a7dce3e | ||
|
|
6c59d7dd5f | ||
|
|
050191f0ef | ||
|
|
4b5a19ce5d | ||
|
|
7c4dd991b9 | ||
|
|
8455569e88 | ||
|
|
d25676559c | ||
|
|
a9437f7985 | ||
|
|
8141a97fc9 | ||
|
|
db842bd7e4 | ||
|
|
6dbec3a864 | ||
|
|
29f658cf4d | ||
|
|
ca83744a84 | ||
|
|
d91a9e2be4 | ||
|
|
8408c3f467 | ||
|
|
544c83a64c | ||
|
|
3800cd5e4f | ||
|
|
259f2cd419 | ||
|
|
20eb022c36 | ||
|
|
531e555b52 | ||
|
|
a9024aa34b | ||
|
|
d4e3b7842c | ||
|
|
167fddfbcc | ||
|
|
a48d642648 | ||
|
|
9a70f054c7 | ||
|
|
ca43d13bd6 | ||
|
|
5b71004179 | ||
|
|
63befccdbf | ||
|
|
4ecf7c02d0 | ||
|
|
f25badc18c |
@@ -12,8 +12,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 22
|
||||
versionCode 14
|
||||
versionName "0.14"
|
||||
versionCode 1610
|
||||
versionName "0.16.10"
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
@@ -32,31 +32,31 @@ dependencies {
|
||||
def torBinaryDir = 'src/main/res/raw'
|
||||
|
||||
task downloadTorGeoIp(type: Download) {
|
||||
src 'https://briarproject.org/build/geoip-2017-05-02.zip'
|
||||
src 'https://briarproject.org/build/geoip-2017-09-06.zip'
|
||||
dest "$torBinaryDir/geoip.zip"
|
||||
onlyIfNewer true
|
||||
}
|
||||
|
||||
task downloadTorBinaryArm(type: Download) {
|
||||
src 'https://briarproject.org/build/tor-0.2.9.11-arm.zip'
|
||||
src 'https://briarproject.org/build/tor-0.2.9.12-arm.zip'
|
||||
dest "$torBinaryDir/tor_arm.zip"
|
||||
onlyIfNewer true
|
||||
}
|
||||
|
||||
task downloadTorBinaryArmPie(type: Download) {
|
||||
src 'https://briarproject.org/build/tor-0.2.9.11-arm-pie.zip'
|
||||
src 'https://briarproject.org/build/tor-0.2.9.12-arm-pie.zip'
|
||||
dest "$torBinaryDir/tor_arm_pie.zip"
|
||||
onlyIfNewer true
|
||||
}
|
||||
|
||||
task downloadTorBinaryX86(type: Download) {
|
||||
src 'https://briarproject.org/build/tor-0.2.9.11-x86.zip'
|
||||
src 'https://briarproject.org/build/tor-0.2.9.12-x86.zip'
|
||||
dest "$torBinaryDir/tor_x86.zip"
|
||||
onlyIfNewer true
|
||||
}
|
||||
|
||||
task downloadTorBinaryX86Pie(type: Download) {
|
||||
src 'https://briarproject.org/build/tor-0.2.9.11-x86-pie.zip'
|
||||
src 'https://briarproject.org/build/tor-0.2.9.12-x86-pie.zip'
|
||||
dest "$torBinaryDir/tor_x86_pie.zip"
|
||||
onlyIfNewer true
|
||||
}
|
||||
@@ -64,31 +64,31 @@ task downloadTorBinaryX86Pie(type: Download) {
|
||||
task verifyTorGeoIp(type: Verify, dependsOn: 'downloadTorGeoIp') {
|
||||
src "$torBinaryDir/geoip.zip"
|
||||
algorithm 'SHA-256'
|
||||
checksum '51f4d1272fb867e1f3b36b67a584e2a33c40b40f62305457d799fd399cd77c9b'
|
||||
checksum 'fe49d3adb86d3c512373101422a017dbb86c85a570524663f09dd8ce143a24f3'
|
||||
}
|
||||
|
||||
task verifyTorBinaryArm(type: Verify, dependsOn: 'downloadTorBinaryArm') {
|
||||
src "$torBinaryDir/tor_arm.zip"
|
||||
algorithm 'SHA-256'
|
||||
checksum '1da6008663a8ad98b349e62acbbf42c379f65ec504fa467cb119c187cd5a4c6b'
|
||||
checksum '8ed0b347ffed1d6a4d2fd14495118eb92be83e9cc06e057e15220dc288b31688'
|
||||
}
|
||||
|
||||
task verifyTorBinaryArmPie(type: Verify, dependsOn: 'downloadTorBinaryArmPie') {
|
||||
src "$torBinaryDir/tor_arm_pie.zip"
|
||||
algorithm 'SHA-256'
|
||||
checksum 'eb061f880829e05f104690ac744848133f2dacef04759d425a2cff0df32c271e'
|
||||
checksum '64403262511c29f462ca5e7c7621bfc3c944898364d1d5ad35a016bb8a034283'
|
||||
}
|
||||
|
||||
task verifyTorBinaryX86(type: Verify, dependsOn: 'downloadTorBinaryX86') {
|
||||
src "$torBinaryDir/tor_x86.zip"
|
||||
algorithm 'SHA-256'
|
||||
checksum 'f5308aff8303daca082f82227d02b51ddedba4ab1d1420739ada0427ae5dbb41'
|
||||
checksum '61e014607a2079bcf1646289c67bff6372b1aded6e1d8d83d7791efda9a4d5ab'
|
||||
}
|
||||
|
||||
task verifyTorBinaryX86Pie(type: Verify, dependsOn: 'downloadTorBinaryX86Pie') {
|
||||
src "$torBinaryDir/tor_x86_pie.zip"
|
||||
algorithm 'SHA-256'
|
||||
checksum '889a6c81ac73d05d35ed610ca5a913cee44d333e4ae1749c2a107f2f7dd8197b'
|
||||
checksum '18fbc98356697dd0895836ab46d5c9877d1c539193464f7db1e82a65adaaf288'
|
||||
}
|
||||
|
||||
project.afterEvaluate {
|
||||
|
||||
@@ -11,7 +11,6 @@ import android.content.IntentFilter;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||
@@ -30,23 +29,14 @@ import org.briarproject.bramble.util.StringUtils;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletionService;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorCompletionService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -61,8 +51,6 @@ import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERA
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_ON;
|
||||
import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
|
||||
@@ -79,10 +67,6 @@ class DroidtoothPlugin implements DuplexPlugin {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(DroidtoothPlugin.class.getName());
|
||||
private static final String FOUND =
|
||||
"android.bluetooth.device.action.FOUND";
|
||||
private static final String DISCOVERY_FINISHED =
|
||||
"android.bluetooth.adapter.action.DISCOVERY_FINISHED";
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final AndroidExecutor androidExecutor;
|
||||
@@ -374,90 +358,6 @@ class DroidtoothPlugin implements DuplexPlugin {
|
||||
return new DroidtoothTransportConnection(this, s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsInvitations() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout, boolean alice) {
|
||||
if (!isRunning()) return null;
|
||||
// Use the invitation codes to generate the UUID
|
||||
byte[] b = r.nextBytes(UUID_BYTES);
|
||||
UUID uuid = UUID.nameUUIDFromBytes(b);
|
||||
if (LOG.isLoggable(INFO)) LOG.info("Invitation UUID " + uuid);
|
||||
// Bind a server socket for receiving invitation connections
|
||||
BluetoothServerSocket ss;
|
||||
try {
|
||||
ss = adapter.listenUsingInsecureRfcommWithServiceRecord(
|
||||
"RFCOMM", uuid);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
// Create the background tasks
|
||||
CompletionService<BluetoothSocket> complete =
|
||||
new ExecutorCompletionService<>(ioExecutor);
|
||||
List<Future<BluetoothSocket>> futures = new ArrayList<>();
|
||||
if (alice) {
|
||||
// Return the first connected socket
|
||||
futures.add(complete.submit(new ListeningTask(ss)));
|
||||
futures.add(complete.submit(new DiscoveryTask(uuid.toString())));
|
||||
} else {
|
||||
// Return the first socket with readable data
|
||||
futures.add(complete.submit(new ReadableTask(
|
||||
new ListeningTask(ss))));
|
||||
futures.add(complete.submit(new ReadableTask(
|
||||
new DiscoveryTask(uuid.toString()))));
|
||||
}
|
||||
BluetoothSocket chosen = null;
|
||||
try {
|
||||
Future<BluetoothSocket> f = complete.poll(timeout, MILLISECONDS);
|
||||
if (f == null) return null; // No task completed within the timeout
|
||||
chosen = f.get();
|
||||
return new DroidtoothTransportConnection(this, chosen);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.info("Interrupted while exchanging invitations");
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
} catch (ExecutionException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
} finally {
|
||||
// Closing the socket will terminate the listener task
|
||||
tryToClose(ss);
|
||||
closeSockets(futures, chosen);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeSockets(final List<Future<BluetoothSocket>> futures,
|
||||
@Nullable final BluetoothSocket chosen) {
|
||||
ioExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (Future<BluetoothSocket> f : futures) {
|
||||
try {
|
||||
if (f.cancel(true)) {
|
||||
LOG.info("Cancelled task");
|
||||
} else {
|
||||
BluetoothSocket s = f.get();
|
||||
if (s != null && s != chosen) {
|
||||
LOG.info("Closing unwanted socket");
|
||||
s.close();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOG.info("Interrupted while closing sockets");
|
||||
return;
|
||||
} catch (ExecutionException | IOException e) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsKeyAgreement() {
|
||||
return true;
|
||||
@@ -472,7 +372,7 @@ class DroidtoothPlugin implements DuplexPlugin {
|
||||
// No truncation necessary because COMMIT_LENGTH = 16
|
||||
UUID uuid = UUID.nameUUIDFromBytes(commitment);
|
||||
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
|
||||
// Bind a server socket for receiving invitation connections
|
||||
// Bind a server socket for receiving key agreement connections
|
||||
BluetoothServerSocket ss;
|
||||
try {
|
||||
ss = adapter.listenUsingInsecureRfcommWithServiceRecord(
|
||||
@@ -536,115 +436,6 @@ class DroidtoothPlugin implements DuplexPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private class DiscoveryTask implements Callable<BluetoothSocket> {
|
||||
|
||||
private final String uuid;
|
||||
|
||||
private DiscoveryTask(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BluetoothSocket call() throws Exception {
|
||||
// Repeat discovery until we connect or get interrupted
|
||||
while (true) {
|
||||
// Discover nearby devices
|
||||
LOG.info("Discovering nearby devices");
|
||||
List<String> addresses = discoverDevices();
|
||||
if (addresses.isEmpty()) {
|
||||
LOG.info("No devices discovered");
|
||||
continue;
|
||||
}
|
||||
// Connect to any device with the right UUID
|
||||
for (String address : addresses) {
|
||||
BluetoothSocket s = connect(address, uuid);
|
||||
if (s != null) {
|
||||
LOG.info("Outgoing connection");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> discoverDevices() throws InterruptedException {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(FOUND);
|
||||
filter.addAction(DISCOVERY_FINISHED);
|
||||
DiscoveryReceiver disco = new DiscoveryReceiver();
|
||||
appContext.registerReceiver(disco, filter);
|
||||
LOG.info("Starting discovery");
|
||||
adapter.startDiscovery();
|
||||
return disco.waitForAddresses();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DiscoveryReceiver extends BroadcastReceiver {
|
||||
|
||||
private final CountDownLatch finished = new CountDownLatch(1);
|
||||
private final List<String> addresses = new CopyOnWriteArrayList<>();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(DISCOVERY_FINISHED)) {
|
||||
LOG.info("Discovery finished");
|
||||
ctx.unregisterReceiver(this);
|
||||
finished.countDown();
|
||||
} else if (action.equals(FOUND)) {
|
||||
BluetoothDevice d = intent.getParcelableExtra(EXTRA_DEVICE);
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Discovered device: " +
|
||||
scrubMacAddress(d.getAddress()));
|
||||
}
|
||||
addresses.add(d.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> waitForAddresses() throws InterruptedException {
|
||||
finished.await();
|
||||
List<String> shuffled = new ArrayList<>(addresses);
|
||||
Collections.shuffle(shuffled);
|
||||
return shuffled;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ListeningTask implements Callable<BluetoothSocket> {
|
||||
|
||||
private final BluetoothServerSocket serverSocket;
|
||||
|
||||
private ListeningTask(BluetoothServerSocket serverSocket) {
|
||||
this.serverSocket = serverSocket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BluetoothSocket call() throws IOException {
|
||||
BluetoothSocket s = serverSocket.accept();
|
||||
LOG.info("Incoming connection");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReadableTask implements Callable<BluetoothSocket> {
|
||||
|
||||
private final Callable<BluetoothSocket> connectionTask;
|
||||
|
||||
private ReadableTask(Callable<BluetoothSocket> connectionTask) {
|
||||
this.connectionTask = connectionTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BluetoothSocket call() throws Exception {
|
||||
BluetoothSocket s = connectionTask.call();
|
||||
InputStream in = s.getInputStream();
|
||||
while (in.available() == 0) {
|
||||
LOG.info("Waiting for data");
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
LOG.info("Data available");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
|
||||
|
||||
private final BluetoothServerSocket ss;
|
||||
|
||||
@@ -17,7 +17,6 @@ import net.freehaven.tor.control.EventHandler;
|
||||
import net.freehaven.tor.control.TorControlConnection;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
@@ -589,17 +588,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsInvitations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout, boolean alice) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsKeyAgreement() {
|
||||
return false;
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.tor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Plugin;
|
||||
import org.briarproject.bramble.api.plugin.duplex.AbstractDuplexTransportConnection;
|
||||
import org.briarproject.bramble.util.IoUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -21,12 +22,12 @@ class TorTransportConnection extends AbstractDuplexTransportConnection {
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException {
|
||||
return socket.getInputStream();
|
||||
return IoUtils.getInputStream(socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream getOutputStream() throws IOException {
|
||||
return socket.getOutputStream();
|
||||
return IoUtils.getOutputStream(socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,8 +10,6 @@ public interface CryptoComponent {
|
||||
|
||||
SecretKey generateSecretKey();
|
||||
|
||||
PseudoRandom getPseudoRandom(int seed1, int seed2);
|
||||
|
||||
SecureRandom getSecureRandom();
|
||||
|
||||
KeyPair generateAgreementKeyPair();
|
||||
@@ -24,15 +22,6 @@ public interface CryptoComponent {
|
||||
|
||||
KeyParser getMessageKeyParser();
|
||||
|
||||
/** Generates a random invitation code. */
|
||||
int generateBTInvitationCode();
|
||||
|
||||
/**
|
||||
* Derives a confirmation code from the given master secret.
|
||||
* @param alice whether the code is for use by Alice or Bob.
|
||||
*/
|
||||
int deriveBTConfirmationCode(SecretKey master, boolean alice);
|
||||
|
||||
/**
|
||||
* Derives a stream header key from the given master secret.
|
||||
* @param alice whether the key is for use by Alice or Bob.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.briarproject.bramble.api.crypto;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
/**
|
||||
* A deterministic pseudo-random number generator.
|
||||
*/
|
||||
@NotNullByDefault
|
||||
public interface PseudoRandom {
|
||||
|
||||
byte[] nextBytes(int bytes);
|
||||
}
|
||||
@@ -14,8 +14,9 @@ public interface StreamDecrypterFactory {
|
||||
StreamDecrypter createStreamDecrypter(InputStream in, StreamContext ctx);
|
||||
|
||||
/**
|
||||
* Creates a {@link StreamDecrypter} for decrypting an invitation stream.
|
||||
* Creates a {@link StreamDecrypter} for decrypting a contact exchange
|
||||
* stream.
|
||||
*/
|
||||
StreamDecrypter createInvitationStreamDecrypter(InputStream in,
|
||||
StreamDecrypter createContactExchangeStreamDecrypter(InputStream in,
|
||||
SecretKey headerKey);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ public interface StreamEncrypterFactory {
|
||||
StreamEncrypter createStreamEncrypter(OutputStream out, StreamContext ctx);
|
||||
|
||||
/**
|
||||
* Creates a {@link StreamEncrypter} for encrypting an invitation stream.
|
||||
* Creates a {@link StreamEncrypter} for encrypting a contact exchange
|
||||
* stream.
|
||||
*/
|
||||
StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
|
||||
StreamEncrypter createContactExchangeStreamDecrypter(OutputStream out,
|
||||
SecretKey headerKey);
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.briarproject.bramble.api.invitation;
|
||||
|
||||
public interface InvitationConstants {
|
||||
|
||||
/**
|
||||
* The connection timeout in milliseconds.
|
||||
*/
|
||||
long CONNECTION_TIMEOUT = 60 * 1000;
|
||||
|
||||
/**
|
||||
* The confirmation timeout in milliseconds.
|
||||
*/
|
||||
long CONFIRMATION_TIMEOUT = 60 * 1000;
|
||||
|
||||
/**
|
||||
* The number of bits in an invitation or confirmation code. Codes must fit
|
||||
* into six decimal digits.
|
||||
*/
|
||||
int CODE_BITS = 19;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.briarproject.bramble.api.invitation;
|
||||
|
||||
/**
|
||||
* An interface for receiving updates about the state of an
|
||||
* {@link InvitationTask}.
|
||||
*/
|
||||
public interface InvitationListener {
|
||||
|
||||
/** Called if a connection to the remote peer is established. */
|
||||
void connectionSucceeded();
|
||||
|
||||
/**
|
||||
* Called if a connection to the remote peer cannot be established. This
|
||||
* indicates that the protocol has ended unsuccessfully.
|
||||
*/
|
||||
void connectionFailed();
|
||||
|
||||
/** Called if key agreement with the remote peer succeeds. */
|
||||
void keyAgreementSucceeded(int localCode, int remoteCode);
|
||||
|
||||
/**
|
||||
* Called if key agreement with the remote peer fails or the connection is
|
||||
* lost. This indicates that the protocol has ended unsuccessfully.
|
||||
*/
|
||||
void keyAgreementFailed();
|
||||
|
||||
/** Called if the remote peer's confirmation check succeeds. */
|
||||
void remoteConfirmationSucceeded();
|
||||
|
||||
/**
|
||||
* Called if remote peer's confirmation check fails or the connection is
|
||||
* lost. This indicates that the protocol has ended unsuccessfully.
|
||||
*/
|
||||
void remoteConfirmationFailed();
|
||||
|
||||
/**
|
||||
* Called if the exchange of pseudonyms succeeds. This indicates that the
|
||||
* protocol has ended successfully.
|
||||
*/
|
||||
void pseudonymExchangeSucceeded(String remoteName);
|
||||
|
||||
/**
|
||||
* Called if the exchange of pseudonyms fails or the connection is lost.
|
||||
* This indicates that the protocol has ended unsuccessfully.
|
||||
*/
|
||||
void pseudonymExchangeFailed();
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package org.briarproject.bramble.api.invitation;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* A snapshot of the state of an {@link InvitationTask}.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class InvitationState {
|
||||
|
||||
private final int localInvitationCode, remoteInvitationCode;
|
||||
private final int localConfirmationCode, remoteConfirmationCode;
|
||||
private final boolean connected, connectionFailed;
|
||||
private final boolean localCompared, remoteCompared;
|
||||
private final boolean localMatched, remoteMatched;
|
||||
@Nullable
|
||||
private final String contactName;
|
||||
|
||||
public InvitationState(int localInvitationCode, int remoteInvitationCode,
|
||||
int localConfirmationCode, int remoteConfirmationCode,
|
||||
boolean connected, boolean connectionFailed, boolean localCompared,
|
||||
boolean remoteCompared, boolean localMatched,
|
||||
boolean remoteMatched, @Nullable String contactName) {
|
||||
this.localInvitationCode = localInvitationCode;
|
||||
this.remoteInvitationCode = remoteInvitationCode;
|
||||
this.localConfirmationCode = localConfirmationCode;
|
||||
this.remoteConfirmationCode = remoteConfirmationCode;
|
||||
this.connected = connected;
|
||||
this.connectionFailed = connectionFailed;
|
||||
this.localCompared = localCompared;
|
||||
this.remoteCompared = remoteCompared;
|
||||
this.localMatched = localMatched;
|
||||
this.remoteMatched = remoteMatched;
|
||||
this.contactName = contactName;
|
||||
}
|
||||
|
||||
public int getLocalInvitationCode() {
|
||||
return localInvitationCode;
|
||||
}
|
||||
|
||||
public int getRemoteInvitationCode() {
|
||||
return remoteInvitationCode;
|
||||
}
|
||||
|
||||
public int getLocalConfirmationCode() {
|
||||
return localConfirmationCode;
|
||||
}
|
||||
|
||||
public int getRemoteConfirmationCode() {
|
||||
return remoteConfirmationCode;
|
||||
}
|
||||
|
||||
public boolean getConnected() {
|
||||
return connected;
|
||||
}
|
||||
|
||||
public boolean getConnectionFailed() {
|
||||
return connectionFailed;
|
||||
}
|
||||
|
||||
public boolean getLocalCompared() {
|
||||
return localCompared;
|
||||
}
|
||||
|
||||
public boolean getRemoteCompared() {
|
||||
return remoteCompared;
|
||||
}
|
||||
|
||||
public boolean getLocalMatched() {
|
||||
return localMatched;
|
||||
}
|
||||
|
||||
public boolean getRemoteMatched() {
|
||||
return remoteMatched;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getContactName() {
|
||||
return contactName;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package org.briarproject.bramble.api.invitation;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
/**
|
||||
* A task for exchanging invitations with a remote peer.
|
||||
*/
|
||||
@NotNullByDefault
|
||||
public interface InvitationTask {
|
||||
|
||||
/**
|
||||
* Adds a listener to be informed of state changes and returns the
|
||||
* task's current state.
|
||||
*/
|
||||
InvitationState addListener(InvitationListener l);
|
||||
|
||||
/**
|
||||
* Removes the given listener.
|
||||
*/
|
||||
void removeListener(InvitationListener l);
|
||||
|
||||
/**
|
||||
* Asynchronously starts the connection process.
|
||||
*/
|
||||
void connect();
|
||||
|
||||
/**
|
||||
* Asynchronously informs the remote peer that the local peer's
|
||||
* confirmation codes matched.
|
||||
*/
|
||||
void localConfirmationSucceeded();
|
||||
|
||||
/**
|
||||
* Asynchronously informs the remote peer that the local peer's
|
||||
* confirmation codes did not match.
|
||||
*/
|
||||
void localConfirmationFailed();
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.briarproject.bramble.api.invitation;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
/**
|
||||
* Creates tasks for exchanging invitations with remote peers.
|
||||
*/
|
||||
@NotNullByDefault
|
||||
public interface InvitationTaskFactory {
|
||||
|
||||
/**
|
||||
* Creates a task using the given local and remote invitation codes.
|
||||
*/
|
||||
InvitationTask createTask(int localCode, int remoteCode);
|
||||
}
|
||||
@@ -32,11 +32,6 @@ public interface PluginManager {
|
||||
*/
|
||||
Collection<DuplexPlugin> getDuplexPlugins();
|
||||
|
||||
/**
|
||||
* Returns any duplex plugins that support invitations.
|
||||
*/
|
||||
Collection<DuplexPlugin> getInvitationPlugins();
|
||||
|
||||
/**
|
||||
* Returns any duplex plugins that support key agreement.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.briarproject.bramble.api.plugin.duplex;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
@@ -23,20 +22,6 @@ public interface DuplexPlugin extends Plugin {
|
||||
@Nullable
|
||||
DuplexTransportConnection createConnection(ContactId c);
|
||||
|
||||
/**
|
||||
* Returns true if the plugin supports exchanging invitations.
|
||||
*/
|
||||
boolean supportsInvitations();
|
||||
|
||||
/**
|
||||
* Attempts to create and return an invitation connection to the remote
|
||||
* peer. Returns null if no connection can be established within the given
|
||||
* time.
|
||||
*/
|
||||
@Nullable
|
||||
DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout, boolean alice);
|
||||
|
||||
/**
|
||||
* Returns true if the plugin supports short-range key agreement.
|
||||
*/
|
||||
|
||||
@@ -15,9 +15,9 @@ public interface StreamReaderFactory {
|
||||
InputStream createStreamReader(InputStream in, StreamContext ctx);
|
||||
|
||||
/**
|
||||
* Creates an {@link InputStream InputStream} for reading from an
|
||||
* invitation stream.
|
||||
* Creates an {@link InputStream InputStream} for reading from a contact
|
||||
* exchangestream.
|
||||
*/
|
||||
InputStream createInvitationStreamReader(InputStream in,
|
||||
InputStream createContactExchangeStreamReader(InputStream in,
|
||||
SecretKey headerKey);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ public interface StreamWriterFactory {
|
||||
OutputStream createStreamWriter(OutputStream out, StreamContext ctx);
|
||||
|
||||
/**
|
||||
* Creates an {@link OutputStream OutputStream} for writing to an
|
||||
* invitation stream.
|
||||
* Creates an {@link OutputStream OutputStream} for writing to a contact
|
||||
* exchange stream.
|
||||
*/
|
||||
OutputStream createInvitationStreamWriter(OutputStream out,
|
||||
OutputStream createContactExchangeStreamWriter(OutputStream out,
|
||||
SecretKey headerKey);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@@ -59,4 +60,24 @@ public class IoUtils {
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for a bug in Android 7, see
|
||||
// https://android-review.googlesource.com/#/c/271775/
|
||||
public static InputStream getInputStream(Socket s) throws IOException {
|
||||
try {
|
||||
return s.getInputStream();
|
||||
} catch (NullPointerException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for a bug in Android 7, see
|
||||
// https://android-review.googlesource.com/#/c/271775/
|
||||
public static OutputStream getOutputStream(Socket s) throws IOException {
|
||||
try {
|
||||
return s.getOutputStream();
|
||||
} catch (NullPointerException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.briarproject.bramble.db.DatabaseExecutorModule;
|
||||
import org.briarproject.bramble.db.DatabaseModule;
|
||||
import org.briarproject.bramble.event.EventModule;
|
||||
import org.briarproject.bramble.identity.IdentityModule;
|
||||
import org.briarproject.bramble.invitation.InvitationModule;
|
||||
import org.briarproject.bramble.keyagreement.KeyAgreementModule;
|
||||
import org.briarproject.bramble.lifecycle.LifecycleModule;
|
||||
import org.briarproject.bramble.plugin.PluginModule;
|
||||
@@ -32,7 +31,6 @@ import dagger.Module;
|
||||
DatabaseExecutorModule.class,
|
||||
EventModule.class,
|
||||
IdentityModule.class,
|
||||
InvitationModule.class,
|
||||
KeyAgreementModule.class,
|
||||
LifecycleModule.class,
|
||||
PluginModule.class,
|
||||
@@ -54,6 +52,7 @@ public class BrambleCoreModule {
|
||||
c.inject(new IdentityModule.EagerSingletons());
|
||||
c.inject(new LifecycleModule.EagerSingletons());
|
||||
c.inject(new PluginModule.EagerSingletons());
|
||||
c.inject(new PropertiesModule.EagerSingletons());
|
||||
c.inject(new SyncModule.EagerSingletons());
|
||||
c.inject(new SystemModule.EagerSingletons());
|
||||
c.inject(new TransportModule.EagerSingletons());
|
||||
|
||||
@@ -80,7 +80,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
private volatile boolean alice;
|
||||
|
||||
@Inject
|
||||
public ContactExchangeTaskImpl(DatabaseComponent db,
|
||||
ContactExchangeTaskImpl(DatabaseComponent db,
|
||||
AuthorFactory authorFactory, BdfReaderFactory bdfReaderFactory,
|
||||
BdfWriterFactory bdfWriterFactory, Clock clock,
|
||||
ConnectionManager connectionManager, ContactManager contactManager,
|
||||
@@ -146,12 +146,12 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
|
||||
// Create the readers
|
||||
InputStream streamReader =
|
||||
streamReaderFactory.createInvitationStreamReader(in,
|
||||
streamReaderFactory.createContactExchangeStreamReader(in,
|
||||
alice ? bobHeaderKey : aliceHeaderKey);
|
||||
BdfReader r = bdfReaderFactory.createReader(streamReader);
|
||||
// Create the writers
|
||||
OutputStream streamWriter =
|
||||
streamWriterFactory.createInvitationStreamWriter(out,
|
||||
streamWriterFactory.createContactExchangeStreamWriter(out,
|
||||
alice ? aliceHeaderKey : bobHeaderKey);
|
||||
BdfWriter w = bdfWriterFactory.createWriter(streamWriter);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.KeyPair;
|
||||
import org.briarproject.bramble.api.crypto.KeyParser;
|
||||
import org.briarproject.bramble.api.crypto.PrivateKey;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.crypto.PublicKey;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
@@ -41,7 +40,6 @@ import java.util.logging.Logger;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static org.briarproject.bramble.api.invitation.InvitationConstants.CODE_BITS;
|
||||
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
|
||||
import static org.briarproject.bramble.crypto.EllipticCurveConstants.PARAMETERS;
|
||||
@@ -68,9 +66,6 @@ class CryptoComponentImpl implements CryptoComponent {
|
||||
return s.getBytes(Charset.forName("US-ASCII"));
|
||||
}
|
||||
|
||||
// KDF labels for bluetooth confirmation code derivation
|
||||
private static final byte[] BT_A_CONFIRM = ascii("ALICE_CONFIRMATION_CODE");
|
||||
private static final byte[] BT_B_CONFIRM = ascii("BOB_CONFIRMATION_CODE");
|
||||
// KDF labels for contact exchange stream header key derivation
|
||||
private static final byte[] A_INVITE = ascii("ALICE_INVITATION_KEY");
|
||||
private static final byte[] B_INVITE = ascii("BOB_INVITATION_KEY");
|
||||
@@ -171,14 +166,6 @@ class CryptoComponentImpl implements CryptoComponent {
|
||||
return new SecretKey(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PseudoRandom getPseudoRandom(int seed1, int seed2) {
|
||||
byte[] seed = new byte[INT_32_BYTES * 2];
|
||||
ByteUtils.writeUint32(seed1, seed, 0);
|
||||
ByteUtils.writeUint32(seed2, seed, INT_32_BYTES);
|
||||
return new PseudoRandomImpl(seed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecureRandom getSecureRandom() {
|
||||
return secureRandom;
|
||||
@@ -250,20 +237,6 @@ class CryptoComponentImpl implements CryptoComponent {
|
||||
return messageEncrypter.getKeyParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int generateBTInvitationCode() {
|
||||
int codeBytes = (CODE_BITS + 7) / 8;
|
||||
byte[] random = new byte[codeBytes];
|
||||
secureRandom.nextBytes(random);
|
||||
return ByteUtils.readUint(random, CODE_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deriveBTConfirmationCode(SecretKey master, boolean alice) {
|
||||
byte[] b = macKdf(master, alice ? BT_A_CONFIRM : BT_B_CONFIRM);
|
||||
return ByteUtils.readUint(b, CODE_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecretKey deriveHeaderKey(SecretKey master,
|
||||
boolean alice) {
|
||||
|
||||
@@ -32,7 +32,7 @@ class StreamDecrypterFactoryImpl implements StreamDecrypterFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamDecrypter createInvitationStreamDecrypter(InputStream in,
|
||||
public StreamDecrypter createContactExchangeStreamDecrypter(InputStream in,
|
||||
SecretKey headerKey) {
|
||||
return new StreamDecrypterImpl(in, cipherProvider.get(), 0, headerKey);
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
|
||||
SecretKey headerKey) {
|
||||
public StreamEncrypter createContactExchangeStreamDecrypter(
|
||||
OutputStream out, SecretKey headerKey) {
|
||||
AuthenticatedCipher cipher = cipherProvider.get();
|
||||
byte[] streamHeaderNonce = new byte[STREAM_HEADER_NONCE_LENGTH];
|
||||
crypto.getSecureRandom().nextBytes(streamHeaderNonce);
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package org.briarproject.bramble.invitation;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactExchangeTask;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.data.BdfReader;
|
||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||
import org.briarproject.bramble.api.data.BdfWriter;
|
||||
import org.briarproject.bramble.api.data.BdfWriterFactory;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
/**
|
||||
* A connection thread for the peer being Alice in the invitation protocol.
|
||||
*/
|
||||
@NotNullByDefault
|
||||
class AliceConnector extends Connector {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(AliceConnector.class.getName());
|
||||
|
||||
AliceConnector(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
|
||||
BdfWriterFactory bdfWriterFactory,
|
||||
ContactExchangeTask contactExchangeTask, ConnectorGroup group,
|
||||
DuplexPlugin plugin, LocalAuthor localAuthor, PseudoRandom random) {
|
||||
super(crypto, bdfReaderFactory, bdfWriterFactory, contactExchangeTask,
|
||||
group, plugin, localAuthor, random);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Create an incoming or outgoing connection
|
||||
DuplexTransportConnection conn = createInvitationConnection(true);
|
||||
if (conn == null) return;
|
||||
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " connected");
|
||||
// Don't proceed with more than one connection
|
||||
if (group.getAndSetConnected()) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " redundant");
|
||||
tryToClose(conn, false);
|
||||
return;
|
||||
}
|
||||
// Carry out the key agreement protocol
|
||||
InputStream in;
|
||||
OutputStream out;
|
||||
BdfReader r;
|
||||
BdfWriter w;
|
||||
SecretKey master;
|
||||
try {
|
||||
in = conn.getReader().getInputStream();
|
||||
out = conn.getWriter().getOutputStream();
|
||||
r = bdfReaderFactory.createReader(in);
|
||||
w = bdfWriterFactory.createWriter(out);
|
||||
// Alice goes first
|
||||
sendPublicKeyHash(w);
|
||||
byte[] hash = receivePublicKeyHash(r);
|
||||
sendPublicKey(w);
|
||||
byte[] key = receivePublicKey(r);
|
||||
master = deriveMasterSecret(hash, key, true);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
group.keyAgreementFailed();
|
||||
tryToClose(conn, true);
|
||||
return;
|
||||
} catch (GeneralSecurityException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
group.keyAgreementFailed();
|
||||
tryToClose(conn, true);
|
||||
return;
|
||||
}
|
||||
// The key agreement succeeded - derive the confirmation codes
|
||||
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " agreement succeeded");
|
||||
int aliceCode = crypto.deriveBTConfirmationCode(master, true);
|
||||
int bobCode = crypto.deriveBTConfirmationCode(master, false);
|
||||
group.keyAgreementSucceeded(aliceCode, bobCode);
|
||||
// Exchange confirmation results
|
||||
boolean localMatched, remoteMatched;
|
||||
try {
|
||||
localMatched = group.waitForLocalConfirmationResult();
|
||||
sendConfirmation(w, localMatched);
|
||||
remoteMatched = receiveConfirmation(r);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
group.remoteConfirmationFailed();
|
||||
tryToClose(conn, true);
|
||||
return;
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warning("Interrupted while waiting for confirmation");
|
||||
group.remoteConfirmationFailed();
|
||||
tryToClose(conn, true);
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
if (remoteMatched) group.remoteConfirmationSucceeded();
|
||||
else group.remoteConfirmationFailed();
|
||||
if (!(localMatched && remoteMatched)) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " confirmation failed");
|
||||
tryToClose(conn, false);
|
||||
return;
|
||||
}
|
||||
// Confirmation succeeded - upgrade to a secure connection
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " confirmation succeeded");
|
||||
contactExchangeTask.startExchange(group, localAuthor, master, conn,
|
||||
plugin.getId(), true);
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package org.briarproject.bramble.invitation;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactExchangeTask;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.data.BdfReader;
|
||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||
import org.briarproject.bramble.api.data.BdfWriter;
|
||||
import org.briarproject.bramble.api.data.BdfWriterFactory;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
/**
|
||||
* A connection thread for the peer being Bob in the invitation protocol.
|
||||
*/
|
||||
@NotNullByDefault
|
||||
class BobConnector extends Connector {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(BobConnector.class.getName());
|
||||
|
||||
BobConnector(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
|
||||
BdfWriterFactory bdfWriterFactory,
|
||||
ContactExchangeTask contactExchangeTask, ConnectorGroup group,
|
||||
DuplexPlugin plugin, LocalAuthor localAuthor, PseudoRandom random) {
|
||||
super(crypto, bdfReaderFactory, bdfWriterFactory, contactExchangeTask,
|
||||
group, plugin, localAuthor, random);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Create an incoming or outgoing connection
|
||||
DuplexTransportConnection conn = createInvitationConnection(false);
|
||||
if (conn == null) return;
|
||||
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " connected");
|
||||
// Carry out the key agreement protocol
|
||||
InputStream in;
|
||||
OutputStream out;
|
||||
BdfReader r;
|
||||
BdfWriter w;
|
||||
SecretKey master;
|
||||
try {
|
||||
in = conn.getReader().getInputStream();
|
||||
out = conn.getWriter().getOutputStream();
|
||||
r = bdfReaderFactory.createReader(in);
|
||||
w = bdfWriterFactory.createWriter(out);
|
||||
// Alice goes first
|
||||
byte[] hash = receivePublicKeyHash(r);
|
||||
// Don't proceed with more than one connection
|
||||
if (group.getAndSetConnected()) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " redundant");
|
||||
tryToClose(conn, false);
|
||||
return;
|
||||
}
|
||||
sendPublicKeyHash(w);
|
||||
byte[] key = receivePublicKey(r);
|
||||
sendPublicKey(w);
|
||||
master = deriveMasterSecret(hash, key, false);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
group.keyAgreementFailed();
|
||||
tryToClose(conn, true);
|
||||
return;
|
||||
} catch (GeneralSecurityException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
group.keyAgreementFailed();
|
||||
tryToClose(conn, true);
|
||||
return;
|
||||
}
|
||||
// The key agreement succeeded - derive the confirmation codes
|
||||
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " agreement succeeded");
|
||||
int aliceCode = crypto.deriveBTConfirmationCode(master, true);
|
||||
int bobCode = crypto.deriveBTConfirmationCode(master, false);
|
||||
group.keyAgreementSucceeded(bobCode, aliceCode);
|
||||
// Exchange confirmation results
|
||||
boolean localMatched, remoteMatched;
|
||||
try {
|
||||
remoteMatched = receiveConfirmation(r);
|
||||
localMatched = group.waitForLocalConfirmationResult();
|
||||
sendConfirmation(w, localMatched);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
group.remoteConfirmationFailed();
|
||||
tryToClose(conn, true);
|
||||
return;
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warning("Interrupted while waiting for confirmation");
|
||||
group.remoteConfirmationFailed();
|
||||
tryToClose(conn, true);
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
if (remoteMatched) group.remoteConfirmationSucceeded();
|
||||
else group.remoteConfirmationFailed();
|
||||
if (!(localMatched && remoteMatched)) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " confirmation failed");
|
||||
tryToClose(conn, false);
|
||||
return;
|
||||
}
|
||||
// Confirmation succeeded - upgrade to a secure connection
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " confirmation succeeded");
|
||||
contactExchangeTask.startExchange(group, localAuthor, master, conn,
|
||||
plugin.getId(), false);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package org.briarproject.bramble.invitation;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.contact.ContactExchangeTask;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.KeyPair;
|
||||
import org.briarproject.bramble.api.crypto.KeyParser;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.data.BdfReader;
|
||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||
import org.briarproject.bramble.api.data.BdfWriter;
|
||||
import org.briarproject.bramble.api.data.BdfWriterFactory;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.bramble.api.invitation.InvitationConstants.CONNECTION_TIMEOUT;
|
||||
|
||||
// FIXME: This class has way too many dependencies
|
||||
@NotNullByDefault
|
||||
abstract class Connector extends Thread {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(Connector.class.getName());
|
||||
private static final String LABEL_PUBLIC_KEY =
|
||||
"org.briarproject.bramble.invitation.PUBLIC_KEY";
|
||||
|
||||
protected final CryptoComponent crypto;
|
||||
protected final BdfReaderFactory bdfReaderFactory;
|
||||
protected final BdfWriterFactory bdfWriterFactory;
|
||||
protected final ContactExchangeTask contactExchangeTask;
|
||||
protected final ConnectorGroup group;
|
||||
protected final DuplexPlugin plugin;
|
||||
protected final LocalAuthor localAuthor;
|
||||
protected final PseudoRandom random;
|
||||
protected final String pluginName;
|
||||
|
||||
private final KeyPair keyPair;
|
||||
private final KeyParser keyParser;
|
||||
|
||||
Connector(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
|
||||
BdfWriterFactory bdfWriterFactory,
|
||||
ContactExchangeTask contactExchangeTask, ConnectorGroup group,
|
||||
DuplexPlugin plugin, LocalAuthor localAuthor, PseudoRandom random) {
|
||||
super("Connector");
|
||||
this.crypto = crypto;
|
||||
this.bdfReaderFactory = bdfReaderFactory;
|
||||
this.bdfWriterFactory = bdfWriterFactory;
|
||||
this.contactExchangeTask = contactExchangeTask;
|
||||
this.group = group;
|
||||
this.plugin = plugin;
|
||||
this.localAuthor = localAuthor;
|
||||
this.random = random;
|
||||
pluginName = plugin.getClass().getName();
|
||||
keyPair = crypto.generateAgreementKeyPair();
|
||||
keyParser = crypto.getAgreementKeyParser();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
DuplexTransportConnection createInvitationConnection(boolean alice) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " creating invitation connection");
|
||||
return plugin.createInvitationConnection(random, CONNECTION_TIMEOUT,
|
||||
alice);
|
||||
}
|
||||
|
||||
void sendPublicKeyHash(BdfWriter w) throws IOException {
|
||||
byte[] hash =
|
||||
crypto.hash(LABEL_PUBLIC_KEY, keyPair.getPublic().getEncoded());
|
||||
w.writeRaw(hash);
|
||||
w.flush();
|
||||
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " sent hash");
|
||||
}
|
||||
|
||||
byte[] receivePublicKeyHash(BdfReader r) throws IOException {
|
||||
int hashLength = crypto.getHashLength();
|
||||
byte[] b = r.readRaw(hashLength);
|
||||
if (b.length < hashLength) throw new FormatException();
|
||||
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " received hash");
|
||||
return b;
|
||||
}
|
||||
|
||||
void sendPublicKey(BdfWriter w) throws IOException {
|
||||
byte[] key = keyPair.getPublic().getEncoded();
|
||||
w.writeRaw(key);
|
||||
w.flush();
|
||||
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " sent key");
|
||||
}
|
||||
|
||||
byte[] receivePublicKey(BdfReader r)
|
||||
throws GeneralSecurityException, IOException {
|
||||
byte[] b = r.readRaw(MAX_PUBLIC_KEY_LENGTH);
|
||||
keyParser.parsePublicKey(b);
|
||||
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " received key");
|
||||
return b;
|
||||
}
|
||||
|
||||
SecretKey deriveMasterSecret(byte[] hash, byte[] key, boolean alice)
|
||||
throws GeneralSecurityException {
|
||||
// Check that the hash matches the key
|
||||
byte[] keyHash =
|
||||
crypto.hash(LABEL_PUBLIC_KEY, keyPair.getPublic().getEncoded());
|
||||
if (!Arrays.equals(hash, keyHash)) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " hash does not match key");
|
||||
throw new GeneralSecurityException();
|
||||
}
|
||||
// Derive the master secret
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " deriving master secret");
|
||||
return crypto.deriveMasterSecret(key, keyPair, alice);
|
||||
}
|
||||
|
||||
void sendConfirmation(BdfWriter w, boolean confirmed) throws IOException {
|
||||
w.writeBoolean(confirmed);
|
||||
w.flush();
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " sent confirmation: " + confirmed);
|
||||
}
|
||||
|
||||
boolean receiveConfirmation(BdfReader r) throws IOException {
|
||||
boolean confirmed = r.readBoolean();
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " received confirmation: " + confirmed);
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
protected void tryToClose(DuplexTransportConnection conn,
|
||||
boolean exception) {
|
||||
try {
|
||||
LOG.info("Closing connection");
|
||||
conn.getReader().dispose(exception, true);
|
||||
conn.getWriter().dispose(exception);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package org.briarproject.bramble.invitation;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactExchangeListener;
|
||||
import org.briarproject.bramble.api.contact.ContactExchangeTask;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||
import org.briarproject.bramble.api.data.BdfWriterFactory;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.invitation.InvitationListener;
|
||||
import org.briarproject.bramble.api.invitation.InvitationState;
|
||||
import org.briarproject.bramble.api.invitation.InvitationTask;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.PluginManager;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.invitation.InvitationConstants.CONFIRMATION_TIMEOUT;
|
||||
|
||||
/**
|
||||
* A task consisting of one or more parallel connection attempts.
|
||||
*/
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class ConnectorGroup extends Thread implements InvitationTask,
|
||||
ContactExchangeListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ConnectorGroup.class.getName());
|
||||
|
||||
private final CryptoComponent crypto;
|
||||
private final BdfReaderFactory bdfReaderFactory;
|
||||
private final BdfWriterFactory bdfWriterFactory;
|
||||
private final ContactExchangeTask contactExchangeTask;
|
||||
private final IdentityManager identityManager;
|
||||
private final PluginManager pluginManager;
|
||||
private final int localInvitationCode, remoteInvitationCode;
|
||||
private final Collection<InvitationListener> listeners;
|
||||
private final AtomicBoolean connected;
|
||||
private final CountDownLatch localConfirmationLatch;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
|
||||
// The following are locking: lock
|
||||
private int localConfirmationCode = -1, remoteConfirmationCode = -1;
|
||||
private boolean connectionFailed = false;
|
||||
private boolean localCompared = false, remoteCompared = false;
|
||||
private boolean localMatched = false, remoteMatched = false;
|
||||
private String remoteName = null;
|
||||
|
||||
ConnectorGroup(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
|
||||
BdfWriterFactory bdfWriterFactory,
|
||||
ContactExchangeTask contactExchangeTask,
|
||||
IdentityManager identityManager, PluginManager pluginManager,
|
||||
int localInvitationCode, int remoteInvitationCode) {
|
||||
super("ConnectorGroup");
|
||||
this.crypto = crypto;
|
||||
this.bdfReaderFactory = bdfReaderFactory;
|
||||
this.bdfWriterFactory = bdfWriterFactory;
|
||||
this.contactExchangeTask = contactExchangeTask;
|
||||
this.identityManager = identityManager;
|
||||
this.pluginManager = pluginManager;
|
||||
this.localInvitationCode = localInvitationCode;
|
||||
this.remoteInvitationCode = remoteInvitationCode;
|
||||
listeners = new CopyOnWriteArrayList<InvitationListener>();
|
||||
connected = new AtomicBoolean(false);
|
||||
localConfirmationLatch = new CountDownLatch(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvitationState addListener(InvitationListener l) {
|
||||
lock.lock();
|
||||
try {
|
||||
listeners.add(l);
|
||||
return new InvitationState(localInvitationCode,
|
||||
remoteInvitationCode, localConfirmationCode,
|
||||
remoteConfirmationCode, connected.get(), connectionFailed,
|
||||
localCompared, remoteCompared, localMatched, remoteMatched,
|
||||
remoteName);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvitationListener l) {
|
||||
listeners.remove(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {
|
||||
start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
LocalAuthor localAuthor;
|
||||
// Load the local pseudonym
|
||||
try {
|
||||
localAuthor = identityManager.getLocalAuthor();
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
lock.lock();
|
||||
try {
|
||||
connectionFailed = true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
for (InvitationListener l : listeners) l.connectionFailed();
|
||||
return;
|
||||
}
|
||||
// Start the connection threads
|
||||
Collection<Connector> connectors = new ArrayList<Connector>();
|
||||
// Alice is the party with the smaller invitation code
|
||||
if (localInvitationCode < remoteInvitationCode) {
|
||||
for (DuplexPlugin plugin : pluginManager.getInvitationPlugins()) {
|
||||
Connector c = createAliceConnector(plugin, localAuthor);
|
||||
connectors.add(c);
|
||||
c.start();
|
||||
}
|
||||
} else {
|
||||
for (DuplexPlugin plugin : pluginManager.getInvitationPlugins()) {
|
||||
Connector c = createBobConnector(plugin, localAuthor);
|
||||
connectors.add(c);
|
||||
c.start();
|
||||
}
|
||||
}
|
||||
// Wait for the connection threads to finish
|
||||
try {
|
||||
for (Connector c : connectors) c.join();
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warning("Interrupted while waiting for connectors");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
// If none of the threads connected, inform the listeners
|
||||
if (!connected.get()) {
|
||||
lock.lock();
|
||||
try {
|
||||
connectionFailed = true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
for (InvitationListener l : listeners) l.connectionFailed();
|
||||
}
|
||||
}
|
||||
|
||||
private Connector createAliceConnector(DuplexPlugin plugin,
|
||||
LocalAuthor localAuthor) {
|
||||
PseudoRandom random = crypto.getPseudoRandom(localInvitationCode,
|
||||
remoteInvitationCode);
|
||||
return new AliceConnector(crypto, bdfReaderFactory, bdfWriterFactory,
|
||||
contactExchangeTask, this, plugin, localAuthor, random);
|
||||
}
|
||||
|
||||
private Connector createBobConnector(DuplexPlugin plugin,
|
||||
LocalAuthor localAuthor) {
|
||||
PseudoRandom random = crypto.getPseudoRandom(remoteInvitationCode,
|
||||
localInvitationCode);
|
||||
return new BobConnector(crypto, bdfReaderFactory, bdfWriterFactory,
|
||||
contactExchangeTask, this, plugin, localAuthor, random);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void localConfirmationSucceeded() {
|
||||
lock.lock();
|
||||
try {
|
||||
localCompared = true;
|
||||
localMatched = true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
localConfirmationLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void localConfirmationFailed() {
|
||||
lock.lock();
|
||||
try {
|
||||
localCompared = true;
|
||||
localMatched = false;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
localConfirmationLatch.countDown();
|
||||
}
|
||||
|
||||
boolean getAndSetConnected() {
|
||||
boolean redundant = connected.getAndSet(true);
|
||||
if (!redundant)
|
||||
for (InvitationListener l : listeners) l.connectionSucceeded();
|
||||
return redundant;
|
||||
}
|
||||
|
||||
void keyAgreementSucceeded(int localCode, int remoteCode) {
|
||||
lock.lock();
|
||||
try {
|
||||
localConfirmationCode = localCode;
|
||||
remoteConfirmationCode = remoteCode;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
for (InvitationListener l : listeners)
|
||||
l.keyAgreementSucceeded(localCode, remoteCode);
|
||||
}
|
||||
|
||||
void keyAgreementFailed() {
|
||||
for (InvitationListener l : listeners) l.keyAgreementFailed();
|
||||
}
|
||||
|
||||
boolean waitForLocalConfirmationResult() throws InterruptedException {
|
||||
localConfirmationLatch.await(CONFIRMATION_TIMEOUT, MILLISECONDS);
|
||||
lock.lock();
|
||||
try {
|
||||
return localMatched;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void remoteConfirmationSucceeded() {
|
||||
lock.lock();
|
||||
try {
|
||||
remoteCompared = true;
|
||||
remoteMatched = true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
for (InvitationListener l : listeners) l.remoteConfirmationSucceeded();
|
||||
}
|
||||
|
||||
void remoteConfirmationFailed() {
|
||||
lock.lock();
|
||||
try {
|
||||
remoteCompared = true;
|
||||
remoteMatched = false;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
for (InvitationListener l : listeners) l.remoteConfirmationFailed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contactExchangeSucceeded(Author remoteAuthor) {
|
||||
String name = remoteAuthor.getName();
|
||||
lock.lock();
|
||||
try {
|
||||
remoteName = name;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
for (InvitationListener l : listeners)
|
||||
l.pseudonymExchangeSucceeded(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void duplicateContact(Author remoteAuthor) {
|
||||
// TODO differentiate
|
||||
for (InvitationListener l : listeners) l.pseudonymExchangeFailed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contactExchangeFailed() {
|
||||
for (InvitationListener l : listeners) l.pseudonymExchangeFailed();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.briarproject.bramble.invitation;
|
||||
|
||||
import org.briarproject.bramble.api.invitation.InvitationTaskFactory;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
@Module
|
||||
public class InvitationModule {
|
||||
|
||||
@Provides
|
||||
InvitationTaskFactory provideInvitationTaskFactory(
|
||||
InvitationTaskFactoryImpl invitationTaskFactory) {
|
||||
return invitationTaskFactory;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.briarproject.bramble.invitation;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactExchangeTask;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||
import org.briarproject.bramble.api.data.BdfWriterFactory;
|
||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.api.invitation.InvitationTask;
|
||||
import org.briarproject.bramble.api.invitation.InvitationTaskFactory;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.PluginManager;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class InvitationTaskFactoryImpl implements InvitationTaskFactory {
|
||||
|
||||
private final CryptoComponent crypto;
|
||||
private final BdfReaderFactory bdfReaderFactory;
|
||||
private final BdfWriterFactory bdfWriterFactory;
|
||||
private final ContactExchangeTask contactExchangeTask;
|
||||
private final IdentityManager identityManager;
|
||||
private final PluginManager pluginManager;
|
||||
|
||||
@Inject
|
||||
InvitationTaskFactoryImpl(CryptoComponent crypto,
|
||||
BdfReaderFactory bdfReaderFactory,
|
||||
BdfWriterFactory bdfWriterFactory,
|
||||
ContactExchangeTask contactExchangeTask,
|
||||
IdentityManager identityManager, PluginManager pluginManager) {
|
||||
this.crypto = crypto;
|
||||
this.bdfReaderFactory = bdfReaderFactory;
|
||||
this.bdfWriterFactory = bdfWriterFactory;
|
||||
this.contactExchangeTask = contactExchangeTask;
|
||||
this.identityManager = identityManager;
|
||||
this.pluginManager = pluginManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvitationTask createTask(int localCode, int remoteCode) {
|
||||
return new ConnectorGroup(crypto, bdfReaderFactory, bdfWriterFactory,
|
||||
contactExchangeTask, identityManager, pluginManager,
|
||||
localCode, remoteCode);
|
||||
}
|
||||
}
|
||||
@@ -164,14 +164,6 @@ class PluginManagerImpl implements PluginManager, Service {
|
||||
return new ArrayList<DuplexPlugin>(duplexPlugins);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<DuplexPlugin> getInvitationPlugins() {
|
||||
List<DuplexPlugin> supported = new ArrayList<DuplexPlugin>();
|
||||
for (DuplexPlugin d : duplexPlugins)
|
||||
if (d.supportsInvitations()) supported.add(d);
|
||||
return supported;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<DuplexPlugin> getKeyAgreementPlugins() {
|
||||
List<DuplexPlugin> supported = new ArrayList<DuplexPlugin>();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.briarproject.bramble.plugin.tcp;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
@@ -281,17 +280,6 @@ abstract class TcpPlugin implements DuplexPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsInvitations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout, boolean alice) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsKeyAgreement() {
|
||||
return false;
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.tcp;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Plugin;
|
||||
import org.briarproject.bramble.api.plugin.duplex.AbstractDuplexTransportConnection;
|
||||
import org.briarproject.bramble.util.IoUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -24,12 +25,12 @@ class TcpTransportConnection extends AbstractDuplexTransportConnection {
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException {
|
||||
return socket.getInputStream();
|
||||
return IoUtils.getInputStream(socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream getOutputStream() throws IOException {
|
||||
return socket.getOutputStream();
|
||||
return IoUtils.getOutputStream(socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.briarproject.bramble.reporting;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.util.IoUtils;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
@@ -130,7 +131,7 @@ public class DevReportServer {
|
||||
OutputStream out = null;
|
||||
try {
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||
in = socket.getInputStream();
|
||||
in = IoUtils.getInputStream(socket);
|
||||
reportDir.mkdirs();
|
||||
reportFile = File.createTempFile(FILE_PREFIX, FILE_SUFFIX,
|
||||
reportDir);
|
||||
|
||||
@@ -93,7 +93,7 @@ class DevReporterImpl implements DevReporter {
|
||||
InputStream in = null;
|
||||
try {
|
||||
Socket s = connectToDevelopers();
|
||||
out = s.getOutputStream();
|
||||
out = IoUtils.getOutputStream(s);
|
||||
in = new FileInputStream(f);
|
||||
IoUtils.copyAndClose(in, out);
|
||||
f.delete();
|
||||
|
||||
@@ -57,8 +57,8 @@ class SocksSocket extends Socket {
|
||||
|
||||
// Connect to the proxy
|
||||
super.connect(proxy, connectToProxyTimeout);
|
||||
OutputStream out = getOutputStream();
|
||||
InputStream in = getInputStream();
|
||||
OutputStream out = IoUtils.getOutputStream(this);
|
||||
InputStream in = IoUtils.getInputStream(this);
|
||||
|
||||
// Request SOCKS 5 with no authentication
|
||||
sendMethodRequest(out);
|
||||
|
||||
@@ -29,10 +29,10 @@ class StreamReaderFactoryImpl implements StreamReaderFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream createInvitationStreamReader(InputStream in,
|
||||
public InputStream createContactExchangeStreamReader(InputStream in,
|
||||
SecretKey headerKey) {
|
||||
return new StreamReaderImpl(
|
||||
streamDecrypterFactory.createInvitationStreamDecrypter(in,
|
||||
streamDecrypterFactory.createContactExchangeStreamDecrypter(in,
|
||||
headerKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ class StreamWriterFactoryImpl implements StreamWriterFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream createInvitationStreamWriter(OutputStream out,
|
||||
public OutputStream createContactExchangeStreamWriter(OutputStream out,
|
||||
SecretKey headerKey) {
|
||||
return new StreamWriterImpl(
|
||||
streamEncrypterFactory.createInvitationStreamEncrypter(out,
|
||||
streamEncrypterFactory.createContactExchangeStreamDecrypter(out,
|
||||
headerKey));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.briarproject.bramble.crypto;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.spongycastle.crypto.Digest;
|
||||
import org.spongycastle.crypto.engines.Salsa20Engine;
|
||||
@@ -11,11 +10,11 @@ import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
@NotThreadSafe
|
||||
@NotNullByDefault
|
||||
class PseudoRandomImpl implements PseudoRandom {
|
||||
class PseudoRandom {
|
||||
|
||||
private final Salsa20Engine cipher = new Salsa20Engine();
|
||||
|
||||
PseudoRandomImpl(byte[] seed) {
|
||||
PseudoRandom(byte[] seed) {
|
||||
// Hash the seed to produce a 32-byte key
|
||||
byte[] key = new byte[32];
|
||||
Digest digest = new Blake2sDigest();
|
||||
@@ -26,8 +25,7 @@ class PseudoRandomImpl implements PseudoRandom {
|
||||
cipher.init(true, new ParametersWithIV(new KeyParameter(key), nonce));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] nextBytes(int length) {
|
||||
byte[] nextBytes(int length) {
|
||||
byte[] in = new byte[length], out = new byte[length];
|
||||
cipher.processBytes(in, 0, length, out, 0);
|
||||
return out;
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.briarproject.bramble.crypto;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SecureRandomSpi;
|
||||
@@ -19,7 +17,7 @@ class PseudoSecureRandom extends SecureRandom {
|
||||
private final PseudoRandom pseudoRandom;
|
||||
|
||||
private PseudoSecureRandomSpi(byte[] seed) {
|
||||
pseudoRandom = new PseudoRandomImpl(seed);
|
||||
pseudoRandom = new PseudoRandom(seed);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.briarproject.bramble.plugin.bluetooth;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||
@@ -19,33 +18,23 @@ import org.briarproject.bramble.util.OsUtils;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletionService;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorCompletionService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.bluetooth.BluetoothStateException;
|
||||
import javax.bluetooth.DiscoveryAgent;
|
||||
import javax.bluetooth.LocalDevice;
|
||||
import javax.microedition.io.Connector;
|
||||
import javax.microedition.io.StreamConnection;
|
||||
import javax.microedition.io.StreamConnectionNotifier;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static javax.bluetooth.DiscoveryAgent.GIAC;
|
||||
@@ -67,7 +56,6 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
private final Backoff backoff;
|
||||
private final DuplexPluginCallback callback;
|
||||
private final int maxLatency;
|
||||
private final Semaphore discoverySemaphore = new Semaphore(1);
|
||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||
|
||||
private volatile boolean running = false;
|
||||
@@ -273,95 +261,6 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
return new BluetoothTransportConnection(this, s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsInvitations() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout, boolean alice) {
|
||||
if (!running) return null;
|
||||
// Use the invitation codes to generate the UUID
|
||||
byte[] b = r.nextBytes(UUID_BYTES);
|
||||
String uuid = UUID.nameUUIDFromBytes(b).toString();
|
||||
String url = makeUrl("localhost", uuid);
|
||||
// Make the device discoverable if possible
|
||||
makeDeviceDiscoverable();
|
||||
// Bind a server socket for receiving invitation connections
|
||||
final StreamConnectionNotifier ss;
|
||||
try {
|
||||
ss = (StreamConnectionNotifier) Connector.open(url);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
if (!running) {
|
||||
tryToClose(ss);
|
||||
return null;
|
||||
}
|
||||
// Create the background tasks
|
||||
CompletionService<StreamConnection> complete =
|
||||
new ExecutorCompletionService<>(ioExecutor);
|
||||
List<Future<StreamConnection>> futures = new ArrayList<>();
|
||||
if (alice) {
|
||||
// Return the first connected socket
|
||||
futures.add(complete.submit(new ListeningTask(ss)));
|
||||
futures.add(complete.submit(new DiscoveryTask(uuid)));
|
||||
} else {
|
||||
// Return the first socket with readable data
|
||||
futures.add(complete.submit(new ReadableTask(
|
||||
new ListeningTask(ss))));
|
||||
futures.add(complete.submit(new ReadableTask(
|
||||
new DiscoveryTask(uuid))));
|
||||
}
|
||||
StreamConnection chosen = null;
|
||||
try {
|
||||
Future<StreamConnection> f = complete.poll(timeout, MILLISECONDS);
|
||||
if (f == null) return null; // No task completed within the timeout
|
||||
chosen = f.get();
|
||||
return new BluetoothTransportConnection(this, chosen);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.info("Interrupted while exchanging invitations");
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
} catch (ExecutionException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
} finally {
|
||||
// Closing the socket will terminate the listener task
|
||||
tryToClose(ss);
|
||||
closeSockets(futures, chosen);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeSockets(final List<Future<StreamConnection>> futures,
|
||||
@Nullable final StreamConnection chosen) {
|
||||
ioExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (Future<StreamConnection> f : futures) {
|
||||
try {
|
||||
if (f.cancel(true)) {
|
||||
LOG.info("Cancelled task");
|
||||
} else {
|
||||
StreamConnection s = f.get();
|
||||
if (s != null && s != chosen) {
|
||||
LOG.info("Closing unwanted socket");
|
||||
s.close();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOG.info("Interrupted while closing sockets");
|
||||
return;
|
||||
} catch (ExecutionException | IOException e) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsKeyAgreement() {
|
||||
return true;
|
||||
@@ -376,7 +275,7 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
String url = makeUrl("localhost", uuid);
|
||||
// Make the device discoverable if possible
|
||||
makeDeviceDiscoverable();
|
||||
// Bind a server socket for receiving invitation connections
|
||||
// Bind a server socket for receiving key agreementconnections
|
||||
final StreamConnectionNotifier ss;
|
||||
try {
|
||||
ss = (StreamConnectionNotifier) Connector.open(url);
|
||||
@@ -431,77 +330,6 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private class DiscoveryTask implements Callable<StreamConnection> {
|
||||
|
||||
private final String uuid;
|
||||
|
||||
private DiscoveryTask(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamConnection call() throws Exception {
|
||||
// Repeat discovery until we connect or get interrupted
|
||||
DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent();
|
||||
while (true) {
|
||||
if (!discoverySemaphore.tryAcquire())
|
||||
throw new Exception("Discovery is already in progress");
|
||||
try {
|
||||
InvitationListener listener =
|
||||
new InvitationListener(discoveryAgent, uuid);
|
||||
discoveryAgent.startInquiry(GIAC, listener);
|
||||
String url = listener.waitForUrl();
|
||||
if (url != null) {
|
||||
StreamConnection s = connect(url);
|
||||
if (s != null) {
|
||||
LOG.info("Outgoing connection");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
discoverySemaphore.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ListeningTask implements Callable<StreamConnection> {
|
||||
|
||||
private final StreamConnectionNotifier serverSocket;
|
||||
|
||||
private ListeningTask(StreamConnectionNotifier serverSocket) {
|
||||
this.serverSocket = serverSocket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamConnection call() throws Exception {
|
||||
StreamConnection s = serverSocket.acceptAndOpen();
|
||||
LOG.info("Incoming connection");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReadableTask implements Callable<StreamConnection> {
|
||||
|
||||
private final Callable<StreamConnection> connectionTask;
|
||||
|
||||
private ReadableTask(Callable<StreamConnection> connectionTask) {
|
||||
this.connectionTask = connectionTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamConnection call() throws Exception {
|
||||
StreamConnection s = connectionTask.call();
|
||||
InputStream in = s.openInputStream();
|
||||
while (in.available() == 0) {
|
||||
LOG.info("Waiting for data");
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
LOG.info("Data available");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
|
||||
|
||||
private final StreamConnectionNotifier ss;
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package org.briarproject.bramble.plugin.bluetooth;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.bluetooth.BluetoothStateException;
|
||||
import javax.bluetooth.DataElement;
|
||||
import javax.bluetooth.DeviceClass;
|
||||
import javax.bluetooth.DiscoveryAgent;
|
||||
import javax.bluetooth.DiscoveryListener;
|
||||
import javax.bluetooth.RemoteDevice;
|
||||
import javax.bluetooth.ServiceRecord;
|
||||
import javax.bluetooth.UUID;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
class InvitationListener implements DiscoveryListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(InvitationListener.class.getName());
|
||||
|
||||
private final AtomicInteger searches = new AtomicInteger(1);
|
||||
private final CountDownLatch finished = new CountDownLatch(1);
|
||||
private final DiscoveryAgent discoveryAgent;
|
||||
private final String uuid;
|
||||
|
||||
private volatile String url = null;
|
||||
|
||||
InvitationListener(DiscoveryAgent discoveryAgent, String uuid) {
|
||||
this.discoveryAgent = discoveryAgent;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
String waitForUrl() throws InterruptedException {
|
||||
finished.await();
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceDiscovered(RemoteDevice device, DeviceClass deviceClass) {
|
||||
UUID[] uuids = new UUID[] {new UUID(uuid, false)};
|
||||
// Try to discover the services associated with the UUID
|
||||
try {
|
||||
discoveryAgent.searchServices(null, uuids, device, this);
|
||||
searches.incrementAndGet();
|
||||
} catch (BluetoothStateException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void servicesDiscovered(int transaction, ServiceRecord[] services) {
|
||||
for (ServiceRecord record : services) {
|
||||
// Does this service have a URL?
|
||||
String serviceUrl = record.getConnectionURL(
|
||||
ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
|
||||
if (serviceUrl == null) continue;
|
||||
// Does this service have the UUID we're looking for?
|
||||
Collection<String> uuids = new TreeSet<>();
|
||||
findNestedClassIds(record.getAttributeValue(0x1), uuids);
|
||||
for (String u : uuids) {
|
||||
if (uuid.equalsIgnoreCase(u)) {
|
||||
// The UUID matches - store the URL
|
||||
url = serviceUrl;
|
||||
finished.countDown();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inquiryCompleted(int discoveryType) {
|
||||
if (searches.decrementAndGet() == 0) finished.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceSearchCompleted(int transaction, int response) {
|
||||
if (searches.decrementAndGet() == 0) finished.countDown();
|
||||
}
|
||||
|
||||
// UUIDs are sometimes buried in nested data elements
|
||||
private void findNestedClassIds(Object o, Collection<String> ids) {
|
||||
o = getDataElementValue(o);
|
||||
if (o instanceof Enumeration<?>) {
|
||||
for (Object o1 : Collections.list((Enumeration<?>) o))
|
||||
findNestedClassIds(o1, ids);
|
||||
} else if (o instanceof UUID) {
|
||||
ids.add(o.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private Object getDataElementValue(Object o) {
|
||||
if (o instanceof DataElement) {
|
||||
// Bluecove throws an exception if the type is unknown
|
||||
try {
|
||||
return ((DataElement) o).getValue();
|
||||
} catch (ClassCastException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.briarproject.bramble.plugin.modem;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.PseudoRandom;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
@@ -167,17 +166,6 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
|
||||
return new ModemTransportConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsInvitations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout, boolean alice) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsKeyAgreement() {
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = pt_BR: pt-rBR, fr_FR: fr
|
||||
lang_map = pt_BR: pt-rBR, fr_FR: fr, nb_NO: nb
|
||||
|
||||
[briar.stringsxml-5]
|
||||
file_filter = src/main/res/values-<lang>/strings.xml
|
||||
|
||||
@@ -78,15 +78,19 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 22
|
||||
versionCode 1601
|
||||
versionName "0.16.1"
|
||||
versionCode 1610
|
||||
versionName "0.16.10"
|
||||
applicationId "org.briarproject.briar.beta"
|
||||
resValue "string", "app_package", "org.briarproject.briar.beta"
|
||||
resValue "string", "app_name", "Briar Beta"
|
||||
buildConfigField "String", "GitHash", "\"${getGitHash()}\""
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
resValue "string", "app_package", "org.briarproject.briar.beta.debug"
|
||||
resValue "string", "app_name", "Briar Debug"
|
||||
shrinkResources false
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
<uses-permission android:name="android.permission.READ_LOGS"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<!-- Since API 23, this is needed to add contacts via Bluetooth -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:name="org.briarproject.briar.android.BriarApplicationImpl"
|
||||
@@ -292,16 +290,6 @@
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.invitation.AddContactActivity"
|
||||
android:label="@string/add_contact_title"
|
||||
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.keyagreement.KeyAgreementActivity"
|
||||
android:label="@string/add_contact_title"
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.briarproject.bramble.api.db.DatabaseConfig;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.api.invitation.InvitationTaskFactory;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory;
|
||||
import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
|
||||
import org.briarproject.bramble.api.keyagreement.PayloadParser;
|
||||
@@ -89,8 +88,6 @@ public interface AndroidComponent
|
||||
|
||||
EventBus eventBus();
|
||||
|
||||
InvitationTaskFactory invitationTaskFactory();
|
||||
|
||||
AndroidNotificationManager androidNotificationManager();
|
||||
|
||||
ScreenFilterMonitor screenFilterMonitor();
|
||||
|
||||
@@ -5,11 +5,8 @@ import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
@@ -25,12 +22,14 @@ import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.contact.ConversationActivity;
|
||||
import org.briarproject.briar.android.forum.ForumActivity;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
|
||||
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
|
||||
import org.briarproject.briar.android.util.BriarNotificationBuilder;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
|
||||
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
|
||||
@@ -48,6 +47,7 @@ import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -61,8 +61,6 @@ import static android.content.Context.NOTIFICATION_SERVICE;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||
import static android.support.v4.app.NotificationCompat.CATEGORY_MESSAGE;
|
||||
import static android.support.v4.app.NotificationCompat.CATEGORY_SOCIAL;
|
||||
import static android.support.v4.app.NotificationCompat.VISIBILITY_PRIVATE;
|
||||
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||
import static org.briarproject.briar.android.contact.ConversationActivity.CONTACT_ID;
|
||||
@@ -95,6 +93,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
private static final String BLOG_URI =
|
||||
"content://org.briarproject.briar/blog";
|
||||
|
||||
private static final long SOUND_DELAY = TimeUnit.SECONDS.toMillis(2);
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(AndroidNotificationManagerImpl.class.getName());
|
||||
|
||||
@@ -102,6 +102,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
private final SettingsManager settingsManager;
|
||||
private final AndroidExecutor androidExecutor;
|
||||
private final Context appContext;
|
||||
private final Clock clock;
|
||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||
|
||||
// The following must only be accessed on the main UI thread
|
||||
@@ -117,16 +118,18 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
private boolean blockContacts = false, blockGroups = false;
|
||||
private boolean blockForums = false, blockBlogs = false;
|
||||
private boolean blockIntroductions = false;
|
||||
private long lastSound = 0;
|
||||
|
||||
private volatile Settings settings = new Settings();
|
||||
|
||||
@Inject
|
||||
AndroidNotificationManagerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
SettingsManager settingsManager, AndroidExecutor androidExecutor,
|
||||
Application app) {
|
||||
Application app, Clock clock) {
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.settingsManager = settingsManager;
|
||||
this.androidExecutor = androidExecutor;
|
||||
this.clock = clock;
|
||||
appContext = app.getApplicationContext();
|
||||
}
|
||||
|
||||
@@ -288,22 +291,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
if (contactTotal == 0) {
|
||||
clearContactNotification();
|
||||
} else if (settings.getBoolean(PREF_NOTIFY_PRIVATE, true)) {
|
||||
NotificationCompat.Builder b =
|
||||
new NotificationCompat.Builder(appContext);
|
||||
BriarNotificationBuilder b =
|
||||
new BriarNotificationBuilder(appContext);
|
||||
b.setSmallIcon(R.drawable.notification_private_message);
|
||||
b.setColor(ContextCompat.getColor(appContext,
|
||||
R.color.briar_primary));
|
||||
b.setColorRes(R.color.briar_primary);
|
||||
b.setContentTitle(appContext.getText(R.string.app_name));
|
||||
b.setContentText(appContext.getResources().getQuantityString(
|
||||
R.plurals.private_message_notification_text, contactTotal,
|
||||
contactTotal));
|
||||
boolean sound = settings.getBoolean(PREF_NOTIFY_SOUND, true);
|
||||
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
|
||||
if (sound && !StringUtils.isNullOrEmpty(ringtoneUri))
|
||||
b.setSound(Uri.parse(ringtoneUri));
|
||||
b.setDefaults(getDefaults());
|
||||
b.setOnlyAlertOnce(true);
|
||||
b.setAutoCancel(true);
|
||||
b.setNumber(contactTotal);
|
||||
boolean showOnLockScreen =
|
||||
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
|
||||
b.setLockscreenVisibility(CATEGORY_MESSAGE, showOnLockScreen);
|
||||
playSound(b);
|
||||
if (contactCounts.size() == 1) {
|
||||
// Touching the notification shows the relevant conversation
|
||||
Intent i = new Intent(appContext, ConversationActivity.class);
|
||||
@@ -326,21 +326,27 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
t.addNextIntent(i);
|
||||
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
b.setCategory(CATEGORY_MESSAGE);
|
||||
boolean showOnLockScreen =
|
||||
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
|
||||
if (showOnLockScreen)
|
||||
b.setVisibility(VISIBILITY_PRIVATE);
|
||||
else
|
||||
b.setVisibility(VISIBILITY_SECRET);
|
||||
}
|
||||
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationManager nm = (NotificationManager) o;
|
||||
nm.notify(PRIVATE_MESSAGE_NOTIFICATION_ID, b.build());
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void playSound(BriarNotificationBuilder b) {
|
||||
boolean sound = settings.getBoolean(PREF_NOTIFY_SOUND, true);
|
||||
if (!sound) return;
|
||||
|
||||
long currentTime = clock.currentTimeMillis();
|
||||
if (currentTime - lastSound > SOUND_DELAY) {
|
||||
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
|
||||
if (!StringUtils.isNullOrEmpty(ringtoneUri))
|
||||
b.setSound(Uri.parse(ringtoneUri));
|
||||
b.setDefaults(getDefaults());
|
||||
lastSound = clock.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private int getDefaults() {
|
||||
int defaults = DEFAULT_LIGHTS;
|
||||
@@ -387,21 +393,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
if (groupTotal == 0) {
|
||||
clearGroupMessageNotification();
|
||||
} else if (settings.getBoolean(PREF_NOTIFY_GROUP, true)) {
|
||||
NotificationCompat.Builder b =
|
||||
new NotificationCompat.Builder(appContext);
|
||||
BriarNotificationBuilder b =
|
||||
new BriarNotificationBuilder(appContext);
|
||||
b.setSmallIcon(R.drawable.notification_private_group);
|
||||
b.setColor(ContextCompat.getColor(appContext,
|
||||
R.color.briar_primary));
|
||||
b.setColorRes(R.color.briar_primary);
|
||||
b.setContentTitle(appContext.getText(R.string.app_name));
|
||||
b.setContentText(appContext.getResources().getQuantityString(
|
||||
R.plurals.group_message_notification_text, groupTotal,
|
||||
groupTotal));
|
||||
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
|
||||
if (!StringUtils.isNullOrEmpty(ringtoneUri))
|
||||
b.setSound(Uri.parse(ringtoneUri));
|
||||
b.setDefaults(getDefaults());
|
||||
b.setOnlyAlertOnce(true);
|
||||
b.setAutoCancel(true);
|
||||
b.setNumber(groupTotal);
|
||||
boolean showOnLockScreen =
|
||||
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
|
||||
b.setLockscreenVisibility(CATEGORY_SOCIAL, showOnLockScreen);
|
||||
playSound(b);
|
||||
if (groupCounts.size() == 1) {
|
||||
// Touching the notification shows the relevant group
|
||||
Intent i = new Intent(appContext, GroupActivity.class);
|
||||
@@ -425,15 +429,6 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
t.addNextIntent(i);
|
||||
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
b.setCategory(CATEGORY_SOCIAL);
|
||||
boolean showOnLockScreen =
|
||||
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
|
||||
if (showOnLockScreen)
|
||||
b.setVisibility(VISIBILITY_PRIVATE);
|
||||
else
|
||||
b.setVisibility(VISIBILITY_SECRET);
|
||||
}
|
||||
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationManager nm = (NotificationManager) o;
|
||||
nm.notify(GROUP_MESSAGE_NOTIFICATION_ID, b.build());
|
||||
@@ -474,21 +469,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
if (forumTotal == 0) {
|
||||
clearForumPostNotification();
|
||||
} else if (settings.getBoolean(PREF_NOTIFY_FORUM, true)) {
|
||||
NotificationCompat.Builder b =
|
||||
new NotificationCompat.Builder(appContext);
|
||||
BriarNotificationBuilder b =
|
||||
new BriarNotificationBuilder(appContext);
|
||||
b.setSmallIcon(R.drawable.notification_forum);
|
||||
b.setColor(ContextCompat.getColor(appContext,
|
||||
R.color.briar_primary));
|
||||
b.setColorRes(R.color.briar_primary);
|
||||
b.setContentTitle(appContext.getText(R.string.app_name));
|
||||
b.setContentText(appContext.getResources().getQuantityString(
|
||||
R.plurals.forum_post_notification_text, forumTotal,
|
||||
forumTotal));
|
||||
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
|
||||
if (!StringUtils.isNullOrEmpty(ringtoneUri))
|
||||
b.setSound(Uri.parse(ringtoneUri));
|
||||
b.setDefaults(getDefaults());
|
||||
b.setOnlyAlertOnce(true);
|
||||
b.setAutoCancel(true);
|
||||
b.setNumber(forumTotal);
|
||||
boolean showOnLockScreen =
|
||||
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
|
||||
b.setLockscreenVisibility(CATEGORY_SOCIAL, showOnLockScreen);
|
||||
playSound(b);
|
||||
if (forumCounts.size() == 1) {
|
||||
// Touching the notification shows the relevant forum
|
||||
Intent i = new Intent(appContext, ForumActivity.class);
|
||||
@@ -512,15 +505,6 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
t.addNextIntent(i);
|
||||
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
b.setCategory(CATEGORY_SOCIAL);
|
||||
boolean showOnLockScreen =
|
||||
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
|
||||
if (showOnLockScreen)
|
||||
b.setVisibility(VISIBILITY_PRIVATE);
|
||||
else
|
||||
b.setVisibility(VISIBILITY_SECRET);
|
||||
}
|
||||
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationManager nm = (NotificationManager) o;
|
||||
nm.notify(FORUM_POST_NOTIFICATION_ID, b.build());
|
||||
@@ -561,21 +545,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
if (blogTotal == 0) {
|
||||
clearBlogPostNotification();
|
||||
} else if (settings.getBoolean(PREF_NOTIFY_BLOG, true)) {
|
||||
NotificationCompat.Builder b =
|
||||
new NotificationCompat.Builder(appContext);
|
||||
BriarNotificationBuilder b =
|
||||
new BriarNotificationBuilder(appContext);
|
||||
b.setSmallIcon(R.drawable.notification_blog);
|
||||
b.setColor(ContextCompat.getColor(appContext,
|
||||
R.color.briar_primary));
|
||||
b.setColorRes(R.color.briar_primary);
|
||||
b.setContentTitle(appContext.getText(R.string.app_name));
|
||||
b.setContentText(appContext.getResources().getQuantityString(
|
||||
R.plurals.blog_post_notification_text, blogTotal,
|
||||
blogTotal));
|
||||
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
|
||||
if (!StringUtils.isNullOrEmpty(ringtoneUri))
|
||||
b.setSound(Uri.parse(ringtoneUri));
|
||||
b.setDefaults(getDefaults());
|
||||
b.setOnlyAlertOnce(true);
|
||||
b.setAutoCancel(true);
|
||||
b.setNumber(blogTotal);
|
||||
boolean showOnLockScreen =
|
||||
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
|
||||
b.setLockscreenVisibility(CATEGORY_SOCIAL, showOnLockScreen);
|
||||
playSound(b);
|
||||
// Touching the notification shows the combined blog feed
|
||||
Intent i = new Intent(appContext, NavDrawerActivity.class);
|
||||
i.putExtra(INTENT_BLOGS, true);
|
||||
@@ -585,15 +567,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
t.addParentStack(NavDrawerActivity.class);
|
||||
t.addNextIntent(i);
|
||||
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
b.setCategory(CATEGORY_SOCIAL);
|
||||
boolean showOnLockScreen =
|
||||
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
|
||||
if (showOnLockScreen)
|
||||
b.setVisibility(VISIBILITY_PRIVATE);
|
||||
else
|
||||
b.setVisibility(VISIBILITY_SECRET);
|
||||
}
|
||||
|
||||
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationManager nm = (NotificationManager) o;
|
||||
nm.notify(BLOG_POST_NOTIFICATION_ID, b.build());
|
||||
@@ -623,20 +597,17 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
|
||||
@UiThread
|
||||
private void updateIntroductionNotification() {
|
||||
NotificationCompat.Builder b =
|
||||
new NotificationCompat.Builder(appContext);
|
||||
BriarNotificationBuilder b = new BriarNotificationBuilder(appContext);
|
||||
b.setSmallIcon(R.drawable.notification_introduction);
|
||||
b.setColor(ContextCompat.getColor(appContext, R.color.briar_primary));
|
||||
b.setColorRes(R.color.briar_primary);
|
||||
b.setContentTitle(appContext.getText(R.string.app_name));
|
||||
b.setContentText(appContext.getResources().getQuantityString(
|
||||
R.plurals.introduction_notification_text, introductionTotal,
|
||||
introductionTotal));
|
||||
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
|
||||
if (!StringUtils.isNullOrEmpty(ringtoneUri))
|
||||
b.setSound(Uri.parse(ringtoneUri));
|
||||
b.setDefaults(getDefaults());
|
||||
b.setOnlyAlertOnce(true);
|
||||
b.setAutoCancel(true);
|
||||
boolean showOnLockScreen =
|
||||
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
|
||||
b.setLockscreenVisibility(CATEGORY_MESSAGE, showOnLockScreen);
|
||||
playSound(b);
|
||||
// Touching the notification shows the contact list
|
||||
Intent i = new Intent(appContext, NavDrawerActivity.class);
|
||||
i.putExtra(INTENT_CONTACTS, true);
|
||||
@@ -646,15 +617,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
t.addParentStack(NavDrawerActivity.class);
|
||||
t.addNextIntent(i);
|
||||
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
b.setCategory(CATEGORY_MESSAGE);
|
||||
boolean showOnLockScreen =
|
||||
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
|
||||
if (showOnLockScreen)
|
||||
b.setVisibility(VISIBILITY_PRIVATE);
|
||||
else
|
||||
b.setVisibility(VISIBILITY_SECRET);
|
||||
}
|
||||
|
||||
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationManager nm = (NotificationManager) o;
|
||||
nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build());
|
||||
|
||||
@@ -6,8 +6,8 @@ package org.briarproject.briar.android;
|
||||
*/
|
||||
public interface BriarApplication {
|
||||
|
||||
// This build expires on 15 October 2017
|
||||
long EXPIRY_DATE = 1508022000 * 1000L;
|
||||
// This build expires on 21 October 2017
|
||||
long EXPIRY_DATE = 1508544000 * 1000L;
|
||||
|
||||
AndroidComponent getApplicationComponent();
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ package org.briarproject.briar.android;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.StrictMode;
|
||||
import android.os.StrictMode.ThreadPolicy;
|
||||
import android.os.StrictMode.VmPolicy;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.ReportingInteractionMode;
|
||||
@@ -33,6 +36,8 @@ import static org.acra.ReportField.REPORT_ID;
|
||||
import static org.acra.ReportField.STACK_TRACE;
|
||||
import static org.acra.ReportField.USER_APP_START_DATE;
|
||||
import static org.acra.ReportField.USER_CRASH_DATE;
|
||||
import static org.briarproject.briar.android.TestingConstants.DEFAULT_LOG_LEVEL;
|
||||
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
|
||||
@ReportsCrashes(
|
||||
reportPrimerClass = BriarReportPrimer.class,
|
||||
@@ -72,6 +77,9 @@ public class BriarApplicationImpl extends Application
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
if (IS_DEBUG_BUILD) enableStrictMode();
|
||||
Logger.getLogger("").setLevel(DEFAULT_LOG_LEVEL);
|
||||
LOG.info("Created");
|
||||
|
||||
applicationComponent = DaggerAndroidComponent.builder()
|
||||
@@ -85,6 +93,17 @@ public class BriarApplicationImpl extends Application
|
||||
AndroidEagerSingletons.initEagerSingletons(applicationComponent);
|
||||
}
|
||||
|
||||
private void enableStrictMode() {
|
||||
ThreadPolicy.Builder threadPolicy = new ThreadPolicy.Builder();
|
||||
threadPolicy.detectAll();
|
||||
threadPolicy.penaltyLog();
|
||||
StrictMode.setThreadPolicy(threadPolicy.build());
|
||||
VmPolicy.Builder vmPolicy = new VmPolicy.Builder();
|
||||
vmPolicy.detectAll();
|
||||
vmPolicy.penaltyLog();
|
||||
StrictMode.setVmPolicy(vmPolicy.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidComponent getApplicationComponent() {
|
||||
return applicationComponent;
|
||||
|
||||
@@ -10,13 +10,21 @@ import static java.util.logging.Level.OFF;
|
||||
public interface TestingConstants {
|
||||
|
||||
/**
|
||||
* Whether this is an alpha or beta build. This should be set to false for
|
||||
* Whether this is a debug build.
|
||||
*/
|
||||
boolean IS_DEBUG_BUILD = BuildConfig.DEBUG;
|
||||
|
||||
/**
|
||||
* Whether this is a beta build. This should be set to false for final
|
||||
* release builds.
|
||||
*/
|
||||
boolean TESTING = BuildConfig.DEBUG;
|
||||
boolean IS_BETA_BUILD = true;
|
||||
|
||||
/** Default log level. */
|
||||
Level DEFAULT_LOG_LEVEL = TESTING ? INFO : OFF;
|
||||
/**
|
||||
* Default log level. Disable logging for final release builds.
|
||||
*/
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
Level DEFAULT_LOG_LEVEL = IS_DEBUG_BUILD || IS_BETA_BUILD ? INFO : OFF;
|
||||
|
||||
/**
|
||||
* Whether to prevent screenshots from being taken. Setting this to true
|
||||
@@ -24,5 +32,5 @@ public interface TestingConstants {
|
||||
* Unfortunately this also prevents the user from taking screenshots
|
||||
* intentionally.
|
||||
*/
|
||||
boolean PREVENT_SCREENSHOTS = !TESTING;
|
||||
boolean PREVENT_SCREENSHOTS = !IS_DEBUG_BUILD;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.briarproject.briar.android.forum.ForumModule;
|
||||
import org.briarproject.briar.android.introduction.ContactChooserFragment;
|
||||
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
||||
import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
|
||||
import org.briarproject.briar.android.invitation.AddContactActivity;
|
||||
import org.briarproject.briar.android.keyagreement.IntroFragment;
|
||||
import org.briarproject.briar.android.keyagreement.KeyAgreementActivity;
|
||||
import org.briarproject.briar.android.keyagreement.ShowQrCodeFragment;
|
||||
@@ -91,8 +90,6 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(PanicPreferencesActivity activity);
|
||||
|
||||
void inject(AddContactActivity activity);
|
||||
|
||||
void inject(KeyAgreementActivity activity);
|
||||
|
||||
void inject(ConversationActivity activity);
|
||||
|
||||
@@ -60,8 +60,7 @@ abstract class BasePostFragment extends BaseFragment {
|
||||
false);
|
||||
progressBar = (ProgressBar) view.findViewById(R.id.progressBar);
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
ui = new BlogPostViewHolder(view);
|
||||
ui.setOnBlogPostClickListener(new OnBlogPostClickListener() {
|
||||
ui = new BlogPostViewHolder(view, true, new OnBlogPostClickListener() {
|
||||
@Override
|
||||
public void onBlogPostClick(BlogPostItem post) {
|
||||
// We're already there
|
||||
|
||||
@@ -106,7 +106,7 @@ class BlogControllerImpl extends BaseControllerImpl
|
||||
BlogInvitationResponseReceivedEvent b =
|
||||
(BlogInvitationResponseReceivedEvent) e;
|
||||
InvitationResponse r = b.getResponse();
|
||||
if (r.getGroupId().equals(groupId) && r.wasAccepted()) {
|
||||
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
|
||||
LOG.info("Blog invitation accepted");
|
||||
onBlogInvitationAccepted(b.getContactId());
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ class BlogPostAdapter
|
||||
int viewType) {
|
||||
View v = LayoutInflater.from(ctx).inflate(
|
||||
R.layout.list_item_blog_post, parent, false);
|
||||
BlogPostViewHolder ui = new BlogPostViewHolder(v);
|
||||
ui.setOnBlogPostClickListener(listener);
|
||||
BlogPostViewHolder ui = new BlogPostViewHolder(v, false, listener);
|
||||
return ui;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.blog.BaseController.BlogListener;
|
||||
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.briar.api.blog.BlogPostHeader;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@UiThread
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class BlogPostFragment extends BasePostFragment {
|
||||
public class BlogPostFragment extends BasePostFragment implements BlogListener {
|
||||
|
||||
private static final String TAG = BlogPostFragment.class.getName();
|
||||
|
||||
@@ -40,6 +42,7 @@ public class BlogPostFragment extends BasePostFragment {
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
blogController.setBlogListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,4 +62,15 @@ public class BlogPostFragment extends BasePostFragment {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
|
||||
// doesn't matter here
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlogRemoved() {
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
@@ -47,12 +48,16 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
||||
private final ImageView reblogButton;
|
||||
private final TextView body;
|
||||
private final ViewGroup commentContainer;
|
||||
private final boolean fullText;
|
||||
|
||||
@Nullable
|
||||
private OnBlogPostClickListener listener;
|
||||
@NonNull
|
||||
private final OnBlogPostClickListener listener;
|
||||
|
||||
BlogPostViewHolder(View v) {
|
||||
BlogPostViewHolder(View v, boolean fullText,
|
||||
@NonNull OnBlogPostClickListener listener) {
|
||||
super(v);
|
||||
this.fullText = fullText;
|
||||
this.listener = listener;
|
||||
|
||||
ctx = v.getContext();
|
||||
layout = (ViewGroup) v.findViewById(R.id.postLayout);
|
||||
@@ -64,10 +69,6 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
||||
(ViewGroup) v.findViewById(R.id.commentContainer);
|
||||
}
|
||||
|
||||
void setOnBlogPostClickListener(OnBlogPostClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
void setVisibility(int visibility) {
|
||||
layout.setVisibility(visibility);
|
||||
}
|
||||
@@ -92,7 +93,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
||||
if (item == null) return;
|
||||
|
||||
setTransitionName(item.getId());
|
||||
if (listener != null) {
|
||||
if (!fullText) {
|
||||
layout.setClickable(true);
|
||||
layout.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
@@ -111,7 +112,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
||||
author.setPersona(
|
||||
item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL);
|
||||
// TODO make author clickable more often #624
|
||||
if (listener != null && item.getHeader().getType() == POST) {
|
||||
if (!fullText && item.getHeader().getType() == POST) {
|
||||
author.setAuthorClickable(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -124,7 +125,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
// post body
|
||||
Spanned bodyText = getSpanned(item.getBody());
|
||||
if (listener == null) {
|
||||
if (fullText) {
|
||||
body.setText(bodyText);
|
||||
body.setTextIsSelectable(true);
|
||||
makeLinksClickable(body);
|
||||
@@ -170,7 +171,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
||||
reblogger.setAuthor(item.getAuthor());
|
||||
reblogger.setAuthorStatus(item.getAuthorStatus());
|
||||
reblogger.setDate(item.getTimestamp());
|
||||
if (listener != null) {
|
||||
if (!fullText) {
|
||||
reblogger.setAuthorClickable(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -200,7 +201,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
||||
// TODO make author clickable #624
|
||||
|
||||
body.setText(c.getComment());
|
||||
if (listener == null) body.setTextIsSelectable(true);
|
||||
if (fullText) body.setTextIsSelectable(true);
|
||||
|
||||
commentContainer.addView(v);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ public class ReblogActivity extends BriarActivity implements
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setSceneTransitionAnimation();
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Intent intent = getIntent();
|
||||
byte[] groupId = intent.getByteArrayExtra(GROUP_ID);
|
||||
|
||||
@@ -161,7 +161,18 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
|
||||
private ViewHolder(View v) {
|
||||
scrollView = (ScrollView) v.findViewById(R.id.scrollView);
|
||||
progressBar = (ProgressBar) v.findViewById(R.id.progressBar);
|
||||
post = new BlogPostViewHolder(v.findViewById(R.id.postLayout));
|
||||
post = new BlogPostViewHolder(v.findViewById(R.id.postLayout),
|
||||
true, new OnBlogPostClickListener() {
|
||||
@Override
|
||||
public void onBlogPostClick(BlogPostItem post) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthorClick(BlogPostItem post) {
|
||||
// probably don't want to allow author clicks here
|
||||
}
|
||||
});
|
||||
input = (TextInputView) v.findViewById(R.id.inputText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,8 +186,8 @@ public class ConversationActivity extends BriarActivity
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle state) {
|
||||
super.onCreate(state);
|
||||
setSceneTransitionAnimation();
|
||||
super.onCreate(state);
|
||||
|
||||
Intent i = getIntent();
|
||||
int id = i.getIntExtra(CONTACT_ID, -1);
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.briar.android.forum;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
@@ -43,10 +44,10 @@ public class CreateForumActivity extends BriarActivity {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(CreateForumActivity.class.getName());
|
||||
|
||||
private TextInputLayout nameEntryLayout;
|
||||
private EditText nameEntry;
|
||||
private Button createForumButton;
|
||||
private ProgressBar progress;
|
||||
private TextView feedback;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject
|
||||
@@ -58,6 +59,8 @@ public class CreateForumActivity extends BriarActivity {
|
||||
|
||||
setContentView(R.layout.activity_create_forum);
|
||||
|
||||
nameEntryLayout =
|
||||
(TextInputLayout) findViewById(R.id.createForumNameLayout);
|
||||
nameEntry = (EditText) findViewById(R.id.createForumNameEntry);
|
||||
nameEntry.addTextChangedListener(new TextWatcher() {
|
||||
|
||||
@@ -85,8 +88,6 @@ public class CreateForumActivity extends BriarActivity {
|
||||
}
|
||||
});
|
||||
|
||||
feedback = (TextView) findViewById(R.id.createForumFeedback);
|
||||
|
||||
createForumButton = (Button) findViewById(R.id.createForumButton);
|
||||
createForumButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
@@ -118,10 +119,10 @@ public class CreateForumActivity extends BriarActivity {
|
||||
String name = nameEntry.getText().toString();
|
||||
int length = StringUtils.toUtf8(name).length;
|
||||
if (length > MAX_FORUM_NAME_LENGTH) {
|
||||
feedback.setText(R.string.name_too_long);
|
||||
nameEntryLayout.setError(getString(R.string.name_too_long));
|
||||
return false;
|
||||
}
|
||||
feedback.setText("");
|
||||
nameEntryLayout.setError(null);
|
||||
return length > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.briarproject.briar.android.threaded.ThreadItemAdapter;
|
||||
import org.briarproject.briar.android.threaded.ThreadListActivity;
|
||||
import org.briarproject.briar.android.threaded.ThreadListController;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
import org.briarproject.briar.api.forum.ForumPostHeader;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
@@ -41,7 +40,7 @@ import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_BOD
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ForumActivity extends
|
||||
ThreadListActivity<Forum, ThreadItemAdapter<ForumItem>, ForumItem, ForumPostHeader>
|
||||
ThreadListActivity<Forum, ForumItem, ThreadItemAdapter<ForumItem>>
|
||||
implements ForumListener {
|
||||
|
||||
@Inject
|
||||
@@ -53,7 +52,7 @@ public class ForumActivity extends
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThreadListController<Forum, ForumItem, ForumPostHeader> getController() {
|
||||
protected ThreadListController<Forum, ForumItem> getController() {
|
||||
return forumController;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,11 @@ import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.android.threaded.ThreadListController;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
import org.briarproject.briar.api.forum.ForumPostHeader;
|
||||
|
||||
@NotNullByDefault
|
||||
interface ForumController
|
||||
extends ThreadListController<Forum, ForumItem, ForumPostHeader> {
|
||||
interface ForumController extends ThreadListController<Forum, ForumItem> {
|
||||
|
||||
interface ForumListener extends ThreadListListener<ForumPostHeader> {
|
||||
interface ForumListener extends ThreadListListener<ForumItem> {
|
||||
@UiThread
|
||||
void onForumLeft(ContactId c);
|
||||
}
|
||||
|
||||
@@ -75,25 +75,25 @@ class ForumControllerImpl extends
|
||||
super.eventOccurred(e);
|
||||
|
||||
if (e instanceof ForumPostReceivedEvent) {
|
||||
ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
|
||||
if (pe.getGroupId().equals(getGroupId())) {
|
||||
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
|
||||
if (f.getGroupId().equals(getGroupId())) {
|
||||
LOG.info("Forum post received, adding...");
|
||||
onForumPostHeaderReceived(pe.getForumPostHeader());
|
||||
onForumPostReceived(f.getHeader(), f.getBody());
|
||||
}
|
||||
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
|
||||
ForumInvitationResponseReceivedEvent f =
|
||||
(ForumInvitationResponseReceivedEvent) e;
|
||||
ForumInvitationResponse r =
|
||||
(ForumInvitationResponse) f.getResponse();
|
||||
if (r.getGroupId().equals(getGroupId()) && r.wasAccepted()) {
|
||||
if (r.getShareableId().equals(getGroupId()) && r.wasAccepted()) {
|
||||
LOG.info("Forum invitation was accepted");
|
||||
onForumInvitationAccepted(r.getContactId());
|
||||
}
|
||||
} else if (e instanceof ContactLeftShareableEvent) {
|
||||
ContactLeftShareableEvent s = (ContactLeftShareableEvent) e;
|
||||
if (s.getGroupId().equals(getGroupId())) {
|
||||
ContactLeftShareableEvent c = (ContactLeftShareableEvent) e;
|
||||
if (c.getGroupId().equals(getGroupId())) {
|
||||
LOG.info("Forum left by contact");
|
||||
onForumLeft(s.getContactId());
|
||||
onForumLeft(c.getContactId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,11 +195,12 @@ class ForumControllerImpl extends
|
||||
return new ForumItem(header, body);
|
||||
}
|
||||
|
||||
private void onForumPostHeaderReceived(final ForumPostHeader h) {
|
||||
private void onForumPostReceived(ForumPostHeader h, String body) {
|
||||
final ForumItem item = buildItem(h, body);
|
||||
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onHeaderReceived(h);
|
||||
listener.onItemReceived(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ public class ForumListFragment extends BaseEventFragment implements
|
||||
} else if (e instanceof ForumPostReceivedEvent) {
|
||||
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
|
||||
LOG.info("Forum post added, updating item");
|
||||
updateItem(f.getGroupId(), f.getForumPostHeader());
|
||||
updateItem(f.getGroupId(), f.getHeader());
|
||||
} else if (e instanceof ForumInvitationRequestReceivedEvent) {
|
||||
LOG.info("Forum invitation received, reloading available forums");
|
||||
loadAvailableForums();
|
||||
|
||||
@@ -1,449 +0,0 @@
|
||||
package org.briarproject.briar.android.invitation;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.invitation.InvitationListener;
|
||||
import org.briarproject.bramble.api.invitation.InvitationState;
|
||||
import org.briarproject.bramble.api.invitation.InvitationTask;
|
||||
import org.briarproject.bramble.api.invitation.InvitationTaskFactory;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BriarActivity;
|
||||
import org.briarproject.briar.api.android.ReferenceManager;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH;
|
||||
import static org.briarproject.briar.android.invitation.ConfirmationCodeView.ConfirmationState.CONNECTED;
|
||||
import static org.briarproject.briar.android.invitation.ConfirmationCodeView.ConfirmationState.DETAILS;
|
||||
import static org.briarproject.briar.android.invitation.ConfirmationCodeView.ConfirmationState.WAIT_FOR_CONTACT;
|
||||
|
||||
public class AddContactActivity extends BriarActivity
|
||||
implements InvitationListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(AddContactActivity.class.getName());
|
||||
|
||||
@Inject
|
||||
CryptoComponent crypto;
|
||||
@Inject
|
||||
InvitationTaskFactory invitationTaskFactory;
|
||||
@Inject
|
||||
ReferenceManager referenceManager;
|
||||
|
||||
private AddContactView view = null;
|
||||
private InvitationTask task = null;
|
||||
private long taskHandle = -1;
|
||||
private AuthorId localAuthorId = null;
|
||||
private int localInvitationCode = -1, remoteInvitationCode = -1;
|
||||
private int localConfirmationCode = -1, remoteConfirmationCode = -1;
|
||||
private boolean connected = false, connectionFailed = false;
|
||||
private boolean localCompared = false, remoteCompared = false;
|
||||
private boolean localMatched = false, remoteMatched = false;
|
||||
private String contactName = null;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject
|
||||
volatile IdentityManager identityManager;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
if (state == null) {
|
||||
// This is a new activity
|
||||
setView(new ChooseIdentityView(this));
|
||||
} else {
|
||||
// Restore the activity's state
|
||||
byte[] b = state.getByteArray("briar.LOCAL_AUTHOR_ID");
|
||||
if (b != null) localAuthorId = new AuthorId(b);
|
||||
taskHandle = state.getLong("briar.TASK_HANDLE", -1);
|
||||
task = referenceManager.getReference(taskHandle,
|
||||
InvitationTask.class);
|
||||
|
||||
if (task == null) {
|
||||
// No background task - we must be in an initial or final state
|
||||
localInvitationCode = state.getInt("briar.LOCAL_CODE");
|
||||
remoteInvitationCode = state.getInt("briar.REMOTE_CODE");
|
||||
connectionFailed = state.getBoolean("briar.FAILED");
|
||||
contactName = state.getString("briar.CONTACT_NAME");
|
||||
if (contactName != null) {
|
||||
localCompared = remoteCompared = true;
|
||||
localMatched = remoteMatched = true;
|
||||
}
|
||||
// Set the appropriate view for the state
|
||||
if (localInvitationCode == -1) {
|
||||
setView(new ChooseIdentityView(this));
|
||||
} else if (remoteInvitationCode == -1) {
|
||||
setView(new InvitationCodeView(this));
|
||||
} else if (connectionFailed) {
|
||||
setView(new ErrorView(this, R.string.connection_failed,
|
||||
R.string.could_not_find_contact));
|
||||
} else if (contactName == null) {
|
||||
setView(new ErrorView(this, R.string.codes_do_not_match,
|
||||
R.string.interfering));
|
||||
} else {
|
||||
showToastAndFinish();
|
||||
}
|
||||
} else {
|
||||
// A background task exists - listen to it and get its state
|
||||
InvitationState s = task.addListener(this);
|
||||
localInvitationCode = s.getLocalInvitationCode();
|
||||
remoteInvitationCode = s.getRemoteInvitationCode();
|
||||
localConfirmationCode = s.getLocalConfirmationCode();
|
||||
remoteConfirmationCode = s.getRemoteConfirmationCode();
|
||||
connected = s.getConnected();
|
||||
connectionFailed = s.getConnectionFailed();
|
||||
localCompared = s.getLocalCompared();
|
||||
remoteCompared = s.getRemoteCompared();
|
||||
localMatched = s.getLocalMatched();
|
||||
remoteMatched = s.getRemoteMatched();
|
||||
contactName = s.getContactName();
|
||||
// Set the appropriate view for the state
|
||||
if (localInvitationCode == -1) {
|
||||
setView(new ChooseIdentityView(this));
|
||||
} else if (remoteInvitationCode == -1) {
|
||||
setView(new InvitationCodeView(this));
|
||||
} else if (connectionFailed) {
|
||||
setView(new ErrorView(AddContactActivity.this,
|
||||
R.string.connection_failed,
|
||||
R.string.could_not_find_contact));
|
||||
} else if (connected && localConfirmationCode == -1) {
|
||||
setView(new ConfirmationCodeView(this, CONNECTED));
|
||||
} else if (localConfirmationCode == -1) {
|
||||
setView(new InvitationCodeView(this, true));
|
||||
} else if (!localCompared) {
|
||||
setView(new ConfirmationCodeView(this));
|
||||
} else if (!remoteCompared) {
|
||||
setView(new ConfirmationCodeView(this, WAIT_FOR_CONTACT));
|
||||
} else if (localMatched && remoteMatched) {
|
||||
if (contactName == null) {
|
||||
setView(new ConfirmationCodeView(this, DETAILS));
|
||||
} else {
|
||||
showToastAndFinish();
|
||||
}
|
||||
} else {
|
||||
setView(new ErrorView(this, R.string.codes_do_not_match,
|
||||
R.string.interfering));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
private void showToastAndFinish() {
|
||||
String format = getString(R.string.contact_added_toast);
|
||||
String text = String.format(format, contactName);
|
||||
Toast.makeText(this, text, LENGTH_LONG).show();
|
||||
supportFinishAfterTransition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
view.populate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle state) {
|
||||
super.onSaveInstanceState(state);
|
||||
if (localAuthorId != null) {
|
||||
byte[] b = localAuthorId.getBytes();
|
||||
state.putByteArray("briar.LOCAL_AUTHOR_ID", b);
|
||||
}
|
||||
state.putInt("briar.LOCAL_CODE", localInvitationCode);
|
||||
state.putInt("briar.REMOTE_CODE", remoteInvitationCode);
|
||||
state.putBoolean("briar.FAILED", connectionFailed);
|
||||
state.putString("briar.CONTACT_NAME", contactName);
|
||||
if (task != null) state.putLong("briar.TASK_HANDLE", taskHandle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (task != null) task.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int request, int result, Intent data) {
|
||||
if (request == REQUEST_BLUETOOTH) {
|
||||
if (result != RESULT_CANCELED) reset(new InvitationCodeView(this));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
void setView(AddContactView view) {
|
||||
this.view = view;
|
||||
view.init(this);
|
||||
setContentView(view);
|
||||
getSupportActionBar().setTitle(R.string.add_contact_title);
|
||||
}
|
||||
|
||||
void reset(AddContactView view) {
|
||||
// Don't reset localAuthorId
|
||||
task = null;
|
||||
taskHandle = -1;
|
||||
localInvitationCode = -1;
|
||||
localConfirmationCode = remoteConfirmationCode = -1;
|
||||
connected = connectionFailed = false;
|
||||
localCompared = remoteCompared = false;
|
||||
localMatched = remoteMatched = false;
|
||||
contactName = null;
|
||||
setView(view);
|
||||
}
|
||||
|
||||
void loadLocalAuthor() {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading author took " + duration + " ms");
|
||||
setLocalAuthorId(author.getId());
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setLocalAuthorId(final AuthorId localAuthorId) {
|
||||
runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
AddContactActivity.this.localAuthorId = localAuthorId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int getLocalInvitationCode() {
|
||||
if (localInvitationCode == -1)
|
||||
localInvitationCode = crypto.generateBTInvitationCode();
|
||||
return localInvitationCode;
|
||||
}
|
||||
|
||||
int getRemoteInvitationCode() {
|
||||
return remoteInvitationCode;
|
||||
}
|
||||
|
||||
void remoteInvitationCodeEntered(int code) {
|
||||
if (localAuthorId == null) throw new IllegalStateException();
|
||||
if (localInvitationCode == -1) throw new IllegalStateException();
|
||||
remoteInvitationCode = code;
|
||||
|
||||
// change UI to show a progress indicator
|
||||
setView(new InvitationCodeView(this, true));
|
||||
|
||||
task = invitationTaskFactory.createTask(localInvitationCode, code);
|
||||
taskHandle = referenceManager.putReference(task, InvitationTask.class);
|
||||
task.addListener(AddContactActivity.this);
|
||||
// Add a second listener so we can remove the first in onDestroy(),
|
||||
// allowing the activity to be garbage collected if it's destroyed
|
||||
task.addListener(new ReferenceCleaner(referenceManager, taskHandle));
|
||||
task.connect();
|
||||
}
|
||||
|
||||
int getLocalConfirmationCode() {
|
||||
return localConfirmationCode;
|
||||
}
|
||||
|
||||
void remoteConfirmationCodeEntered(int code) {
|
||||
localCompared = true;
|
||||
if (code == remoteConfirmationCode) {
|
||||
localMatched = true;
|
||||
if (remoteMatched) {
|
||||
setView(new ConfirmationCodeView(this, DETAILS));
|
||||
} else if (remoteCompared) {
|
||||
setView(new ErrorView(this, R.string.codes_do_not_match,
|
||||
R.string.interfering));
|
||||
} else {
|
||||
setView(new ConfirmationCodeView(this, WAIT_FOR_CONTACT));
|
||||
}
|
||||
task.localConfirmationSucceeded();
|
||||
} else {
|
||||
localMatched = false;
|
||||
setView(new ErrorView(this, R.string.codes_do_not_match,
|
||||
R.string.interfering));
|
||||
task.localConfirmationFailed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionSucceeded() {
|
||||
runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
connected = true;
|
||||
setView(new ConfirmationCodeView(AddContactActivity.this,
|
||||
CONNECTED));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionFailed() {
|
||||
runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
connectionFailed = true;
|
||||
setView(new ErrorView(AddContactActivity.this,
|
||||
R.string.connection_failed,
|
||||
R.string.could_not_find_contact));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyAgreementSucceeded(final int localCode,
|
||||
final int remoteCode) {
|
||||
runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
localConfirmationCode = localCode;
|
||||
remoteConfirmationCode = remoteCode;
|
||||
setView(new ConfirmationCodeView(AddContactActivity.this));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyAgreementFailed() {
|
||||
runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
connectionFailed = true;
|
||||
setView(new ErrorView(AddContactActivity.this,
|
||||
R.string.connection_failed,
|
||||
R.string.could_not_find_contact));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteConfirmationSucceeded() {
|
||||
runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
remoteCompared = true;
|
||||
remoteMatched = true;
|
||||
if (localMatched) {
|
||||
setView(new ConfirmationCodeView(AddContactActivity.this,
|
||||
DETAILS));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteConfirmationFailed() {
|
||||
runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
remoteCompared = true;
|
||||
remoteMatched = false;
|
||||
if (localMatched) {
|
||||
setView(new ErrorView(AddContactActivity.this,
|
||||
R.string.codes_do_not_match, R.string.interfering));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pseudonymExchangeSucceeded(final String remoteName) {
|
||||
runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
contactName = remoteName;
|
||||
showToastAndFinish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pseudonymExchangeFailed() {
|
||||
runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setView(new ErrorView(AddContactActivity.this,
|
||||
R.string.connection_failed,
|
||||
R.string.could_not_find_contact));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the reference to the invitation task when the task completes.
|
||||
* This class is static to prevent memory leaks.
|
||||
*/
|
||||
private static class ReferenceCleaner implements InvitationListener {
|
||||
|
||||
private final ReferenceManager referenceManager;
|
||||
private final long handle;
|
||||
|
||||
private ReferenceCleaner(ReferenceManager referenceManager,
|
||||
long handle) {
|
||||
this.referenceManager = referenceManager;
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionSucceeded() {
|
||||
// Wait for key agreement to succeed or fail
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionFailed() {
|
||||
referenceManager.removeReference(handle, InvitationTask.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyAgreementSucceeded(int localCode, int remoteCode) {
|
||||
// Wait for remote confirmation to succeed or fail
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyAgreementFailed() {
|
||||
referenceManager.removeReference(handle, InvitationTask.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteConfirmationSucceeded() {
|
||||
// Wait for the pseudonym exchange to succeed or fail
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteConfirmationFailed() {
|
||||
referenceManager.removeReference(handle, InvitationTask.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pseudonymExchangeSucceeded(String remoteName) {
|
||||
referenceManager.removeReference(handle, InvitationTask.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pseudonymExchangeFailed() {
|
||||
referenceManager.removeReference(handle, InvitationTask.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.briarproject.briar.android.invitation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
abstract class AddContactView extends LinearLayout {
|
||||
|
||||
static final public int CODE_LEN = 6;
|
||||
protected AddContactActivity container = null;
|
||||
|
||||
AddContactView(Context ctx) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
void init(AddContactActivity container) {
|
||||
this.container = container;
|
||||
populate();
|
||||
}
|
||||
|
||||
abstract void populate();
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package org.briarproject.briar.android.invitation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH;
|
||||
|
||||
class ChooseIdentityView extends AddContactView implements OnClickListener {
|
||||
|
||||
ChooseIdentityView(Context ctx) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
void populate() {
|
||||
removeAllViews();
|
||||
Context ctx = getContext();
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService
|
||||
(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View view = inflater.inflate(R.layout.invitation_bluetooth_start, this);
|
||||
|
||||
Button continueButton = (Button) view.findViewById(R.id.continueButton);
|
||||
continueButton.setOnClickListener(this);
|
||||
|
||||
container.loadLocalAuthor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
|
||||
i.putExtra(EXTRA_DISCOVERABLE_DURATION, 120);
|
||||
container.startActivityForResult(i, REQUEST_BLUETOOTH);
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package org.briarproject.briar.android.invitation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import static android.content.Context.INPUT_METHOD_SERVICE;
|
||||
|
||||
class ConfirmationCodeView extends AddContactView {
|
||||
|
||||
public enum ConfirmationState { CONNECTED, ENTER_CODE, WAIT_FOR_CONTACT, DETAILS }
|
||||
private ConfirmationState state;
|
||||
|
||||
ConfirmationCodeView(Context ctx) {
|
||||
super(ctx);
|
||||
this.state = ConfirmationState.ENTER_CODE;
|
||||
}
|
||||
|
||||
ConfirmationCodeView(Context ctx, ConfirmationState state) {
|
||||
super(ctx);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
void populate() {
|
||||
removeAllViews();
|
||||
Context ctx = getContext();
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService
|
||||
(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View view = inflater.inflate(R.layout.invitation_bluetooth_confirmation_code, this);
|
||||
|
||||
// local confirmation code
|
||||
TextView code = (TextView) view.findViewById(R.id.codeView);
|
||||
int localCode = container.getLocalConfirmationCode();
|
||||
code.setText(String.format("%06d", localCode));
|
||||
|
||||
if (state != ConfirmationState.ENTER_CODE) {
|
||||
// hide views we no longer need
|
||||
view.findViewById(R.id.enterCodeTextView).setVisibility(View.GONE);
|
||||
view.findViewById(R.id.codeEntryView).setVisibility(View.GONE);
|
||||
view.findViewById(R.id.continueButton).setVisibility(View.GONE);
|
||||
|
||||
// show progress indicator
|
||||
view.findViewById(R.id.progressBar).setVisibility(View.VISIBLE);
|
||||
|
||||
// show what we are waiting for
|
||||
TextView connecting = (TextView) view.findViewById(R.id.waitingView);
|
||||
int textId;
|
||||
if (state == ConfirmationState.CONNECTED) {
|
||||
textId = R.string.calculating_confirmation_code;
|
||||
view.findViewById(R.id.yourConfirmationCodeView).setVisibility(View.GONE);
|
||||
view.findViewById(R.id.codeView).setVisibility(View.GONE);
|
||||
} else if (state == ConfirmationState.WAIT_FOR_CONTACT) {
|
||||
textId = R.string.waiting_for_contact;
|
||||
} else {
|
||||
textId = R.string.exchanging_contact_details;
|
||||
}
|
||||
connecting.setText(ctx.getString(textId));
|
||||
connecting.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
// handle click on continue button
|
||||
final EditText codeEntry = (EditText) view.findViewById(R.id.codeEntryView);
|
||||
final Button continueButton = (Button) view.findViewById(R.id.continueButton);
|
||||
continueButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
send(codeEntry);
|
||||
}
|
||||
});
|
||||
|
||||
// activate continue button only when we have a 6 digit (CODE_LEN) code
|
||||
codeEntry.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
continueButton.setEnabled(codeEntry.getText().length() == CODE_LEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
codeEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (actionId == EditorInfo.IME_ACTION_GO && v.getText().length() == CODE_LEN) {
|
||||
send(v);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void send(TextView codeEntry) {
|
||||
int code = Integer.parseInt(codeEntry.getText().toString());
|
||||
container.remoteConfirmationCodeEntered(code);
|
||||
|
||||
// Hide the soft keyboard
|
||||
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
|
||||
((InputMethodManager) o).hideSoftInputFromWindow(codeEntry.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.briarproject.briar.android.invitation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH;
|
||||
|
||||
class ErrorView extends AddContactView implements OnClickListener {
|
||||
|
||||
private final int error;
|
||||
private final int explanation;
|
||||
|
||||
ErrorView(Context ctx) {
|
||||
super(ctx);
|
||||
this.error = R.string.connection_failed;
|
||||
this.explanation = R.string.could_not_find_contact;
|
||||
}
|
||||
|
||||
ErrorView(Context ctx, int error, int explanation) {
|
||||
super(ctx);
|
||||
this.error = error;
|
||||
this.explanation = explanation;
|
||||
}
|
||||
|
||||
@Override
|
||||
void populate() {
|
||||
removeAllViews();
|
||||
Context ctx = getContext();
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService
|
||||
(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View view = inflater.inflate(R.layout.invitation_error, this);
|
||||
|
||||
TextView errorView = (TextView) view.findViewById(R.id.errorTextView);
|
||||
errorView.setText(ctx.getString(error));
|
||||
|
||||
TextView explanationView = (TextView) view.findViewById(R.id.explanationTextView);
|
||||
explanationView.setText(ctx.getString(explanation));
|
||||
|
||||
Button tryAgainButton = (Button) view.findViewById(R.id.tryAgainButton);
|
||||
tryAgainButton.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
|
||||
i.putExtra(EXTRA_DISCOVERABLE_DURATION, 120);
|
||||
container.startActivityForResult(i, REQUEST_BLUETOOTH);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package org.briarproject.briar.android.invitation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import static android.content.Context.INPUT_METHOD_SERVICE;
|
||||
|
||||
class InvitationCodeView extends AddContactView {
|
||||
|
||||
private boolean waiting;
|
||||
|
||||
InvitationCodeView(Context ctx, boolean waiting) {
|
||||
super(ctx);
|
||||
this.waiting = waiting;
|
||||
}
|
||||
|
||||
InvitationCodeView(Context ctx) {
|
||||
this(ctx, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
void populate() {
|
||||
removeAllViews();
|
||||
Context ctx = getContext();
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService
|
||||
(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View view = inflater.inflate(R.layout.invitation_bluetooth_invitation_code, this);
|
||||
|
||||
// local invitation code
|
||||
TextView code = (TextView) view.findViewById(R.id.codeView);
|
||||
int localCode = container.getLocalInvitationCode();
|
||||
code.setText(String.format("%06d", localCode));
|
||||
|
||||
if (waiting) {
|
||||
// hide views we no longer need
|
||||
view.findViewById(R.id.enterCodeTextView).setVisibility(View.GONE);
|
||||
view.findViewById(R.id.codeEntryView).setVisibility(View.GONE);
|
||||
view.findViewById(R.id.continueButton).setVisibility(View.GONE);
|
||||
|
||||
// show progress indicator
|
||||
view.findViewById(R.id.progressBar).setVisibility(View.VISIBLE);
|
||||
|
||||
// show which code we are waiting for
|
||||
TextView connecting = (TextView) view.findViewById(R.id.waitingView);
|
||||
int remoteCode = container.getRemoteInvitationCode();
|
||||
String format = container.getString(R.string.searching_format);
|
||||
connecting.setText(String.format(format, remoteCode));
|
||||
connecting.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
// handle click on continue button
|
||||
final EditText codeEntry = (EditText) view.findViewById(R.id.codeEntryView);
|
||||
final Button continueButton = (Button) view.findViewById(R.id.continueButton);
|
||||
continueButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
send(codeEntry);
|
||||
}
|
||||
});
|
||||
|
||||
// activate continue button only when we have a 6 digit (CODE_LEN) code
|
||||
codeEntry.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
continueButton.setEnabled(codeEntry.getText().length() == CODE_LEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
codeEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (actionId == EditorInfo.IME_ACTION_GO && v.getText().length() == CODE_LEN) {
|
||||
send(v);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void send(TextView codeEntry) {
|
||||
int code = Integer.parseInt(codeEntry.getText().toString());
|
||||
container.remoteInvitationCodeEntered(code);
|
||||
|
||||
// Hide the soft keyboard
|
||||
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
|
||||
((InputMethodManager) o).hideSoftInputFromWindow(codeEntry.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.briarproject.briar.android.keyagreement;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class CameraException extends IOException {
|
||||
|
||||
CameraException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
CameraException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.hardware.Camera.CameraInfo;
|
||||
import android.hardware.Camera.Parameters;
|
||||
import android.hardware.Camera.Size;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Surface;
|
||||
@@ -43,6 +44,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(CameraView.class.getName());
|
||||
|
||||
@Nullable
|
||||
private Camera camera = null;
|
||||
private PreviewConsumer previewConsumer = null;
|
||||
private Surface surface = null;
|
||||
@@ -82,14 +84,14 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void start() {
|
||||
public void start() throws CameraException {
|
||||
LOG.info("Opening camera");
|
||||
try {
|
||||
LOG.info("Opening camera");
|
||||
camera = Camera.open();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.log(WARNING, "Error opening camera", e);
|
||||
return;
|
||||
throw new CameraException(e);
|
||||
}
|
||||
if (camera == null) throw new CameraException("No back-facing camera");
|
||||
setDisplayOrientation(0);
|
||||
// Use barcode scene mode if it's available
|
||||
Parameters params = camera.getParameters();
|
||||
@@ -113,60 +115,81 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void stop() {
|
||||
public void stop() throws CameraException {
|
||||
if (camera == null) return;
|
||||
stopPreview();
|
||||
LOG.info("Releasing camera");
|
||||
try {
|
||||
LOG.info("Releasing camera");
|
||||
camera.release();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.log(WARNING, "Error releasing camera", e);
|
||||
throw new CameraException(e);
|
||||
}
|
||||
camera = null;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void startPreview(SurfaceHolder holder) {
|
||||
private void startPreview(SurfaceHolder holder) throws CameraException {
|
||||
LOG.info("Starting preview");
|
||||
if (camera == null) throw new CameraException("Camera is null");
|
||||
try {
|
||||
camera.setPreviewDisplay(holder);
|
||||
camera.startPreview();
|
||||
previewStarted = true;
|
||||
startConsumer();
|
||||
} catch (IOException | RuntimeException e) {
|
||||
LOG.log(WARNING, "Error starting camera preview", e);
|
||||
throw new CameraException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void stopPreview() {
|
||||
private void stopPreview() throws CameraException {
|
||||
LOG.info("Stopping preview");
|
||||
if (camera == null) throw new CameraException("Camera is null");
|
||||
try {
|
||||
stopConsumer();
|
||||
camera.stopPreview();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.log(WARNING, "Error stopping camera preview", e);
|
||||
throw new CameraException(e);
|
||||
}
|
||||
previewStarted = false;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void startConsumer() {
|
||||
if (autoFocus) camera.autoFocus(this);
|
||||
private void startConsumer() throws CameraException {
|
||||
if (camera == null) throw new CameraException("Camera is null");
|
||||
if (autoFocus) {
|
||||
try {
|
||||
camera.autoFocus(this);
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
}
|
||||
previewConsumer.start(camera);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void stopConsumer() {
|
||||
if (autoFocus) camera.cancelAutoFocus();
|
||||
private void stopConsumer() throws CameraException {
|
||||
if (camera == null) throw new CameraException("Camera is null");
|
||||
if (autoFocus) {
|
||||
try {
|
||||
camera.cancelAutoFocus();
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
}
|
||||
previewConsumer.stop();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void setDisplayOrientation(int rotationDegrees) {
|
||||
private void setDisplayOrientation(int rotationDegrees)
|
||||
throws CameraException {
|
||||
int orientation;
|
||||
CameraInfo info = new CameraInfo();
|
||||
Camera.getCameraInfo(0, info);
|
||||
try {
|
||||
Camera.getCameraInfo(0, info);
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
if (info.facing == CAMERA_FACING_FRONT) {
|
||||
orientation = (info.orientation + rotationDegrees) % 360;
|
||||
orientation = (360 - orientation) % 360;
|
||||
@@ -175,49 +198,70 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
}
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Display orientation " + orientation + " degrees");
|
||||
if (camera == null) throw new CameraException("Camera is null");
|
||||
try {
|
||||
camera.setDisplayOrientation(orientation);
|
||||
} catch (RuntimeException e) {
|
||||
LOG.log(WARNING, "Error setting display orientation", e);
|
||||
throw new CameraException(e);
|
||||
}
|
||||
displayOrientation = orientation;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private Parameters setSceneMode(Camera camera, Parameters params) {
|
||||
private Parameters setSceneMode(Camera camera, Parameters params)
|
||||
throws CameraException {
|
||||
List<String> sceneModes = params.getSupportedSceneModes();
|
||||
if (sceneModes == null) return params;
|
||||
if (LOG.isLoggable(INFO)) LOG.info("Scene modes: " + sceneModes);
|
||||
if (sceneModes.contains(SCENE_MODE_BARCODE)) {
|
||||
params.setSceneMode(SCENE_MODE_BARCODE);
|
||||
camera.setParameters(params);
|
||||
return camera.getParameters();
|
||||
try {
|
||||
camera.setParameters(params);
|
||||
return camera.getParameters();
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private Parameters disableFlash(Camera camera, Parameters params) {
|
||||
private Parameters disableFlash(Camera camera, Parameters params)
|
||||
throws CameraException {
|
||||
params.setFlashMode(FLASH_MODE_OFF);
|
||||
camera.setParameters(params);
|
||||
return camera.getParameters();
|
||||
try {
|
||||
camera.setParameters(params);
|
||||
return camera.getParameters();
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private Parameters disableSceneMode(Camera camera, Parameters params) {
|
||||
private Parameters disableSceneMode(Camera camera, Parameters params)
|
||||
throws CameraException {
|
||||
params.setSceneMode(SCENE_MODE_AUTO);
|
||||
camera.setParameters(params);
|
||||
return camera.getParameters();
|
||||
try {
|
||||
camera.setParameters(params);
|
||||
return camera.getParameters();
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private Parameters setBestParameters(Camera camera, Parameters params) {
|
||||
private Parameters setBestParameters(Camera camera, Parameters params)
|
||||
throws CameraException {
|
||||
setVideoStabilisation(params);
|
||||
setFocusMode(params);
|
||||
params.setFlashMode(FLASH_MODE_OFF);
|
||||
setPreviewSize(params);
|
||||
camera.setParameters(params);
|
||||
return camera.getParameters();
|
||||
try {
|
||||
camera.setParameters(params);
|
||||
return camera.getParameters();
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@@ -286,9 +330,15 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void logCameraParameters() {
|
||||
private void logCameraParameters() throws CameraException {
|
||||
if (camera == null) throw new AssertionError();
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
Parameters params = camera.getParameters();
|
||||
Parameters params;
|
||||
try {
|
||||
params = camera.getParameters();
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 15) {
|
||||
LOG.info("Video stabilisation enabled: "
|
||||
+ params.getVideoStabilization());
|
||||
@@ -306,13 +356,18 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
surfaceCreatedUi(holder);
|
||||
try {
|
||||
surfaceCreatedUi(holder);
|
||||
} catch (CameraException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void surfaceCreatedUi(SurfaceHolder holder) {
|
||||
private void surfaceCreatedUi(SurfaceHolder holder) throws CameraException {
|
||||
LOG.info("Surface created");
|
||||
if (surface != null && surface != holder.getSurface()) {
|
||||
LOG.info("Releasing old surface");
|
||||
@@ -329,13 +384,19 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
surfaceChangedUi(holder, w, h);
|
||||
try {
|
||||
surfaceChangedUi(holder, w, h);
|
||||
} catch (CameraException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void surfaceChangedUi(SurfaceHolder holder, int w, int h) {
|
||||
private void surfaceChangedUi(SurfaceHolder holder, int w, int h)
|
||||
throws CameraException {
|
||||
if (LOG.isLoggable(INFO)) LOG.info("Surface changed: " + w + "x" + h);
|
||||
if (surface != null && surface != holder.getSurface()) {
|
||||
LOG.info("Releasing old surface");
|
||||
@@ -352,7 +413,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
camera.setParameters(params);
|
||||
logCameraParameters();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.log(WARNING, "Error setting preview size", e);
|
||||
throw new CameraException(e);
|
||||
}
|
||||
startPreview(holder);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@MethodsNotNullByDefault
|
||||
@@ -60,8 +61,12 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
||||
@Override
|
||||
public void onPreviewFrame(byte[] data, Camera camera) {
|
||||
if (camera == this.camera) {
|
||||
Size size = camera.getParameters().getPreviewSize();
|
||||
new DecoderTask(data, size.width, size.height).execute();
|
||||
try {
|
||||
Size size = camera.getParameters().getPreviewSize();
|
||||
new DecoderTask(data, size.width, size.height).execute();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.log(WARNING, "Error getting camera parameters.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +75,7 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
||||
private final byte[] data;
|
||||
private final int width, height;
|
||||
|
||||
DecoderTask(byte[] data, int width, int height) {
|
||||
private DecoderTask(byte[] data, int width, int height) {
|
||||
this.data = data;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
@@ -56,6 +56,7 @@ import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
@@ -143,6 +144,12 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
||||
try {
|
||||
cameraView.start();
|
||||
} catch (CameraException e) {
|
||||
logCameraExceptionAndFinish(e);
|
||||
}
|
||||
|
||||
// Listen for changes to the Bluetooth state
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_STATE_CHANGED);
|
||||
@@ -162,7 +169,6 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
} else {
|
||||
startListening();
|
||||
}
|
||||
cameraView.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -170,7 +176,19 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
super.onStop();
|
||||
stopListening();
|
||||
if (receiver != null) getActivity().unregisterReceiver(receiver);
|
||||
cameraView.stop();
|
||||
try {
|
||||
cameraView.stop();
|
||||
} catch (CameraException e) {
|
||||
logCameraExceptionAndFinish(e);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void logCameraExceptionAndFinish(CameraException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
Toast.makeText(getActivity(), R.string.camera_error,
|
||||
LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@@ -218,7 +236,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
statusView.setVisibility(VISIBLE);
|
||||
status.setText(R.string.connecting_to_device);
|
||||
task.connectAndRunProtocol(remotePayload);
|
||||
} catch (IOException e) {
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
// TODO show failure
|
||||
Toast.makeText(getActivity(), R.string.qr_code_invalid,
|
||||
LENGTH_LONG).show();
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListAct
|
||||
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
|
||||
import org.briarproject.briar.android.threaded.ThreadListActivity;
|
||||
import org.briarproject.briar.android.threaded.ThreadListController;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.briar.api.privategroup.Visibility;
|
||||
|
||||
@@ -44,7 +43,7 @@ import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class GroupActivity extends
|
||||
ThreadListActivity<PrivateGroup, GroupMessageAdapter, GroupMessageItem, GroupMessageHeader>
|
||||
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageAdapter>
|
||||
implements GroupListener, OnClickListener {
|
||||
|
||||
@Inject
|
||||
@@ -60,7 +59,7 @@ public class GroupActivity extends
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThreadListController<PrivateGroup, GroupMessageItem, GroupMessageHeader> getController() {
|
||||
protected ThreadListController<PrivateGroup, GroupMessageItem> getController() {
|
||||
return controller;
|
||||
}
|
||||
|
||||
@@ -276,7 +275,7 @@ public class GroupActivity extends
|
||||
public void onGroupDissolved() {
|
||||
setGroupEnabled(false);
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
|
||||
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
|
||||
builder.setTitle(getString(R.string.groups_dissolved_dialog_title));
|
||||
builder.setMessage(getString(R.string.groups_dissolved_dialog_message));
|
||||
builder.setNeutralButton(R.string.ok, null);
|
||||
|
||||
@@ -8,13 +8,11 @@ import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.briar.android.threaded.ThreadListController;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.briar.api.privategroup.Visibility;
|
||||
|
||||
public interface GroupController
|
||||
extends
|
||||
ThreadListController<PrivateGroup, GroupMessageItem, GroupMessageHeader> {
|
||||
extends ThreadListController<PrivateGroup, GroupMessageItem> {
|
||||
|
||||
void loadLocalAuthor(
|
||||
ResultExceptionHandler<LocalAuthor, DbException> handler);
|
||||
@@ -22,7 +20,8 @@ public interface GroupController
|
||||
void isDissolved(
|
||||
ResultExceptionHandler<Boolean, DbException> handler);
|
||||
|
||||
interface GroupListener extends ThreadListListener<GroupMessageHeader> {
|
||||
interface GroupListener extends ThreadListListener<GroupMessageItem> {
|
||||
|
||||
@UiThread
|
||||
void onContactRelationshipRevealed(AuthorId memberId,
|
||||
ContactId contactId, Visibility v);
|
||||
|
||||
@@ -80,14 +80,15 @@ class GroupControllerImpl extends
|
||||
super.eventOccurred(e);
|
||||
|
||||
if (e instanceof GroupMessageAddedEvent) {
|
||||
GroupMessageAddedEvent gmae = (GroupMessageAddedEvent) e;
|
||||
if (!gmae.isLocal() && gmae.getGroupId().equals(getGroupId())) {
|
||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||
if (!g.isLocal() && g.getGroupId().equals(getGroupId())) {
|
||||
LOG.info("Group message received, adding...");
|
||||
final GroupMessageHeader h = gmae.getHeader();
|
||||
final GroupMessageItem item =
|
||||
buildItem(g.getHeader(), g.getBody());
|
||||
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onHeaderReceived(h);
|
||||
listener.onItemReceived(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -108,7 +109,7 @@ class GroupControllerImpl extends
|
||||
(GroupInvitationResponseReceivedEvent) e;
|
||||
final GroupInvitationResponse r =
|
||||
(GroupInvitationResponse) g.getResponse();
|
||||
if (getGroupId().equals(r.getGroupId()) && r.wasAccepted()) {
|
||||
if (getGroupId().equals(r.getShareableId()) && r.wasAccepted()) {
|
||||
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.briar.android.privategroup.creation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
@@ -11,6 +12,7 @@ import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
|
||||
@@ -19,6 +21,8 @@ import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
|
||||
|
||||
public class CreateGroupFragment extends BaseFragment {
|
||||
@@ -28,7 +32,8 @@ public class CreateGroupFragment extends BaseFragment {
|
||||
private CreateGroupListener listener;
|
||||
private EditText nameEntry;
|
||||
private Button createGroupButton;
|
||||
private TextView feedback;
|
||||
private TextInputLayout nameLayout;
|
||||
private ProgressBar progress;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
@@ -69,7 +74,7 @@ public class CreateGroupFragment extends BaseFragment {
|
||||
}
|
||||
});
|
||||
|
||||
feedback = (TextView) v.findViewById(R.id.feedback);
|
||||
nameLayout = (TextInputLayout) v.findViewById(R.id.nameLayout);
|
||||
|
||||
createGroupButton = (Button) v.findViewById(R.id.button);
|
||||
createGroupButton.setOnClickListener(new OnClickListener() {
|
||||
@@ -79,6 +84,8 @@ public class CreateGroupFragment extends BaseFragment {
|
||||
}
|
||||
});
|
||||
|
||||
progress = (ProgressBar) v.findViewById(R.id.progressBar);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -107,16 +114,18 @@ public class CreateGroupFragment extends BaseFragment {
|
||||
String name = nameEntry.getText().toString();
|
||||
int length = StringUtils.toUtf8(name).length;
|
||||
if (length > MAX_GROUP_NAME_LENGTH) {
|
||||
feedback.setText(R.string.name_too_long);
|
||||
nameLayout.setError(getString(R.string.name_too_long));
|
||||
return false;
|
||||
}
|
||||
feedback.setText("");
|
||||
nameLayout.setError(null);
|
||||
return length > 0;
|
||||
}
|
||||
|
||||
private void createGroup() {
|
||||
if (!validateName()) return;
|
||||
listener.hideSoftKeyboard(nameEntry);
|
||||
createGroupButton.setVisibility(GONE);
|
||||
progress.setVisibility(VISIBLE);
|
||||
listener.onGroupNameChosen(nameEntry.getText().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.support.v7.preference.PreferenceFragmentCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
@@ -46,6 +47,7 @@ import static android.media.RingtoneManager.EXTRA_RINGTONE_TITLE;
|
||||
import static android.media.RingtoneManager.EXTRA_RINGTONE_TYPE;
|
||||
import static android.media.RingtoneManager.TYPE_NOTIFICATION;
|
||||
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_BT_ENABLE;
|
||||
@@ -400,10 +402,15 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
} else {
|
||||
// The user chose a ringtone other than the default
|
||||
Ringtone r = RingtoneManager.getRingtone(getContext(), uri);
|
||||
String name = r.getTitle(getContext());
|
||||
s.putBoolean(PREF_NOTIFY_SOUND, true);
|
||||
s.put(PREF_NOTIFY_RINGTONE_NAME, name);
|
||||
s.put(PREF_NOTIFY_RINGTONE_URI, uri.toString());
|
||||
if (r == null) {
|
||||
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
|
||||
LENGTH_SHORT).show();
|
||||
} else {
|
||||
String name = r.getTitle(getContext());
|
||||
s.putBoolean(PREF_NOTIFY_SOUND, true);
|
||||
s.put(PREF_NOTIFY_RINGTONE_NAME, name);
|
||||
s.put(PREF_NOTIFY_RINGTONE_URI, uri.toString());
|
||||
}
|
||||
}
|
||||
storeSettings(s);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.StrictMode;
|
||||
import android.os.StrictMode.ThreadPolicy;
|
||||
import android.os.StrictMode.VmPolicy;
|
||||
import android.support.v7.preference.PreferenceManager;
|
||||
import android.transition.Fade;
|
||||
|
||||
@@ -23,8 +20,6 @@ import java.util.logging.Logger;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.briar.android.BriarApplication.EXPIRY_DATE;
|
||||
import static org.briarproject.briar.android.TestingConstants.DEFAULT_LOG_LEVEL;
|
||||
import static org.briarproject.briar.android.TestingConstants.TESTING;
|
||||
|
||||
public class SplashScreenActivity extends BaseActivity {
|
||||
|
||||
@@ -36,11 +31,6 @@ public class SplashScreenActivity extends BaseActivity {
|
||||
@Inject
|
||||
protected AndroidExecutor androidExecutor;
|
||||
|
||||
public SplashScreenActivity() {
|
||||
Logger.getLogger("").setLevel(DEFAULT_LOG_LEVEL);
|
||||
enableStrictMode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
@@ -81,19 +71,6 @@ public class SplashScreenActivity extends BaseActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void enableStrictMode() {
|
||||
if (TESTING) {
|
||||
ThreadPolicy.Builder threadPolicy = new ThreadPolicy.Builder();
|
||||
threadPolicy.detectAll();
|
||||
threadPolicy.penaltyLog();
|
||||
StrictMode.setThreadPolicy(threadPolicy.build());
|
||||
VmPolicy.Builder vmPolicy = new VmPolicy.Builder();
|
||||
vmPolicy.detectAll();
|
||||
vmPolicy.penaltyLog();
|
||||
StrictMode.setVmPolicy(vmPolicy.build());
|
||||
}
|
||||
}
|
||||
|
||||
private void setPreferencesDefaults() {
|
||||
androidExecutor.runOnBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
|
||||
@@ -3,7 +3,9 @@ package org.briarproject.briar.android.threaded;
|
||||
import android.support.annotation.UiThread;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.api.client.MessageTree;
|
||||
import org.briarproject.briar.api.client.MessageTree.MessageNode;
|
||||
import org.briarproject.briar.client.MessageTreeImpl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -13,8 +15,7 @@ import java.util.List;
|
||||
|
||||
@UiThread
|
||||
@NotNullByDefault
|
||||
public class NestedTreeList<T extends MessageTree.MessageNode>
|
||||
implements Iterable<T> {
|
||||
public class NestedTreeList<T extends MessageNode> implements Iterable<T> {
|
||||
|
||||
private final MessageTree<T> tree = new MessageTreeImpl<>();
|
||||
private List<T> depthFirstCollection = new ArrayList<>();
|
||||
@@ -38,14 +39,14 @@ public class NestedTreeList<T extends MessageTree.MessageNode>
|
||||
return depthFirstCollection.get(index);
|
||||
}
|
||||
|
||||
public int indexOf(T elem) {
|
||||
return depthFirstCollection.indexOf(elem);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return depthFirstCollection.size();
|
||||
}
|
||||
|
||||
public boolean contains(MessageId m) {
|
||||
return tree.contains(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return depthFirstCollection.iterator();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.briarproject.briar.android.threaded;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
@@ -28,7 +27,6 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
||||
protected final NestedTreeList<I> items = new NestedTreeList<>();
|
||||
private final ThreadItemListener<I> listener;
|
||||
private final LinearLayoutManager layoutManager;
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
private volatile int revision = 0;
|
||||
|
||||
@@ -104,6 +102,10 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
||||
return NO_POSITION; // Not found
|
||||
}
|
||||
|
||||
boolean contains(MessageId m) {
|
||||
return items.contains(m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights the item with the given {@link MessageId}
|
||||
* and disables the highlight for a previously highlighted item, if any.
|
||||
@@ -184,6 +186,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
||||
}
|
||||
|
||||
static class UnreadCount {
|
||||
|
||||
final int top, bottom;
|
||||
|
||||
private UnreadCount(int top, int bottom) {
|
||||
@@ -193,6 +196,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
||||
}
|
||||
|
||||
public interface ThreadItemListener<I> {
|
||||
|
||||
void onUnreadItemVisible(I item);
|
||||
|
||||
void onReplyClick(I item);
|
||||
|
||||
@@ -16,6 +16,7 @@ public class ThreadItemListImpl<I extends ThreadItem> extends ArrayList<I>
|
||||
return bottomVisibleItemId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFirstVisibleId(@Nullable MessageId bottomVisibleItemId) {
|
||||
this.bottomVisibleItemId = bottomVisibleItemId;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import org.briarproject.briar.android.view.TextInputView;
|
||||
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
|
||||
import org.briarproject.briar.android.view.UnreadMessageButton;
|
||||
import org.briarproject.briar.api.client.NamedGroup;
|
||||
import org.briarproject.briar.api.client.PostHeader;
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -49,9 +48,9 @@ import static org.briarproject.briar.android.threaded.ThreadItemAdapter.UnreadCo
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadItemAdapter<I>, I extends ThreadItem, H extends PostHeader>
|
||||
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, A extends ThreadItemAdapter<I>>
|
||||
extends BriarActivity
|
||||
implements ThreadListListener<H>, TextInputListener, SharingListener,
|
||||
implements ThreadListListener<I>, TextInputListener, SharingListener,
|
||||
ThreadItemListener<I>, ThreadListDataSource {
|
||||
|
||||
protected static final String KEY_REPLY_ID = "replyId";
|
||||
@@ -68,7 +67,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
|
||||
@Nullable
|
||||
private MessageId replyId;
|
||||
|
||||
protected abstract ThreadListController<G, I, H> getController();
|
||||
protected abstract ThreadListController<G, I> getController();
|
||||
|
||||
@Inject
|
||||
protected SharingController sharingController;
|
||||
@@ -190,8 +189,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
|
||||
if (items.isEmpty()) {
|
||||
list.showData();
|
||||
} else {
|
||||
initList(items);
|
||||
updateTextInput(replyId);
|
||||
displayItems(items);
|
||||
updateTextInput();
|
||||
}
|
||||
} else {
|
||||
LOG.info("Concurrent update, reloading");
|
||||
@@ -206,7 +205,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
|
||||
});
|
||||
}
|
||||
|
||||
private void initList(final ThreadItemList<I> items) {
|
||||
private void displayItems(final ThreadItemList<I> items) {
|
||||
adapter.setItems(items);
|
||||
MessageId messageId = items.getFirstVisibleItemId();
|
||||
if (messageId != null)
|
||||
@@ -253,9 +252,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
ThreadItem replyItem = adapter.getHighlightedItem();
|
||||
if (replyItem != null) {
|
||||
outState.putByteArray(KEY_REPLY_ID, replyItem.getId().getBytes());
|
||||
if (replyId != null) {
|
||||
outState.putByteArray(KEY_REPLY_ID, replyId.getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +271,9 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (adapter.getHighlightedItem() != null) {
|
||||
updateTextInput(null);
|
||||
textInput.setText("");
|
||||
replyId = null;
|
||||
updateTextInput();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
@@ -289,7 +289,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
|
||||
|
||||
@Override
|
||||
public void onReplyClick(final I item) {
|
||||
updateTextInput(item.getId());
|
||||
replyId = item.getId();
|
||||
updateTextInput();
|
||||
if (textInput.isKeyboardOpen()) {
|
||||
scrollToItemAtTop(item);
|
||||
} else {
|
||||
@@ -339,15 +340,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
private void updateTextInput(@Nullable MessageId replyItemId) {
|
||||
if (replyItemId != null) {
|
||||
private void updateTextInput() {
|
||||
if (replyId != null) {
|
||||
textInput.setHint(R.string.forum_message_reply_hint);
|
||||
textInput.requestFocus();
|
||||
textInput.showSoftKeyboard();
|
||||
} else {
|
||||
textInput.setHint(R.string.forum_new_message_hint);
|
||||
}
|
||||
adapter.setHighlightedItem(replyItemId);
|
||||
adapter.setHighlightedItem(replyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -374,25 +375,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
|
||||
getController().createAndStoreMessage(text, replyItem, handler);
|
||||
textInput.hideSoftKeyboard();
|
||||
textInput.setText("");
|
||||
updateTextInput(null);
|
||||
replyId = null;
|
||||
updateTextInput();
|
||||
}
|
||||
|
||||
protected abstract int getMaxBodyLength();
|
||||
|
||||
@Override
|
||||
public void onHeaderReceived(H header) {
|
||||
getController().loadItem(header,
|
||||
new UiResultExceptionHandler<I, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(final I result) {
|
||||
addItem(result, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
}
|
||||
});
|
||||
public void onItemReceived(I item) {
|
||||
addItem(item, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -400,8 +391,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
|
||||
supportFinishAfterTransition();
|
||||
}
|
||||
|
||||
protected void addItem(I item, boolean isLocal) {
|
||||
private void addItem(I item, boolean isLocal) {
|
||||
adapter.incrementRevision();
|
||||
MessageId parent = item.getParentId();
|
||||
if (parent != null && !adapter.contains(parent)) {
|
||||
// We've incremented the adapter's revision, so the item will be
|
||||
// loaded when its parent has been loaded
|
||||
LOG.info("Ignoring item with missing parent");
|
||||
return;
|
||||
}
|
||||
adapter.add(item);
|
||||
|
||||
if (isLocal) {
|
||||
|
||||
@@ -12,14 +12,13 @@ import org.briarproject.briar.android.controller.ActivityLifecycleController;
|
||||
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
||||
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.briar.api.client.NamedGroup;
|
||||
import org.briarproject.briar.api.client.PostHeader;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface ThreadListController<G extends NamedGroup, I extends ThreadItem, H extends PostHeader>
|
||||
public interface ThreadListController<G extends NamedGroup, I extends ThreadItem>
|
||||
extends ActivityLifecycleController {
|
||||
|
||||
void setGroupId(GroupId groupId);
|
||||
@@ -29,9 +28,8 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
|
||||
void loadSharingContacts(
|
||||
ResultExceptionHandler<Collection<ContactId>, DbException> handler);
|
||||
|
||||
void loadItem(H header, ResultExceptionHandler<I, DbException> handler);
|
||||
|
||||
void loadItems(ResultExceptionHandler<ThreadItemList<I>, DbException> handler);
|
||||
void loadItems(
|
||||
ResultExceptionHandler<ThreadItemList<I>, DbException> handler);
|
||||
|
||||
void markItemRead(I item);
|
||||
|
||||
@@ -42,9 +40,10 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
|
||||
|
||||
void deleteNamedGroup(ExceptionHandler<DbException> handler);
|
||||
|
||||
interface ThreadListListener<H> extends ThreadListDataSource {
|
||||
interface ThreadListListener<I> extends ThreadListDataSource {
|
||||
|
||||
@UiThread
|
||||
void onHeaderReceived(H header);
|
||||
void onItemReceived(I item);
|
||||
|
||||
@UiThread
|
||||
void onGroupRemoved();
|
||||
|
||||
@@ -39,9 +39,9 @@ import static java.util.logging.Level.WARNING;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, M extends ThreadedMessage, L extends ThreadListListener<H>>
|
||||
public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, M extends ThreadedMessage, L extends ThreadListListener<I>>
|
||||
extends DbControllerImpl
|
||||
implements ThreadListController<G, I, H>, EventListener {
|
||||
implements ThreadListController<G, I>, EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ThreadListControllerImpl.class.getName());
|
||||
@@ -203,35 +203,6 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
|
||||
@DatabaseExecutor
|
||||
protected abstract String loadMessageBody(H header) throws DbException;
|
||||
|
||||
@Override
|
||||
public void loadItem(final H header,
|
||||
final ResultExceptionHandler<I, DbException> handler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
String body;
|
||||
if (!bodyCache.containsKey(header.getId())) {
|
||||
body = loadMessageBody(header);
|
||||
bodyCache.put(header.getId(), body);
|
||||
} else {
|
||||
body = bodyCache.get(header.getId());
|
||||
}
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading item took " + duration + " ms");
|
||||
I item = buildItem(header, body);
|
||||
handler.onResult(item);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
handler.onException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markItemRead(I item) {
|
||||
markItemsRead(Collections.singletonList(item));
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.briarproject.briar.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.ColorRes;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
|
||||
import static android.support.v4.app.NotificationCompat.CATEGORY_MESSAGE;
|
||||
import static android.support.v4.app.NotificationCompat.VISIBILITY_PRIVATE;
|
||||
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
|
||||
|
||||
|
||||
public class BriarNotificationBuilder extends NotificationCompat.Builder {
|
||||
|
||||
public BriarNotificationBuilder(Context context) {
|
||||
super(context);
|
||||
setAutoCancel(true);
|
||||
}
|
||||
|
||||
public BriarNotificationBuilder setColorRes(@ColorRes int res) {
|
||||
setColor(ContextCompat.getColor(mContext, res));
|
||||
return this;
|
||||
}
|
||||
|
||||
public BriarNotificationBuilder setLockscreenVisibility(String category,
|
||||
boolean show) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
setCategory(category);
|
||||
if (show)
|
||||
setVisibility(VISIBILITY_PRIVATE);
|
||||
else
|
||||
setVisibility(VISIBILITY_SECRET);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -104,8 +104,9 @@ public class EmojiPageView extends FrameLayout {
|
||||
emojiSize + 2 * pad));
|
||||
view = emojiView;
|
||||
}
|
||||
String emoji = model.getEmoji()[position];
|
||||
view.setEmoji(emoji);
|
||||
|
||||
view.setEmoji(model.getEmoji()[position]);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(RecentEmojiPageModel.class.getName());
|
||||
|
||||
private static final String EMOJI_LRU_PREFERENCE = "pref_emoji_recent";
|
||||
private static final String EMOJI_LRU_PREFERENCE = "pref_emoji_recent2";
|
||||
private static final int EMOJI_LRU_SIZE = 50;
|
||||
|
||||
private final LinkedHashSet<String> recentlyUsed; // UI thread
|
||||
@@ -98,12 +98,12 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
}
|
||||
|
||||
private String serialize(LinkedHashSet<String> emojis) {
|
||||
return StringUtils.join(emojis, ";");
|
||||
return StringUtils.join(emojis, "\t");
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> deserialize(@Nullable String serialized) {
|
||||
if (serialized == null) return new LinkedHashSet<>();
|
||||
String[] list = serialized.split(";");
|
||||
String[] list = serialized.split("\t");
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>(list.length);
|
||||
Collections.addAll(result, list);
|
||||
return result;
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="@dimen/margin_large">
|
||||
android:padding="@dimen/margin_large">
|
||||
|
||||
<EditText
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/createForumNameLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/createForumNameEntry"
|
||||
android:maxLines="1"
|
||||
android:inputType="text|textCapSentences"
|
||||
android:hint="@string/choose_forum_hint" />
|
||||
app:errorEnabled="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/createForumFeedback"
|
||||
android:gravity="center" />
|
||||
<EditText
|
||||
android:id="@+id/createForumNameEntry"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/choose_forum_hint"
|
||||
android:inputType="text|textCapSentences"
|
||||
android:maxLines="1"/>
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<Button
|
||||
style="@style/BriarButton"
|
||||
android:id="@+id/createForumButton"
|
||||
style="@style/BriarButton"
|
||||
android:enabled="false"
|
||||
android:text="@string/create_forum_button" />
|
||||
android:text="@string/create_forum_button"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/createForumProgressBar"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toLeftOf="@+id/expiryWarningClose"
|
||||
android:text="@string/expiry_warning"
|
||||
android:text="@plurals/expiry_warning"
|
||||
android:textColor="@color/briar_text_primary_inverse"
|
||||
android:textSize="@dimen/text_size_small"/>
|
||||
|
||||
|
||||
@@ -1,30 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="@dimen/margin_large">
|
||||
android:padding="@dimen/margin_large">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/name"
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/nameLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:inputType="text|textCapSentences"
|
||||
android:hint="@string/groups_create_group_hint"/>
|
||||
app:errorEnabled="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/feedback"
|
||||
android:gravity="center" />
|
||||
<EditText
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/groups_create_group_hint"
|
||||
android:inputType="text|textCapSentences"
|
||||
android:maxLines="1"/>
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<Button
|
||||
style="@style/BriarButton"
|
||||
android:id="@+id/button"
|
||||
style="@style/BriarButton"
|
||||
android:enabled="false"
|
||||
android:text="@string/groups_create_group_button"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/margin_activity_vertical"
|
||||
android:paddingEnd="@dimen/margin_activity_horizontal"
|
||||
android:paddingRight="@dimen/margin_activity_horizontal"
|
||||
android:paddingStart="@dimen/margin_activity_horizontal"
|
||||
android:paddingLeft="@dimen/margin_activity_horizontal"
|
||||
android:paddingTop="@dimen/margin_activity_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connectedView"
|
||||
style="@style/BriarTextTitle"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/connected_to_contact"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:drawableLeft="@drawable/navigation_accept"
|
||||
android:drawableStart="@drawable/navigation_accept"
|
||||
android:gravity="center_vertical"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/yourConfirmationCodeView"
|
||||
style="@style/BriarTextBody"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/your_confirmation_code"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:layout_below="@+id/connectedView"
|
||||
android:layout_centerHorizontal="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/codeView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:textSize="50sp"
|
||||
android:textColor="@color/briar_text_secondary"
|
||||
android:layout_below="@+id/yourConfirmationCodeView"
|
||||
android:layout_centerHorizontal="true"
|
||||
tools:text="1337"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/waitingView"
|
||||
style="@style/BriarTextBody"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/searching_format"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:layout_below="@+id/codeView"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:visibility="gone"
|
||||
android:gravity="center_horizontal"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:layout_below="@+id/waitingView"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/enterCodeTextView"
|
||||
style="@style/BriarTextBody"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/enter_confirmation_code"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:layout_below="@+id/codeView"
|
||||
android:layout_centerHorizontal="true"/>
|
||||
|
||||
<include
|
||||
android:id="@+id/codeEntryView"
|
||||
layout="@layout/view_code_entry"
|
||||
android:layout_below="@+id/enterCodeTextView"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_medium"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/continueButton"
|
||||
style="@style/BriarButton.Default"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/continue_button"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:enabled="false"
|
||||
android:layout_below="@+id/codeEntryView"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_margin="@dimen/margin_medium"/>
|
||||
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
@@ -1,96 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/margin_activity_vertical"
|
||||
android:paddingEnd="@dimen/margin_activity_horizontal"
|
||||
android:paddingRight="@dimen/margin_activity_horizontal"
|
||||
android:paddingStart="@dimen/margin_activity_horizontal"
|
||||
android:paddingLeft="@dimen/margin_activity_horizontal"
|
||||
android:paddingTop="@dimen/margin_activity_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/yourCodeView"
|
||||
style="@style/BriarTextBody"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/your_invitation_code"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:layout_centerHorizontal="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/codeView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:textSize="50sp"
|
||||
android:textColor="@color/briar_text_secondary"
|
||||
android:layout_below="@+id/yourCodeView"
|
||||
android:layout_centerHorizontal="true"
|
||||
tools:text="1337"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/waitingView"
|
||||
style="@style/BriarTextBody"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/searching_format"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:layout_below="@+id/codeView"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:visibility="gone"
|
||||
android:gravity="center_horizontal"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:indeterminate="true"
|
||||
android:layout_below="@+id/waitingView"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/enterCodeTextView"
|
||||
style="@style/BriarTextBody"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/enter_invitation_code"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:layout_below="@+id/codeView"
|
||||
android:layout_centerHorizontal="true"/>
|
||||
|
||||
<include
|
||||
android:id="@+id/codeEntryView"
|
||||
layout="@layout/view_code_entry"
|
||||
android:layout_below="@+id/enterCodeTextView"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_medium"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/continueButton"
|
||||
style="@style/BriarButton.Default"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/continue_button"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:enabled="false"
|
||||
android:layout_below="@+id/codeEntryView"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_margin="@dimen/margin_medium"/>
|
||||
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
@@ -1,60 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/margin_activity_vertical"
|
||||
android:paddingEnd="@dimen/margin_activity_horizontal"
|
||||
android:paddingLeft="@dimen/margin_activity_horizontal"
|
||||
android:paddingRight="@dimen/margin_activity_horizontal"
|
||||
android:paddingStart="@dimen/margin_activity_horizontal"
|
||||
android:paddingTop="@dimen/margin_activity_vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/BriarTextBody"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/your_nickname"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/border_spinner"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:spinnerMode="dropdown"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_xlarge"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/bluetooth"/>
|
||||
|
||||
<TextView
|
||||
style="@style/BriarTextBody"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_xlarge"
|
||||
android:text="@string/face_to_face"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/continueButton"
|
||||
style="@style/BriarButton.Default"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:text="@string/continue_button"/>
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/margin_activity_vertical"
|
||||
android:paddingEnd="@dimen/margin_activity_horizontal"
|
||||
android:paddingStart="@dimen/margin_activity_horizontal"
|
||||
android:paddingTop="@dimen/margin_activity_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/connection_failed"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
android:textColor="@color/briar_text_primary"
|
||||
android:drawableStart="@drawable/alerts_and_states_error"
|
||||
android:drawableLeft="@drawable/alerts_and_states_error"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="@dimen/margin_medium"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/explanationTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/interfering"
|
||||
android:textColor="@color/briar_text_primary"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:padding="@dimen/margin_medium"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/tryAgainButton"
|
||||
style="@style/BriarButton.Default"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/try_again_button"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_margin="@dimen/margin_medium"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<EditText
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/codeEntryView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textSize="@dimen/text_size_xlarge"
|
||||
android:ems="4"
|
||||
android:maxLines="1"
|
||||
android:maxLength="6"
|
||||
android:layout_margin="@dimen/margin_medium"
|
||||
android:imeOptions="actionGo"
|
||||
tools:text="123456"/>
|
||||
@@ -3,18 +3,18 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_group_member_list"
|
||||
android:icon="@drawable/ic_group_white"
|
||||
android:title="@string/groups_member_list"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_group_invite"
|
||||
android:icon="@drawable/social_share_white"
|
||||
android:title="@string/groups_invite_members"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_group_member_list"
|
||||
android:icon="@drawable/ic_group_white"
|
||||
android:title="@string/groups_member_list"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_group_reveal"
|
||||
android:icon="@drawable/ic_visibility_white"
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<resources>
|
||||
<!--Setup-->
|
||||
<string name="setup_title">Briar einrichten</string>
|
||||
<string name="setup_explanation">Dein Briar-Konto wird verschlüsselt auf Deinem Gerät gespeichert und nicht mit der \"Cloud\" synchronisiert. Wenn Du Briar deinstallierst oder Dein Passwort vergisst, können das Konto und Deine Daten nicht wiederhergestellt werden.</string>
|
||||
<string name="choose_nickname">Wähle Deinen Benutzernamen</string>
|
||||
<string name="choose_password">Wähle Dein Passwort</string>
|
||||
<string name="setup_explanation">Dein Briar-Konto wird verschlüsselt auf deinem Gerät gespeichert und nicht mit der \"Cloud\" synchronisiert. Wenn du Briar deinstallierst oder dein Passwort vergisst, können das Konto und deine Daten nicht wiederhergestellt werden.</string>
|
||||
<string name="choose_nickname">Wähle deinen Benutzernamen</string>
|
||||
<string name="choose_password">Wähle dein Passwort</string>
|
||||
<string name="confirm_password">Passwort bestätigen</string>
|
||||
<string name="name_too_long">Name zu lang</string>
|
||||
<string name="password_too_weak">Passwort zu schwach</string>
|
||||
@@ -22,7 +22,11 @@
|
||||
<string name="startup_failed_activity_title">Fehler beim Starten von Briar</string>
|
||||
<string name="startup_failed_db_error">Deine Briar-Datenbank ist korrupt. Briar-Konto, Daten und alle Verbindungen zu Kontakten können nicht mehr wiederhergestellt werden. Deinstalliere Briar und erstelle nach Installation der aktuellen Briar-Version ein neues Konto.</string>
|
||||
<string name="startup_failed_service_error">Briar konnte ein benötigtes Plugin nicht starten. Normalerweise kann das Problem durch eine Neuinstallation von Briar gelöst werden. Eine Neuinstallation führt jedoch zum Verlust des Kontos und aller dazugehörigen Daten, da Briar deine Daten nicht auf zentralen Servern speichert</string>
|
||||
<string name="expiry_warning">Diese Version von Briar ist nicht mehr aktuell.\nBitte installiere eine neuere Version.</string>
|
||||
<plurals name="expiry_warning">
|
||||
<item quantity="one">Dies ist eine Beta-Version von Briar. Dein Konto läuft in %d Tag ab und kann nicht verlängert werden.</item>
|
||||
<item quantity="other">Dies ist eine Beta-Version von Briar. Dein Konto läuft in %d Tagen ab und kann nicht verlängert werden.</item>
|
||||
</plurals>
|
||||
<string name="expiry_date_reached">Diese Software ist abgelaufen.\nDanke dass Du Briar getestet hast!</string>
|
||||
<!--Navigation Drawer-->
|
||||
<string name="nav_drawer_open_description">Navigationsleiste öffnen</string>
|
||||
<string name="nav_drawer_close_description">Navigationsleiste schliessen</string>
|
||||
@@ -87,7 +91,7 @@
|
||||
<!--Adding Contacts-->
|
||||
<string name="add_contact_title">Kontakt hinzufügen</string>
|
||||
<string name="your_nickname">Wähle die zu verwendende Identität:</string>
|
||||
<string name="face_to_face">Um einen neuen Kontakt hinzufügen zu können, ist eine reale Begegnung erforderlich.\n\nDadurch wird eine betrügerische Impersonation und der unautorisierte Zugriff auf die Kommunikation verhindert.</string>
|
||||
<string name="face_to_face">Um einen neuen Kontakt hinzuzufügen, ist es notwendig, dass sich beide Kontakte an einem Ort treffen.\n\nDadurch wird betrügerische Identitätsvortäuschung und unautorisierter Kommunikationszugriff verhindert.</string>
|
||||
<string name="continue_button">Weiter</string>
|
||||
<string name="your_invitation_code">Dein Einladungs-Code ist</string>
|
||||
<string name="enter_invitation_code">Bitte gib den Einladungs-Code Deines Kontakts an:</string>
|
||||
@@ -117,6 +121,7 @@
|
||||
<string name="introduction_onboarding_text">Du kannst Deine Kontakte untereinander bekannt machen. So können sie sich über Briar verbinden, ohne sich persönlich treffen zu müssen.</string>
|
||||
<string name="introduction_activity_title">Kontakt auswählen</string>
|
||||
<string name="introduction_message_title">Kontakte untereinander bekannt machen</string>
|
||||
<string name="introduction_message_hint">Nachricht hinzufügen (optional)</string>
|
||||
<string name="introduction_button">Kontaktempfehlung abgeben</string>
|
||||
<string name="introduction_sent">Deine Kontaktempfehlung wurde verschickt</string>
|
||||
<string name="introduction_error">Es gab einen Fehler beim Versuch, die Kontaktempfehlung zu verschicken</string>
|
||||
@@ -147,6 +152,7 @@
|
||||
<string name="groups_create_group_title">Private Gruppe erstellen</string>
|
||||
<string name="groups_create_group_button">Gruppe erstellen</string>
|
||||
<string name="groups_create_group_invitation_button">Einladung schicken</string>
|
||||
<string name="groups_create_group_hint">Wähle einen Namen für deine private Gruppe</string>
|
||||
<string name="groups_invitation_sent">Gruppeneinladung versendet</string>
|
||||
<string name="groups_message_sent">Nachricht gesendet</string>
|
||||
<string name="groups_member_list">Mitglieder</string>
|
||||
@@ -188,6 +194,8 @@
|
||||
<string name="groups_reveal_invisible">Beziehung zum Kontakt ist für diese Gruppe nicht sichtbar</string>
|
||||
<!--Forums-->
|
||||
<string name="no_forums">Du hast noch keine Foren.\n\nWarum erstellst du nicht einfach selbst ein neues Forum, indem du auf das \"+\"-Symbol am oberen Bildschirmrand tippst?\n\nDu kannst auch deine Kontakte auffordern, Foren mit dir zu teilen.</string>
|
||||
<string name="create_forum_title">Forum erstellen</string>
|
||||
<string name="choose_forum_hint">Wähle einen Namen für Dein Forum</string>
|
||||
<string name="create_forum_button">Forum erstellen</string>
|
||||
<string name="forum_created_toast">Forum wurde erstellt</string>
|
||||
<string name="no_forum_posts">Dieses Forum ist leer.\n\nBenutze das Stift-Icon am oberen Bildschirmrand um den ersten Beitrag zu verfassen.\n\nFühlst du dich einsam hier? Dann teile das Forum mit weiteren Kontakten!</string>
|
||||
@@ -211,6 +219,7 @@
|
||||
<string name="activity_share_toolbar_header">Kontakte auswählen</string>
|
||||
<string name="no_contacts_selector">Du scheinst hier neu zu sein und noch keine Kontakte zu haben.\n\nBitte komm zurück, wenn du deinen ersten Kontakt hinzugefügt hast.</string>
|
||||
<string name="forum_shared_snackbar">Forum mit gewählten Kontakten geteilt</string>
|
||||
<string name="forum_share_message">Nachricht hinzufügen (optional)</string>
|
||||
<string name="forum_share_error">Es gab einen Fehler beim Versuch, dieses Forum zu teilen.</string>
|
||||
<string name="forum_invitation_received">%1$s hat das Forum \"%2$s\" mit dir geteilt.</string>
|
||||
<string name="forum_invitation_sent">Du hast das Forum \"%1$s\" mit %2$s geteilt.</string>
|
||||
@@ -241,12 +250,12 @@
|
||||
<string name="blogs_blog_post_created">Blogbeitrag erstellt</string>
|
||||
<string name="blogs_blog_post_received">Neuen Blogbeitrag empfangen</string>
|
||||
<string name="blogs_blog_post_scroll_to">Scrolle zu</string>
|
||||
<string name="blogs_feed_empty_state">Dies ist die globale Blog-Zeitleiste.\n\nOffensichtlich hat noch niemand etwas veröffentlicht.\n\nSei die oder der erste und tipp auf das Stift-Icon, um einen neuen Blogbeitrag zu verfassen.</string>
|
||||
<string name="blogs_personal_blog">%ss persönliches Blog</string>
|
||||
<string name="blogs_feed_empty_state">Dies ist die globale Blog-Zeitleiste.\n\nOffensichtlich hat noch niemand etwas veröffentlicht.\n\nSei die oder der Erste und tippe auf das Stift-Icon, um einen neuen Blogbeitrag zu verfassen.</string>
|
||||
<string name="blogs_remove_blog">Blog entfernen</string>
|
||||
<string name="blogs_remove_blog_dialog_message">Bist Du sicher, dass Du diesen Blog und alle dazugehörigen Beiträge löschen möchtest?\nBeachte, dass dies nicht den Blog auf Geräten anderer Leute löscht.</string>
|
||||
<string name="blogs_remove_blog_ok">Blog entfernen</string>
|
||||
<string name="blogs_blog_removed">Blog wurde entfernt</string>
|
||||
<string name="blogs_reblog_comment_hint">Kommentar hinzufügen (optional)</string>
|
||||
<string name="blogs_reblog_button">Rebloggen</string>
|
||||
<!--Blog Sharing-->
|
||||
<string name="blogs_sharing_share">Blog teilen</string>
|
||||
@@ -257,8 +266,8 @@
|
||||
<string name="blogs_sharing_response_declined_sent">Du hast die Blogeinladung von %s abgelehnt.</string>
|
||||
<string name="blogs_sharing_response_accepted_received">%s hat die Blogeinladung akzeptiert.</string>
|
||||
<string name="blogs_sharing_response_declined_received">%s hat die Blogeinladung abgelehnt.</string>
|
||||
<string name="blogs_sharing_invitation_received">%1$s hat das persönliche Blog von %2$s mit dir geteilt.</string>
|
||||
<string name="blogs_sharing_invitation_sent">Du hast das persönliche Blog von %1$s mit %2$s geteilt.</string>
|
||||
<string name="blogs_sharing_invitation_received">%1$shat den Blog \"%2$s\" mit Dir geteilt.</string>
|
||||
<string name="blogs_sharing_invitation_sent">Du teilst den Blog \"%1$s\" mit %2$s.</string>
|
||||
<string name="blogs_sharing_invitations_title">Blogeinladungen</string>
|
||||
<string name="blogs_sharing_joined_toast">Blog abonniert</string>
|
||||
<string name="blogs_sharing_declined_toast">Blogeinladung abgelehnt</string>
|
||||
@@ -276,7 +285,7 @@
|
||||
<string name="blogs_rss_remove_feed_dialog_message">Soll der Feed mit allen Posts wirklich gelöscht werden?\nGeteilte Posts werden dabei nicht von anderen Geräten gelöscht.</string>
|
||||
<string name="blogs_rss_remove_feed_ok">Feed entfernen</string>
|
||||
<string name="blogs_rss_feeds_manage_delete_error">Der Feed konnte nicht gelöscht werden!</string>
|
||||
<string name="blogs_rss_feeds_manage_empty_state">Du hast bisher noch keine RSS Feeds importiert. Tippe auf das \"+\"-Icon am oberen Bildschirmrand um einen neuen Feed hinzuzufügen.</string>
|
||||
<string name="blogs_rss_feeds_manage_empty_state">Du hast bisher noch keine RSS-Feeds importiert. Tippe auf das \"+\"-Icon am oberen Bildschirmrand, um einen neuen Feed hinzuzufügen.</string>
|
||||
<string name="blogs_rss_feeds_manage_error">Es gab ein Problem beim Laden deiner Feeds. Bitte versuche es später erneut.</string>
|
||||
<!--Settings Network-->
|
||||
<string name="network_settings_title">Netzwerke</string>
|
||||
@@ -311,7 +320,17 @@
|
||||
<string name="uninstall_setting_summary">Diese Aktion benötigt manuelle Bestätigung im Falle eines Panik-Ereignisses</string>
|
||||
<!--Settings Notifications-->
|
||||
<string name="notification_settings_title">Benachrichtigungen</string>
|
||||
<string name="notify_private_messages_setting_title">Private Nachrichten</string>
|
||||
<string name="notify_private_messages_setting_summary">Zeige Benachrichtigungen für private Nachrichten</string>
|
||||
<string name="notify_group_messages_setting_title">Gruppennachrichten</string>
|
||||
<string name="notify_group_messages_setting_summary">Benachrichtigungen für Gruppennachrichten anzeigen</string>
|
||||
<string name="notify_forum_posts_setting_title">Forenbeiträge</string>
|
||||
<string name="notify_forum_posts_setting_summary">Benachrichtigungen für Forenbeiträge anzeigen</string>
|
||||
<string name="notify_blog_posts_setting_title">Blogbeiträge</string>
|
||||
<string name="notify_blog_posts_setting_summary">Benachrichtigungen für Blogbeiträge anzeigen</string>
|
||||
<string name="notify_vibration_setting">Vibration</string>
|
||||
<string name="notify_lock_screen_setting_title">Sperrbildschirm</string>
|
||||
<string name="notify_lock_screen_setting_summary">Zeigt Benachrichtigungen auf dem Sperrbildschirm an</string>
|
||||
<string name="notify_sound_setting">Tonsignal</string>
|
||||
<string name="notify_sound_setting_default">Standardklingelton</string>
|
||||
<string name="notify_sound_setting_disabled">Keine</string>
|
||||
@@ -327,7 +346,7 @@
|
||||
<!--Crash Reporter-->
|
||||
<string name="crash_report_title">Briar-Absturzbericht</string>
|
||||
<string name="briar_crashed">Es tut uns leid, Briar ist abgestürzt.</string>
|
||||
<string name="not_your_fault">Das ist nicht Deine Schuld.</string>
|
||||
<string name="not_your_fault">Das ist nicht deine Schuld.</string>
|
||||
<string name="please_send_report">Bitte hilf uns, Briar zu verbessern, indem Du einen Absturzbericht sendest.</string>
|
||||
<string name="report_is_encrypted">Wir versprechen, dass der Bericht verschlüsselt ist und über eine sichere Verbindung geschickt wird.</string>
|
||||
<string name="feedback_title">Feedback</string>
|
||||
@@ -343,4 +362,6 @@
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">Von Briar abmelden ...</string>
|
||||
<!--Screen Filters & Tapjacking-->
|
||||
<string name="screen_filter_title">Bildschirmüberlagerung erkannt</string>
|
||||
<string name="screen_filter_body">Eine andere App überlagert Briar. Um deine Sicherheit zu gewährleisten, reagiert Briar in diesem Fall nicht auf deine Eingaben.\n\nBeende deswegen die folgenden Apps während der Verwendung von Briar:\n\n%1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
<string name="startup_failed_activity_title">Fallo al iniciar Briar</string>
|
||||
<string name="startup_failed_db_error">Por alguna razón, la base de datos de Briar ha sufrido daños irreparables. Tu cuenta, tus datos y todos tus contactos se han perdido. Desafortunadamente, tendrás que reinstalar Briar y registrar una nueva cuenta.</string>
|
||||
<string name="startup_failed_service_error">Briar no pudo iniciar un complemento necesario. Reinstalar Briar suele solucionar el problema. Sin embargo, ten en cuenta que perderás tu cuenta y todos los datos asociados ya que Briar no almacena esta información en ningún servidor central.</string>
|
||||
<string name="expiry_warning">Esta versión ha caducado.\nInstala una más reciente, por favor.</string>
|
||||
<plurals name="expiry_warning">
|
||||
<item quantity="one">Esta es una versión preliminar de Briar. Tu cuenta caducará en %d día y no podrá renovarse.</item>
|
||||
<item quantity="other">Esta es una versión preliminar de Briar. Tu cuenta caducará en %d días y no podrá renovarse.</item>
|
||||
</plurals>
|
||||
<string name="expiry_date_reached">Esta versión ha caducado.\n¡Gracias por probarla!</string>
|
||||
<!--Navigation Drawer-->
|
||||
<string name="nav_drawer_open_description">Abrir el panel de navegación</string>
|
||||
<string name="nav_drawer_close_description">Cierra el panel de navegación</string>
|
||||
@@ -76,7 +80,7 @@
|
||||
<string name="text_too_long">El texto es demasiado largo</string>
|
||||
<string name="show_onboarding">Mostrar diálogo de ayuda</string>
|
||||
<!--Contacts and Private Conversations-->
|
||||
<string name="no_contacts">Parece que eres nuevo por aquí y no tienes aún contactos.\n\nPulsa el signo + en la parte superior y sigue las instrucciones para añadir amigos a tu lista.\n\nPor favor, recuerda: sólo puedes añadir nuevos contactos cara a cara para evitar que nadie suplante tu identidad o lea tus mensajes en el futuro. </string>
|
||||
<string name="no_contacts">Parece que eres nuevo por aquí y no tienes aún contactos.\n\nPulsa el signo + en la parte superior y sigue las instrucciones para añadir amigos a tu lista.\n\nPor favor, recuerda: sólo puedes añadir nuevos contactos cara a cara para evitar que nadie suplante tu identidad o lea tus mensajes en el futuro. </string>
|
||||
<string name="date_no_private_messages">Sin mensajes.</string>
|
||||
<string name="no_private_messages">Esta es la vista de conversación.\n\nParece que aún no hay ninguna.\n\nPulsa el campo de texto en la parte inferior para empezar la conversación.</string>
|
||||
<string name="message_hint">Escribe un mensaje</string>
|
||||
@@ -118,7 +122,7 @@
|
||||
<string name="introduction_activity_title">Seleccionar contacto</string>
|
||||
<string name="introduction_message_title">Presentar a dos contactos</string>
|
||||
<string name="introduction_message_hint">Añade un mensaje (opcional)</string>
|
||||
<string name="introduction_button">Realizar la presentación</string>
|
||||
<string name="introduction_button">Presentar contactos</string>
|
||||
<string name="introduction_sent">Tu presentación se ha mandado.</string>
|
||||
<string name="introduction_error">Ocurrió un error realizando la presentación.</string>
|
||||
<string name="introduction_response_error">Error al responder a la presentación</string>
|
||||
@@ -136,7 +140,7 @@
|
||||
<item quantity="other">%d nuevos contactos añadido</item>
|
||||
</plurals>
|
||||
<!--Private Groups-->
|
||||
<string name="groups_list_empty">Aún no participas en ningún grupo.\n\nCrea un grupo pulsando en el signo + arriba o pide a tus contactos que te inviten a uno de sus grupos.</string>
|
||||
<string name="groups_list_empty">Aún no participas en ningún grupo.\n\nCrea un grupo pulsando en el signo + arriba o pide a tus contactos que te inviten a uno de sus grupos.</string>
|
||||
<string name="groups_created_by">Creado por %s</string>
|
||||
<plurals name="messages">
|
||||
<item quantity="one">%d mensaje</item>
|
||||
@@ -189,7 +193,9 @@
|
||||
<string name="groups_reveal_visible_revealed_by_contact">Las relaciones entre los contactos son visibles al grupo (las reveló %s)</string>
|
||||
<string name="groups_reveal_invisible">Las relaciones entre los contactos no son visibles al grupo</string>
|
||||
<!--Forums-->
|
||||
<string name="no_forums">No tienes ningún foro aún.\n\n¿Por qué no creas uno pulsando el signo + de la parte superior?\n\nTambién puedes pedirle a tus contactos que compartan foros contigo.</string>
|
||||
<string name="no_forums">No tienes ningún foro aún.\n\n¿Por qué no creas uno pulsando el signo + de la parte superior?\n\nTambién puedes pedirle a tus contactos que compartan foros contigo.</string>
|
||||
<string name="create_forum_title">Crear foro</string>
|
||||
<string name="choose_forum_hint">Elige un nombre para el foro</string>
|
||||
<string name="create_forum_button">Crear foro</string>
|
||||
<string name="forum_created_toast">Foro creado</string>
|
||||
<string name="no_forum_posts">Este foro está vacío.\n\nUsa el signo del lápiz de la parte superior para redactar una primera publicación.\n\n¿No te sientes un poco solo aquí? ¡Comparte este foro con más contactos!</string>
|
||||
@@ -245,7 +251,6 @@
|
||||
<string name="blogs_blog_post_received">Recibido nuevo artículo del blog</string>
|
||||
<string name="blogs_blog_post_scroll_to">Desplazarse hasta</string>
|
||||
<string name="blogs_feed_empty_state">Esta es la lista global de entradas de blogs.\n\nParece que nadie ha publicado nada todavía.\n\nSé el primero: pulsa el signo del lápiz para escribir una nueva entrada de blog.</string>
|
||||
<string name="blogs_personal_blog">Blog personal de %s</string>
|
||||
<string name="blogs_remove_blog">Eliminar blog</string>
|
||||
<string name="blogs_remove_blog_dialog_message">¿Seguro que quieres eliminar este blog y todos sus artículos?\nTen en cuenta que no se eliminará el blog de los dispositivos de otras personas.</string>
|
||||
<string name="blogs_remove_blog_ok">Eliminar blog</string>
|
||||
@@ -261,8 +266,8 @@
|
||||
<string name="blogs_sharing_response_declined_sent">Rechazaste la invitación al blog de %s.</string>
|
||||
<string name="blogs_sharing_response_accepted_received">%s aceptó la invitación al blog.</string>
|
||||
<string name="blogs_sharing_response_declined_received">%s rechazó la invitación al blog.</string>
|
||||
<string name="blogs_sharing_invitation_received">%1$s ha compartido el blog personal de %2$s contigo.</string>
|
||||
<string name="blogs_sharing_invitation_sent">Has compartido el blog personal de %1$s con %2$s.</string>
|
||||
<string name="blogs_sharing_invitation_received">%1$s ha compartido el blog \"%2$s\" contigo.</string>
|
||||
<string name="blogs_sharing_invitation_sent">Has compartido el blog \"%1$s\" con %2$s.</string>
|
||||
<string name="blogs_sharing_invitations_title">Invitaciones a blogs</string>
|
||||
<string name="blogs_sharing_joined_toast">Suscrito al blog</string>
|
||||
<string name="blogs_sharing_declined_toast">Rechazada invitación al blog</string>
|
||||
@@ -284,7 +289,7 @@
|
||||
<string name="blogs_rss_feeds_manage_error">Hubo un problema cargando tus canales RSS. Por favor, prueba más tarde.</string>
|
||||
<!--Settings Network-->
|
||||
<string name="network_settings_title">Redes</string>
|
||||
<string name="bluetooth_setting">Connectar mediante Bluetooth</string>
|
||||
<string name="bluetooth_setting">Conectar mediante Bluetooth</string>
|
||||
<string name="bluetooth_setting_enabled">Cuando haya contactos cerca</string>
|
||||
<string name="bluetooth_setting_disabled">Solo al añadir contactos</string>
|
||||
<string name="tor_network_setting">Conectar a través de Tor</string>
|
||||
@@ -324,6 +329,8 @@
|
||||
<string name="notify_blog_posts_setting_title">Entradas de blog</string>
|
||||
<string name="notify_blog_posts_setting_summary">Notificar entradas de blog</string>
|
||||
<string name="notify_vibration_setting">Vibrar</string>
|
||||
<string name="notify_lock_screen_setting_title">Pantalla de bloqueo</string>
|
||||
<string name="notify_lock_screen_setting_summary">Notificaciones en la pantalla de bloqueo</string>
|
||||
<string name="notify_sound_setting">Sonar</string>
|
||||
<string name="notify_sound_setting_default">Tono de notificación predeterminado</string>
|
||||
<string name="notify_sound_setting_disabled">Ninguna</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user