Compare commits

..

50 Commits

Author SHA1 Message Date
akwizgran
89f50bbdaf Bump version numbers for beta release. 2018-03-29 16:38:03 +01:00
akwizgran
3eed7df1a4 Merge branch '1171-wifi-access-point' into 'maintenance-0.16'
Backport: Enable LAN plugin when providing a wifi access point

See merge request akwizgran/briar!755
2018-03-29 15:36:11 +00:00
akwizgran
f7af0dc3b0 Delay handling of AP enabled event. 2018-03-29 16:19:03 +01:00
akwizgran
fbaf446570 AP state change event races with address appearing. 2018-03-29 16:19:03 +01:00
akwizgran
fb6d962131 Enable LAN plugin to use wifi AP interface. 2018-03-29 16:19:03 +01:00
akwizgran
d007de48ac Serialise concurrent calls to updateConnectionStatus(). 2018-03-29 16:19:03 +01:00
akwizgran
95a08eed5c Serialise concurrent calls to bind(). 2018-03-29 16:19:02 +01:00
akwizgran
040894b205 Merge branch '1190-shutdown-from-background' into 'maintenance-0.16'
Backport: Shut down cleanly when phone is shutting down or memory is low

See merge request akwizgran/briar!754
2018-03-29 15:04:21 +00:00
akwizgran
41d3bd4f19 Show notification for low memory shutdown. 2018-03-29 15:50:25 +01:00
akwizgran
347868684c Shut down cleanly when device shuts down. 2018-03-29 15:50:23 +01:00
akwizgran
1038a3532b Shut down cleanly when memory is low. 2018-03-29 15:48:29 +01:00
Torsten Grote
4e6d514a0d Backport translation update, add Romanian 2018-03-29 11:07:31 -03:00
akwizgran
f178ce807f Merge branch '965-empty-state-messages' into 'maintenance-0.16'
Backport: Shorten and clean up various strings, remove empty forum warning bubble

See merge request akwizgran/briar!751
2018-03-29 11:47:53 +00:00
akwizgran
a2c827ef24 Merge branch 'hide-ui-during-shutdown' into 'maintenance-0.16'
Backport: Hide UI during shutdown

See merge request akwizgran/briar!750
2018-03-29 11:39:02 +00:00
akwizgran
9496148182 Merge branch '346-full-screen-qr-code' into 'maintenance-0.16'
Backport: Add fullscreen button to QR code view

See merge request akwizgran/briar!749
2018-03-29 11:29:55 +00:00
akwizgran
bb27ca186a Merge branch '845-wifi-without-internet' into 'maintenance-0.16'
Backport: Use WifiManager to get wifi network information

See merge request akwizgran/briar!748
2018-03-29 11:21:15 +00:00
akwizgran
be38431e03 Merge branch '1184-rejected-execution-exception' into 'maintenance-0.16'
Backport: Discard tasks submitted to ScheduledExecutorService during shutdown

See merge request akwizgran/briar!747
2018-03-29 11:12:23 +00:00
akwizgran
e314b39661 Merge branch '965-forum-empty-state' into 'maintenance-0.16'
Backport: Remove mention of pen icon from forum empty state message

See merge request akwizgran/briar!746
2018-03-29 11:03:28 +00:00
akwizgran
4aa8d0b6c0 Remove empty forum warning bubble. 2018-03-29 12:03:17 +01:00
akwizgran
6220a8c00e Consistent text for blogs and forums. 2018-03-29 12:03:17 +01:00
akwizgran
dcd9b0a637 Shorter empty state messages. 2018-03-29 12:03:16 +01:00
akwizgran
94b17caf0f Consistent explanation of account deletion options. 2018-03-29 12:03:16 +01:00
akwizgran
fce8d9fa9f Finish if back button is pressed in SignOutFragment. 2018-03-29 12:00:41 +01:00
akwizgran
f4c798a2da Use database icon for SignOutFragment. 2018-03-29 12:00:41 +01:00
akwizgran
accef2e51b Close NavDrawerActivity immediately when signing out. 2018-03-29 12:00:41 +01:00
akwizgran
34b4c35f44 Use selectable item background to get touch effect. 2018-03-29 11:55:47 +01:00
akwizgran
9b253fc965 Adjust layout weights when resizing QR code view. 2018-03-29 11:55:46 +01:00
akwizgran
4d97cad842 Add fullscreen button to QR code view. 2018-03-29 11:55:46 +01:00
akwizgran
ba99f58559 Use wifi network's socket factory on API 21+. 2018-03-29 11:53:03 +01:00
akwizgran
edbb0a3c13 Use WifiManager to get wifi network information.
This ensures we bind to the wifi interface even if it doesn't have internet access and there's another interface with internet access (e.g. mobile data).
2018-03-29 11:53:03 +01:00
akwizgran
fdbcc0736c Discard tasks submitted during shutdown. 2018-03-29 11:50:21 +01:00
akwizgran
f4722b2a67 Remove mention of pen icon from forum empty state message. 2018-03-29 11:48:29 +01:00
akwizgran
d316e126a9 Merge branch '1159-android-8-notification-settings' into 'maintenance-0.16'
Backport: Overhaul notifications for Android 8

See merge request akwizgran/briar!744
2018-03-29 10:24:09 +00:00
Torsten Grote
20bd72844c Use a different notification preference summary for Android 8 2018-03-26 13:38:55 -03:00
Torsten Grote
02c88eb907 Show different notification settings for Android O
This also makes the defaults consistent with Android versions below O.
2018-03-26 13:38:54 -03:00
akwizgran
1afc0d4fda Merge branch '545-remove-clientid-from-validator-db-methods' into 'maintenance-0.16'
Backport: Remove client ID from validator's DB methods

See merge request akwizgran/briar!738
2018-03-20 17:33:30 +00:00
akwizgran
5a7f39df4d Backport some inconsequential changes from master.
Should make it easier to backport test changes in future.
2018-03-20 17:24:36 +00:00
akwizgran
e30b190209 Remove client ID from validator's DB methods. 2018-03-20 17:23:26 +00:00
akwizgran
31d35a7dd8 Merge branch '1177-blank-viewfinder' into 'maintenance-0.16'
Backport: Show viewfinder again after connection fails

See merge request akwizgran/briar!736
2018-03-20 16:05:57 +00:00
akwizgran
53f85d4b71 When resetting, restart camera if we've stopped it. 2018-03-20 15:51:06 +00:00
akwizgran
54b0bb6084 Don't create a stack of QR code fragments. 2018-03-20 15:38:34 +00:00
akwizgran
f2cfca1460 Remove performance logging. 2018-03-20 15:38:31 +00:00
akwizgran
0cbdc47649 Merge branch '545-denormalise-statuses' into 'maintenance-0.16'
Backport: Add denormalised columns to statuses table

See merge request akwizgran/briar!730
2018-03-09 15:41:37 +00:00
Torsten Grote
536853343e Merge branch '1169-settings-npe' into 'maintenance-0.16'
Backport: Disable settings until they have been loaded

See merge request akwizgran/briar!731
2018-03-08 15:58:33 +00:00
akwizgran
93de06ed0c Merge branch '1181-blurry-error-icon' into 'maintenance-0.16'
Unblur error icon

See merge request akwizgran/briar!729
2018-03-08 15:57:20 +00:00
Torsten Grote
d7f5da305a Disable settings until they have been loaded
In practise, this is not noticeable in the UI.
Only when the database is congested, it should become visible and
prevent a crash when the sound setting is clicked.
2018-03-08 12:45:46 -03:00
Torsten Grote
7c48bc5a00 Unblur error icon 2018-03-08 11:54:12 -03:00
akwizgran
9493e242cc Add migration to schema version 32. 2018-03-08 14:49:22 +00:00
akwizgran
3e28323ab1 Test that visibility change affects expected contacts. 2018-03-08 12:36:30 +00:00
akwizgran
c7e496230b Add denormalised columns to statuses table. 2018-03-08 12:35:46 +00:00
61 changed files with 2076 additions and 794 deletions

View File

@@ -12,8 +12,8 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion 26
versionCode 1619
versionName "0.16.19"
versionCode 1620
versionName "0.16.20"
consumerProguardFiles 'proguard-rules.txt'
}

View File

@@ -48,7 +48,7 @@ public class AndroidPluginModule {
appContext, locationUtils, reporter, eventBus,
torSocketFactory, backoffFactory);
DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
backoffFactory, appContext);
scheduler, backoffFactory, appContext);
Collection<DuplexPluginFactory> duplex =
Arrays.asList(bluetooth, tor, lan);
@NotNullByDefault

View File

@@ -5,37 +5,84 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.net.SocketFactory;
import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.wifi.WifiManager.EXTRA_WIFI_STATE;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.SECONDS;
@NotNullByDefault
class AndroidLanTcpPlugin extends LanTcpPlugin {
// See android.net.wifi.WifiManager
private static final String WIFI_AP_STATE_CHANGED_ACTION =
"android.net.wifi.WIFI_AP_STATE_CHANGED";
private static final int WIFI_AP_STATE_ENABLED = 13;
private static final byte[] WIFI_AP_ADDRESS_BYTES =
{(byte) 192, (byte) 168, 43, 1};
private static final InetAddress WIFI_AP_ADDRESS;
private static final Logger LOG =
Logger.getLogger(AndroidLanTcpPlugin.class.getName());
static {
try {
WIFI_AP_ADDRESS = InetAddress.getByAddress(WIFI_AP_ADDRESS_BYTES);
} catch (UnknownHostException e) {
// Should only be thrown if the address has an illegal length
throw new AssertionError(e);
}
}
private final ScheduledExecutorService scheduler;
private final Context appContext;
private final ConnectivityManager connectivityManager;
@Nullable
private final WifiManager wifiManager;
@Nullable
private volatile BroadcastReceiver networkStateReceiver = null;
private volatile SocketFactory socketFactory;
AndroidLanTcpPlugin(Executor ioExecutor, Backoff backoff,
Context appContext, DuplexPluginCallback callback, int maxLatency,
int maxIdleTime) {
AndroidLanTcpPlugin(Executor ioExecutor, ScheduledExecutorService scheduler,
Backoff backoff, Context appContext, DuplexPluginCallback callback,
int maxLatency, int maxIdleTime) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
this.scheduler = scheduler;
this.appContext = appContext;
ConnectivityManager connectivityManager = (ConnectivityManager)
appContext.getSystemService(CONNECTIVITY_SERVICE);
if (connectivityManager == null) throw new AssertionError();
this.connectivityManager = connectivityManager;
wifiManager = (WifiManager) appContext.getApplicationContext()
.getSystemService(WIFI_SERVICE);
socketFactory = SocketFactory.getDefault();
}
@Override
@@ -44,7 +91,9 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
running = true;
// Register to receive network status events
networkStateReceiver = new NetworkStateReceiver();
IntentFilter filter = new IntentFilter(CONNECTIVITY_ACTION);
IntentFilter filter = new IntentFilter();
filter.addAction(CONNECTIVITY_ACTION);
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
appContext.registerReceiver(networkStateReceiver, filter);
}
@@ -56,21 +105,92 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
tryToClose(socket);
}
@Override
protected Socket createSocket() throws IOException {
return socketFactory.createSocket();
}
@Override
protected Collection<InetAddress> getLocalIpAddresses() {
// If the device doesn't have wifi, don't open any sockets
if (wifiManager == null) return emptyList();
// If we're connected to a wifi network, use that network
WifiInfo info = wifiManager.getConnectionInfo();
if (info != null && info.getIpAddress() != 0)
return singletonList(intToInetAddress(info.getIpAddress()));
// If we're running an access point, return its address
if (super.getLocalIpAddresses().contains(WIFI_AP_ADDRESS))
return singletonList(WIFI_AP_ADDRESS);
// No suitable addresses
return emptyList();
}
private InetAddress intToInetAddress(int ip) {
byte[] ipBytes = new byte[4];
ipBytes[0] = (byte) (ip & 0xFF);
ipBytes[1] = (byte) ((ip >> 8) & 0xFF);
ipBytes[2] = (byte) ((ip >> 16) & 0xFF);
ipBytes[3] = (byte) ((ip >> 24) & 0xFF);
try {
return InetAddress.getByAddress(ipBytes);
} catch (UnknownHostException e) {
// Should only be thrown if address has illegal length
throw new AssertionError(e);
}
}
// On API 21 and later, a socket that is not created with the wifi
// network's socket factory may try to connect via another network
private SocketFactory getSocketFactory() {
if (SDK_INT < 21) return SocketFactory.getDefault();
for (Network net : connectivityManager.getAllNetworks()) {
NetworkInfo info = connectivityManager.getNetworkInfo(net);
if (info != null && info.getType() == TYPE_WIFI)
return net.getSocketFactory();
}
LOG.warning("Could not find suitable socket factory");
return SocketFactory.getDefault();
}
private class NetworkStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent i) {
if (!running) return;
Object o = ctx.getSystemService(CONNECTIVITY_SERVICE);
ConnectivityManager cm = (ConnectivityManager) o;
NetworkInfo net = cm.getActiveNetworkInfo();
if (net != null && net.getType() == TYPE_WIFI && net.isConnected()) {
LOG.info("Connected to Wi-Fi");
if (socket == null || socket.isClosed()) bind();
if (isApEnabledEvent(i)) {
// The state change may be broadcast before the AP address is
// visible, so delay handling the event
scheduler.schedule(this::handleConnectivityChange, 1, SECONDS);
} else {
LOG.info("Not connected to Wi-Fi");
tryToClose(socket);
handleConnectivityChange();
}
}
private void handleConnectivityChange() {
if (!running) return;
Collection<InetAddress> addrs = getLocalIpAddresses();
if (addrs.contains(WIFI_AP_ADDRESS)) {
LOG.info("Providing wifi hotspot");
// There's no corresponding Network object and thus no way
// to get a suitable socket factory, so we won't be able to
// make outgoing connections on API 21+ if another network
// has internet access
socketFactory = SocketFactory.getDefault();
if (socket == null || socket.isClosed()) bind();
} else if (addrs.isEmpty()) {
LOG.info("Not connected to wifi");
socketFactory = SocketFactory.getDefault();
tryToClose(socket);
} else {
LOG.info("Connected to wifi");
socketFactory = getSocketFactory();
if (socket == null || socket.isClosed()) bind();
}
}
private boolean isApEnabledEvent(Intent i) {
return WIFI_AP_STATE_CHANGED_ACTION.equals(i.getAction()) &&
i.getIntExtra(EXTRA_WIFI_STATE, 0) == WIFI_AP_STATE_ENABLED;
}
}
}

View File

@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.concurrent.Immutable;
@@ -27,12 +28,15 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor;
private final ScheduledExecutorService scheduler;
private final BackoffFactory backoffFactory;
private final Context appContext;
public AndroidLanTcpPluginFactory(Executor ioExecutor,
BackoffFactory backoffFactory, Context appContext) {
ScheduledExecutorService scheduler, BackoffFactory backoffFactory,
Context appContext) {
this.ioExecutor = ioExecutor;
this.scheduler = scheduler;
this.backoffFactory = backoffFactory;
this.appContext = appContext;
}
@@ -51,7 +55,7 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new AndroidLanTcpPlugin(ioExecutor, backoff, appContext,
callback, MAX_LATENCY, MAX_IDLE_TIME);
return new AndroidLanTcpPlugin(ioExecutor, scheduler, backoff,
appContext, callback, MAX_LATENCY, MAX_IDLE_TIME);
}
}

View File

@@ -16,6 +16,7 @@ import android.os.PowerManager;
import net.freehaven.tor.control.EventHandler;
import net.freehaven.tor.control.TorControlConnection;
import org.briarproject.bramble.PoliteExecutor;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
@@ -63,8 +64,6 @@ import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.ZipInputStream;
@@ -111,7 +110,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private static final Logger LOG =
Logger.getLogger(TorPlugin.class.getName());
private final Executor ioExecutor;
private final Executor ioExecutor, connectionStatusExecutor;
private final ScheduledExecutorService scheduler;
private final Context appContext;
private final LocationUtils locationUtils;
@@ -125,7 +124,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private final File torDirectory, torFile, geoIpFile, configFile;
private final File doneFile, cookieFile;
private final PowerManager.WakeLock wakeLock;
private final Lock connectionStatusLock;
private final AtomicReference<Future<?>> connectivityCheck =
new AtomicReference<>();
private final AtomicBoolean used = new AtomicBoolean(false);
@@ -167,7 +165,9 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
// This tag will prevent Huawei's powermanager from killing us.
wakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, "LocationManagerService");
wakeLock.setReferenceCounted(false);
connectionStatusLock = new ReentrantLock();
// Don't execute more than one connection status check at a time
connectionStatusExecutor = new PoliteExecutor("TorPlugin",
ioExecutor, 1);
}
@Override
@@ -697,56 +697,46 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
private void updateConnectionStatus() {
ioExecutor.execute(() -> {
connectionStatusExecutor.execute(() -> {
if (!running) return;
Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
ConnectivityManager cm = (ConnectivityManager) o;
NetworkInfo net = cm.getActiveNetworkInfo();
boolean online = net != null && net.isConnected();
boolean wifi = online && net.getType() == TYPE_WIFI;
String country = locationUtils.getCurrentCountry();
boolean blocked = TorNetworkMetadata.isTorProbablyBlocked(
country);
Settings s = callback.getSettings();
int network = s.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_ALWAYS);
if (LOG.isLoggable(INFO)) {
LOG.info("Online: " + online + ", wifi: " + wifi);
if ("".equals(country)) LOG.info("Country code unknown");
else LOG.info("Country code: " + country);
}
try {
connectionStatusLock.lock();
updateConnectionStatusLocked();
} finally {
connectionStatusLock.unlock();
if (!online) {
LOG.info("Disabling network, device is offline");
enableNetwork(false);
} else if (blocked) {
LOG.info("Disabling network, country is blocked");
enableNetwork(false);
} else if (network == PREF_TOR_NETWORK_NEVER
|| (network == PREF_TOR_NETWORK_WIFI && !wifi)) {
LOG.info("Disabling network due to data setting");
enableNetwork(false);
} else {
LOG.info("Enabling network");
enableNetwork(true);
}
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
});
}
// Locking: connectionStatusLock
private void updateConnectionStatusLocked() {
Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
ConnectivityManager cm = (ConnectivityManager) o;
NetworkInfo net = cm.getActiveNetworkInfo();
boolean online = net != null && net.isConnected();
boolean wifi = online && net.getType() == TYPE_WIFI;
String country = locationUtils.getCurrentCountry();
boolean blocked = TorNetworkMetadata.isTorProbablyBlocked(
country);
Settings s = callback.getSettings();
int network = s.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_ALWAYS);
if (LOG.isLoggable(INFO)) {
LOG.info("Online: " + online + ", wifi: " + wifi);
if ("".equals(country)) LOG.info("Country code unknown");
else LOG.info("Country code: " + country);
}
try {
if (!online) {
LOG.info("Disabling network, device is offline");
enableNetwork(false);
} else if (blocked) {
LOG.info("Disabling network, country is blocked");
enableNetwork(false);
} else if (network == PREF_TOR_NETWORK_NEVER
|| (network == PREF_TOR_NETWORK_WIFI && !wifi)) {
LOG.info("Disabling network due to data setting");
enableNetwork(false);
} else {
LOG.info("Enabling network");
enableNetwork(true);
}
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
private void scheduleConnectionStatusUpdate() {
Future<?> newConnectivityCheck =
scheduler.schedule(this::updateConnectionStatus, 1, MINUTES);

View File

@@ -259,31 +259,30 @@ public interface DatabaseComponent {
Collection<LocalAuthor> getLocalAuthors(Transaction txn) throws DbException;
/**
* Returns the IDs of any messages that need to be validated by the given
* client.
* Returns the IDs of any messages that need to be validated.
* <p/>
* Read-only.
*/
Collection<MessageId> getMessagesToValidate(Transaction txn, ClientId c)
Collection<MessageId> getMessagesToValidate(Transaction txn)
throws DbException;
/**
* Returns the IDs of any messages that are valid but pending delivery due
* to dependencies on other messages for the given client.
* Returns the IDs of any messages that are pending delivery due to
* dependencies on other messages.
* <p/>
* Read-only.
*/
Collection<MessageId> getPendingMessages(Transaction txn, ClientId c)
Collection<MessageId> getPendingMessages(Transaction txn)
throws DbException;
/**
* Returns the IDs of any messages from the given client
* that have a shared dependent, but are still not shared themselves.
* Returns the IDs of any messages that have shared dependents but have
* not yet been shared themselves.
* <p/>
* Read-only.
*/
Collection<MessageId> getMessagesToShare(Transaction txn,
ClientId c) throws DbException;
Collection<MessageId> getMessagesToShare(Transaction txn)
throws DbException;
/**
* Returns the message with the given ID, in serialised form, or null if

View File

@@ -97,9 +97,12 @@ interface Database<T> {
/**
* Stores a message.
*
* @param sender the contact from whom the message was received, or null
* if the message was created locally.
*/
void addMessage(T txn, Message m, State state, boolean shared)
throws DbException;
void addMessage(T txn, Message m, State state, boolean shared,
@Nullable ContactId sender) throws DbException;
/**
* Adds a dependency between two messages in the given group.
@@ -112,16 +115,6 @@ interface Database<T> {
*/
void addOfferedMessage(T txn, ContactId c, MessageId m) throws DbException;
/**
* Initialises the status of the given message with respect to the given
* contact.
*
* @param ack whether the message needs to be acknowledged.
* @param seen whether the contact has seen the message.
*/
void addStatus(T txn, ContactId c, MessageId m, boolean ack, boolean seen)
throws DbException;
/**
* Stores a transport.
*/
@@ -280,7 +273,7 @@ interface Database<T> {
* <p/>
* Read-only.
*/
Collection<ContactId> getGroupVisibility(T txn, GroupId g)
Map<ContactId, Boolean> getGroupVisibility(T txn, GroupId g)
throws DbException;
/**
@@ -431,31 +424,27 @@ interface Database<T> {
throws DbException;
/**
* Returns the IDs of any messages that need to be validated by the given
* client.
* Returns the IDs of any messages that need to be validated.
* <p/>
* Read-only.
*/
Collection<MessageId> getMessagesToValidate(T txn, ClientId c)
throws DbException;
Collection<MessageId> getMessagesToValidate(T txn) throws DbException;
/**
* Returns the IDs of any messages that are still pending due to
* dependencies to other messages for the given client.
* Returns the IDs of any messages that are pending delivery due to
* dependencies on other messages.
* <p/>
* Read-only.
*/
Collection<MessageId> getPendingMessages(T txn, ClientId c)
throws DbException;
Collection<MessageId> getPendingMessages(T txn) throws DbException;
/**
* Returns the IDs of any messages from the given client
* that have a shared dependent, but are still not shared themselves.
* Returns the IDs of any messages that have a shared dependent but have
* not yet been shared themselves.
* <p/>
* Read-only.
*/
Collection<MessageId> getMessagesToShare(T txn, ClientId c)
throws DbException;
Collection<MessageId> getMessagesToShare(T txn) throws DbException;
/**
* Returns the next time (in milliseconds since the Unix epoch) when a
@@ -584,13 +573,6 @@ interface Database<T> {
*/
void removeMessage(T txn, MessageId m) throws DbException;
/**
* Removes an offered message that was offered by the given contact, or
* returns false if there is no such message.
*/
boolean removeOfferedMessage(T txn, ContactId c, MessageId m)
throws DbException;
/**
* Removes the given offered messages that were offered by the given
* contact.
@@ -598,12 +580,6 @@ interface Database<T> {
void removeOfferedMessages(T txn, ContactId c,
Collection<MessageId> requested) throws DbException;
/**
* Removes the status of the given message with respect to the given
* contact.
*/
void removeStatus(T txn, ContactId c, MessageId m) throws DbException;
/**
* Removes a transport (and all associated state) from the database.
*/

View File

@@ -215,7 +215,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
if (!db.containsGroup(txn, m.getGroupId()))
throw new NoSuchGroupException();
if (!db.containsMessage(txn, m.getId())) {
addMessage(txn, m, DELIVERED, shared, null);
db.addMessage(txn, m, DELIVERED, shared, null);
transaction.attach(new MessageAddedEvent(m, null));
transaction.attach(new MessageStateChangedEvent(m.getId(), true,
DELIVERED));
@@ -224,16 +224,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.mergeMessageMetadata(txn, m.getId(), meta);
}
private void addMessage(T txn, Message m, State state, boolean shared,
@Nullable ContactId sender) throws DbException {
db.addMessage(txn, m, state, shared);
for (ContactId c : db.getGroupVisibility(txn, m.getGroupId())) {
boolean offered = db.removeOfferedMessage(txn, c, m.getId());
boolean seen = offered || (sender != null && c.equals(sender));
db.addStatus(txn, c, m.getId(), seen, seen);
}
}
@Override
public void addTransport(Transaction transaction, TransportId t,
int maxLatency) throws DbException {
@@ -465,24 +455,24 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
}
@Override
public Collection<MessageId> getMessagesToValidate(Transaction transaction,
ClientId c) throws DbException {
public Collection<MessageId> getMessagesToValidate(Transaction transaction)
throws DbException {
T txn = unbox(transaction);
return db.getMessagesToValidate(txn, c);
return db.getMessagesToValidate(txn);
}
@Override
public Collection<MessageId> getPendingMessages(Transaction transaction,
ClientId c) throws DbException {
public Collection<MessageId> getPendingMessages(Transaction transaction)
throws DbException {
T txn = unbox(transaction);
return db.getPendingMessages(txn, c);
return db.getPendingMessages(txn);
}
@Override
public Collection<MessageId> getMessagesToShare(
Transaction transaction, ClientId c) throws DbException {
public Collection<MessageId> getMessagesToShare(Transaction transaction)
throws DbException {
T txn = unbox(transaction);
return db.getMessagesToShare(txn, c);
return db.getMessagesToShare(txn);
}
@Nullable
@@ -583,7 +573,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
@Override
public long getNextSendTime(Transaction transaction, ContactId c)
throws DbException {
throws DbException {
T txn = unbox(transaction);
return db.getNextSendTime(txn, c);
}
@@ -682,7 +672,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.raiseSeenFlag(txn, c, m.getId());
db.raiseAckFlag(txn, c, m.getId());
} else {
addMessage(txn, m, UNKNOWN, false, c);
db.addMessage(txn, m, UNKNOWN, false, c);
transaction.attach(new MessageAddedEvent(m, c));
}
transaction.attach(new MessageToAckEvent(c));
@@ -750,7 +740,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
GroupId id = g.getId();
if (!db.containsGroup(txn, id))
throw new NoSuchGroupException();
Collection<ContactId> affected = db.getGroupVisibility(txn, id);
Collection<ContactId> affected =
db.getGroupVisibility(txn, id).keySet();
db.removeGroup(txn, id);
transaction.attach(new GroupRemovedEvent(g));
transaction.attach(new GroupVisibilityUpdatedEvent(affected));
@@ -820,19 +811,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
throw new NoSuchGroupException();
Visibility old = db.getGroupVisibility(txn, c, g);
if (old == v) return;
if (old == INVISIBLE) {
db.addGroupVisibility(txn, c, g, v == SHARED);
for (MessageId m : db.getMessageIds(txn, g)) {
boolean seen = db.removeOfferedMessage(txn, c, m);
db.addStatus(txn, c, m, seen, seen);
}
} else if (v == INVISIBLE) {
db.removeGroupVisibility(txn, c, g);
for (MessageId m : db.getMessageIds(txn, g))
db.removeStatus(txn, c, m);
} else {
db.setGroupVisibility(txn, c, g, v == SHARED);
}
if (old == INVISIBLE) db.addGroupVisibility(txn, c, g, v == SHARED);
else if (v == INVISIBLE) db.removeGroupVisibility(txn, c, g);
else db.setGroupVisibility(txn, c, g, v == SHARED);
List<ContactId> affected = Collections.singletonList(c);
transaction.attach(new GroupVisibilityUpdatedEvent(affected));
}

View File

@@ -34,6 +34,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -72,7 +73,7 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
abstract class JdbcDatabase implements Database<Connection> {
// Package access for testing
static final int CODE_SCHEMA_VERSION = 31;
static final int CODE_SCHEMA_VERSION = 32;
private static final String CREATE_SETTINGS =
"CREATE TABLE settings"
@@ -188,6 +189,13 @@ abstract class JdbcDatabase implements Database<Connection> {
"CREATE TABLE statuses"
+ " (messageId HASH NOT NULL,"
+ " contactId INT NOT NULL,"
+ " groupId HASH NOT NULL," // Denormalised
+ " timestamp BIGINT NOT NULL," // Denormalised
+ " length INT NOT NULL," // Denormalised
+ " state INT NOT NULL," // Denormalised
+ " groupShared BOOLEAN NOT NULL," // Denormalised
+ " messageShared BOOLEAN NOT NULL," // Denormalised
+ " deleted BOOLEAN NOT NULL," // Denormalised
+ " ack BOOLEAN NOT NULL,"
+ " seen BOOLEAN NOT NULL,"
+ " requested BOOLEAN NOT NULL,"
@@ -199,6 +207,9 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " ON DELETE CASCADE,"
+ " FOREIGN KEY (contactId)"
+ " REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE,"
+ " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE)";
private static final String CREATE_TRANSPORTS =
@@ -252,6 +263,14 @@ abstract class JdbcDatabase implements Database<Connection> {
"CREATE INDEX IF NOT EXISTS messageMetadataByGroupIdState"
+ " ON messageMetadata (groupId, state)";
private static final String INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID =
"CREATE INDEX IF NOT EXISTS statusesByContactIdGroupId"
+ " ON statuses (contactId, groupId)";
private static final String INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP =
"CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp"
+ " ON statuses (contactId, timestamp)";
private static final Logger LOG =
Logger.getLogger(JdbcDatabase.class.getName());
@@ -343,7 +362,7 @@ abstract class JdbcDatabase implements Database<Connection> {
// Package access for testing
List<Migration<Connection>> getMigrations() {
return Collections.singletonList(new Migration30_31());
return Arrays.asList(new Migration30_31(), new Migration31_32());
}
private void storeSchemaVersion(Connection txn, int version)
@@ -401,6 +420,8 @@ abstract class JdbcDatabase implements Database<Connection> {
s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
s.executeUpdate(INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
s.close();
} catch (SQLException e) {
tryToClose(s);
@@ -578,7 +599,7 @@ abstract class JdbcDatabase implements Database<Connection> {
@Override
public void addGroupVisibility(Connection txn, ContactId c, GroupId g,
boolean shared) throws DbException {
boolean groupShared) throws DbException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO groupVisibilities"
@@ -587,16 +608,50 @@ abstract class JdbcDatabase implements Database<Connection> {
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setBytes(2, g.getBytes());
ps.setBoolean(3, shared);
ps.setBoolean(3, groupShared);
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
// Create a status row for each message in the group
addStatus(txn, c, g, groupShared);
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
}
}
private void addStatus(Connection txn, ContactId c, GroupId g,
boolean groupShared) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT messageId, timestamp, state, shared,"
+ " length, raw IS NULL"
+ " FROM messages"
+ " WHERE groupId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes());
rs = ps.executeQuery();
while (rs.next()) {
MessageId id = new MessageId(rs.getBytes(1));
long timestamp = rs.getLong(2);
State state = State.fromValue(rs.getInt(3));
boolean messageShared = rs.getBoolean(4);
int length = rs.getInt(5);
boolean deleted = rs.getBoolean(6);
boolean seen = removeOfferedMessage(txn, c, id);
addStatus(txn, id, c, g, timestamp, length, state, groupShared,
messageShared, deleted, seen);
}
rs.close();
ps.close();
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
@Override
public void addLocalAuthor(Connection txn, LocalAuthor a)
throws DbException {
@@ -622,7 +677,8 @@ abstract class JdbcDatabase implements Database<Connection> {
@Override
public void addMessage(Connection txn, Message m, State state,
boolean shared) throws DbException {
boolean messageShared, @Nullable ContactId sender)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
@@ -633,13 +689,24 @@ abstract class JdbcDatabase implements Database<Connection> {
ps.setBytes(2, m.getGroupId().getBytes());
ps.setLong(3, m.getTimestamp());
ps.setInt(4, state.getValue());
ps.setBoolean(5, shared);
ps.setBoolean(5, messageShared);
byte[] raw = m.getRaw();
ps.setInt(6, raw.length);
ps.setBytes(7, raw);
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
// Create a status row for each contact that can see the group
Map<ContactId, Boolean> visibility =
getGroupVisibility(txn, m.getGroupId());
for (Entry<ContactId, Boolean> e : visibility.entrySet()) {
ContactId c = e.getKey();
boolean offered = removeOfferedMessage(txn, c, m.getId());
boolean seen = offered || (sender != null && c.equals(sender));
addStatus(txn, m.getId(), c, m.getGroupId(), m.getTimestamp(),
m.getLength(), state, e.getValue(), messageShared,
false, seen);
}
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
@@ -677,19 +744,28 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public void addStatus(Connection txn, ContactId c, MessageId m, boolean ack,
boolean seen) throws DbException {
private void addStatus(Connection txn, MessageId m, ContactId c, GroupId g,
long timestamp, int length, State state, boolean groupShared,
boolean messageShared, boolean deleted, boolean seen)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO statuses (messageId, contactId, ack,"
+ " seen, requested, expiry, txCount)"
+ " VALUES (?, ?, ?, ?, FALSE, 0, 0)";
String sql = "INSERT INTO statuses (messageId, contactId, groupId,"
+ " timestamp, length, state, groupShared, messageShared,"
+ " deleted, ack, seen, requested, expiry, txCount)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, 0, 0)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
ps.setBoolean(3, ack);
ps.setBoolean(4, seen);
ps.setBytes(3, g.getBytes());
ps.setLong(4, timestamp);
ps.setInt(5, length);
ps.setInt(6, state.getValue());
ps.setBoolean(7, groupShared);
ps.setBoolean(8, messageShared);
ps.setBoolean(9, deleted);
ps.setBoolean(10, seen);
ps.setBoolean(11, seen);
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
@@ -941,12 +1017,9 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT NULL FROM messages AS m"
+ " JOIN groupVisibilities AS gv"
+ " ON m.groupId = gv.groupId"
+ " WHERE messageId = ?"
+ " AND contactId = ?"
+ " AND m.shared = TRUE";
String sql = "SELECT NULL FROM statuses"
+ " WHERE messageId = ? AND contactId = ?"
+ " AND messageShared = TRUE";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
@@ -998,6 +1071,13 @@ abstract class JdbcDatabase implements Database<Connection> {
if (affected < 0) throw new DbStateException();
if (affected > 1) throw new DbStateException();
ps.close();
// Update denormalised column in statuses
sql = "UPDATE statuses SET deleted = TRUE WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
@@ -1220,18 +1300,19 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public Collection<ContactId> getGroupVisibility(Connection txn, GroupId g)
public Map<ContactId, Boolean> getGroupVisibility(Connection txn, GroupId g)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT contactId FROM groupVisibilities"
String sql = "SELECT contactId, shared FROM groupVisibilities"
+ " WHERE groupId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes());
rs = ps.executeQuery();
List<ContactId> visible = new ArrayList<>();
while (rs.next()) visible.add(new ContactId(rs.getInt(1)));
Map<ContactId, Boolean> visible = new HashMap<>();
while (rs.next())
visible.put(new ContactId(rs.getInt(1)), rs.getBoolean(2));
rs.close();
ps.close();
return visible;
@@ -1509,12 +1590,8 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT m.messageId, txCount > 0, seen"
+ " FROM messages AS m"
+ " JOIN statuses AS s"
+ " ON m.messageId = s.messageId"
+ " WHERE groupId = ?"
+ " AND contactId = ?";
String sql = "SELECT messageId, txCount > 0, seen FROM statuses"
+ " WHERE groupId = ? AND contactId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes());
ps.setInt(2, c.getInt());
@@ -1537,15 +1614,13 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public MessageStatus getMessageStatus(Connection txn,
ContactId c, MessageId m) throws DbException {
public MessageStatus getMessageStatus(Connection txn, ContactId c,
MessageId m) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT txCount > 0, seen"
+ " FROM statuses"
+ " WHERE messageId = ?"
+ " AND contactId = ?";
String sql = "SELECT txCount > 0, seen FROM statuses"
+ " WHERE messageId = ? AND contactId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
@@ -1687,14 +1762,10 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT m.messageId FROM messages AS m"
+ " JOIN groupVisibilities AS gv"
+ " ON m.groupId = gv.groupId"
+ " JOIN statuses AS s"
+ " ON m.messageId = s.messageId"
+ " AND gv.contactId = s.contactId"
+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
String sql = "SELECT messageId FROM statuses"
+ " WHERE contactId = ? AND state = ?"
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE"
+ " AND seen = FALSE AND requested = FALSE"
+ " AND expiry < ?"
+ " ORDER BY timestamp LIMIT ?";
@@ -1748,14 +1819,10 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT length, m.messageId FROM messages AS m"
+ " JOIN groupVisibilities AS gv"
+ " ON m.groupId = gv.groupId"
+ " JOIN statuses AS s"
+ " ON m.messageId = s.messageId"
+ " AND gv.contactId = s.contactId"
+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
String sql = "SELECT length, messageId FROM statuses"
+ " WHERE contactId = ? AND state = ?"
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE"
+ " AND seen = FALSE"
+ " AND expiry < ?"
+ " ORDER BY timestamp";
@@ -1783,28 +1850,26 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public Collection<MessageId> getMessagesToValidate(Connection txn,
ClientId c) throws DbException {
return getMessagesInState(txn, c, UNKNOWN);
public Collection<MessageId> getMessagesToValidate(Connection txn)
throws DbException {
return getMessagesInState(txn, UNKNOWN);
}
@Override
public Collection<MessageId> getPendingMessages(Connection txn,
ClientId c) throws DbException {
return getMessagesInState(txn, c, PENDING);
public Collection<MessageId> getPendingMessages(Connection txn)
throws DbException {
return getMessagesInState(txn, PENDING);
}
private Collection<MessageId> getMessagesInState(Connection txn, ClientId c,
private Collection<MessageId> getMessagesInState(Connection txn,
State state) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT messageId FROM messages AS m"
+ " JOIN groups AS g ON m.groupId = g.groupId"
+ " WHERE state = ? AND clientId = ? AND raw IS NOT NULL";
String sql = "SELECT messageId FROM messages"
+ " WHERE state = ? AND raw IS NOT NULL";
ps = txn.prepareStatement(sql);
ps.setInt(1, state.getValue());
ps.setString(2, c.getString());
rs = ps.executeQuery();
List<MessageId> ids = new ArrayList<>();
while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
@@ -1819,8 +1884,8 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public Collection<MessageId> getMessagesToShare(
Connection txn, ClientId c) throws DbException {
public Collection<MessageId> getMessagesToShare(Connection txn)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
@@ -1829,12 +1894,10 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " ON m.messageId = d.dependencyId"
+ " JOIN messages AS m1"
+ " ON d.messageId = m1.messageId"
+ " JOIN groups AS g"
+ " ON m.groupId = g.groupId"
+ " WHERE m.shared = FALSE AND m1.shared = TRUE"
+ " AND g.clientId = ?";
+ " WHERE m.state = ?"
+ " AND m.shared = FALSE AND m1.shared = TRUE";
ps = txn.prepareStatement(sql);
ps.setString(1, c.getString());
ps.setInt(1, DELIVERED.getValue());
rs = ps.executeQuery();
List<MessageId> ids = new ArrayList<>();
while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
@@ -1854,15 +1917,10 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT expiry FROM messages AS m"
+ " JOIN groupVisibilities AS gv"
+ " ON m.groupId = gv.groupId"
+ " JOIN statuses AS s"
+ " ON m.messageId = s.messageId"
+ " AND gv.contactId = s.contactId"
+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
+ " AND seen = FALSE"
String sql = "SELECT expiry FROM statuses"
+ " WHERE contactId = ? AND state = ?"
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE AND seen = FALSE"
+ " ORDER BY expiry LIMIT 1";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
@@ -1914,14 +1972,10 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT length, m.messageId FROM messages AS m"
+ " JOIN groupVisibilities AS gv"
+ " ON m.groupId = gv.groupId"
+ " JOIN statuses AS s"
+ " ON m.messageId = s.messageId"
+ " AND gv.contactId = s.contactId"
+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
String sql = "SELECT length, messageId FROM statuses"
+ " WHERE contactId = ? AND state = ?"
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE"
+ " AND seen = FALSE AND requested = TRUE"
+ " AND expiry < ?"
+ " ORDER BY timestamp";
@@ -2397,6 +2451,8 @@ abstract class JdbcDatabase implements Database<Connection> {
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
// Remove status rows for the messages in the group
for (MessageId m : getMessageIds(txn, g)) removeStatus(txn, c, m);
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
@@ -2436,8 +2492,7 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public boolean removeOfferedMessage(Connection txn, ContactId c,
private boolean removeOfferedMessage(Connection txn, ContactId c,
MessageId m) throws DbException {
PreparedStatement ps = null;
try {
@@ -2481,16 +2536,15 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public void removeStatus(Connection txn, ContactId c, MessageId m)
private void removeStatus(Connection txn, ContactId c, MessageId m)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "DELETE FROM statuses"
+ " WHERE contactId = ? AND messageId = ?";
+ " WHERE messageId = ? AND contactId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setBytes(2, m.getBytes());
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
@@ -2586,6 +2640,16 @@ abstract class JdbcDatabase implements Database<Connection> {
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
// Update denormalised column in statuses
sql = "UPDATE statuses SET groupShared = ?"
+ " WHERE contactId = ? AND groupId = ?";
ps = txn.prepareStatement(sql);
ps.setBoolean(1, shared);
ps.setInt(2, c.getInt());
ps.setBytes(3, g.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
@@ -2604,6 +2668,14 @@ abstract class JdbcDatabase implements Database<Connection> {
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
// Update denormalised column in statuses
sql = "UPDATE statuses SET messageShared = TRUE"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
@@ -2630,6 +2702,14 @@ abstract class JdbcDatabase implements Database<Connection> {
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
// Update denormalised column in statuses
sql = "UPDATE statuses SET state = ? WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, state.getValue());
ps.setBytes(2, m.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);

View File

@@ -0,0 +1,84 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DbException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.logging.Level.WARNING;
class Migration31_32 implements Migration<Connection> {
private static final Logger LOG =
Logger.getLogger(Migration31_32.class.getName());
@Override
public int getStartVersion() {
return 31;
}
@Override
public int getEndVersion() {
return 32;
}
@Override
public void migrate(Connection txn) throws DbException {
Statement s = null;
try {
s = txn.createStatement();
// Add denormalised columns
s.execute("ALTER TABLE statuses ADD COLUMN"
+ " (groupId BINARY(32),"
+ " timestamp BIGINT,"
+ " length INT,"
+ " state INT,"
+ " groupShared BOOLEAN,"
+ " messageShared BOOLEAN,"
+ " deleted BOOLEAN)");
// Populate columns from messages table
s.execute("UPDATE statuses AS s SET (groupId, timestamp, length,"
+ " state, messageShared, deleted) ="
+ " (SELECT groupId, timestamp, length, state, shared,"
+ " raw IS NULL FROM messages AS m"
+ " WHERE s.messageId = m.messageId)");
// Populate column from groupVisibilities table
s.execute("UPDATE statuses AS s SET groupShared ="
+ " (SELECT shared FROM groupVisibilities AS gv"
+ " WHERE s.contactId = gv.contactId"
+ " AND s.groupId = gv.groupId)");
// Add not null constraints now columns have been populated
s.execute("ALTER TABLE statuses ALTER COLUMN groupId SET NOT NULL");
s.execute("ALTER TABLE statuses ALTER COLUMN timestamp"
+ " SET NOT NULL");
s.execute("ALTER TABLE statuses ALTER COLUMN length SET NOT NULL");
s.execute("ALTER TABLE statuses ALTER COLUMN state SET NOT NULL");
s.execute("ALTER TABLE statuses ALTER COLUMN groupShared"
+ " SET NOT NULL");
s.execute("ALTER TABLE statuses ALTER COLUMN messageShared"
+ " SET NOT NULL");
s.execute("ALTER TABLE statuses ALTER COLUMN deleted SET NOT NULL");
// Add foreign key constraint
s.execute("ALTER TABLE statuses"
+ " ADD CONSTRAINT statusesForeignKeyGroupId"
+ " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE");
} catch (SQLException e) {
tryToClose(s);
throw new DbException(e);
}
}
private void tryToClose(@Nullable Statement s) {
try {
if (s != null) s.close();
} catch (SQLException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
}

View File

@@ -241,10 +241,11 @@ class LanTcpPlugin extends TcpPlugin {
}
return null;
}
Socket s = new Socket();
try {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubSocketAddress(remote));
Socket s = createSocket();
s.bind(new InetSocketAddress(socket.getInetAddress(), 0));
s.connect(remote);
s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO))

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.plugin.tcp;
import org.briarproject.bramble.PoliteExecutor;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
@@ -47,7 +48,7 @@ abstract class TcpPlugin implements DuplexPlugin {
private static final Logger LOG =
Logger.getLogger(TcpPlugin.class.getName());
protected final Executor ioExecutor;
protected final Executor ioExecutor, bindExecutor;
protected final Backoff backoff;
protected final DuplexPluginCallback callback;
protected final int maxLatency, maxIdleTime, socketTimeout;
@@ -90,6 +91,8 @@ abstract class TcpPlugin implements DuplexPlugin {
if (maxIdleTime > Integer.MAX_VALUE / 2)
socketTimeout = Integer.MAX_VALUE;
else socketTimeout = maxIdleTime * 2;
// Don't execute more than one bind operation at a time
bindExecutor = new PoliteExecutor("TcpPlugin", ioExecutor, 1);
}
@Override
@@ -110,8 +113,9 @@ abstract class TcpPlugin implements DuplexPlugin {
}
protected void bind() {
ioExecutor.execute(() -> {
bindExecutor.execute(() -> {
if (!running) return;
if (socket != null && !socket.isClosed()) return;
ServerSocket ss = null;
for (InetSocketAddress addr : getLocalSocketAddresses()) {
try {
@@ -243,10 +247,11 @@ abstract class TcpPlugin implements DuplexPlugin {
}
continue;
}
Socket s = new Socket();
try {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubSocketAddress(remote));
Socket s = createSocket();
s.bind(new InetSocketAddress(socket.getInetAddress(), 0));
s.connect(remote);
s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO))
@@ -261,6 +266,10 @@ abstract class TcpPlugin implements DuplexPlugin {
return null;
}
protected Socket createSocket() throws IOException {
return new Socket();
}
@Nullable
InetSocketAddress parseSocketAddress(String ipPort) {
if (StringUtils.isNullOrEmpty(ipPort)) return null;

View File

@@ -71,11 +71,9 @@ class ValidationManagerImpl implements ValidationManager, Service,
@Override
public void startService() {
if (used.getAndSet(true)) throw new IllegalStateException();
for (ClientId c : validators.keySet()) {
validateOutstandingMessagesAsync(c);
deliverOutstandingMessagesAsync(c);
shareOutstandingMessagesAsync(c);
}
validateOutstandingMessagesAsync();
deliverOutstandingMessagesAsync();
shareOutstandingMessagesAsync();
}
@Override
@@ -93,17 +91,17 @@ class ValidationManagerImpl implements ValidationManager, Service,
hooks.put(c, hook);
}
private void validateOutstandingMessagesAsync(ClientId c) {
dbExecutor.execute(() -> validateOutstandingMessages(c));
private void validateOutstandingMessagesAsync() {
dbExecutor.execute(this::validateOutstandingMessages);
}
@DatabaseExecutor
private void validateOutstandingMessages(ClientId c) {
private void validateOutstandingMessages() {
try {
Queue<MessageId> unvalidated = new LinkedList<>();
Transaction txn = db.startTransaction(true);
try {
unvalidated.addAll(db.getMessagesToValidate(txn, c));
unvalidated.addAll(db.getMessagesToValidate(txn));
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
@@ -148,17 +146,17 @@ class ValidationManagerImpl implements ValidationManager, Service,
}
}
private void deliverOutstandingMessagesAsync(ClientId c) {
dbExecutor.execute(() -> deliverOutstandingMessages(c));
private void deliverOutstandingMessagesAsync() {
dbExecutor.execute(this::deliverOutstandingMessages);
}
@DatabaseExecutor
private void deliverOutstandingMessages(ClientId c) {
private void deliverOutstandingMessages() {
try {
Queue<MessageId> pending = new LinkedList<>();
Transaction txn = db.startTransaction(true);
try {
pending.addAll(db.getPendingMessages(txn, c));
pending.addAll(db.getPendingMessages(txn));
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
@@ -353,17 +351,17 @@ class ValidationManagerImpl implements ValidationManager, Service,
return pending;
}
private void shareOutstandingMessagesAsync(ClientId c) {
dbExecutor.execute(() -> shareOutstandingMessages(c));
private void shareOutstandingMessagesAsync() {
dbExecutor.execute(this::shareOutstandingMessages);
}
@DatabaseExecutor
private void shareOutstandingMessages(ClientId c) {
private void shareOutstandingMessages() {
try {
Queue<MessageId> toShare = new LinkedList<>();
Transaction txn = db.startTransaction(true);
try {
toShare.addAll(db.getMessagesToShare(txn, c));
toShare.addAll(db.getMessagesToShare(txn));
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);

View File

@@ -4,8 +4,9 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -25,7 +26,10 @@ public class SystemModule {
private final ScheduledExecutorService scheduler;
public SystemModule() {
scheduler = Executors.newSingleThreadScheduledExecutor();
// Discard tasks that are submitted during shutdown
RejectedExecutionHandler policy =
new ScheduledThreadPoolExecutor.DiscardPolicy();
scheduler = new ScheduledThreadPoolExecutor(1, policy);
}
@Provides

View File

@@ -48,6 +48,7 @@ import org.briarproject.bramble.api.transport.IncomingKeys;
import org.briarproject.bramble.api.transport.OutgoingKeys;
import org.briarproject.bramble.api.transport.TransportKeys;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.CaptureArgumentAction;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.StringUtils;
import org.jmock.Expectations;
@@ -56,9 +57,12 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
@@ -160,7 +164,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
ContactStatusChangedEvent.class)));
// getContacts()
oneOf(database).getContacts(txn);
will(returnValue(Collections.singletonList(contact)));
will(returnValue(singletonList(contact)));
// addGroup()
oneOf(database).containsGroup(txn, groupId);
will(returnValue(false));
@@ -171,12 +175,12 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(true));
// getGroups()
oneOf(database).getGroups(txn, clientId);
will(returnValue(Collections.singletonList(group)));
will(returnValue(singletonList(group)));
// removeGroup()
oneOf(database).containsGroup(txn, groupId);
will(returnValue(true));
oneOf(database).getGroupVisibility(txn, groupId);
will(returnValue(Collections.emptyList()));
will(returnValue(emptyMap()));
oneOf(database).removeGroup(txn, groupId);
oneOf(eventBus).broadcast(with(any(GroupRemovedEvent.class)));
oneOf(eventBus).broadcast(with(any(
@@ -206,11 +210,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
assertEquals(contactId,
db.addContact(transaction, author, localAuthorId, true,
true));
assertEquals(Collections.singletonList(contact),
assertEquals(singletonList(contact),
db.getContacts(transaction));
db.addGroup(transaction, group); // First time - listeners called
db.addGroup(transaction, group); // Second time - not called
assertEquals(Collections.singletonList(group),
assertEquals(singletonList(group),
db.getGroups(transaction, clientId));
db.removeGroup(transaction, group);
db.removeContact(transaction, contactId);
@@ -255,13 +259,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(true));
oneOf(database).containsMessage(txn, messageId);
will(returnValue(false));
oneOf(database).addMessage(txn, message, DELIVERED, true);
oneOf(database).addMessage(txn, message, DELIVERED, true, null);
oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
oneOf(database).getGroupVisibility(txn, groupId);
will(returnValue(Collections.singletonList(contactId)));
oneOf(database).removeOfferedMessage(txn, contactId, messageId);
will(returnValue(false));
oneOf(database).addStatus(txn, contactId, messageId, false, false);
oneOf(database).commitTransaction(txn);
// The message was added, so the listeners should be called
oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
@@ -397,7 +396,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
transaction = db.startTransaction(false);
try {
Ack a = new Ack(Collections.singletonList(messageId));
Ack a = new Ack(singletonList(messageId));
db.receiveAck(transaction, contactId, a);
fail();
} catch (NoSuchContactException expected) {
@@ -418,7 +417,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
transaction = db.startTransaction(false);
try {
Offer o = new Offer(Collections.singletonList(messageId));
Offer o = new Offer(singletonList(messageId));
db.receiveOffer(transaction, contactId, o);
fail();
} catch (NoSuchContactException expected) {
@@ -429,7 +428,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
transaction = db.startTransaction(false);
try {
Request r = new Request(Collections.singletonList(messageId));
Request r = new Request(singletonList(messageId));
db.receiveRequest(transaction, contactId, r);
fail();
} catch (NoSuchContactException expected) {
@@ -1022,7 +1021,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
Transaction transaction = db.startTransaction(false);
try {
Ack a = new Ack(Collections.singletonList(messageId));
Ack a = new Ack(singletonList(messageId));
db.receiveAck(transaction, contactId, a);
db.commitTransaction(transaction);
} finally {
@@ -1042,12 +1041,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(VISIBLE));
oneOf(database).containsMessage(txn, messageId);
will(returnValue(false));
oneOf(database).addMessage(txn, message, UNKNOWN, false);
oneOf(database).getGroupVisibility(txn, groupId);
will(returnValue(Collections.singletonList(contactId)));
oneOf(database).removeOfferedMessage(txn, contactId, messageId);
will(returnValue(false));
oneOf(database).addStatus(txn, contactId, messageId, true, true);
oneOf(database).addMessage(txn, message, UNKNOWN, false, contactId);
// Second time
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
@@ -1197,7 +1191,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
Transaction transaction = db.startTransaction(false);
try {
Request r = new Request(Collections.singletonList(messageId));
Request r = new Request(singletonList(messageId));
db.receiveRequest(transaction, contactId, r);
db.commitTransaction(transaction);
} finally {
@@ -1206,7 +1200,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
@Test
public void testChangingVisibilityCallsListeners() throws Exception {
public void testChangingVisibilityFromInvisibleToVisibleCallsListeners()
throws Exception {
AtomicReference<GroupVisibilityUpdatedEvent> event =
new AtomicReference<>();
context.checking(new Expectations() {{
oneOf(database).startTransaction();
will(returnValue(txn));
@@ -1215,16 +1213,13 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
oneOf(database).containsGroup(txn, groupId);
will(returnValue(true));
oneOf(database).getGroupVisibility(txn, contactId, groupId);
will(returnValue(INVISIBLE)); // Not yet visible
will(returnValue(INVISIBLE));
oneOf(database).addGroupVisibility(txn, contactId, groupId, false);
oneOf(database).getMessageIds(txn, groupId);
will(returnValue(Collections.singletonList(messageId)));
oneOf(database).removeOfferedMessage(txn, contactId, messageId);
will(returnValue(false));
oneOf(database).addStatus(txn, contactId, messageId, false, false);
oneOf(database).commitTransaction(txn);
oneOf(eventBus).broadcast(with(any(
GroupVisibilityUpdatedEvent.class)));
will(new CaptureArgumentAction<>(event,
GroupVisibilityUpdatedEvent.class, 0));
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
shutdown);
@@ -1236,6 +1231,48 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} finally {
db.endTransaction(transaction);
}
GroupVisibilityUpdatedEvent e = event.get();
assertNotNull(e);
assertEquals(singletonList(contactId), e.getAffectedContacts());
}
@Test
public void testChangingVisibilityFromVisibleToInvisibleCallsListeners()
throws Exception {
AtomicReference<GroupVisibilityUpdatedEvent> event =
new AtomicReference<>();
context.checking(new Expectations() {{
oneOf(database).startTransaction();
will(returnValue(txn));
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
oneOf(database).containsGroup(txn, groupId);
will(returnValue(true));
oneOf(database).getGroupVisibility(txn, contactId, groupId);
will(returnValue(VISIBLE));
oneOf(database).removeGroupVisibility(txn, contactId, groupId);
oneOf(database).commitTransaction(txn);
oneOf(eventBus).broadcast(with(any(
GroupVisibilityUpdatedEvent.class)));
will(new CaptureArgumentAction<>(event,
GroupVisibilityUpdatedEvent.class, 0));
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
shutdown);
Transaction transaction = db.startTransaction(false);
try {
db.setGroupVisibility(transaction, contactId, groupId, INVISIBLE);
db.commitTransaction(transaction);
} finally {
db.endTransaction(transaction);
}
GroupVisibilityUpdatedEvent e = event.get();
assertNotNull(e);
assertEquals(singletonList(contactId), e.getAffectedContacts());
}
@Test
@@ -1267,8 +1304,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@Test
public void testTransportKeys() throws Exception {
TransportKeys transportKeys = createTransportKeys();
Map<ContactId, TransportKeys> keys = Collections.singletonMap(
contactId, transportKeys);
Map<ContactId, TransportKeys> keys =
singletonMap(contactId, transportKeys);
context.checking(new Expectations() {{
// startTransaction()
oneOf(database).startTransaction();
@@ -1476,13 +1513,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(true));
oneOf(database).containsMessage(txn, messageId);
will(returnValue(false));
oneOf(database).addMessage(txn, message, DELIVERED, true);
oneOf(database).getGroupVisibility(txn, groupId);
will(returnValue(Collections.singletonList(contactId)));
oneOf(database).addMessage(txn, message, DELIVERED, true, null);
oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
oneOf(database).removeOfferedMessage(txn, contactId, messageId);
will(returnValue(false));
oneOf(database).addStatus(txn, contactId, messageId, false, false);
// addMessageDependencies()
oneOf(database).containsMessage(txn, messageId);
will(returnValue(true));

View File

@@ -6,7 +6,6 @@ import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.settings.Settings;
@@ -25,7 +24,6 @@ import org.briarproject.bramble.system.SystemClock;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestDatabaseConfig;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.StringUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -45,7 +43,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
@@ -55,6 +52,12 @@ import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERE
import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -74,7 +77,6 @@ public class H2DatabaseTest extends BrambleTestCase {
private final ClientId clientId;
private final Group group;
private final Author author;
private final AuthorId localAuthorId;
private final LocalAuthor localAuthor;
private final MessageId messageId;
private final long timestamp;
@@ -85,19 +87,16 @@ public class H2DatabaseTest extends BrambleTestCase {
private final ContactId contactId;
public H2DatabaseTest() throws Exception {
groupId = new GroupId(TestUtils.getRandomId());
clientId = new ClientId(StringUtils.getRandomString(5));
groupId = new GroupId(getRandomId());
clientId = new ClientId(getRandomString(123));
byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
group = new Group(groupId, clientId, descriptor);
AuthorId authorId = new AuthorId(TestUtils.getRandomId());
author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]);
localAuthorId = new AuthorId(TestUtils.getRandomId());
author = getAuthor();
localAuthor = getLocalAuthor();
messageId = new MessageId(getRandomId());
timestamp = System.currentTimeMillis();
localAuthor = new LocalAuthor(localAuthorId, "Bob",
new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
messageId = new MessageId(TestUtils.getRandomId());
size = 1234;
raw = TestUtils.getRandomBytes(size);
raw = getRandomBytes(size);
message = new Message(messageId, groupId, timestamp, raw);
transportId = new TransportId("id");
contactId = new ContactId(1);
@@ -115,14 +114,14 @@ public class H2DatabaseTest extends BrambleTestCase {
Connection txn = db.startTransaction();
assertFalse(db.containsContact(txn, contactId));
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
assertTrue(db.containsContact(txn, contactId));
assertFalse(db.containsGroup(txn, groupId));
db.addGroup(txn, group);
assertTrue(db.containsGroup(txn, groupId));
assertFalse(db.containsMessage(txn, messageId));
db.addMessage(txn, message, DELIVERED, true);
db.addMessage(txn, message, DELIVERED, true, null);
assertTrue(db.containsMessage(txn, messageId));
db.commitTransaction(txn);
db.close();
@@ -160,7 +159,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true);
db.addMessage(txn, message, DELIVERED, true, null);
// Removing the group should remove the message
assertTrue(db.containsMessage(txn, messageId));
@@ -178,22 +177,15 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact, a shared group and a shared message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true);
db.addMessage(txn, message, DELIVERED, true, null);
// The message has no status yet, so it should not be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
ONE_MEGABYTE);
assertTrue(ids.isEmpty());
ids = db.getMessagesToOffer(txn, contactId, 100);
assertTrue(ids.isEmpty());
// Adding a status with seen = false should make the message sendable
db.addStatus(txn, contactId, messageId, false, false);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
// The contact has not seen the message, so it should be sendable
Collection<MessageId> ids =
db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertEquals(Collections.singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100);
assertEquals(Collections.singletonList(messageId), ids);
@@ -216,12 +208,11 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact, a shared group and a shared but unvalidated message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, UNKNOWN, true);
db.addStatus(txn, contactId, messageId, false, false);
db.addMessage(txn, message, UNKNOWN, true, null);
// The message has not been validated, so it should not be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -262,11 +253,10 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact, an invisible group and a shared message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true);
db.addStatus(txn, contactId, messageId, false, false);
db.addMessage(txn, message, DELIVERED, true, null);
// The group is invisible, so the message should not be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -314,12 +304,11 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact, a shared group and an unshared message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, false);
db.addStatus(txn, contactId, messageId, false, false);
db.addMessage(txn, message, DELIVERED, false, null);
// The message is not shared, so it should not be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -346,12 +335,11 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact, a shared group and a shared message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true);
db.addStatus(txn, contactId, messageId, false, false);
db.addMessage(txn, message, DELIVERED, true, null);
// The message is sendable, but too large to send
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -373,20 +361,16 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact and a visible group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, false);
// Add some messages to ack
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
MessageId messageId1 = new MessageId(getRandomId());
Message message1 = new Message(messageId1, groupId, timestamp, raw);
db.addMessage(txn, message, DELIVERED, true);
db.addStatus(txn, contactId, messageId, false, true);
db.raiseAckFlag(txn, contactId, messageId);
db.addMessage(txn, message1, DELIVERED, true);
db.addStatus(txn, contactId, messageId1, false, true);
db.raiseAckFlag(txn, contactId, messageId1);
db.addMessage(txn, message, DELIVERED, true, contactId);
db.addMessage(txn, message1, DELIVERED, true, contactId);
// Both message IDs should be returned
Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
@@ -399,6 +383,14 @@ public class H2DatabaseTest extends BrambleTestCase {
assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
contactId, 1234));
// Raise the ack flag again
db.raiseAckFlag(txn, contactId, messageId);
db.raiseAckFlag(txn, contactId, messageId1);
// Both message IDs should be returned
ids = db.getMessagesToAck(txn, contactId, 1234);
assertEquals(Arrays.asList(messageId, messageId1), ids);
db.commitTransaction(txn);
db.close();
}
@@ -410,12 +402,11 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact, a shared group and a shared message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true);
db.addStatus(txn, contactId, messageId, false, false);
db.addMessage(txn, message, DELIVERED, true, null);
// Retrieve the message from the database and mark it as sent
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -456,7 +447,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Storing a message should reduce the free space
Connection txn = db.startTransaction();
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true);
db.addMessage(txn, message, DELIVERED, true, null);
db.commitTransaction(txn);
assertTrue(db.getFreeSpace() < free);
@@ -568,7 +559,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact and a shared group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
@@ -588,7 +579,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
// The group is not in the database
@@ -604,15 +595,14 @@ public class H2DatabaseTest extends BrambleTestCase {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact, a group and a message
// Add a contact, an invisible group and a message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true);
db.addStatus(txn, contactId, messageId, false, false);
db.addMessage(txn, message, DELIVERED, true, null);
// The group is not visible
// The group is not visible so the message should not be visible
assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
db.commitTransaction(txn);
@@ -626,37 +616,37 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact and a group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
// The group should not be visible to the contact
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(Collections.emptyList(),
assertEquals(Collections.emptyMap(),
db.getGroupVisibility(txn, groupId));
// Make the group visible to the contact
db.addGroupVisibility(txn, contactId, groupId, false);
assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(Collections.singletonList(contactId),
assertEquals(Collections.singletonMap(contactId, false),
db.getGroupVisibility(txn, groupId));
// Share the group with the contact
db.setGroupVisibility(txn, contactId, groupId, true);
assertEquals(SHARED, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(Collections.singletonList(contactId),
assertEquals(Collections.singletonMap(contactId, true),
db.getGroupVisibility(txn, groupId));
// Unshare the group with the contact
db.setGroupVisibility(txn, contactId, groupId, false);
assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(Collections.singletonList(contactId),
assertEquals(Collections.singletonMap(contactId, false),
db.getGroupVisibility(txn, groupId));
// Make the group invisible again
db.removeGroupVisibility(txn, contactId, groupId);
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(Collections.emptyList(),
assertEquals(Collections.emptyMap(),
db.getGroupVisibility(txn, groupId));
db.commitTransaction(txn);
@@ -676,7 +666,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add the contact, the transport and the transport keys
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addTransport(txn, transportId, 123);
db.addTransportKeys(txn, contactId, keys);
@@ -738,7 +728,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add the contact, transport and transport keys
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addTransport(txn, transportId, 123);
db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
@@ -774,7 +764,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add the contact, transport and transport keys
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addTransport(txn, transportId, 123);
db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
@@ -809,7 +799,7 @@ public class H2DatabaseTest extends BrambleTestCase {
db.addLocalAuthor(txn, localAuthor);
// Add a contact associated with the local author
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
// Ensure contact is returned from database by Author ID
@@ -834,18 +824,19 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a local author - no contacts should be associated
db.addLocalAuthor(txn, localAuthor);
Collection<ContactId> contacts = db.getContacts(txn, localAuthorId);
Collection<ContactId> contacts =
db.getContacts(txn, localAuthor.getId());
assertEquals(Collections.emptyList(), contacts);
// Add a contact associated with the local author
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
contacts = db.getContacts(txn, localAuthorId);
contacts = db.getContacts(txn, localAuthor.getId());
assertEquals(Collections.singletonList(contactId), contacts);
// Remove the local author - the contact should be removed
db.removeLocalAuthor(txn, localAuthorId);
contacts = db.getContacts(txn, localAuthorId);
db.removeLocalAuthor(txn, localAuthor.getId());
contacts = db.getContacts(txn, localAuthor.getId());
assertEquals(Collections.emptyList(), contacts);
assertFalse(db.containsContact(txn, contactId));
@@ -860,14 +851,14 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact - initially there should be no offered messages
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
assertEquals(0, db.countOfferedMessages(txn, contactId));
// Add some offered messages and count them
List<MessageId> ids = new ArrayList<>();
for (int i = 0; i < 10; i++) {
MessageId m = new MessageId(TestUtils.getRandomId());
MessageId m = new MessageId(getRandomId());
db.addOfferedMessage(txn, contactId, m);
ids.add(m);
}
@@ -876,8 +867,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Remove some of the offered messages and count again
List<MessageId> half = ids.subList(0, 5);
db.removeOfferedMessages(txn, contactId, half);
assertTrue(db.removeOfferedMessage(txn, contactId, ids.get(5)));
assertEquals(4, db.countOfferedMessages(txn, contactId));
assertEquals(5, db.countOfferedMessages(txn, contactId));
db.commitTransaction(txn);
db.close();
@@ -928,7 +918,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true);
db.addMessage(txn, message, DELIVERED, true, null);
// Attach some metadata to the message
Metadata metadata = new Metadata();
@@ -999,7 +989,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true);
db.addMessage(txn, message, DELIVERED, true, null);
// Attach some metadata to the message
Metadata metadata = new Metadata();
@@ -1052,7 +1042,7 @@ public class H2DatabaseTest extends BrambleTestCase {
@Test
public void testMetadataQueries() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
MessageId messageId1 = new MessageId(getRandomId());
Message message1 = new Message(messageId1, groupId, timestamp, raw);
Database<Connection> db = open(false);
@@ -1060,8 +1050,8 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a group and two messages
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true);
db.addMessage(txn, message1, DELIVERED, true);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message1, DELIVERED, true, null);
// Attach some metadata to the messages
Metadata metadata = new Metadata();
@@ -1156,7 +1146,7 @@ public class H2DatabaseTest extends BrambleTestCase {
@Test
public void testMetadataQueriesOnlyForDeliveredMessages() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
MessageId messageId1 = new MessageId(getRandomId());
Message message1 = new Message(messageId1, groupId, timestamp, raw);
Database<Connection> db = open(false);
@@ -1164,8 +1154,8 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a group and two messages
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true);
db.addMessage(txn, message1, DELIVERED, true);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message1, DELIVERED, true, null);
// Attach some metadata to the messages
Metadata metadata = new Metadata();
@@ -1227,10 +1217,10 @@ public class H2DatabaseTest extends BrambleTestCase {
@Test
public void testMessageDependencies() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
MessageId messageId2 = new MessageId(TestUtils.getRandomId());
MessageId messageId3 = new MessageId(TestUtils.getRandomId());
MessageId messageId4 = new MessageId(TestUtils.getRandomId());
MessageId messageId1 = new MessageId(getRandomId());
MessageId messageId2 = new MessageId(getRandomId());
MessageId messageId3 = new MessageId(getRandomId());
MessageId messageId4 = new MessageId(getRandomId());
Message message1 = new Message(messageId1, groupId, timestamp, raw);
Message message2 = new Message(messageId2, groupId, timestamp, raw);
@@ -1239,9 +1229,9 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a group and some messages
db.addGroup(txn, group);
db.addMessage(txn, message, PENDING, true);
db.addMessage(txn, message1, DELIVERED, true);
db.addMessage(txn, message2, INVALID, true);
db.addMessage(txn, message, PENDING, true, contactId);
db.addMessage(txn, message1, DELIVERED, true, contactId);
db.addMessage(txn, message2, INVALID, true, contactId);
// Add dependencies
db.addMessageDependency(txn, groupId, messageId, messageId1);
@@ -1308,26 +1298,26 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, PENDING, true);
db.addMessage(txn, message, PENDING, true, contactId);
// Add a second group
GroupId groupId1 = new GroupId(TestUtils.getRandomId());
GroupId groupId1 = new GroupId(getRandomId());
Group group1 = new Group(groupId1, clientId,
TestUtils.getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH));
getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH));
db.addGroup(txn, group1);
// Add a message to the second group
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
MessageId messageId1 = new MessageId(getRandomId());
Message message1 = new Message(messageId1, groupId1, timestamp, raw);
db.addMessage(txn, message1, DELIVERED, true);
db.addMessage(txn, message1, DELIVERED, true, contactId);
// Create an ID for a missing message
MessageId messageId2 = new MessageId(TestUtils.getRandomId());
MessageId messageId2 = new MessageId(getRandomId());
// Add another message to the first group
MessageId messageId3 = new MessageId(TestUtils.getRandomId());
MessageId messageId3 = new MessageId(getRandomId());
Message message3 = new Message(messageId3, groupId, timestamp, raw);
db.addMessage(txn, message3, DELIVERED, true);
db.addMessage(txn, message3, DELIVERED, true, contactId);
// Add dependencies between the messages
db.addMessageDependency(txn, groupId, messageId, messageId1);
@@ -1360,10 +1350,10 @@ public class H2DatabaseTest extends BrambleTestCase {
@Test
public void testGetPendingMessagesForDelivery() throws Exception {
MessageId mId1 = new MessageId(TestUtils.getRandomId());
MessageId mId2 = new MessageId(TestUtils.getRandomId());
MessageId mId3 = new MessageId(TestUtils.getRandomId());
MessageId mId4 = new MessageId(TestUtils.getRandomId());
MessageId mId1 = new MessageId(getRandomId());
MessageId mId2 = new MessageId(getRandomId());
MessageId mId3 = new MessageId(getRandomId());
MessageId mId4 = new MessageId(getRandomId());
Message m1 = new Message(mId1, groupId, timestamp, raw);
Message m2 = new Message(mId2, groupId, timestamp, raw);
Message m3 = new Message(mId3, groupId, timestamp, raw);
@@ -1374,20 +1364,20 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a group and some messages with different states
db.addGroup(txn, group);
db.addMessage(txn, m1, UNKNOWN, true);
db.addMessage(txn, m2, INVALID, true);
db.addMessage(txn, m3, PENDING, true);
db.addMessage(txn, m4, DELIVERED, true);
db.addMessage(txn, m1, UNKNOWN, true, contactId);
db.addMessage(txn, m2, INVALID, true, contactId);
db.addMessage(txn, m3, PENDING, true, contactId);
db.addMessage(txn, m4, DELIVERED, true, contactId);
Collection<MessageId> result;
// Retrieve messages to be validated
result = db.getMessagesToValidate(txn, clientId);
result = db.getMessagesToValidate(txn);
assertEquals(1, result.size());
assertTrue(result.contains(mId1));
// Retrieve pending messages
result = db.getPendingMessages(txn, clientId);
result = db.getPendingMessages(txn);
assertEquals(1, result.size());
assertTrue(result.contains(mId3));
@@ -1397,10 +1387,10 @@ public class H2DatabaseTest extends BrambleTestCase {
@Test
public void testGetMessagesToShare() throws Exception {
MessageId mId1 = new MessageId(TestUtils.getRandomId());
MessageId mId2 = new MessageId(TestUtils.getRandomId());
MessageId mId3 = new MessageId(TestUtils.getRandomId());
MessageId mId4 = new MessageId(TestUtils.getRandomId());
MessageId mId1 = new MessageId(getRandomId());
MessageId mId2 = new MessageId(getRandomId());
MessageId mId3 = new MessageId(getRandomId());
MessageId mId4 = new MessageId(getRandomId());
Message m1 = new Message(mId1, groupId, timestamp, raw);
Message m2 = new Message(mId2, groupId, timestamp, raw);
Message m3 = new Message(mId3, groupId, timestamp, raw);
@@ -1411,10 +1401,10 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a group and some messages
db.addGroup(txn, group);
db.addMessage(txn, m1, DELIVERED, true);
db.addMessage(txn, m2, DELIVERED, false);
db.addMessage(txn, m3, DELIVERED, false);
db.addMessage(txn, m4, DELIVERED, true);
db.addMessage(txn, m1, DELIVERED, true, contactId);
db.addMessage(txn, m2, DELIVERED, false, contactId);
db.addMessage(txn, m3, DELIVERED, false, contactId);
db.addMessage(txn, m4, DELIVERED, true, contactId);
// Introduce dependencies between the messages
db.addMessageDependency(txn, groupId, mId1, mId2);
@@ -1422,8 +1412,7 @@ public class H2DatabaseTest extends BrambleTestCase {
db.addMessageDependency(txn, groupId, mId4, mId3);
// Retrieve messages to be shared
Collection<MessageId> result =
db.getMessagesToShare(txn, clientId);
Collection<MessageId> result = db.getMessagesToShare(txn);
assertEquals(2, result.size());
assertTrue(result.contains(mId2));
assertTrue(result.contains(mId3));
@@ -1439,12 +1428,11 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact, a shared group and a shared message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true);
db.addStatus(txn, contactId, messageId, false, false);
db.addMessage(txn, message, DELIVERED, true, null);
// The message should not be sent or seen
MessageStatus status = db.getMessageStatus(txn, contactId, messageId);
@@ -1508,9 +1496,7 @@ public class H2DatabaseTest extends BrambleTestCase {
@Test
public void testDifferentLocalAuthorsCanHaveTheSameContact()
throws Exception {
AuthorId localAuthorId1 = new AuthorId(TestUtils.getRandomId());
LocalAuthor localAuthor1 = new LocalAuthor(localAuthorId1, "Carol",
new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
LocalAuthor localAuthor1 = getLocalAuthor();
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
@@ -1521,15 +1507,15 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add the same contact for each local author
ContactId contactId =
db.addContact(txn, author, localAuthorId, true, true);
db.addContact(txn, author, localAuthor.getId(), true, true);
ContactId contactId1 =
db.addContact(txn, author, localAuthorId1, true, true);
db.addContact(txn, author, localAuthor1.getId(), true, true);
// The contacts should be distinct
assertNotEquals(contactId, contactId1);
assertEquals(2, db.getContacts(txn).size());
assertEquals(1, db.getContacts(txn, localAuthorId).size());
assertEquals(1, db.getContacts(txn, localAuthorId1).size());
assertEquals(1, db.getContacts(txn, localAuthor.getId()).size());
assertEquals(1, db.getContacts(txn, localAuthor1.getId()).size());
db.commitTransaction(txn);
db.close();
@@ -1542,12 +1528,11 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact, a shared group and a shared message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true);
db.addStatus(txn, contactId, messageId, false, false);
db.addMessage(txn, message, DELIVERED, true, null);
// The message should be visible to the contact
assertTrue(db.containsVisibleMessage(txn, contactId, messageId));
@@ -1588,7 +1573,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a contact
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
// The contact should be active
@@ -1621,7 +1606,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, UNKNOWN, false);
db.addMessage(txn, message, UNKNOWN, false, contactId);
// Walk the message through the validation and delivery states
assertEquals(UNKNOWN, db.getMessageState(txn, messageId));
@@ -1647,14 +1632,13 @@ public class H2DatabaseTest extends BrambleTestCase {
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addMessage(txn, message, UNKNOWN, false);
db.addMessage(txn, message, UNKNOWN, false, null);
// There should be no messages to send
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
// Share the group with the contact - still no messages to send
db.addGroupVisibility(txn, contactId, groupId, true);
db.addStatus(txn, contactId, messageId, false, false);
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
// Set the message's state to DELIVERED - still no messages to send
@@ -1665,6 +1649,10 @@ public class H2DatabaseTest extends BrambleTestCase {
db.setMessageShared(txn, messageId);
assertEquals(0, db.getNextSendTime(txn, contactId));
// Mark the message as requested - it should still be sendable
db.raiseRequestedFlag(txn, contactId, messageId);
assertEquals(0, db.getNextSendTime(txn, contactId));
// Update the message's expiry time as though we sent it - now the
// message should be sendable after one round-trip
db.updateExpiryTime(txn, contactId, messageId, 1000);
@@ -1713,20 +1701,20 @@ public class H2DatabaseTest extends BrambleTestCase {
}
private TransportKeys createTransportKeys() {
SecretKey inPrevTagKey = TestUtils.getSecretKey();
SecretKey inPrevHeaderKey = TestUtils.getSecretKey();
SecretKey inPrevTagKey = getSecretKey();
SecretKey inPrevHeaderKey = getSecretKey();
IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey,
1, 123, new byte[4]);
SecretKey inCurrTagKey = TestUtils.getSecretKey();
SecretKey inCurrHeaderKey = TestUtils.getSecretKey();
SecretKey inCurrTagKey = getSecretKey();
SecretKey inCurrHeaderKey = getSecretKey();
IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey,
2, 234, new byte[4]);
SecretKey inNextTagKey = TestUtils.getSecretKey();
SecretKey inNextHeaderKey = TestUtils.getSecretKey();
SecretKey inNextTagKey = getSecretKey();
SecretKey inNextHeaderKey = getSecretKey();
IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey,
3, 345, new byte[4]);
SecretKey outCurrTagKey = TestUtils.getSecretKey();
SecretKey outCurrHeaderKey = TestUtils.getSecretKey();
SecretKey outCurrTagKey = getSecretKey();
SecretKey outCurrHeaderKey = getSecretKey();
OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey,
2, 456);
return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);

View File

@@ -24,6 +24,7 @@ import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertTrue;
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
@@ -34,7 +35,7 @@ public class Migration30_31Test extends BrambleTestCase {
private static final String CREATE_GROUPS_STUB =
"CREATE TABLE groups"
+ " (groupID BINARY(32) NOT NULL,"
+ " (groupId BINARY(32) NOT NULL,"
+ " PRIMARY KEY (groupId))";
private static final String CREATE_MESSAGES =
@@ -66,8 +67,8 @@ public class Migration30_31Test extends BrambleTestCase {
private final String url = "jdbc:h2:" + db.getAbsolutePath();
private final GroupId groupId = new GroupId(getRandomId());
private final GroupId groupId1 = new GroupId(getRandomId());
private final Message message = TestUtils.getMessage(groupId);
private final Message message1 = TestUtils.getMessage(groupId1);
private final Message message = getMessage(groupId);
private final Message message1 = getMessage(groupId1);
private final Metadata meta = new Metadata(), meta1 = new Metadata();
private Connection connection = null;

View File

@@ -0,0 +1,369 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.ValidationManager.State;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import static java.sql.Types.BINARY;
import static junit.framework.Assert.assertFalse;
import static junit.framework.TestCase.assertTrue;
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.junit.Assert.assertEquals;
public class Migration31_32Test extends BrambleTestCase {
private static final String CREATE_GROUPS_STUB =
"CREATE TABLE groups"
+ " (groupId BINARY(32) NOT NULL,"
+ " PRIMARY KEY (groupId))";
private static final String CREATE_CONTACTS_STUB =
"CREATE TABLE contacts"
+ " (contactId INT NOT NULL,"
+ " PRIMARY KEY (contactId))";
private static final String CREATE_GROUP_VISIBILITIES_STUB =
"CREATE TABLE groupVisibilities"
+ " (contactId INT NOT NULL,"
+ " groupId BINARY(32) NOT NULL,"
+ " shared BOOLEAN NOT NULL,"
+ " PRIMARY KEY (contactId, groupId))";
private static final String CREATE_MESSAGES =
"CREATE TABLE messages"
+ " (messageId BINARY(32) NOT NULL,"
+ " groupId BINARY(32) NOT NULL,"
+ " timestamp BIGINT NOT NULL,"
+ " state INT NOT NULL,"
+ " shared BOOLEAN NOT NULL,"
+ " length INT NOT NULL,"
+ " raw BLOB," // Null if message has been deleted
+ " PRIMARY KEY (messageId),"
+ " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE)";
private static final String CREATE_STATUSES_31 =
"CREATE TABLE statuses"
+ " (messageId BINARY(32) NOT NULL,"
+ " contactId INT NOT NULL,"
+ " ack BOOLEAN NOT NULL,"
+ " seen BOOLEAN NOT NULL,"
+ " requested BOOLEAN NOT NULL,"
+ " expiry BIGINT NOT NULL,"
+ " txCount INT NOT NULL,"
+ " PRIMARY KEY (messageId, contactId),"
+ " FOREIGN KEY (messageId)"
+ " REFERENCES messages (messageId)"
+ " ON DELETE CASCADE,"
+ " FOREIGN KEY (contactId)"
+ " REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE)";
private final File testDir = TestUtils.getTestDirectory();
private final File db = new File(testDir, "db");
private final String url = "jdbc:h2:" + db.getAbsolutePath();
private final GroupId groupId = new GroupId(getRandomId());
private final GroupId groupId1 = new GroupId(getRandomId());
private final ContactId contactId = new ContactId(123);
private final ContactId contactId1 = new ContactId(234);
private final Message message = getMessage(groupId);
private final Message message1 = getMessage(groupId1);
private final Message message2 = getMessage(groupId1);
private Connection connection = null;
@Before
public void setUp() throws Exception {
assertTrue(testDir.mkdirs());
Class.forName("org.h2.Driver");
connection = DriverManager.getConnection(url);
}
@After
public void tearDown() throws Exception {
if (connection != null) connection.close();
TestUtils.deleteTestDirectory(testDir);
}
@Test
public void testMigration() throws Exception {
try {
Statement s = connection.createStatement();
s.execute(CREATE_GROUPS_STUB);
s.execute(CREATE_CONTACTS_STUB);
s.execute(CREATE_GROUP_VISIBILITIES_STUB);
s.execute(CREATE_MESSAGES);
s.execute(CREATE_STATUSES_31);
s.close();
addGroup(groupId);
addMessage(message, DELIVERED, true, false);
addGroup(groupId1);
addMessage(message1, UNKNOWN, false, false);
addMessage(message2, DELIVERED, true, true);
addContact(contactId);
addGroupVisibility(contactId, groupId, true);
addStatus31(message.getId(), contactId);
addGroupVisibility(contactId, groupId1, false);
addStatus31(message1.getId(), contactId);
addStatus31(message2.getId(), contactId);
addContact(contactId1);
addGroupVisibility(contactId1, groupId1, true);
addStatus31(message1.getId(), contactId1);
addStatus31(message2.getId(), contactId1);
new Migration31_32().migrate(connection);
assertTrue(containsStatus(message.getId(), contactId));
Status32 status = getStatus32(message.getId(), contactId);
assertEquals(groupId, status.groupId);
assertEquals(message.getTimestamp(), status.timestamp);
assertEquals(message.getLength(), status.length);
assertEquals(DELIVERED, status.state);
assertTrue(status.groupShared);
assertTrue(status.messageShared);
assertFalse(status.deleted);
assertTrue(containsStatus(message1.getId(), contactId));
status = getStatus32(message1.getId(), contactId);
assertEquals(groupId1, status.groupId);
assertEquals(message1.getTimestamp(), status.timestamp);
assertEquals(message1.getLength(), status.length);
assertEquals(UNKNOWN, status.state);
assertFalse(status.groupShared);
assertFalse(status.messageShared);
assertFalse(status.deleted);
assertTrue(containsStatus(message2.getId(), contactId));
status = getStatus32(message2.getId(), contactId);
assertEquals(groupId1, status.groupId);
assertEquals(message2.getTimestamp(), status.timestamp);
assertEquals(message2.getLength(), status.length);
assertEquals(DELIVERED, status.state);
assertFalse(status.groupShared);
assertTrue(status.messageShared);
assertTrue(status.deleted);
assertFalse(containsStatus(message.getId(), contactId1));
assertTrue(containsStatus(message1.getId(), contactId1));
status = getStatus32(message1.getId(), contactId1);
assertEquals(groupId1, status.groupId);
assertEquals(message1.getTimestamp(), status.timestamp);
assertEquals(message1.getLength(), status.length);
assertEquals(UNKNOWN, status.state);
assertTrue(status.groupShared);
assertFalse(status.messageShared);
assertFalse(status.deleted);
assertTrue(containsStatus(message2.getId(), contactId1));
status = getStatus32(message2.getId(), contactId1);
assertEquals(groupId1, status.groupId);
assertEquals(message2.getTimestamp(), status.timestamp);
assertEquals(message2.getLength(), status.length);
assertEquals(DELIVERED, status.state);
assertTrue(status.groupShared);
assertTrue(status.messageShared);
assertTrue(status.deleted);
} catch (SQLException e) {
connection.close();
throw e;
}
}
private void addGroup(GroupId g) throws SQLException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO groups (groupId) VALUES (?)";
ps = connection.prepareStatement(sql);
ps.setBytes(1, g.getBytes());
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
if (ps != null) ps.close();
throw e;
}
}
private void addContact(ContactId c) throws SQLException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO contacts (contactId) VALUES (?)";
ps = connection.prepareStatement(sql);
ps.setInt(1, c.getInt());
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
if (ps != null) ps.close();
throw e;
}
}
private void addGroupVisibility(ContactId c, GroupId g, boolean shared)
throws SQLException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO groupVisibilities"
+ " (contactId, groupId, shared) VALUES (?, ?, ?)";
ps = connection.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setBytes(2, g.getBytes());
ps.setBoolean(3, shared);
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
if (ps != null) ps.close();
throw e;
}
}
private void addMessage(Message m, State state, boolean shared,
boolean deleted) throws SQLException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
+ " state, shared, length, raw)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
ps = connection.prepareStatement(sql);
ps.setBytes(1, m.getId().getBytes());
ps.setBytes(2, m.getGroupId().getBytes());
ps.setLong(3, m.getTimestamp());
ps.setInt(4, state.getValue());
ps.setBoolean(5, shared);
byte[] raw = m.getRaw();
ps.setInt(6, raw.length);
if (deleted) ps.setNull(7, BINARY);
else ps.setBytes(7, raw);
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
if (ps != null) ps.close();
throw e;
}
}
private void addStatus31(MessageId m, ContactId c) throws SQLException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO statuses (messageId, contactId, ack,"
+ " seen, requested, expiry, txCount)"
+ " VALUES (?, ?, FALSE, FALSE, FALSE, 0, 0)";
ps = connection.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
if (ps != null) ps.close();
throw e;
}
}
private boolean containsStatus(MessageId m, ContactId c)
throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT COUNT (*) FROM statuses"
+ " WHERE messageId = ? AND contactId = ?";
ps = connection.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
int count = rs.getInt(1);
if (count < 0 || count > 1) throw new DbStateException();
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return count > 0;
} catch (SQLException e) {
if (rs != null) rs.close();
if (ps != null) ps.close();
throw e;
}
}
private Status32 getStatus32(MessageId m, ContactId c) throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT groupId, timestamp, length, state,"
+ " groupShared, messageShared, deleted"
+ " FROM statuses"
+ " WHERE messageId = ? AND contactId = ?";
ps = connection.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
GroupId groupId = new GroupId(rs.getBytes(1));
long timestamp = rs.getLong(2);
int length = rs.getInt(3);
State state = State.fromValue(rs.getInt(4));
boolean groupShared = rs.getBoolean(5);
boolean messageShared = rs.getBoolean(6);
boolean deleted = rs.getBoolean(7);
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return new Status32(groupId, timestamp, length, state,
groupShared, messageShared, deleted);
} catch (SQLException e) {
if (rs != null) rs.close();
if (ps != null) ps.close();
throw e;
}
}
private static class Status32 {
private final GroupId groupId;
private final long timestamp;
private final int length;
private final State state;
private final boolean groupShared, messageShared, deleted;
private Status32(GroupId groupId, long timestamp, int length,
State state, boolean groupShared, boolean messageShared,
boolean deleted) {
this.groupId = groupId;
this.timestamp = timestamp;
this.length = length;
this.state = state;
this.groupShared = groupShared;
this.messageShared = messageShared;
this.deleted = deleted;
}
}
}

View File

@@ -100,21 +100,21 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// validateOutstandingMessages()
oneOf(db).startTransaction(true);
will(returnValue(txn));
oneOf(db).getMessagesToValidate(txn, clientId);
oneOf(db).getMessagesToValidate(txn);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
// deliverOutstandingMessages()
oneOf(db).startTransaction(true);
will(returnValue(txn1));
oneOf(db).getPendingMessages(txn1, clientId);
oneOf(db).getPendingMessages(txn1);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn1);
oneOf(db).endTransaction(txn1);
// shareOutstandingMessages()
oneOf(db).startTransaction(true);
will(returnValue(txn2));
oneOf(db).getMessagesToShare(txn2, clientId);
oneOf(db).getMessagesToShare(txn2);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn2);
oneOf(db).endTransaction(txn2);
@@ -138,7 +138,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// Get messages to validate
oneOf(db).startTransaction(true);
will(returnValue(txn));
oneOf(db).getMessagesToValidate(txn, clientId);
oneOf(db).getMessagesToValidate(txn);
will(returnValue(Arrays.asList(messageId, messageId1)));
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
@@ -199,14 +199,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// Get pending messages to deliver
oneOf(db).startTransaction(true);
will(returnValue(txn5));
oneOf(db).getPendingMessages(txn5, clientId);
oneOf(db).getPendingMessages(txn5);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn5);
oneOf(db).endTransaction(txn5);
// Get messages to share
oneOf(db).startTransaction(true);
will(returnValue(txn6));
oneOf(db).getMessagesToShare(txn6, clientId);
oneOf(db).getMessagesToShare(txn6);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn6);
oneOf(db).endTransaction(txn6);
@@ -227,14 +227,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// Get messages to validate
oneOf(db).startTransaction(true);
will(returnValue(txn));
oneOf(db).getMessagesToValidate(txn, clientId);
oneOf(db).getMessagesToValidate(txn);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
// Get pending messages to deliver
oneOf(db).startTransaction(true);
will(returnValue(txn1));
oneOf(db).getPendingMessages(txn1, clientId);
oneOf(db).getPendingMessages(txn1);
will(returnValue(Collections.singletonList(messageId)));
oneOf(db).commitTransaction(txn1);
oneOf(db).endTransaction(txn1);
@@ -292,7 +292,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// Get messages to share
oneOf(db).startTransaction(true);
will(returnValue(txn4));
oneOf(db).getMessagesToShare(txn4, clientId);
oneOf(db).getMessagesToShare(txn4);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn4);
oneOf(db).endTransaction(txn4);
@@ -313,14 +313,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// No messages to validate
oneOf(db).startTransaction(true);
will(returnValue(txn));
oneOf(db).getMessagesToValidate(txn, clientId);
oneOf(db).getMessagesToValidate(txn);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
// No pending messages to deliver
oneOf(db).startTransaction(true);
will(returnValue(txn1));
oneOf(db).getPendingMessages(txn1, clientId);
oneOf(db).getPendingMessages(txn1);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn1);
oneOf(db).endTransaction(txn1);
@@ -328,7 +328,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// Get messages to share
oneOf(db).startTransaction(true);
will(returnValue(txn2));
oneOf(db).getMessagesToShare(txn2, clientId);
oneOf(db).getMessagesToShare(txn2);
will(returnValue(Collections.singletonList(messageId)));
oneOf(db).commitTransaction(txn2);
oneOf(db).endTransaction(txn2);
@@ -416,7 +416,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// Get messages to validate
oneOf(db).startTransaction(true);
will(returnValue(txn));
oneOf(db).getMessagesToValidate(txn, clientId);
oneOf(db).getMessagesToValidate(txn);
will(returnValue(Arrays.asList(messageId, messageId1)));
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
@@ -457,14 +457,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// Get pending messages to deliver
oneOf(db).startTransaction(true);
will(returnValue(txn4));
oneOf(db).getPendingMessages(txn4, clientId);
oneOf(db).getPendingMessages(txn4);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn4);
oneOf(db).endTransaction(txn4);
// Get messages to share
oneOf(db).startTransaction(true);
will(returnValue(txn5));
oneOf(db).getMessagesToShare(txn5, clientId);
oneOf(db).getMessagesToShare(txn5);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn5);
oneOf(db).endTransaction(txn5);
@@ -487,7 +487,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// Get messages to validate
oneOf(db).startTransaction(true);
will(returnValue(txn));
oneOf(db).getMessagesToValidate(txn, clientId);
oneOf(db).getMessagesToValidate(txn);
will(returnValue(Arrays.asList(messageId, messageId1)));
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
@@ -533,14 +533,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
// Get pending messages to deliver
oneOf(db).startTransaction(true);
will(returnValue(txn4));
oneOf(db).getPendingMessages(txn4, clientId);
oneOf(db).getPendingMessages(txn4);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn4);
oneOf(db).endTransaction(txn4);
// Get messages to share
oneOf(db).startTransaction(true);
will(returnValue(txn5));
oneOf(db).getMessagesToShare(txn5, clientId);
oneOf(db).getMessagesToShare(txn5);
will(returnValue(Collections.emptyList()));
oneOf(db).commitTransaction(txn5);
oneOf(db).endTransaction(txn5);

View File

@@ -189,8 +189,8 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion 26
versionCode 1619
versionName "0.16.19"
versionCode 1620
versionName "0.16.20"
applicationId "org.briarproject.briar.beta"
resValue "string", "app_package", "org.briarproject.briar.beta"
resValue "string", "app_name", "Briar Beta"

View File

@@ -374,7 +374,12 @@
</activity>
<activity
android:name="org.briarproject.briar.android.panic.ExitActivity"
android:name="org.briarproject.briar.android.logout.ExitActivity"
android:theme="@android:style/Theme.NoDisplay">
</activity>
<activity
android:name=".android.logout.HideUiActivity"
android:theme="@android:style/Theme.NoDisplay">
</activity>

View File

@@ -62,6 +62,7 @@ import javax.inject.Inject;
import static android.app.Notification.DEFAULT_LIGHTS;
import static android.app.Notification.DEFAULT_SOUND;
import static android.app.Notification.DEFAULT_VIBRATE;
import static android.app.Notification.VISIBILITY_SECRET;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.content.Context.NOTIFICATION_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
@@ -90,12 +91,6 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private static final int BLOG_POST_NOTIFICATION_ID = 6;
private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 7;
// Channel IDs
private static final String CONTACT_CHANNEL_ID = "contacts";
private static final String GROUP_CHANNEL_ID = "groups";
private static final String FORUM_CHANNEL_ID = "forums";
private static final String BLOG_CHANNEL_ID = "blogs";
private static final long SOUND_DELAY = TimeUnit.SECONDS.toMillis(2);
private static final Logger LOG =
@@ -175,6 +170,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
NotificationChannel nc =
new NotificationChannel(channelId, appContext.getString(name),
IMPORTANCE_DEFAULT);
nc.setLockscreenVisibility(VISIBILITY_SECRET);
nc.enableVibration(true);
nc.enableLights(true);
nc.setLightColor(
ContextCompat.getColor(appContext, R.color.briar_green_light));

View File

@@ -4,8 +4,11 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
@@ -17,19 +20,26 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.logout.HideUiActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.content.Intent.ACTION_SHUTDOWN;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.app.NotificationCompat.CATEGORY_SERVICE;
import static android.support.v4.app.NotificationCompat.PRIORITY_MIN;
@@ -61,6 +71,9 @@ public class BriarService extends Service {
private final AtomicBoolean created = new AtomicBoolean(false);
private final Binder binder = new BriarBinder();
@Nullable
private BroadcastReceiver receiver = null;
@Inject
protected DatabaseConfig databaseConfig;
// Fields that are accessed from background threads must be volatile
@@ -143,6 +156,19 @@ public class BriarService extends Service {
}
}
}.start();
// Register for device shutdown broadcasts
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
LOG.info("Device is shutting down");
shutdownFromBackground();
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_SHUTDOWN);
filter.addAction("android.intent.action.QUICKBOOT_POWEROFF");
filter.addAction("com.htc.intent.action.QUICKBOOT_POWEROFF");
registerReceiver(receiver, filter);
}
private void showStartupFailureNotification(StartResult result) {
@@ -187,6 +213,7 @@ public class BriarService extends Service {
super.onDestroy();
LOG.info("Destroyed");
stopForeground(true);
if (receiver != null) unregisterReceiver(receiver);
// Stop the services in a background thread
new Thread() {
@Override
@@ -200,7 +227,48 @@ public class BriarService extends Service {
public void onLowMemory() {
super.onLowMemory();
LOG.warning("Memory is low");
// FIXME: Work out what to do about it
shutdownFromBackground();
showLowMemoryShutdownNotification();
}
private void shutdownFromBackground() {
// Stop the service
stopSelf();
// Hide the UI
Intent i = new Intent(this, HideUiActivity.class);
i.addFlags(FLAG_ACTIVITY_NEW_TASK
| FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| FLAG_ACTIVITY_NO_ANIMATION
| FLAG_ACTIVITY_CLEAR_TASK);
startActivity(i);
// Wait for shutdown to complete, then exit
new Thread(() -> {
try {
if (started) lifecycleManager.waitForShutdown();
} catch (InterruptedException e) {
LOG.info("Interrupted while waiting for shutdown");
}
LOG.info("Exiting");
System.exit(0);
}).start();
}
private void showLowMemoryShutdownNotification() {
androidExecutor.runOnUiThread(() -> {
NotificationCompat.Builder b = new NotificationCompat.Builder(
BriarService.this, FAILURE_CHANNEL_ID);
b.setSmallIcon(android.R.drawable.stat_notify_error);
b.setContentTitle(getText(
R.string.low_memory_shutdown_notification_title));
b.setContentText(getText(
R.string.low_memory_shutdown_notification_text));
Intent i = new Intent(this, SplashScreenActivity.class);
b.setContentIntent(PendingIntent.getActivity(this, 0, i, 0));
b.setAutoCancel(true);
Object o = getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(FAILURE_NOTIFICATION_ID, b.build());
});
}
/**

View File

@@ -16,7 +16,7 @@ import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.DbController;
import org.briarproject.briar.android.controller.handler.UiResultHandler;
import org.briarproject.briar.android.login.PasswordActivity;
import org.briarproject.briar.android.panic.ExitActivity;
import org.briarproject.briar.android.logout.ExitActivity;
import java.util.logging.Logger;

View File

@@ -52,19 +52,15 @@ class ForumListAdapter
// Post Count
int postCount = item.getPostCount();
if (postCount > 0) {
ui.avatar.setProblem(false);
ui.postCount.setText(ctx.getResources()
.getQuantityString(R.plurals.posts, postCount,
postCount));
ui.postCount.setTextColor(
ContextCompat
.getColor(ctx, R.color.briar_text_secondary));
ContextCompat.getColor(ctx, R.color.briar_text_secondary));
} else {
ui.avatar.setProblem(true);
ui.postCount.setText(ctx.getString(R.string.no_posts));
ui.postCount.setTextColor(
ContextCompat
.getColor(ctx, R.color.briar_text_tertiary));
ContextCompat.getColor(ctx, R.color.briar_text_tertiary));
}
// Date

View File

@@ -9,6 +9,7 @@ import android.content.IntentFilter;
import android.os.Bundle;
import android.support.annotation.UiThread;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog.Builder;
import android.support.v7.widget.Toolbar;
@@ -206,11 +207,14 @@ public class KeyAgreementActivity extends BriarActivity implements
private void showQrCodeFragment() {
// FIXME #824
BaseFragment f = ShowQrCodeFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
.addToBackStack(f.getUniqueTag())
.commit();
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag(ShowQrCodeFragment.TAG) == null) {
BaseFragment f = ShowQrCodeFragment.newInstance();
fm.beginTransaction()
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
.addToBackStack(f.getUniqueTag())
.commit();
}
}
private boolean checkPermissions() {

View File

@@ -65,7 +65,6 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (camera == this.camera) {
LOG.info("Got preview frame");
try {
Size size = camera.getParameters().getPreviewSize();
// The preview should be in NV21 format: width * height bytes of
@@ -103,19 +102,12 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
@Override
protected Void doInBackground(Void... params) {
long now = System.currentTimeMillis();
BinaryBitmap bitmap = binarize(data, width, height, orientation);
Result result = null;
Result result;
try {
result = reader.decode(bitmap);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Decoding barcode took " + duration + " ms");
} catch (ReaderException e) {
// No barcode found
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("No barcode found after " + duration + " ms");
return null;
} catch (RuntimeException e) {
LOG.warning("Invalid preview frame");

View File

@@ -15,6 +15,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
import android.widget.Toast;
@@ -50,6 +52,8 @@ import javax.inject.Inject;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@@ -59,7 +63,8 @@ import static java.util.logging.Level.WARNING;
public class ShowQrCodeFragment extends BaseEventFragment
implements QrCodeDecoder.ResultCallback {
private static final String TAG = ShowQrCodeFragment.class.getName();
static final String TAG = ShowQrCodeFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
@Inject
@@ -80,6 +85,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
private ImageView qrCode;
private TextView mainProgressTitle;
private ViewGroup mainProgressContainer;
private boolean fullscreen = false;
private boolean gotRemotePayload;
private volatile boolean gotLocalPayload;
@@ -124,6 +130,34 @@ public class ShowQrCodeFragment extends BaseEventFragment
qrCode = view.findViewById(R.id.qr_code);
mainProgressTitle = view.findViewById(R.id.title_progress_bar);
mainProgressContainer = view.findViewById(R.id.container_progress);
ImageView fullscreenButton = view.findViewById(R.id.fullscreen_button);
fullscreenButton.setOnClickListener(v -> {
View qrCodeContainer = view.findViewById(R.id.qr_code_container);
LinearLayout cameraOverlay = view.findViewById(R.id.camera_overlay);
LayoutParams statusParams, qrCodeParams;
if (fullscreen) {
// Shrink the QR code container to fill half its parent
if (cameraOverlay.getOrientation() == HORIZONTAL) {
statusParams = new LayoutParams(0, MATCH_PARENT, 1f);
qrCodeParams = new LayoutParams(0, MATCH_PARENT, 1f);
} else {
statusParams = new LayoutParams(MATCH_PARENT, 0, 1f);
qrCodeParams = new LayoutParams(MATCH_PARENT, 0, 1f);
}
fullscreenButton.setImageResource(
R.drawable.ic_fullscreen_black_48dp);
} else {
// Grow the QR code container to fill its parent
statusParams = new LayoutParams(0, 0, 0f);
qrCodeParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f);
fullscreenButton.setImageResource(
R.drawable.ic_fullscreen_exit_black_48dp);
}
statusView.setLayoutParams(statusParams);
qrCodeContainer.setLayoutParams(qrCodeParams);
cameraOverlay.invalidate();
fullscreen = !fullscreen;
});
}
@Override
@@ -204,6 +238,15 @@ public class ShowQrCodeFragment extends BaseEventFragment
@UiThread
private void reset() {
// If we've stopped the camera view, restart it
if (gotRemotePayload) {
try {
cameraView.start(getScreenRotationDegrees());
} catch (CameraException e) {
logCameraExceptionAndFinish(e);
return;
}
}
statusView.setVisibility(INVISIBLE);
cameraView.setVisibility(VISIBLE);
gotRemotePayload = false;
@@ -218,12 +261,17 @@ public class ShowQrCodeFragment extends BaseEventFragment
if (LOG.isLoggable(INFO))
LOG.info("Remote payload is " + encoded.length + " bytes");
Payload remotePayload = payloadParser.parse(encoded);
gotRemotePayload = true;
cameraView.stop();
cameraView.setVisibility(INVISIBLE);
statusView.setVisibility(VISIBLE);
status.setText(R.string.connecting_to_device);
task.connectAndRunProtocol(remotePayload);
} catch (CameraException e) {
logCameraExceptionAndFinish(e);
} catch (IOException | IllegalArgumentException e) {
// TODO show failure
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, "QR Code Invalid", e);
reset();
Toast.makeText(getActivity(), R.string.qr_code_invalid,
LENGTH_LONG).show();
}
@@ -261,6 +309,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
new AsyncTask<Void, Void, Bitmap>() {
@Override
@Nullable
protected Bitmap doInBackground(Void... params) {
byte[] encoded = payloadEncoder.encode(payload);
if (LOG.isLoggable(INFO))
@@ -325,13 +374,8 @@ public class ShowQrCodeFragment extends BaseEventFragment
runOnUiThreadUnlessDestroyed(() -> {
LOG.info("Got result from decoder");
// Ignore results until the KeyAgreementTask is ready
if (!gotLocalPayload) {
return;
}
if (!gotRemotePayload) {
gotRemotePayload = true;
qrCodeScanned(result.getText());
}
if (!gotLocalPayload) return;
if (!gotRemotePayload) qrCodeScanned(result.getText());
});
}

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.panic;
package org.briarproject.briar.android.logout;
import android.os.Build;
import android.os.Bundle;

View File

@@ -0,0 +1,20 @@
package org.briarproject.briar.android.logout;
import android.os.Bundle;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
public class HideUiActivity extends BaseActivity {
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
finish();
}
@Override
public void injectActivity(ActivityComponent component) {
}
}

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.fragment;
package org.briarproject.briar.android.logout;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -7,15 +7,17 @@ import android.view.ViewGroup;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class SignOutFragment extends BaseFragment {
private static final String TAG = SignOutFragment.class.getName();
public static final String TAG = SignOutFragment.class.getName();
@Override
public View onCreateView(LayoutInflater inflater,
public View onCreateView(@Nonnull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_sign_out, container, false);
@@ -30,5 +32,4 @@ public class SignOutFragment extends BaseFragment {
public void injectFragment(ActivityComponent component) {
// no need to inject
}
}

View File

@@ -7,6 +7,7 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.NavigationView;
import android.support.design.widget.NavigationView.OnNavigationItemSelectedListener;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.DrawerLayout;
@@ -22,6 +23,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants;
@@ -35,7 +37,7 @@ import org.briarproject.briar.android.controller.handler.UiResultHandler;
import org.briarproject.briar.android.forum.ForumListFragment;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.fragment.SignOutFragment;
import org.briarproject.briar.android.logout.SignOutFragment;
import org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning;
import org.briarproject.briar.android.privategroup.list.GroupListFragment;
import org.briarproject.briar.android.settings.SettingsActivity;
@@ -51,6 +53,7 @@ import static android.support.v4.view.GravityCompat.START;
import static android.support.v4.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
import static org.briarproject.briar.android.BriarService.EXTRA_STARTUP_FAILED;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.NO;
@@ -73,6 +76,8 @@ public class NavDrawerActivity extends BriarActivity implements
@Inject
NavDrawerController controller;
@Inject
LifecycleManager lifecycleManager;
private DrawerLayout drawerLayout;
private NavigationView navigation;
@@ -128,7 +133,9 @@ public class NavDrawerActivity extends BriarActivity implements
initializeTransports(getLayoutInflater());
transportsView.setAdapter(transportsAdapter);
if (state == null) {
if (lifecycleManager.getLifecycleState().isAfter(RUNNING)) {
showSignOutFragment();
} else if (state == null) {
startFragment(ContactListFragment.newInstance(),
R.id.nav_btn_contacts);
}
@@ -212,19 +219,23 @@ public class NavDrawerActivity extends BriarActivity implements
public void onBackPressed() {
if (drawerLayout.isDrawerOpen(START)) {
drawerLayout.closeDrawer(START);
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
getSupportFragmentManager()
.findFragmentByTag(ContactListFragment.TAG) == null) {
} else {
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag(SignOutFragment.TAG) != null) {
finish();
} else if (fm.getBackStackEntryCount() == 0
&& fm.findFragmentByTag(ContactListFragment.TAG) == null) {
/*
* This makes sure that the first fragment (ContactListFragment) the
* user sees is the same as the last fragment the user sees before
* exiting. This models the typical Google navigation behaviour such
* as in Gmail/Inbox.
*/
startFragment(ContactListFragment.newInstance(),
R.id.nav_btn_contacts);
} else {
super.onBackPressed();
startFragment(ContactListFragment.newInstance(),
R.id.nav_btn_contacts);
} else {
super.onBackPressed();
}
}
}
@@ -240,10 +251,15 @@ public class NavDrawerActivity extends BriarActivity implements
drawerToggle.onConfigurationChanged(newConfig);
}
private void signOut() {
private void showSignOutFragment() {
drawerLayout.setDrawerLockMode(LOCK_MODE_LOCKED_CLOSED);
startFragment(new SignOutFragment());
}
private void signOut() {
drawerLayout.setDrawerLockMode(LOCK_MODE_LOCKED_CLOSED);
signOut(false);
finish();
}
private void startFragment(BaseFragment fragment, int itemId) {

View File

@@ -74,9 +74,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
if (group.isEmpty()) {
postCount.setVisibility(GONE);
date.setVisibility(GONE);
avatar.setProblem(true);
status
.setText(ctx.getString(R.string.groups_group_is_empty));
status.setText(ctx.getString(R.string.groups_group_is_empty));
status.setVisibility(VISIBLE);
} else {
// Message Count
@@ -91,7 +89,6 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
long lastUpdate = group.getTimestamp();
date.setText(UiUtils.formatDate(ctx, lastUpdate));
date.setVisibility(VISIBLE);
avatar.setProblem(false);
status.setVisibility(GONE);
}
remove.setVisibility(GONE);

View File

@@ -1,12 +1,13 @@
package org.briarproject.briar.android.settings;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
@@ -44,6 +45,10 @@ import static android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT;
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.os.Build.VERSION.SDK_INT;
import static android.provider.Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS;
import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
@@ -53,6 +58,10 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_ALWAYS;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE;
import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.GROUP_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_BLOG;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP;
@@ -128,35 +137,14 @@ public class SettingsFragment extends PreferenceFragmentCompat
"pref_key_notify_lock_screen");
notifySound = findPreference("pref_key_notify_sound");
setSettingsEnabled(false);
enableBluetooth.setOnPreferenceChangeListener(this);
torNetwork.setOnPreferenceChangeListener(this);
notifyPrivateMessages.setOnPreferenceChangeListener(this);
notifyGroupMessages.setOnPreferenceChangeListener(this);
notifyForumPosts.setOnPreferenceChangeListener(this);
notifyBlogPosts.setOnPreferenceChangeListener(this);
notifyVibration.setOnPreferenceChangeListener(this);
if (Build.VERSION.SDK_INT >= 21) {
if (SDK_INT >= 21) {
notifyLockscreen.setVisible(true);
notifyLockscreen.setOnPreferenceChangeListener(this);
}
notifySound.setOnPreferenceClickListener(preference -> {
String title = getString(R.string.choose_ringtone_title);
Intent i = new Intent(ACTION_RINGTONE_PICKER);
i.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
i.putExtra(EXTRA_RINGTONE_TITLE, title);
i.putExtra(EXTRA_RINGTONE_DEFAULT_URI, DEFAULT_NOTIFICATION_URI);
i.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
Uri uri;
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (StringUtils.isNullOrEmpty(ringtoneUri))
uri = DEFAULT_NOTIFICATION_URI;
else uri = Uri.parse(ringtoneUri);
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
}
startActivityForResult(i, REQUEST_RINGTONE);
return true;
});
findPreference("pref_key_send_feedback").setOnPreferenceClickListener(
preference -> {
@@ -218,39 +206,105 @@ public class SettingsFragment extends PreferenceFragmentCompat
enableBluetooth.setValue(Boolean.toString(btSetting));
torNetwork.setValue(Integer.toString(torSetting));
notifyPrivateMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_PRIVATE, true));
notifyGroupMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_GROUP, true));
notifyForumPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_FORUM, true));
notifyBlogPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_BLOG, true));
notifyVibration.setChecked(settings.getBoolean(
PREF_NOTIFY_VIBRATION, true));
notifyLockscreen.setChecked(settings.getBoolean(
PREF_NOTIFY_LOCK_SCREEN, false));
String text;
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
String ringtoneName = settings.get(PREF_NOTIFY_RINGTONE_NAME);
if (StringUtils.isNullOrEmpty(ringtoneName)) {
text = getString(R.string.notify_sound_setting_default);
if (SDK_INT < 26) {
notifyPrivateMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_PRIVATE, true));
notifyGroupMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_GROUP, true));
notifyForumPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_FORUM, true));
notifyBlogPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_BLOG, true));
notifyVibration.setChecked(settings.getBoolean(
PREF_NOTIFY_VIBRATION, true));
notifyPrivateMessages.setOnPreferenceChangeListener(this);
notifyGroupMessages.setOnPreferenceChangeListener(this);
notifyForumPosts.setOnPreferenceChangeListener(this);
notifyBlogPosts.setOnPreferenceChangeListener(this);
notifyVibration.setOnPreferenceChangeListener(this);
notifyLockscreen.setChecked(settings.getBoolean(
PREF_NOTIFY_LOCK_SCREEN, false));
notifySound.setOnPreferenceClickListener(
pref -> onNotificationSoundClicked());
String text;
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
String ringtoneName =
settings.get(PREF_NOTIFY_RINGTONE_NAME);
if (StringUtils.isNullOrEmpty(ringtoneName)) {
text = getString(R.string.notify_sound_setting_default);
} else {
text = ringtoneName;
}
} else {
text = ringtoneName;
text = getString(R.string.notify_sound_setting_disabled);
}
notifySound.setSummary(text);
} else {
text = getString(R.string.notify_sound_setting_disabled);
setupNotificationPreference(notifyPrivateMessages,
CONTACT_CHANNEL_ID,
R.string.notify_private_messages_setting_summary_26);
setupNotificationPreference(notifyGroupMessages,
GROUP_CHANNEL_ID,
R.string.notify_group_messages_setting_summary_26);
setupNotificationPreference(notifyForumPosts, FORUM_CHANNEL_ID,
R.string.notify_forum_posts_setting_summary_26);
setupNotificationPreference(notifyBlogPosts, BLOG_CHANNEL_ID,
R.string.notify_blog_posts_setting_summary_26);
notifyVibration.setVisible(false);
notifyLockscreen.setVisible(false);
notifySound.setVisible(false);
}
notifySound.setSummary(text);
setSettingsEnabled(true);
});
}
private void setSettingsEnabled(boolean enabled) {
enableBluetooth.setEnabled(enabled);
torNetwork.setEnabled(enabled);
notifyPrivateMessages.setEnabled(enabled);
notifyGroupMessages.setEnabled(enabled);
notifyForumPosts.setEnabled(enabled);
notifyBlogPosts.setEnabled(enabled);
notifyVibration.setEnabled(enabled);
notifyLockscreen.setEnabled(enabled);
notifySound.setEnabled(enabled);
}
@TargetApi(26)
private void setupNotificationPreference(CheckBoxPreference pref,
String channelId, @StringRes int summary) {
pref.setWidgetLayoutResource(0);
pref.setSummary(summary);
pref.setOnPreferenceClickListener(clickedPref -> {
Intent intent = new Intent(ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(EXTRA_APP_PACKAGE, getContext().getPackageName())
.putExtra(EXTRA_CHANNEL_ID, channelId);
startActivity(intent);
return true;
});
}
private boolean onNotificationSoundClicked() {
String title = getString(R.string.choose_ringtone_title);
Intent i = new Intent(ACTION_RINGTONE_PICKER);
i.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
i.putExtra(EXTRA_RINGTONE_TITLE, title);
i.putExtra(EXTRA_RINGTONE_DEFAULT_URI,
DEFAULT_NOTIFICATION_URI);
i.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
Uri uri;
String ringtoneUri =
settings.get(PREF_NOTIFY_RINGTONE_URI);
if (StringUtils.isNullOrEmpty(ringtoneUri))
uri = DEFAULT_NOTIFICATION_URI;
else uri = Uri.parse(ringtoneUri);
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
}
startActivityForResult(i, REQUEST_RINGTONE);
return true;
}
private void triggerFeedback() {
androidExecutor.runOnBackgroundThread(() -> ACRA.getErrorReporter()
.handleException(new UserFeedback(), false));

View File

@@ -5,7 +5,6 @@ import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.UiThread;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.LayoutInflater;
@@ -26,7 +25,6 @@ public class TextAvatarView extends FrameLayout {
private final AppCompatTextView character;
private final CircleImageView background;
private final TextView badge;
private int unreadCount;
public TextAvatarView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
@@ -49,30 +47,14 @@ public class TextAvatarView extends FrameLayout {
}
public void setUnreadCount(int count) {
unreadCount = count;
if (count > 0) {
badge.setBackgroundResource(R.drawable.bubble);
badge.setText(String.valueOf(count));
badge.setTextColor(ContextCompat.getColor(getContext(),
R.color.briar_text_primary_inverse));
badge.setVisibility(VISIBLE);
} else {
badge.setVisibility(INVISIBLE);
}
}
public void setProblem(boolean problem) {
if (problem) {
badge.setBackgroundResource(R.drawable.bubble_problem);
badge.setText("!");
badge.setTextColor(ContextCompat
.getColor(getContext(), R.color.briar_primary));
badge.setVisibility(VISIBLE);
} else {
setUnreadCount(unreadCount);
}
}
public void setBackgroundBytes(byte[] bytes) {
int r = getByte(bytes, 0) * 3 / 4 + 96;
int g = getByte(bytes, 1) * 3 / 4 + 96;

View File

@@ -21,6 +21,12 @@ public interface AndroidNotificationManager {
String PREF_NOTIFY_VIBRATION = "notifyVibration";
String PREF_NOTIFY_LOCK_SCREEN = "notifyLockScreen";
// Channel IDs
String CONTACT_CHANNEL_ID = "contacts";
String GROUP_CHANNEL_ID = "groups";
String FORUM_CHANNEL_ID = "forums";
String BLOG_CHANNEL_ID = "blogs";
// Content URIs for pending intents
String CONTACT_URI = "content://org.briarproject.briar/contact";
String GROUP_URI = "content://org.briarproject.briar/group";

View File

@@ -1,5 +1,9 @@
<vector android:alpha="0.54" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15.73,3L8.27,3L3,8.27v7.46L8.27,21h7.46L21,15.73L21,8.27L15.73,3zM12,17.3c-0.72,0 -1.3,-0.58 -1.3,-1.3 0,-0.72 0.58,-1.3 1.3,-1.3 0.72,0 1.3,0.58 1.3,1.3 0,0.72 -0.58,1.3 -1.3,1.3zM13,13h-2L11,7h2v6z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15.73,3L8.27,3L3,8.27v7.46L8.27,21h7.46L21,15.73L21,8.27L15.73,3zM12,17.3c-0.72,0 -1.3,-0.58 -1.3,-1.3 0,-0.72 0.58,-1.3 1.3,-1.3 0.72,0 1.3,0.58 1.3,1.3 0,0.72 -0.58,1.3 -1.3,1.3zM13,13h-2L11,7h2v6z"/>
</vector>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:radius="@dimen/unread_bubble_size"/>
<padding
android:left="@dimen/unread_bubble_padding_horizontal"
android:right="@dimen/unread_bubble_padding_horizontal"/>
<solid
android:color="@color/briar_gold"/>
<stroke
android:color="@color/briar_primary"
android:width="@dimen/avatar_border_width"/>
</shape>

View File

@@ -0,0 +1,4 @@
<vector android:height="48dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
</vector>

View File

@@ -0,0 +1,4 @@
<vector android:height="48dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z"/>
</vector>

View File

@@ -15,39 +15,35 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="2">
android:baselineAligned="false">
<FrameLayout
<LinearLayout
android:id="@+id/status_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
android:layout_weight="1"
android:background="@android:color/background_light"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
android:visibility="invisible">
<LinearLayout
android:id="@+id/status_container"
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/background_light"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
android:visibility="invisible">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
</FrameLayout>
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
<FrameLayout
android:id="@+id/qr_code_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
@@ -59,12 +55,31 @@
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<ImageView
android:id="@+id/qr_code"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:layout_gravity="center"/>
android:layout_height="match_parent">
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:contentDescription="@string/qr_code"
android:scaleType="fitCenter"/>
<ImageView
android:id="@+id/fullscreen_button"
android:background="?selectableItemBackground"
android:src="@drawable/ic_fullscreen_black_48dp"
android:alpha="0.54"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:contentDescription="@string/show_qr_code_fullscreen"/>
</RelativeLayout>
</FrameLayout>
</LinearLayout>
@@ -92,5 +107,4 @@
android:paddingTop="@dimen/margin_large"
tools:text="@string/waiting_for_contact_to_scan"/>
</RelativeLayout>
</FrameLayout>

View File

@@ -15,39 +15,35 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="2">
android:baselineAligned="false">
<FrameLayout
<LinearLayout
android:id="@+id/status_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_weight="1"
android:background="@android:color/background_light"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
android:visibility="invisible">
<LinearLayout
android:id="@+id/status_container"
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/background_light"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
android:visibility="invisible">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
</FrameLayout>
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
<FrameLayout
android:id="@+id/qr_code_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
@@ -59,12 +55,31 @@
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<ImageView
android:id="@+id/qr_code"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:layout_gravity="center"/>
android:layout_height="match_parent">
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:contentDescription="@string/qr_code"
android:scaleType="fitCenter"/>
<ImageView
android:id="@+id/fullscreen_button"
android:background="?selectableItemBackground"
android:src="@drawable/ic_fullscreen_black_48dp"
android:alpha="0.54"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:contentDescription="@string/show_qr_code_fullscreen"/>
</RelativeLayout>
</FrameLayout>
</LinearLayout>
@@ -92,5 +107,4 @@
android:paddingTop="@dimen/margin_large"
tools:text="@string/waiting_for_contact_to_scan"/>
</RelativeLayout>
</FrameLayout>

View File

@@ -1,23 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<android.support.constraint.ConstraintLayout
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">
<ImageView
android:id="@+id/imageView"
android:layout_width="128dp"
android:layout_height="128dp"
android:scaleType="center"
android:src="@drawable/startup_lock"
android:tint="@color/briar_primary"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintVertical_chainStyle="packed"
tools:ignore="ContentDescription"/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/imageView"
app:layout_constraintEnd_toEndOf="@+id/imageView"
app:layout_constraintStart_toStartOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/imageView"/>
<TextView
android:id="@+id/title_progress_bar"
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/progressBar"
android:layout_centerHorizontal="true"
android:paddingTop="@dimen/margin_large"
android:text="@string/progress_title_logout"/>
android:layout_margin="8dp"
android:text="@string/progress_title_logout"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView"/>
</RelativeLayout>
</android.support.constraint.ConstraintLayout>

View File

@@ -234,8 +234,8 @@
<string name="blogs_blog_post_scroll_to">Отвори</string>
<string name="blogs_feed_empty_state">Това е глобалната блог емисия.\n\nНикой още не е публикувал нищо.\n\nБъдете първия и натиснете писалката, за да напишете първата блог публикация.</string>
<string name="blogs_remove_blog">Премахване на блог</string>
<string name="blogs_remove_blog_ok">Премахване</string>
<string name="blogs_remove_blog_dialog_message">Сигурни ли сте, че искате да премахнете този блог и всички публикации?\nБлогът няма да бъдат премахнат от устройствата на други хора.</string>
<string name="blogs_remove_blog_ok">Премахни блог</string>
<string name="blogs_blog_removed">Блогът е премахнат</string>
<string name="blogs_reblog_comment_hint">Добавете съобщение (незадължително)</string>
<string name="blogs_reblog_button">Реблог</string>
@@ -264,8 +264,8 @@
<string name="blogs_rss_feeds_manage_author">Автор:</string>
<string name="blogs_rss_feeds_manage_updated">Последно актуализиране:</string>
<string name="blogs_rss_remove_feed">Премахване на емисия</string>
<string name="blogs_rss_remove_feed_ok">Премахване</string>
<string name="blogs_rss_remove_feed_dialog_message">Сигурни ли сте, че искате да премахнете тази емисия и всички нейни публикации?\nВсички споделени от вас публикации няма да бъдат премахнати от устройствата на други хора.</string>
<string name="blogs_rss_remove_feed_ok">Емисията е премахната</string>
<string name="blogs_rss_feeds_manage_delete_error">Емисията не можа да бъде изтрита!</string>
<string name="blogs_rss_feeds_manage_empty_state">Нямате добавени RSS емисии.\n\nНатиснете плюса в горния десен ъгъл на екрана, за да добавите нова емисия.</string>
<string name="blogs_rss_feeds_manage_error">Възникна проблем при зареждането на емисиите ви. Моля, опитайте пак по-късно.</string>

View File

@@ -134,8 +134,10 @@
<!--Forum Sharing-->
<string name="forum_share_message">Ouzhpennañ ur gemennadenn (diret)</string>
<!--Blogs-->
<string name="blogs_remove_blog_ok">Dilemel</string>
<!--Blog Sharing-->
<!--RSS Feeds-->
<string name="blogs_rss_remove_feed_ok">Dilemel</string>
<!--Settings Network-->
<!--Settings Security and Panic-->
<string name="lock_setting_title">Digevreañ</string>

View File

@@ -43,6 +43,8 @@
</plurals>
<string name="expiry_update">S\'ha ampliat la data de venciment de la prova. El vostre compte caducarà ara en %d dies.</string>
<string name="expiry_date_reached">Aquest programa ha caducat.\nGràcies per haver-lo provat!</string>
<string name="startup_open_database">S\'està desxifrant la base de dades...</string>
<string name="startup_migrate_database">S\'està actualitzant la base de dades...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Obre el calaix de navegació</string>
<string name="nav_drawer_close_description">Tanca el calaix de navegació</string>
@@ -207,7 +209,7 @@
<string name="choose_forum_hint">Trieu un nom per al fòrum</string>
<string name="create_forum_button">Crea el fòrum</string>
<string name="forum_created_toast">S\'ha creat el fòrum</string>
<string name="no_forum_posts">Aquest fòrum està buit.\n\nUtilitzeu la icona de la ploma a la part superior per redactar la primera publicació.\n\nEsteu aquí sol? Compartiu aquest fòrum amb els vostres contactes!</string>
<string name="no_forum_posts">No hi ha publicacions per mostrar</string>
<string name="no_posts">No hi ha publicacions</string>
<plurals name="posts">
<item quantity="one">%d publicacio</item>
@@ -251,7 +253,7 @@
</plurals>
<string name="nobody">Ningú</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Aquest blog està buit.\n\nPotser l\'autor encara no hi ha escrit res o que la persona que us ha compartit aquest blog hagi de connectar-se, llavors es podran sincronitzar les publicacions.</string>
<string name="blogs_other_blog_empty_state">No hi ha publicacions per mostrar</string>
<string name="read_more">llegir més</string>
<string name="blogs_write_blog_post">Escriviu una publicació de blog</string>
<string name="blogs_write_blog_post_body_hint">Escriviu la vostra publicació al blog aquí</string>

View File

@@ -31,7 +31,9 @@
<string name="dialog_title_lost_password">Pasahitz galdua</string>
<string name="dialog_message_lost_password">Zure Briar kontua zure gailuan zifratuta gordetzen da, ez hodeian, beraz ezin dugu zure pasahitza berrezarri. Zure kontua ezabatu eta berriro hasi nahi duzu?\n\nKontuz: Zure identitateak, kontaktuak eta mezuak betirako galduko dira.</string>
<string name="startup_failed_notification_title">Ezin izan da Briar abiatu</string>
<string name="startup_failed_notification_text">Sakatu informazio gehiagorako</string>
<string name="startup_failed_activity_title">Briar abio-hutsegitea</string>
<string name="startup_failed_data_too_new_error">Aplikazioaren bertsio hau zaharregia da. Eguneratu azken bertsiora eta saiatu berriro.</string>
<string name="startup_failed_service_error">Briar aplikazioak ezin izan du ezinbesteko plugin bat abiatu. Briar berrinstalatzeak arazoa konpondu ohi du. Hala ere, jakin zure kontua eta datuak galduko dituzula Briar aplikazioak ez baititu zerbitzari zentralak erabiltzen zure datuak gordetzeko.</string>
<plurals name="expiry_warning">
<item quantity="one">Hau Briar-en probetarako bertsio bat da. Zure kontua egun %d barru iraungituko da eta ezin da berriztu.</item>
@@ -39,6 +41,8 @@
</plurals>
<string name="expiry_update">Probetarako iraungitze data luzatu da. Zure kontua %d egun barru iraungituko da.</string>
<string name="expiry_date_reached">Programa hau iraungitu da.\nEskerrik asko probatzeagatik!</string>
<string name="startup_open_database">Datu-basea deszifratzen...</string>
<string name="startup_migrate_database">Datu-basea eguneratzen...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Ireki nabigazio tiradera</string>
<string name="nav_drawer_close_description">Itxi nabigazio tiradera</string>
@@ -203,7 +207,7 @@
<string name="choose_forum_hint">Hautatu zure foroaren izena</string>
<string name="create_forum_button">Sortu foroa</string>
<string name="forum_created_toast">Foroa sortuta</string>
<string name="no_forum_posts">Foro hau hutsik dago.\n\nErabili goiko arkatzaren ikonoa lehen mezua idazteko.\n\nBakarrik sentitzen zara hemen? Partekatu foro hau zure kontaktuekin!</string>
<string name="no_forum_posts">Ez dago mezurik erakusteko</string>
<string name="no_posts">Sarrerarik ez</string>
<plurals name="posts">
<item quantity="one">Bidalketa %d</item>
@@ -247,7 +251,7 @@
</plurals>
<string name="nobody">Inor ez</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Blog hau orain hutsik dago.\n\nEgileak ez du ezer idatzi edo blog hau zurekin partekatu duen kontaktua deskonektatuta dago, eta ezin da orain sinkronizatu.</string>
<string name="blogs_other_blog_empty_state">Ez dago mezurik erakusteko</string>
<string name="read_more">irakurri gehiago</string>
<string name="blogs_write_blog_post">Idatzi blog sarrera</string>
<string name="blogs_write_blog_post_body_hint">Idatzi zure blog sarrera hemen</string>

View File

@@ -40,6 +40,8 @@
</plurals>
<string name="expiry_update">Testiaikaa on pidennetty. Tilisi tulee nyt vanhentumaan %d päivän kuluttua.</string>
<string name="expiry_date_reached">Tämä sovellus on vanhentunut.\nKiitos testaamisesta!</string>
<string name="startup_open_database">Puretaan tietokannan salaus...</string>
<string name="startup_migrate_database">Päivitetään tietokanta...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Avaa navigointilaatikko</string>
<string name="nav_drawer_close_description">Sulje navigointilaatikko</string>
@@ -94,11 +96,11 @@
<string name="show_onboarding">Näytä apudialogi</string>
<string name="fix">Korjaa</string>
<string name="help">Ohje</string>
<string name="sorry">Anteeksi</string>
<string name="sorry">Pahoittelemme</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Näyttää siltä, että olet uusi täällä, eikä sinulla vielä ole yhteyshenkilöitä.\n\nNapauta yllä olevaa + -kuvaketta ja seuraa ohjeita lisätäksesi kavereita luetteloon.\n\nMuista: Voit ainoastaan lisätä uusia yhteyshenkilöitä tapaamalla heidät kasvokkain. Tämä estää sen, että joku voisi esittää olevansa sinä tai lukea viestejäsi tulevaisuudessa.</string>
<string name="date_no_private_messages">Ei viestejä.</string>
<string name="no_private_messages">Tämä on keskustelunäkymä.\n\nKeskustelu näyttää puuttuvan.\n\nNapauta syöttökenttää sivun pohjalla aloittaaksesi keskustelun.</string>
<string name="no_private_messages">Ei viestejä</string>
<string name="message_hint">Kirjoita viesti</string>
<string name="delete_contact">Poista yhteystieto</string>
<string name="dialog_title_delete_contact">Vahvista yhteystiedon poistaminen</string>
@@ -203,7 +205,7 @@
<string name="choose_forum_hint">Valitse nimi foorumillesi</string>
<string name="create_forum_button">Luo foorumi</string>
<string name="forum_created_toast">Foorumi luotu</string>
<string name="no_forum_posts">Tämä foorumi on tyhjä.\n\nKäytä yllä olevaa kynää kirjoittaaksesi ensimmäisen viestin.\n\nTuntuuko yksinäiseltä? Kerro tästä foorumista muille!</string>
<string name="no_forum_posts">Ei kirjoituksia</string>
<string name="no_posts">Ei viestejä</string>
<plurals name="posts">
<item quantity="one">%d viesti</item>
@@ -231,7 +233,7 @@
<string name="forum_invitations_title">Kutsut liittyä foorumeihin</string>
<string name="forum_invitation_exists">Olet jo hyväksynyt kutsun liittyä tähän foorumiin. Useamman kutsun hyväksyminen kasvattaa ja vahvistaa foorumin kommunikaatiota.</string>
<string name="forum_joined_toast">Liittyi foorumiin</string>
<string name="forum_declined_toast">Kutsu liittyä foorumiin on hylätty</string>
<string name="forum_declined_toast">Kutsu hylätty</string>
<string name="shared_by_format">Jakanut %s</string>
<string name="forum_invitation_already_sharing">On jo jakamassa</string>
<string name="forum_invitation_response_accepted_sent">Olet hyväksynyt käyttäjän %s lähettämän foorumikutsun.</string>
@@ -247,10 +249,10 @@
</plurals>
<string name="nobody">Ei kukaan</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Blogi on tällä hetkellä tyhjä.\n\nJoko blogin julkaisija ei ole kirjoittanut mitään vielä, tai tämän blogin sinulle jakaneen henkilön on liityttävä takaisin verkkoon, jotta blogikirjoitukset voitaisiin synkronisoida.</string>
<string name="blogs_other_blog_empty_state">Ei kirjoituksia</string>
<string name="read_more">lue lisää</string>
<string name="blogs_write_blog_post">Julkaise blogikirjoitus</string>
<string name="blogs_write_blog_post_body_hint">Kirjoita blogikirjoitus tähän</string>
<string name="blogs_write_blog_post_body_hint">Kirjoita blogikirjoitus</string>
<string name="blogs_publish_blog_post">Julkaise</string>
<string name="blogs_blog_post_created">Blogikirjoitus julkaistu</string>
<string name="blogs_blog_post_received">Uusi blogikirjoitus vastaanotettu</string>
@@ -275,7 +277,7 @@
<string name="blogs_sharing_invitation_sent">Olet jakanut \"%1$s\" -nimisen blogin käyttäjälle %2$s.</string>
<string name="blogs_sharing_invitations_title">Blogi kutsut</string>
<string name="blogs_sharing_joined_toast">Blogi tilattu</string>
<string name="blogs_sharing_declined_toast">Kutsu blogiin on hylätty</string>
<string name="blogs_sharing_declined_toast">Kutsu hylätty</string>
<string name="sharing_status_blog">Kuka tahansa joka tilaa blogin voi jakaa sen tuntemilleen käyttäjille. Olet jakamassa tämän blogin seuraaville käyttäjille. Voi myös olla muita blogin tilaajia, joita sinä et näe.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Tuo RSS syöte</string>
@@ -376,4 +378,6 @@
<string name="permission_camera_request_body">Skannatakseen QR koodin, Briar tarvitsee luvan käyttää kameraa.</string>
<string name="permission_camera_denied_body">Olet kieltänyt käyttämästä kameraa, mutta yhteyshenkilöiden lisääminen vaatii kameran käyttöä.\n\nOle hyvä ja harkitse kameraluvan myöntämistä.</string>
<string name="permission_camera_denied_toast">Kameralupaa ei myönnetty</string>
<string name="qr_code">QR-koodi</string>
<string name="show_qr_code_fullscreen">Näytä QR-koodi koko näytöllä</string>
</resources>

View File

@@ -33,16 +33,18 @@
<string name="startup_failed_notification_title">Impossible de démarrer Briar</string>
<string name="startup_failed_notification_text">Toucher pour plus dinformations.</string>
<string name="startup_failed_activity_title">Échec de démarrage de Briar</string>
<string name="startup_failed_db_error">Pour quelque raison, votre base de données Briar est corrompue sans espoir de réparation. Votre compte, vos données et tous vos contacts sont perdus. Malheureusement, vous devez réinstaller Briar et créer un nouveau compte en choisissant « Jai oublié mon mot de passe » dans linvite de mot de passe.</string>
<string name="startup_failed_data_too_old_error">Votre compte a été créé avec une ancienne version de cette appli et ne peut pas être ouvert avec cette version. Vous devez soit installer lancienne version soit supprimer votre compte en choisissant « Jai oublié mon mot de passe » dans linvite de mot de passe.</string>
<string name="startup_failed_db_error">Pour quelque raison, votre base de données Briar est corrompue sans espoir de réparation. Votre compte, vos données et tous vos contacts sont perdus. Malheureusement, vous devez réinstaller Briar et créer un nouveau compte en choisissant « Jai oublié mon mot de passe » dans linvite de mot de passe.</string>
<string name="startup_failed_data_too_old_error">Votre compte a été créé avec une ancienne version de cette appli et ne peut pas être ouvert avec cette version. Vous devez soit installer lancienne version, soit créer un nouveau compte en choisissant « Jai oublié mon mot de passe » dans linvite de mot de passe.</string>
<string name="startup_failed_data_too_new_error">Cette version de lappli est trop ancienne. Veuillez la mettre à niveau vers la dernière version et ressayer.</string>
<string name="startup_failed_service_error">Briar na pas pu démarrer un greffon exigé. Réinstaller Briar résout généralement ce problème. Veuillez cependant noter que vous perdrez votre compte et toutes données relatives puisque Briar nutilise pas de serveurs centralisés sur lesquels enregistrer vos données.</string>
<plurals name="expiry_warning">
<item quantity="one">Ceci est une version de test de Briar. Votre compte arrivera à expiration dans %d jour et ne peut pas être renouvelé.</item>
<item quantity="other">Ceci est une version de test de Briar. Votre compte arrivera à expiration dans %d jours et ne peut pas être renouvelé.</item>
<item quantity="other">Ceci est une version de test de Briar. Votre compte arrivera à expiration dans %d jours et ne pourra pas être renouvelé.</item>
</plurals>
<string name="expiry_update">La date de fin de test a été repoussée. Votre compte arrivera maintenant à expiration dans %d jours.</string>
<string name="expiry_date_reached">Ce logiciel est arrivé à expiration.\nMerci de lavoir testé!</string>
<string name="startup_open_database">Déchiffrement de la base de données…</string>
<string name="startup_migrate_database">Mise à niveau de la base de données…</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Ouvrir le tiroir de navigation</string>
<string name="nav_drawer_close_description">Fermer le tiroir de navigation</string>
@@ -99,9 +101,9 @@
<string name="help">Aide</string>
<string name="sorry">Désolé</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Il semble que soyez nouveau ici, sans encore aucun contact.\n\nTouchez licône + en haut et suivez les instructions pour ajouter des amis à votre liste.\n\nVeuillez ne pas oublier que vous pouvez seulement ajouter des contacts en les rencontrant directement, afin déviter que quelquun se fasse passer pour vous et puisse lire vos messages à lavenir.</string>
<string name="no_contacts">Aucun contact à afficher\n\nTouchez licône + pour ajouter un contact</string>
<string name="date_no_private_messages">Aucun message.</string>
<string name="no_private_messages">Ceci est la vue des conversations.\n\nIl semble ne pas y en avoir.\n\nIl suffit de toucher le champ de saisie ci-bas pour en démarrer une.</string>
<string name="no_private_messages">Aucun message à afficher</string>
<string name="message_hint">Rédiger le message</string>
<string name="delete_contact">Supprimer le contact</string>
<string name="dialog_title_delete_contact">Confirmer la suppression du contact</string>
@@ -149,7 +151,7 @@
<item quantity="other">%d nouveaux contacts ont été ajoutés.</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">Vous ne participez à aucun groupe.\n\nTouchez licône + ci-haut pour en créer un ou demandez à vos contacts de vous inviter dans lun des leurs.</string>
<string name="groups_list_empty">Aucun groupe à afficher\n\nTouchez licône + pour créer un groupe ou demandez à vos contacts de partager des groupes avec vous</string>
<string name="groups_created_by">Créé par %s</string>
<plurals name="messages">
<item quantity="one">%d message</item>
@@ -202,12 +204,12 @@
<string name="groups_reveal_visible_revealed_by_contact">Votre lien avec le contact est visible par le groupe (dévoilé par %s)</string>
<string name="groups_reveal_invisible">Votre lien avec le contact nest pas visible par le groupe</string>
<!--Forums-->
<string name="no_forums">Vous navez pas encore de forums.\n\nPourquoi ne pas en créer un en touchant licône + ci-haut?\n\nVous pouvez aussi demander à vos contacts den partager avec vous.</string>
<string name="no_forums">Aucun forum à afficher\n\nTouchez licône + pour créer un forum ou demandez à vos contacts de partager des forums avec vous</string>
<string name="create_forum_title">Créer un forum</string>
<string name="choose_forum_hint">Choisir un nom pour votre forum </string>
<string name="create_forum_button">Créer un forum</string>
<string name="forum_created_toast">Le forum a été créé</string>
<string name="no_forum_posts">Ce forum est vide.\n\nUtilisez licône de crayon ci-haut pour rédiger le premier article.\n\nVous sentez-vous seul ici? Partagez ce forum avec vos contacts!</string>
<string name="no_forum_posts">Aucun article à afficher</string>
<string name="no_posts">Aucun article</string>
<plurals name="posts">
<item quantity="one">%d article</item>
@@ -219,23 +221,23 @@
<string name="btn_reply">Répondre</string>
<string name="forum_leave">Quitter le forum</string>
<string name="dialog_title_leave_forum">Confirmer la sortie du forum</string>
<string name="dialog_message_leave_forum">Voulez-vous vraiment quitter ce forum? Les contacts avec qui vous lavez partagé pourraient ne plus en recevoir les mises à jour.</string>
<string name="dialog_message_leave_forum">Voulez-vous vraiment quitter ce forum?\n\nLes contacts avec qui vous lavez partagé pourraient ne plus en recevoir les mises à jour.</string>
<string name="dialog_button_leave">Quitter</string>
<string name="forum_left_toast">A quitté le forum</string>
<string name="forum_left_toast">À quitté le forum</string>
<!--Forum Sharing-->
<string name="forum_share_button">Partager le forum</string>
<string name="contacts_selected">Des contacts ont été sélectionnés</string>
<string name="activity_share_toolbar_header">Choisir des contacts</string>
<string name="no_contacts_selector">Il semble que soyez nouveau ici, sans encore aucun contact.\n\nRevenez ici après avoir ajouté votre premier contact.</string>
<string name="no_contacts_selector">Aucun contact à afficher.\n\nVeuillez revenir ici après avoir ajouté un contact</string>
<string name="forum_shared_snackbar">Le forum a été partagé avec les contacts choisis</string>
<string name="forum_share_message">Ajouter un message (facultatif)</string>
<string name="forum_share_error">Une erreur est survenue lors du partage de ce forum.</string>
<string name="forum_invitation_received">%1$s a partagé le forum « %2$s » avec vous.</string>
<string name="forum_invitation_sent">Vous avez partagé le forum « %1$s » avec %2$s.</string>
<string name="forum_invitations_title">Invitations au forum</string>
<string name="forum_invitation_exists">Vous avez déjà accepté une invitation à ce forum. En acceptant dautres invitations, vous augmenterez et renforcerez la communication de ce forum.</string>
<string name="forum_invitation_exists">Vous avez déjà accepté une invitation à ce forum.\n\n En acceptant dautres invitations, vous rendrez la communication vers ce forum plus rapide et plus fiable.</string>
<string name="forum_joined_toast">Vous vous êtes joint au forum</string>
<string name="forum_declined_toast">Linvitation au forum a été refusée</string>
<string name="forum_declined_toast">Linvitation a été refusée</string>
<string name="shared_by_format">Partagé par %s</string>
<string name="forum_invitation_already_sharing">Le forum est déjà partagé</string>
<string name="forum_invitation_response_accepted_sent">Vous avez accepté linvitation de %s au forum.</string>
@@ -251,18 +253,18 @@
</plurals>
<string name="nobody">Personne</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Ce blogue est actuellement vide.\n\nSoit lauteur na encore rien écrit, soit la personne qui la partagé avec vous doit se connecter pour que les articles soient synchronisés.</string>
<string name="blogs_other_blog_empty_state">Aucun billet à afficher</string>
<string name="read_more">en lire davantage</string>
<string name="blogs_write_blog_post">Écrire un article de blogue</string>
<string name="blogs_write_blog_post_body_hint">Saisir votre message de blogue ici</string>
<string name="blogs_write_blog_post">Écrire un billet de blogue</string>
<string name="blogs_write_blog_post_body_hint">Tapez votre billet de blogue</string>
<string name="blogs_publish_blog_post">Publier</string>
<string name="blogs_blog_post_created">Larticle de blogue a été créé</string>
<string name="blogs_blog_post_received">Un nouvel article de blogue a été reçu</string>
<string name="blogs_blog_post_created">Le billet de blogue a été créé</string>
<string name="blogs_blog_post_received">Un nouvel billet de blogue a été reçu</string>
<string name="blogs_blog_post_scroll_to">Atteindre</string>
<string name="blogs_feed_empty_state">Ceci est le fil global des blogues.\n\nIl semble que personne nait encore rien écrit.\n\nSoyez le premier et touchez licône de crayon pour rédiger un nouvel article de blogue.</string>
<string name="blogs_feed_empty_state">Aucun billet à afficher.\n\n\Les billets de vos contacts et les blogues auxquels vous vous abonnez apparaîtront ici.\n\nTouchez licône de crayon pour rédiger un billet</string>
<string name="blogs_remove_blog">Supprimer le blogue</string>
<string name="blogs_remove_blog_dialog_message">Voulez-vous vraiment supprimer ce blogue et tous ses messages?\nNotez que cela ne le supprimera pas des appareils dautrui.</string>
<string name="blogs_remove_blog_ok">Supprimer le blogue</string>
<string name="blogs_remove_blog_dialog_message">Voulez-vous vraiment supprimer ce blogue?\nLes billets seront supprimés de votre appareil mais pas des appareils dautrui.\n\nLes contacts avec qui vous avez partagé ce blogue pourraient ne plus en recevoir les mises à jour.</string>
<string name="blogs_remove_blog_ok">Supprimer</string>
<string name="blogs_blog_removed">Le blogue a été supprimé</string>
<string name="blogs_reblog_comment_hint">Ajouter un commentaire (facultatif)</string>
<string name="blogs_reblog_button">Rebloguer</string>
@@ -279,7 +281,7 @@
<string name="blogs_sharing_invitation_sent">Vous avez partagé le blogue « %1$s » avec %2$s.</string>
<string name="blogs_sharing_invitations_title">Invitations au blogue</string>
<string name="blogs_sharing_joined_toast">Est abonné au blogue</string>
<string name="blogs_sharing_declined_toast">Linvitation au blogue a été refusée</string>
<string name="blogs_sharing_declined_toast">Linvitation a été refusée</string>
<string name="sharing_status_blog">Quiconque est abonné à un blogue peut le partager avec ses contacts. Vous partagez ce blogue avec les contacts suivants. Il peut aussi y avoir dautres abonnés que vous ne pouvez pas voir.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Importer un fil RSS</string>
@@ -291,10 +293,10 @@
<string name="blogs_rss_feeds_manage_author">Auteur :</string>
<string name="blogs_rss_feeds_manage_updated">Dernière mise à jour :</string>
<string name="blogs_rss_remove_feed">Supprimer le fil</string>
<string name="blogs_rss_remove_feed_dialog_message">Voulez-vous vraiment supprimer ce fil et tous ses messages ?\nLes articles que vous avez partagés ne seront pas supprimés des appareils dautrui.</string>
<string name="blogs_rss_remove_feed_ok">Supprimer le fil</string>
<string name="blogs_rss_remove_feed_dialog_message">Voulez-vous vraiment supprimer ce fil?\nLes billets seront supprimés de votre appareil mais pas des appareils dautrui.\n\nLes contacts avec qui vous avez partagé ce fil pourraient ne plus en recevoir les mises à jour.</string>
<string name="blogs_rss_remove_feed_ok">Supprimer</string>
<string name="blogs_rss_feeds_manage_delete_error">Impossible de supprimer le fil!</string>
<string name="blogs_rss_feeds_manage_empty_state">Vous navez encore importé aucun fil RSS.\n\nPourquoi ne pas cliquer sur licône + en haut à droite pour ajouter votre premier fil?</string>
<string name="blogs_rss_feeds_manage_empty_state">Aucun fil RSS à afficher\n\nTouchez licône + pour importer un fil</string>
<string name="blogs_rss_feeds_manage_error">Un problème est survenu lors du chargement de vos fils. Veuillez ressayer ultérieurement.</string>
<!--Settings Network-->
<string name="network_settings_title">Réseaux</string>
@@ -331,12 +333,16 @@
<string name="notification_settings_title">Notifications</string>
<string name="notify_private_messages_setting_title">Messages privés</string>
<string name="notify_private_messages_setting_summary">Afficher des notifications pour les messages privés</string>
<string name="notify_private_messages_setting_summary_26">Configurer les alertes pour les messages privés</string>
<string name="notify_group_messages_setting_title">Messages de groupe</string>
<string name="notify_group_messages_setting_summary">Afficher des alertes pour les messages de groupe</string>
<string name="notify_group_messages_setting_summary_26">Configurer les alertes pour les messages de groupe</string>
<string name="notify_forum_posts_setting_title">Articles de forum</string>
<string name="notify_forum_posts_setting_summary">Afficher des alertes pour les articles de forum</string>
<string name="notify_blog_posts_setting_title">Articles de blogue</string>
<string name="notify_blog_posts_setting_summary">Afficher des alertes pour pour les articles de blogue</string>
<string name="notify_forum_posts_setting_summary_26">Configurer les alertes pour les articles de forum</string>
<string name="notify_blog_posts_setting_title">Billets de blogue</string>
<string name="notify_blog_posts_setting_summary">Afficher des alertes pour les billets de blogue</string>
<string name="notify_blog_posts_setting_summary_26">Configurer les alertes pour les billets de forum</string>
<string name="notify_vibration_setting">Vibrer</string>
<string name="notify_lock_screen_setting_title">Écran de verrouillage</string>
<string name="notify_lock_screen_setting_summary">Afficher les notifications sur lécran de verrouillage</string>
@@ -380,4 +386,6 @@
<string name="permission_camera_request_body">Pour lire le code QR, Briar doit accéder à la caméra.</string>
<string name="permission_camera_denied_body">Vous avez refusé laccès à la caméra, mais lajout de contacts exige lutilisation de celle-ci.\n\nVeuillez envisager dy accorder laccès.</string>
<string name="permission_camera_denied_toast">Laccès à la caméra na pas été accordé</string>
<string name="qr_code">Code QR</string>
<string name="show_qr_code_fullscreen">Afficher le code QR en plein écran</string>
</resources>

View File

@@ -107,11 +107,13 @@
<!--Blogs-->
<string name="read_more">ler mais</string>
<string name="blogs_publish_blog_post">Publicar</string>
<string name="blogs_remove_blog_ok">Eliminar</string>
<!--Blog Sharing-->
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import_button">Importar</string>
<string name="blogs_rss_feeds_manage_author">Autor/a:</string>
<string name="blogs_rss_feeds_manage_updated">Última actualización:</string>
<string name="blogs_rss_remove_feed_ok">Eliminar</string>
<!--Settings Network-->
<!--Settings Security and Panic-->
<string name="change_password">Cambiar contrasinal</string>

View File

@@ -241,11 +241,13 @@
<string name="forum_declined_toast">הזמנה לפורום נדחתה</string>
<string name="shared_by_format">שותף על ידי %s</string>
<!--Blogs-->
<string name="blogs_remove_blog_ok">להסיר</string>
<!--Blog Sharing-->
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import_button">ייבא</string>
<string name="blogs_rss_feeds_manage_author">מחבר:</string>
<string name="blogs_rss_feeds_manage_updated">עודכן לאחרונה:</string>
<string name="blogs_rss_remove_feed_ok">להסיר</string>
<!--Settings Network-->
<!--Settings Security and Panic-->
<string name="security_settings_title">אבטחה</string>

View File

@@ -43,6 +43,8 @@
</plurals>
<string name="expiry_update">La scadenza della versione di prova è stata prorogata. Il tuo account ora scadrà fra %d giorni.</string>
<string name="expiry_date_reached">Questo software è scaduto.\nGrazie per il test!</string>
<string name="startup_open_database">Decrittazione database...</string>
<string name="startup_migrate_database">Aggiornamento database...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Apri la barra di navigazione</string>
<string name="nav_drawer_close_description">Chiudi la barra di navigazione</string>
@@ -207,7 +209,7 @@
<string name="choose_forum_hint">Scegli un nome per il tuo forum</string>
<string name="create_forum_button">Crea Forum</string>
<string name="forum_created_toast">Forum creato</string>
<string name="no_forum_posts">Questo forum è vuoto.\n\nUsa l\'icona penna in alto per comporre il primo post.\n\nTi senti solo qui? Condividi questo forum con i tuoi contatti!</string>
<string name="no_forum_posts">Nessun post da mostrare</string>
<string name="no_posts">Nessun post.</string>
<plurals name="posts">
<item quantity="one">%d post</item>
@@ -251,7 +253,7 @@
</plurals>
<string name="nobody">Nessuno</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Questo blog è attualmente vuoto.\n\nO l\'autore non ha ancora scritto nulla, oppure la persona che ha condiviso questo blog con te deve tornare in line, in modo che i post possano sincronizzarsi.</string>
<string name="blogs_other_blog_empty_state">Nessun post da mostrare</string>
<string name="read_more">leggi ancora</string>
<string name="blogs_write_blog_post">Scrivere un post sul blog</string>
<string name="blogs_write_blog_post_body_hint">Scrivi qui il tuo post del blog</string>

View File

@@ -97,11 +97,13 @@
<!--Forum Sharing-->
<!--Blogs-->
<string name="blogs_publish_blog_post">公開</string>
<string name="blogs_remove_blog_ok">解除</string>
<!--Blog Sharing-->
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import_button">インポート</string>
<string name="blogs_rss_feeds_manage_author">著者:</string>
<string name="blogs_rss_feeds_manage_updated">最終更新:</string>
<string name="blogs_rss_remove_feed_ok">解除</string>
<!--Settings Network-->
<string name="tor_network_setting_never">二度としない</string>
<!--Settings Security and Panic-->
@@ -113,6 +115,7 @@
<!--Settings Notifications-->
<string name="notification_settings_title">通知</string>
<string name="notify_private_messages_setting_title">プライベート・メッセージ</string>
<string name="notify_lock_screen_setting_title">ロック画面</string>
<string name="notify_sound_setting_disabled">なし</string>
<!--Settings Feedback-->
<!--Link Warning-->

View File

@@ -31,7 +31,9 @@
<string name="dialog_title_lost_password">Wachtwoord vergeten</string>
<string name="dialog_message_lost_password">Je Briar-account is versleuteld opgeslagen op je apparaat, niet in de cloud, dus kunnen we je wachtwoord niet resetten. Wil je je account verwijderen en opnieuw beginnen?\n\nLet op: Je identiteiten, contacten en berichten zullen permanent verloren gaan.</string>
<string name="startup_failed_notification_title">Briar kon niet opstarten</string>
<string name="startup_failed_notification_text">Tap voor meer informatie.</string>
<string name="startup_failed_activity_title">Opstarten Briar mislukt</string>
<string name="startup_failed_data_too_new_error">Deze versie van de is app is te oud. Upgrade a.u.b. naar de laatste versie en probeer het nog een keer.</string>
<string name="startup_failed_service_error">Briar kon de vereiste plug-in niet starten. Herinstalleren van Briar lost dit probleem meestal op. Let op dat je al je je account en alle gegevens die daaraan vast zitten zal verliezen omdat Briar geen centrale servers gebruikt om gegevens op te slaan.</string>
<plurals name="expiry_warning">
<item quantity="one">Dit is een testversie van Briar. Je account verloopt binnen %d dag en kan niet worden vernieuwd.</item>
@@ -39,6 +41,8 @@
</plurals>
<string name="expiry_update">De verloopdatum voor de test is vooruitgeschoven. Je account verloopt nu binnen %d dagen.</string>
<string name="expiry_date_reached">Deze software is verlopen.\nBedankt vor het testen!</string>
<string name="startup_open_database">Database aan het ontsleutelen…</string>
<string name="startup_migrate_database">Database aan het upgraden…</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Open de navigatielade</string>
<string name="nav_drawer_close_description">Sluit de navigatielade</string>
@@ -93,6 +97,7 @@
<string name="show_onboarding">Toon helpdialoog</string>
<string name="fix">Fiks</string>
<string name="help">Help</string>
<string name="sorry">Excuses</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Zo te zien ben je nieuw hier en heb je nog geen contacten.\n\nTap op het +-icoon bovenaan en volg de instructies om een aantal vrienden aan je list toe te voegen.\n\nHerinner a.u.b.: Je kan alleen in levenden lijve nieuwe contacten toevoevoegen om te voorkomen dat anderen zich als jou voor kunnen doen of in de toekomst je berichten kunnen lezen.</string>
<string name="date_no_private_messages">Geen berichten.</string>
@@ -114,6 +119,7 @@
<string name="contact_already_exists">Contact %s bestaat al</string>
<string name="contact_exchange_failed">Uitwisselen contact is mislukt</string>
<string name="qr_code_invalid">De QR-code is ongeldig</string>
<string name="qr_code_unsupported">De QR-code die je probeert te scannen is van een oude ver van %s die niet meer wordt ondersteund.\n\nControleer a.u.b. dat jullie beide de laatste versie gebruiken en probeer het nog een keer.</string>
<string name="camera_error">Camerafout</string>
<string name="connecting_to_device">Aan het verbinden met apparaat\u2026</string>
<string name="authenticating_with_device">Aan het authentificeren met apparaat\u2026</string>
@@ -367,6 +373,8 @@
<string name="progress_title_logout">Uitloggen van Briar…</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Schermoverlay detecteerd</string>
<string name="screen_filter_body">Een andere app ligt zich bovenop Briar. Om je veiligheid te beschermen zal Briar niet reageren op aanrakingen als er een andere app bovenop ligt.\n\nDe volgende apps proberen waarschijnlijk dit te doen:\n\n%1$s</string>
<string name="screen_filter_allow">Sta toe dat deze apps bovenop te liggen</string>
<!--Permission Requests-->
<string name="permission_camera_title">Cameratoestemming</string>
<string name="permission_camera_request_body">Om de QR-code te scannen moet Briar toegang hebben tot de camera.</string>

View File

@@ -33,8 +33,8 @@
<string name="startup_failed_notification_title">Briar não pode iniciar</string>
<string name="startup_failed_notification_text">Pressione para mais informações.</string>
<string name="startup_failed_activity_title">Inicialização do Briar falhou</string>
<string name="startup_failed_db_error">Por algum motivo, o banco de dados do seu Briar está corrompido e não é possível repará-lo. Sua conta, seus dados e seus contatos foram perdidos. Infelizmente, você precisará reinstalar o Briar e criar uma nova conta escolhendo a opção \'Esqueci minha senha\'.</string>
<string name="startup_failed_data_too_old_error">Sua conta foi criada numa versão mais antiga do aplicativo e não pode ser aberta nesta versão. Você pode reinstalar a versão antiga ou deleter sua conta antiga escolhendo a opção \"Esqueci minha senha\".</string>
<string name="startup_failed_db_error">Por algum motivo, o banco de dados do seu Briar está corrompido e não é possível repará-lo. Sua conta, seus dados e seus contatos foram perdidos. Infelizmente, você precisará reinstalar o Briar ou criar uma nova conta escolhendo a opção \'Esqueci minha senha\'.</string>
<string name="startup_failed_data_too_old_error">Sua conta foi criada numa versão mais antiga do aplicativo e não pode ser aberta nesta versão. Você pode reinstalar a versão antiga ou criar uma conta nova escolhendo a opção \"Esqueci minha senha\".</string>
<string name="startup_failed_data_too_new_error">A versão deste aplicativo é muito antiga. Por favor, atualize para a versão mais nova e tente novamente.</string>
<string name="startup_failed_service_error">O Briar não pode iniciar devido a um plugin. Reinstalar o Briar geralmente resolve esse problema. Porém, note que ao fazer isso você perderá sua conta e todos os dados associados a ela, já que o Briar não usa um servidor central para armazenar seus dados.</string>
<plurals name="expiry_warning">
@@ -43,6 +43,8 @@
</plurals>
<string name="expiry_update">O prazo de validade da versão de teste foi extendido. Agora sua conta irá expirar em %d dias.</string>
<string name="expiry_date_reached">Este software expirou.\nObrigado por testar!</string>
<string name="startup_open_database">Descriptografando Banco de Dados...</string>
<string name="startup_migrate_database">Atualizando Banco de Dados...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Abrir aba de navegação</string>
<string name="nav_drawer_close_description">Fechar aba de navegação</string>
@@ -358,7 +360,7 @@
<string name="briar_crashed">Briar encerrou de maneira inesperada</string>
<string name="not_your_fault">Isso não é sua culpa.</string>
<string name="please_send_report">Nós ajude a construir um Briar melhor enviando um relatório de falhas.</string>
<string name="report_is_encrypted">Nós prometemos que o relatório é encriptado e enviado de forma segura.</string>
<string name="report_is_encrypted">Nós prometemos que o relatório é criptografado e enviado de forma segura.</string>
<string name="feedback_title">Comentário</string>
<string name="describe_crash">Descreva o que aconteceu (opcional)</string>
<string name="enter_feedback">Digite seu comentário</string>
@@ -378,6 +380,7 @@
<!--Permission Requests-->
<string name="permission_camera_title">Permissão da câmera</string>
<string name="permission_camera_request_body">O Briar precisa acessar a câmera para pode escanear o QR code.</string>
<string name="permission_camera_denied_body">Você negou acesso à câmera, mas adicionar contatos requer que você use a câmera.\n\nPor favor, considere a concessão de acesso a ela.</string>
<string name="permission_camera_denied_body">Você negou acesso à câmera, mas para adicionar contatos você precisa da câmera.\n\nPor favor, pense em liberar o acesso a ela.</string>
<string name="permission_camera_denied_toast">A permissão da câmera não foi concedida</string>
<string name="qr_code">Código QR</string>
</resources>

View File

@@ -0,0 +1,359 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<!--Setup-->
<string name="setup_title">Bine ai venit la Briar</string>
<string name="setup_name_explanation">Numele dumneavoastră va fi afișat lângă orice conținut trimiteți. Nu îl veți putea schimba după crearea contului.</string>
<string name="setup_next">Următorul</string>
<string name="setup_password_intro">Alegeți o parolă</string>
<string name="setup_doze_title">Conexiuni în fundal</string>
<string name="setup_doze_intro">Pentru a primi mesaje, Briar are nevoie să stea conectat în fundal.</string>
<string name="setup_doze_button">Permite conexiuni</string>
<string name="choose_nickname">Alegeți-vă numele de utilizator</string>
<string name="choose_password">Alegeți-vă parola</string>
<string name="confirm_password">Confirmați parola</string>
<string name="name_too_long">Numele este prea lung</string>
<string name="password_too_weak">Parola este prea slabă</string>
<string name="passwords_do_not_match">Parolele nu se potrivesc</string>
<string name="create_account_button">Creează un cont</string>
<string name="more_info">Informații suplimentare</string>
<string name="don_t_ask_again">Nu mai întreba din nou</string>
<string name="setup_huawei_text">Vă rugăm să apăsați butonul de mai jos și să vă asigurați că Briar este marcat ca protejat în fereastra de \"Aplicații protejate\".</string>
<string name="setup_huawei_button">Protejează Briar</string>
<string name="setup_huawei_help">Dacă Briar nu este adăugat în lista de aplicații protejate, nu v-a fi capabil să ruleze în fundal.</string>
<string name="warning_dozed">%s nu poate rula în fundal</string>
<!--Login-->
<string name="enter_password">Introduceți parola:</string>
<string name="try_again">Parolă greșită, reîncercați</string>
<string name="sign_in_button">Autentificare</string>
<string name="forgotten_password">Am uitat parola</string>
<string name="dialog_title_lost_password">Parolă uitată</string>
<string name="startup_failed_notification_title">Briar nu a putut pornii</string>
<string name="startup_failed_notification_text">Atingeți pentru informații suplimentare</string>
<string name="startup_failed_activity_title">Eroare de pornire Briar</string>
<string name="startup_failed_data_too_new_error">Această versiune a aplicației este prea veche. Vă rugăm să actualizați la cea mai nouă versiune și să încercați din nou.</string>
<plurals name="expiry_warning">
<item quantity="one">Aceasta este o versiune de test pentru Briar. Contul dumneavoastră va expira în %d zi și nu se poate reînnoi</item>
<item quantity="few">Aceasta este o versiune de test pentru Briar. Contul dumneavoastră va expira în %d zile și nu se poate reînnoi.</item>
<item quantity="other">Aceasta este o versiune de test pentru Briar. Contul dumneavoastră va expira în %d de zile și nu se poate reînnoi.</item>
</plurals>
<string name="expiry_update">Data de expirare a versiunii de test a fost extinsă. Contul dumneavoastră va expira acum în %d zile.</string>
<string name="expiry_date_reached">Acest program a expirat.\nVă mulțumim că l-ați testat!</string>
<string name="startup_open_database">Decriptare bază de date...</string>
<string name="startup_migrate_database">Actualizare bază de date...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Deschide bara de navigare</string>
<string name="nav_drawer_close_description">Închide bara de navigare</string>
<string name="contact_list_button">Contacte</string>
<string name="groups_button">Grupuri private</string>
<string name="forums_button">Forumuri</string>
<string name="blogs_button">Blog-uri</string>
<string name="settings_button">Setări</string>
<string name="sign_out_button">Ieșire</string>
<!--Transports-->
<string name="transport_tor">Internet</string>
<string name="transport_bt">Bluetooth</string>
<string name="transport_lan">Wi-Fi</string>
<!--Notifications-->
<string name="ongoing_notification_title">Autentificat în Briar</string>
<string name="ongoing_notification_text">Atingeți pentru a deschide Briar</string>
<plurals name="private_message_notification_text">
<item quantity="one">Mesaj privat nou.</item>
<item quantity="few">%d mesaje private noi.</item>
<item quantity="other">%d de mesaje private noi.</item>
</plurals>
<plurals name="group_message_notification_text">
<item quantity="one">Mesaj nou de grup.</item>
<item quantity="few">%d mesaje de grup noi.</item>
<item quantity="other">%d de mesaje de grup noi.</item>
</plurals>
<plurals name="forum_post_notification_text">
<item quantity="one">Un nou mesaj pe forum.</item>
<item quantity="few">%d mesaje noi pe forum.</item>
<item quantity="other">%d de mesaje noi pe forum.</item>
</plurals>
<plurals name="blog_post_notification_text">
<item quantity="one">Un nou mesaj pe blog.</item>
<item quantity="few">%d mesaje noi pe blog.</item>
<item quantity="other">%d de mesaje noi pe blog.</item>
</plurals>
<!--Misc-->
<string name="now">acum</string>
<string name="show">Arată</string>
<string name="hide">Ascunde</string>
<string name="ok">Bine</string>
<string name="cancel">Anulează</string>
<string name="got_it">Am înțeles</string>
<string name="delete">Șterge</string>
<string name="accept">Acceptă</string>
<string name="decline">Refuză</string>
<string name="options">Opțiuni</string>
<string name="online">Conectat</string>
<string name="offline">Deconectat</string>
<string name="send">Trimite</string>
<string name="allow">Permite</string>
<string name="open">Deschide</string>
<string name="no_data">Fără date</string>
<string name="ellipsis"></string>
<string name="text_too_long">Textul introdus este prea lung</string>
<string name="show_onboarding">Arata fereastra de ajutor</string>
<string name="fix">Rezolvă</string>
<string name="help">Ajutor</string>
<string name="sorry">Ne pare rău</string>
<!--Contacts and Private Conversations-->
<string name="date_no_private_messages">Fără mesaje.</string>
<string name="message_hint">Scrieți mesajul</string>
<string name="delete_contact">Șterge contactul</string>
<string name="dialog_title_delete_contact">Confirmare ștergere contact</string>
<string name="dialog_message_delete_contact">Sigur doriți să ștergeți acest contact și toate mesajele schimbate?</string>
<string name="contact_deleted_toast">Contact șters</string>
<!--Adding Contacts-->
<string name="add_contact_title">Adaugă un contact</string>
<string name="continue_button">Continuă</string>
<string name="connection_failed">Conexiune eșuată</string>
<string name="try_again_button">Încearcă din nou</string>
<string name="waiting_for_contact_to_scan">Se așteaptă scanarea și conectarea contactului\u2026</string>
<string name="exchanging_contact_details">Se face schimbul de date de contact\u2026</string>
<string name="contact_added_toast">Contact adăugat: %s</string>
<string name="contact_already_exists">Contactul %s există deja</string>
<string name="contact_exchange_failed">Schimbul de date de contactului a eșuat</string>
<string name="qr_code_invalid">Codul QR este invalid!</string>
<string name="camera_error">Eroare la camera foto</string>
<string name="connecting_to_device">Conectare la dispozitiv\u2026</string>
<string name="authenticating_with_device">Autentificare cu dispozitivul\u2026</string>
<string name="connection_aborted_local">Conexiune întreruptă! Aceasta poate însemna că cineva încearcă să interfereze conexiunea dumneavoastră</string>
<string name="connection_aborted_remote">Conexiune întreruptă de către contactul dumneavoastră! Aceasta poate însemna că cineva încearcă să interfereze conexiunea dumneavoastră</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Recomandați-vă contactele</string>
<string name="introduction_onboarding_text">Puteți să vă recomandați contactele unele altora, încât sa nu fie nevoie ca să se vadă față în față pentru a se putea conecta la Briar.</string>
<string name="introduction_activity_title">Alege un contact</string>
<string name="introduction_message_title">Recomandă contacte</string>
<string name="introduction_message_hint">Adaugă un mesaj (opțional)</string>
<string name="introduction_button">Fă o recomandare</string>
<string name="introduction_sent">Recomandarea dumneavoastră a fost trimisă.</string>
<string name="introduction_error">A apărut o eroare în procesul de recomandare.</string>
<string name="introduction_response_error">Eroare atunci când s-a răspuns la recomandare</string>
<string name="introduction_request_sent">Ați cerut recomandarea %1$s către %2$s.</string>
<string name="introduction_request_received">%1$s a cerut să vă recomande către %2$s. Doriți să adăugați pe %2$s la lista dumneavoastră de contacte?</string>
<string name="introduction_request_exists_received">%1$s a cerut să vă recomande către %2$s, dar %2$s este deja în lista dumneavoastră de contacte. Cum %1$s s-ar putea să nu știe asta, puteți totuși răspunde:</string>
<string name="introduction_request_answered_received">%1$s vă recomandă pe %2$s.</string>
<string name="introduction_response_accepted_sent">Ați acceptat recomandarea pentru %1$s.</string>
<string name="introduction_response_declined_sent">Ați refuzat recomandarea pentru %1$s.</string>
<string name="introduction_response_accepted_received">%1$s a acceptat recomandarea pentru %2$s.</string>
<string name="introduction_response_declined_received">%1$s a refuzat recomandarea pentru %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s spune că %2$s a refuzat recomandarea.</string>
<plurals name="introduction_notification_text">
<item quantity="one">Un nou contact adăugat.</item>
<item quantity="few">%d contacte noi adăugate.</item>
<item quantity="other">%d de contacte noi adăugate.</item>
</plurals>
<!--Private Groups-->
<string name="groups_created_by">Creat de %s</string>
<plurals name="messages">
<item quantity="one">%d mesaj</item>
<item quantity="few">%d mesaje</item>
<item quantity="other">%d de mesaje</item>
</plurals>
<string name="groups_group_is_empty">Acest grup este gol</string>
<string name="groups_group_is_dissolved">Acest grup a fost dizolvat</string>
<string name="groups_remove">Șterge</string>
<string name="groups_create_group_title">Creează grup privat</string>
<string name="groups_create_group_button">Creează grup</string>
<string name="groups_create_group_invitation_button">Trimite invitație</string>
<string name="groups_create_group_hint">Alegeți un nume pentru grupul dumneavoastră privat</string>
<string name="groups_invitation_sent">Invitația in grup a fost trimisă</string>
<string name="groups_message_sent">Mesaj trimis</string>
<string name="groups_member_list">Lista de membrii</string>
<string name="groups_invite_members">Invită membrii</string>
<string name="groups_member_created_you">Ați creat grupul</string>
<string name="groups_member_created">%s a creat grupul</string>
<string name="groups_member_joined_you">V-ați alăturat grupului</string>
<string name="groups_member_joined">%s s-a alăturat grupului</string>
<string name="groups_leave">Părăsește grupul</string>
<string name="groups_leave_dialog_title">Confirmare părăsire grup</string>
<string name="groups_leave_dialog_message">Sigur doriți să părăsiți acest grup?</string>
<string name="groups_dissolve">Dizolvă grupul</string>
<string name="groups_dissolve_dialog_title">Confirmă dizolvarea grupului</string>
<string name="groups_dissolve_button">Dizolvă</string>
<string name="groups_dissolved_dialog_title">Grupul a fost dizolvat</string>
<!--Private Group Invitations-->
<string name="groups_invitations_title">Invitații în grup</string>
<string name="groups_invitations_invitation_sent">Ați invitat pe %1$s să se alăture grupului \"%2$s\".</string>
<string name="groups_invitations_invitation_received">%1$s v-a invitat să vă alăturați grupului \"%2$s\".</string>
<string name="groups_invitations_joined">V-ați alăturat grupului</string>
<string name="groups_invitations_declined">Invitația în grup a fost refuzată</string>
<plurals name="groups_invitations_open">
<item quantity="one">%d invitație în grup disponibilă</item>
<item quantity="few">%d invitații în grup disponibile</item>
<item quantity="other">%d de invitații în grup disponibile</item>
</plurals>
<string name="groups_invitations_response_accepted_sent">Ați acceptat invitația în grup de la %s.</string>
<string name="groups_invitations_response_declined_sent">Ați refuzat invitația în grup pentru %s.</string>
<string name="groups_invitations_response_accepted_received">%s a acceptat invitația în grup.</string>
<string name="groups_invitations_response_declined_received">%s a refuzat invitația în grup.</string>
<string name="sharing_status_groups">Doar persoana care a creat grupul poate invita noi membrii. Mai jos vedeți membrii actuali ai grupului.</string>
<!--Private Groups Revealing Contacts-->
<string name="groups_reveal_contacts">Arată contactele</string>
<string name="groups_reveal_visible">Lista de contacte este vizibilă grupului</string>
<string name="groups_reveal_visible_revealed_by_us">Lista de contacte este vizibilă grupului (dezvăluită de dumneavoastră)</string>
<string name="groups_reveal_visible_revealed_by_contact">Lista de contacte este vizibilă grupului (dezvăluită de %s)</string>
<string name="groups_reveal_invisible">Lista de contacte nu este vizibilă grupului</string>
<!--Forums-->
<string name="create_forum_title">Creează forum</string>
<string name="choose_forum_hint">Alegeți un nume pentru forumul dumneavoastră</string>
<string name="create_forum_button">Creează forum</string>
<string name="forum_created_toast">Forum creat</string>
<string name="no_posts">Nici un mesaj</string>
<plurals name="posts">
<item quantity="one">%d mesaj</item>
<item quantity="few">%d mesaje</item>
<item quantity="other">%d de mesaje</item>
</plurals>
<string name="forum_new_entry_posted">Mesajul a fost introdus pe forum</string>
<string name="forum_new_message_hint">Mesaj nou</string>
<string name="forum_message_reply_hint">Răspuns nou</string>
<string name="btn_reply">Răspunde</string>
<string name="forum_leave">Părăsește forum</string>
<string name="dialog_title_leave_forum">Confirmare părăsire forum</string>
<string name="dialog_button_leave">Părăsește</string>
<!--Forum Sharing-->
<string name="forum_share_button">Partajează forum</string>
<string name="contacts_selected">Contacte selectate</string>
<string name="activity_share_toolbar_header">Alegeți contactele</string>
<string name="forum_shared_snackbar">Forum partajat cu contactele alese</string>
<string name="forum_share_message">Adaugă un mesaj (opțional)</string>
<string name="forum_share_error">A apărut o eroare la partajarea acestui forum.</string>
<string name="forum_invitation_received">%1$s a partajat forumul \"%2$s\" cu dumneavoastră.</string>
<string name="forum_invitation_sent">Ați partajat forumul \"%1$s\" cu %2$s.</string>
<string name="forum_invitations_title">Invitații la forum</string>
<string name="shared_by_format">Partajat de %s</string>
<string name="forum_invitation_already_sharing">Deja partajat</string>
<string name="forum_invitation_response_accepted_sent">Ați acceptat invitația la forum de la %s.</string>
<string name="forum_invitation_response_declined_sent">Ați refuzat invitația la forum pentru %s.</string>
<string name="forum_invitation_response_accepted_received">%s a acceptat invitația la forum.</string>
<string name="forum_invitation_response_declined_received">%s a refuzat invitația la forum.</string>
<string name="sharing_status">Partajare stare</string>
<string name="shared_with">Partajat cu %1$d (%2$d conectați)</string>
<plurals name="forums_shared">
<item quantity="one">%d forum partajat de contacte</item>
<item quantity="few">%d forumuri partajate de contacte</item>
<item quantity="other">%d de forumuri partajate de contacte</item>
</plurals>
<string name="nobody">Nimeni</string>
<!--Blogs-->
<string name="read_more">citește mai mult</string>
<string name="blogs_write_blog_post">Scrie mesaj pe blog</string>
<string name="blogs_publish_blog_post">Publică</string>
<string name="blogs_blog_post_created">Mesaj pe blog creat</string>
<string name="blogs_blog_post_received">O nou mesaj pe blog s-a primit</string>
<string name="blogs_blog_post_scroll_to">Derulează la</string>
<string name="blogs_remove_blog">Elimină blog</string>
<string name="blogs_remove_blog_ok">Eliminare</string>
<string name="blogs_reblog_comment_hint">Adaugă un comentariu (opțional)</string>
<string name="blogs_reblog_button">Repune mesaj</string>
<!--Blog Sharing-->
<string name="blogs_sharing_share">Partajează blog</string>
<string name="blogs_sharing_error">A apărut o eroare la partajarea acestui blog.</string>
<string name="blogs_sharing_button">Partajează blog</string>
<string name="blogs_sharing_snackbar">Blog partajat cu contactele alese</string>
<string name="blogs_sharing_response_accepted_sent">Ați acceptat invitația la blog de la %s.</string>
<string name="blogs_sharing_response_declined_sent">Ați refuzat invitația la blog de la %s.</string>
<string name="blogs_sharing_response_accepted_received">%s a acceptat invitația la blog.</string>
<string name="blogs_sharing_response_declined_received">%s a refuzat invitația la blog.</string>
<string name="blogs_sharing_invitation_received">%1$s a partajat blogul \"%2$s\" cu dumneavoastră.</string>
<string name="blogs_sharing_invitation_sent">Ați partajat blogul \"%1$s\" cu %2$s.</string>
<string name="blogs_sharing_invitations_title">Invitații la blog-uri</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Importă flux RSS</string>
<string name="blogs_rss_feeds_import_button">Importă</string>
<string name="blogs_rss_feeds_import_hint">Introduceți URL-ul fluxului RSS</string>
<string name="blogs_rss_feeds_import_error">Ne pare rău! A apărut o eroare la importul fluxului dumneavoastră.</string>
<string name="blogs_rss_feeds_manage">Administrare fluxuri RSS</string>
<string name="blogs_rss_feeds_manage_imported">Importat:</string>
<string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Actualizat ultima dată:</string>
<string name="blogs_rss_remove_feed">Șterge flux</string>
<string name="blogs_rss_remove_feed_ok">Eliminare</string>
<string name="blogs_rss_feeds_manage_delete_error">Fluxul nu a putut fi șters!</string>
<string name="blogs_rss_feeds_manage_error">A apărut o eroare la încărcarea fluxurilor dumneavoastră. Vă rugăm să încercați din nou mai târziu.</string>
<!--Settings Network-->
<string name="network_settings_title">Rețele</string>
<string name="bluetooth_setting">Conectare prin Bluetooth</string>
<string name="bluetooth_setting_enabled">Atunci când contactele vă sunt în apropiere</string>
<string name="bluetooth_setting_disabled">Doar la adăugarea contactelor</string>
<string name="tor_network_setting">Conectare prin rețeaua Tor</string>
<string name="tor_network_setting_never">Niciodată</string>
<string name="tor_network_setting_wifi">Doar când se folosește Wi-Fi</string>
<string name="tor_network_setting_always">Când se folosește Wi-Fi sau date mobile</string>
<!--Settings Security and Panic-->
<string name="security_settings_title">Securitate</string>
<string name="change_password">Schimbă parola</string>
<string name="current_password">Introduceți parola curentă:</string>
<string name="choose_new_password">Alegeți-vă parola nouă:</string>
<string name="confirm_new_password">Confirmați parola nouă:</string>
<string name="password_changed">Parola a fost schimbată.</string>
<string name="panic_setting">Setare buton de panică</string>
<string name="panic_setting_title">Buton de panică</string>
<string name="panic_setting_hint">Configurați cum va reacționa Briar atunci când folosiți o aplicație de buton de panică.</string>
<string name="panic_app_setting_title">Aplicația buton de panică</string>
<string name="unknown_app">aplicație necunoscută</string>
<string name="panic_app_setting_summary">Nu a fost setată nici o aplicație</string>
<string name="panic_app_setting_none">Nici una</string>
<string name="dialog_title_connect_panic_app">Confirmare aplicație de panică</string>
<string name="dialog_message_connect_panic_app">Sigur doriți să permiteți %1$s să declanșeze acțiuni destructive pentru butonul de panică?</string>
<string name="lock_setting_title">Ieșire</string>
<string name="lock_setting_summary">Ieși din Briar dacă un buton de panică este apăsat</string>
<string name="purge_setting_title">Șterge cont</string>
<string name="uninstall_setting_title">Dezinstalare Briar</string>
<string name="uninstall_setting_summary">Aceasta necesită o confirmare manuală în timpul unui eveniment de panică</string>
<!--Settings Notifications-->
<string name="notification_settings_title">Notificări</string>
<string name="notify_private_messages_setting_title">Mesaje private</string>
<string name="notify_private_messages_setting_summary">Arată alerte pentru mesajele private</string>
<string name="notify_group_messages_setting_title">Mesaje de grup</string>
<string name="notify_group_messages_setting_summary">Arată alerte pentru mesajele de grup</string>
<string name="notify_forum_posts_setting_title">Mesaje pe forum</string>
<string name="notify_forum_posts_setting_summary">Arată alerte pentru mesajele de pe forum</string>
<string name="notify_blog_posts_setting_title">Mesaje pe blog</string>
<string name="notify_blog_posts_setting_summary">Arată alerte pentru mesajele de pe blog</string>
<string name="notify_vibration_setting">Vibrează</string>
<string name="notify_lock_screen_setting_title">Ecran de blocare</string>
<string name="notify_lock_screen_setting_summary">Arată notificări pe ecranul de blocare</string>
<string name="notify_sound_setting">Sunet</string>
<string name="notify_sound_setting_default">Sunet implicit</string>
<string name="notify_sound_setting_disabled">Nici unul</string>
<string name="choose_ringtone_title">Alegeți sunetul</string>
<string name="cannot_load_ringtone">Nu se poate încărca sunetul</string>
<!--Settings Feedback-->
<string name="feedback_settings_title">Feed-back</string>
<string name="send_feedback">Trimiteți feed-back</string>
<!--Link Warning-->
<string name="link_warning_title">Avertizare adresă</string>
<string name="link_warning_intro">Urmează să deschideți adresa următoare cu o aplicație externă</string>
<string name="link_warning_open_link">Deschide adresă</string>
<!--Crash Reporter-->
<string name="crash_report_title">Raport de erori Briar</string>
<string name="briar_crashed">Ne pare rău, Briar a întâmpinat o eroare.</string>
<string name="not_your_fault">Nu este vina dumneavoastră.</string>
<string name="please_send_report">Vă rugăm să ne ajutați să facem Briar mai bun trimițându-ne raportul de erori.</string>
<string name="report_is_encrypted">Vă promitem ca raportul este criptat și este trimis securizat.</string>
<string name="feedback_title">Feed-back</string>
<string name="describe_crash">Descrieți ce s-a întâmplat (opțional)</string>
<string name="enter_feedback">Introduceți feed-back-ul dumneavoastră</string>
<string name="optional_contact_email">Adresa de email (opțional)</string>
<string name="include_debug_report_crash">Include date anonime despre eroare</string>
<string name="include_debug_report_feedback">Include date anonime despre acest dispozitiv</string>
<string name="could_not_load_report_data">Nu s-au putut încărca datele din raport.</string>
<string name="send_report">Trimite raport</string>
<string name="close">Închide</string>
<string name="dev_report_saved">Raport salvat. V-a fi trimis data viitoare când vă conectați la Briar.</string>
<!--Sign Out-->
<string name="progress_title_logout">Ieșire din Briar...</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">S-a detectat ceva suprapus pe ecran</string>
<string name="screen_filter_allow">Permite acestor aplicații să deseneze deasupra</string>
<!--Permission Requests-->
<string name="permission_camera_title">Permisiune de acces la camera foto</string>
<string name="permission_camera_request_body">Pentru a scana codul QR, Briar are nevoie să acceseze camera foto.</string>
<string name="permission_camera_denied_body">Ați refuzat accesul la camera foto, dar pentru a adăuga contacte este necesară folosirea camerei foto.\n\nVă rugăm să luați în considerare acordarea accesului.</string>
<string name="permission_camera_denied_toast">Permisiunea de acces la camera foto nu a fost acordată</string>
</resources>

View File

@@ -45,6 +45,8 @@
</plurals>
<string name="expiry_update">Дата окончания тестирования была продлена. Срок действия вашей учетной записи истечет через %d дней.</string>
<string name="expiry_date_reached">Срок действия этого программного обеспечения истек.\nСпасибо за тестирование!</string>
<string name="startup_open_database">Расшифровка базы данных...</string>
<string name="startup_migrate_database">Обновление базы данных...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Открыть навигационное меню</string>
<string name="nav_drawer_close_description">Закрыть навигационное меню</string>
@@ -223,7 +225,7 @@
<string name="choose_forum_hint">Выберите имя для вашего форума</string>
<string name="create_forum_button">Создать форум</string>
<string name="forum_created_toast">Форум создан</string>
<string name="no_forum_posts">Этот форум пуст.\n\nСоздайте свою первую запись.\n\nЧувствуете одиночество? Поделитесь этим форумом с вашими контактами!</string>
<string name="no_forum_posts">Нет постов для показа</string>
<string name="no_posts">Нет постов</string>
<plurals name="posts">
<item quantity="one">%d пост</item>
@@ -271,7 +273,7 @@
</plurals>
<string name="nobody">Никого</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Этот блог пуст.\n\nЛибо автор еще ничего не написал, либо человек, который поделился с вами этим блогом, должен выйти в Интернет, чтобы сообщения могли быть синхронизированы.</string>
<string name="blogs_other_blog_empty_state">Нет постов для показа</string>
<string name="read_more">подробнее</string>
<string name="blogs_write_blog_post">Написать в блоге</string>
<string name="blogs_write_blog_post_body_hint">Введите ваше сообщение здесь</string>

View File

@@ -43,6 +43,8 @@
</plurals>
<string name="expiry_update">Data e skadimit të periudhës së testimit është shtyrë më tej. Tani llogaria juaj do të skadojë për %d ditë.</string>
<string name="expiry_date_reached">Ky software ka skaduar.\nFaleminderit që e provuat!</string>
<string name="startup_open_database">Po shfshehtëzohet Baza e të dhënave…</string>
<string name="startup_migrate_database">Po përditësohet Baza e të dhënave…</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Hap sirtarin e lëvizjeve</string>
<string name="nav_drawer_close_description">Mbylle sirtarin e lëvizjeve</string>
@@ -99,9 +101,9 @@
<string name="help">Ndihmë</string>
<string name="sorry">Na ndjeni</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Duket se jeni i ri këtu dhe ende s\keni kontakte.\n\nPrekni ikonën + në krye dhe ndiqni udhëzimet që të shtoni ca shokë e miq te lista juaj.\n\nJu lutemi, mbani mend: Mundeni të shtoni kontakte të rinj vetëm duke qenë ballë për ballë me ta. që kështu të pengoni cilindo të hiqet si ju ose të lexojë në të ardhmen mesazhet tuaj.</string>
<string name="no_contacts">Ska kontakte për shfaqje\n\nPrekni ikonën + që të shtoni një kontakt</string>
<string name="date_no_private_messages">S\ka mesazhe.</string>
<string name="no_private_messages">Kjo është pamja e bisedave.\n\nDuket se ka mungesë bisedash.\n\nQë të filloni një bisedë, thjesht prekni fushën e teksteve në fund.</string>
<string name="no_private_messages">Ska mesazhe për shfaqje</string>
<string name="message_hint">Shtypni mesazhin</string>
<string name="delete_contact">Fshije kontaktin</string>
<string name="dialog_title_delete_contact">Ripohoni Fshirje Kontakti</string>
@@ -149,7 +151,7 @@
<item quantity="other">U shtuan %d kontakte të rinj.</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">S\merrni pjesë në ndonjë grup.\n\nPrekni ikonën + në krye që të krijoni një grup ose kërkojuni kontakteve tuaj t\ju ftojnë në një prej grupeve të tyre.</string>
<string name="groups_list_empty">S\ka grupe për shfaqje.\n\nPrekni ikonën + që të krijoni një grup ose kërkojuni kontakteve tuaj t\ju ftojnë në një prej grupeve të tyre</string>
<string name="groups_created_by">Krijuar nga %s</string>
<plurals name="messages">
<item quantity="one">%d mesazh</item>
@@ -202,12 +204,12 @@
<string name="groups_reveal_visible_revealed_by_contact">Marrëdhënia e kontaktit është e dukshme për grupin (shfaqur nga %s)</string>
<string name="groups_reveal_invisible">Marrëdhënia e kontaktit s\është e dukshme për grupin</string>
<!--Forums-->
<string name="no_forums">Ende s\keni forume.\n\nPse nuk krijoni vetë një të tillë duke prekur ikonën +krye?\n\nMund edhe t\u kërkoni kontakteve tuaj të ndajnë me ju forume.</string>
<string name="no_forums">S\ka forume për shfaqje.\n\nPrekni ikonën + që të krijoni një forum ose kërkojuni kontakteve tuaj t\ju ftojnë në një prej forumeve të tyre</string>
<string name="create_forum_title">Krijoje Forumin</string>
<string name="choose_forum_hint">Zgjidhni një emër për forumin tuaj</string>
<string name="create_forum_button">Krijoje Forumin</string>
<string name="forum_created_toast">Forumi u krijua</string>
<string name="no_forum_posts">Ky forum është i zbrazët.\n\nPërdorni ikonën penë që të hartoni postimin tuaj të parë.\n\nNdiheni vetëm? Ndajeni këtë forum me kontaktet tuaj!</string>
<string name="no_forum_posts">S\ka postime për shfaqje</string>
<string name="no_posts">S\ka postime</string>
<plurals name="posts">
<item quantity="one">%d postim</item>
@@ -219,23 +221,23 @@
<string name="btn_reply">Përgjigju</string>
<string name="forum_leave">Braktiseni Forumin</string>
<string name="dialog_title_leave_forum">Ripohoni Braktisjen e Forumit</string>
<string name="dialog_message_leave_forum">Jeni i sigurt se doni ta braktisni këtë forum? Kontaktet me të cilët e keni ndarë këtë forum mundet të mbeten jashtë marrjes së përditësimeve nga ky forum.</string>
<string name="dialog_message_leave_forum">Jeni i sigurt se doni ta braktisni këtë forum?\n\nÇfarëdo kontaktesh me të cilët e keni ndarë këtë forum mundet të reshtin së marri përditësime.</string>
<string name="dialog_button_leave">Braktise</string>
<string name="forum_left_toast">E braktisët Forumin</string>
<string name="forum_left_toast">E braktisët forumin</string>
<!--Forum Sharing-->
<string name="forum_share_button">Ndajeni Forumin Me të Tjerë</string>
<string name="contacts_selected">Kontaktet u përzgjodhën</string>
<string name="activity_share_toolbar_header">Zgjidhni Kontakte</string>
<string name="no_contacts_selector">Duket se jeni i ri këtu dhe s\keni ende kontakte.\n\nJu lutemi, rikthehuni këtu pasi të keni shtuar kontaktin tuaj të parë.</string>
<string name="no_contacts_selector">Ska kontakte për shfaqje\n\nJu lutemi, rikthehuni këtu pasi të shtoni një kontakt</string>
<string name="forum_shared_snackbar">Forumi u nda me kontaktet e zgjedhur</string>
<string name="forum_share_message">Shtoni një mesazh (në daçi)</string>
<string name="forum_share_error">Pati një gabim në ndarjen e këtij forumi me të tjerët.</string>
<string name="forum_invitation_received">%1$s ndau me ju forumin \"%2$s\".</string>
<string name="forum_invitation_sent">Ndatë me \"%2$s\" forumin %1$s.</string>
<string name="forum_invitations_title">Ftesa Forumi</string>
<string name="forum_invitation_exists">Keni pranuar tashmë një ftesë te ky forum. Pranimi i më shumë ftesave do të rrisë dhe forcojë komunikimin në forum.</string>
<string name="forum_joined_toast">U bëtë Pjesë e Forumit</string>
<string name="forum_declined_toast">Ftesa e Forumit u Hodh Poshtë</string>
<string name="forum_invitation_exists">Keni pranuar tashmë një ftesë për te ky forum.\n\nPranimi i më tepër ftesave do ta bëjë lidhjen tuaj me këtë forum më të shpejtë dhe më të qëndrueshme.</string>
<string name="forum_joined_toast">Hytë në forum</string>
<string name="forum_declined_toast">Ftesa u hodh poshtë</string>
<string name="shared_by_format">Ndarë nga %s</string>
<string name="forum_invitation_already_sharing">Ndarë tashmë</string>
<string name="forum_invitation_response_accepted_sent">Pranuat ftesën e forumit nga %s.</string>
@@ -251,19 +253,19 @@
</plurals>
<string name="nobody">Askush</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Hëpërhë ky blog është i zbrazët.\n\nOse autori s\ka shkruar gjë ende, ose personi që ndau blogun me ju lypset të jetë në linjë, që të mund të njëkohësohen postimet.</string>
<string name="blogs_other_blog_empty_state">S\ka postime për shfaqje</string>
<string name="read_more">lexoni më tepër</string>
<string name="blogs_write_blog_post">Shkruani Postim Blogu</string>
<string name="blogs_write_blog_post_body_hint">Shtypni këtu postimin tuaj të blogut</string>
<string name="blogs_write_blog_post_body_hint">Shtypni postimin tuaj të blogut</string>
<string name="blogs_publish_blog_post">Botoje</string>
<string name="blogs_blog_post_created">Postimi i Blogut u Krijua</string>
<string name="blogs_blog_post_received">U morën Postime të Reja Blogu</string>
<string name="blogs_blog_post_scroll_to">Kalo Te</string>
<string name="blogs_feed_empty_state">Kjo është prurja globale e blogjeve.\n\nDuket se askush s\ka shkruar gjë në ndonjë blog.\n\nBëhuni ju i pari dhe prekni ikonën penë që të shkruani postimin e parë të një blogu të ri.</string>
<string name="blogs_feed_empty_state">Ska postime për shfaqje\n\nPostimet prej kontakteve tuaja dhe blogjeve ku pajtoheni do të shfaqen këtu\n\nPrekni ikonën penë që të shkruani një postim</string>
<string name="blogs_remove_blog">hiqe Blogun</string>
<string name="blogs_remove_blog_dialog_message">Jeni i sigurt se doni të hiqet ky blog dhe krejt postimet?\nMbani parasysh që kjo nuk do ta heqë blogun nga pajisjet e personave të tjerë.</string>
<string name="blogs_remove_blog_ok">Hiqe Blogun</string>
<string name="blogs_blog_removed">Blogu u Hoq</string>
<string name="blogs_remove_blog_dialog_message">Jeni i sigurt se doni të hiqet ky blog?\n\nPostimet do të hiqen nga pajisja juaj, por jo nga pajisjet e personave të tjerë.\n\nÇfarëdo kontaktesh me të cilët e keni ndarë këtë blog mund të reshtin së marri përditësime.</string>
<string name="blogs_remove_blog_ok">Hiqe</string>
<string name="blogs_blog_removed">Blogu u hoq</string>
<string name="blogs_reblog_comment_hint">Shtoni një koment (në daçi)</string>
<string name="blogs_reblog_button">Riblogojeni</string>
<!--Blog Sharing-->
@@ -278,8 +280,8 @@
<string name="blogs_sharing_invitation_received">%1$s ndau me ju \"%2$s\".</string>
<string name="blogs_sharing_invitation_sent">Ndatë blogun \"%1$s\" me %2$s.</string>
<string name="blogs_sharing_invitations_title">Ftesa Blogu</string>
<string name="blogs_sharing_joined_toast">U pajtuat te Blogu</string>
<string name="blogs_sharing_declined_toast">Ftesa e Blogut u Hodh Poshtë</string>
<string name="blogs_sharing_joined_toast">U pajtuat te blogu</string>
<string name="blogs_sharing_declined_toast">Ftesa u hodh poshtë</string>
<string name="sharing_status_blog">Cilido që pajtohet te një blog mund ta ndajë atë me kontaktet e veta. Këtë blog po e ndani me kontaktet vijuese. Mund të ketë edhe pajtimtarë të tjerë, të cilët s\mund t\i shihni.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Importoni Prurje RSS</string>
@@ -291,10 +293,10 @@
<string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Përditësuar Së Fundi:</string>
<string name="blogs_rss_remove_feed">Hiqe Prurjen</string>
<string name="blogs_rss_remove_feed_dialog_message">Jeni i sigurt se doni të hiqet kjo prurje dhe krejt postimet e saj?\nÇfarëdo postimi që keni ndarë me të tjerët, nuk do të hiqet nga pajisjet e personave të tjerë.</string>
<string name="blogs_rss_remove_feed_ok">Hiqe Prurjen</string>
<string name="blogs_rss_remove_feed_dialog_message">Jeni i sigurt se doni të hiqet kjo prurje?\n\nPostimet do të hiqen nga pajisja juaj, por jo nga pajisjet e njerëzve të tjerë.\n\nÇfarëdo kontaktesh me të cilët e keni ndarë këtë prurje mund të reshtin së marri përditësime.</string>
<string name="blogs_rss_remove_feed_ok">Hiqe</string>
<string name="blogs_rss_feeds_manage_delete_error">S\u fshi dot prurja!</string>
<string name="blogs_rss_feeds_manage_empty_state">S\keni importuar ende ndonjë prurje RSS.\n\nPse nuk klikoni mbi shenjën plus në cepin e sipërm djathtas që të shtoni të parën tuaj?</string>
<string name="blogs_rss_feeds_manage_empty_state">Ska prurje RSS për shfaqje\n\nPrekni ikonën + që të importohet një prurje</string>
<string name="blogs_rss_feeds_manage_error">Pati një problem me ngarkimin e prurjeve tuaja. Ju lutemi, riprovoni më vonë.</string>
<!--Settings Network-->
<string name="network_settings_title">Rrjete</string>
@@ -331,12 +333,16 @@
<string name="notification_settings_title">Njoftime</string>
<string name="notify_private_messages_setting_title">Mesazhe private</string>
<string name="notify_private_messages_setting_summary">Shfaq sinjalizime për mesazhe private</string>
<string name="notify_private_messages_setting_summary_26">Formësoni sinjalizimet për mesazhe private</string>
<string name="notify_group_messages_setting_title">Mesazhe grupi</string>
<string name="notify_group_messages_setting_summary">Shfaq sinjalizime për mesazhe grupi</string>
<string name="notify_group_messages_setting_summary_26">Formësoni sinjalizimet për mesazhi grupi</string>
<string name="notify_forum_posts_setting_title">Postime forumi</string>
<string name="notify_forum_posts_setting_summary">Shfaq sinjalizime për postime forumi</string>
<string name="notify_forum_posts_setting_summary_26">Formësoni sinjalizimet për postime forumi</string>
<string name="notify_blog_posts_setting_title">Postime blogu</string>
<string name="notify_blog_posts_setting_summary">Shfaq sinjalizime për postime blogu</string>
<string name="notify_blog_posts_setting_summary_26">Formësoni sinjalizime për postime blogu</string>
<string name="notify_vibration_setting">Dridhu</string>
<string name="notify_lock_screen_setting_title">Kyçe Ekranin</string>
<string name="notify_lock_screen_setting_summary">Shfaqi njoftimet edhe me ekran të kyçur</string>
@@ -380,4 +386,6 @@
<string name="permission_camera_request_body">Që të skanojë kodin QR, Briar-i lypset të hyjë te kamera.</string>
<string name="permission_camera_denied_body">Keni mohuar hyrjen në kamera, por shtimi i kontakteve lyp përdorimin e kamerës.\n\nJu lutemi, shihni mundësinë e akordimit të hyrjes.</string>
<string name="permission_camera_denied_toast">S\u dhanë leje mbi kamerën</string>
<string name="qr_code">Kod QR</string>
<string name="show_qr_code_fullscreen">Shfaqe kodin QR sa tërë ekrani</string>
</resources>

View File

@@ -108,11 +108,13 @@
<string name="dialog_button_leave">Tillbaka</string>
<!--Forum Sharing-->
<!--Blogs-->
<string name="blogs_remove_blog_ok">&amp;Ta bort</string>
<!--Blog Sharing-->
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import_button">Importera</string>
<string name="blogs_rss_feeds_manage_author">Författare:</string>
<string name="blogs_rss_feeds_manage_updated">Senast uppdaterad:</string>
<string name="blogs_rss_remove_feed_ok">&amp;Ta bort</string>
<!--Settings Network-->
<string name="network_settings_title">Nätverk</string>
<string name="bluetooth_setting">Anslut via Bluetooth</string>

View File

@@ -42,6 +42,8 @@
</plurals>
<string name="expiry_update">测试到期时间延长,您的帐户将在 %d 天后过期。</string>
<string name="expiry_date_reached">本软件已过期。\n感谢您的测试</string>
<string name="startup_open_database">正在解密数据库……</string>
<string name="startup_migrate_database">正在升级数据库……</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">打开抽屉式导航栏</string>
<string name="nav_drawer_close_description">关闭抽屉式导航栏</string>
@@ -199,7 +201,7 @@
<string name="choose_forum_hint">为论坛命名</string>
<string name="create_forum_button">创建论坛</string>
<string name="forum_created_toast">论坛已创建</string>
<string name="no_forum_posts">该论坛是空的\n\n使用上方的钢笔按钮创建第一篇博文\n\n感觉很孤单将此论坛分享给您的联系人吧</string>
<string name="no_forum_posts">尚无帖子可供展示</string>
<string name="no_posts">无帖子</string>
<plurals name="posts">
<item quantity="other">%d 条帖子</item>
@@ -241,7 +243,7 @@
</plurals>
<string name="nobody">没有人</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">这个博客目前是空的。\n\n这有可能是因为作者尚未发布任何内容或者是分享该博客给您的人需要上线以使博文同步。</string>
<string name="blogs_other_blog_empty_state">尚无帖子可供展示</string>
<string name="read_more">阅读更多</string>
<string name="blogs_write_blog_post">写博文</string>
<string name="blogs_write_blog_post_body_hint">在此输入博文</string>

View File

@@ -36,8 +36,8 @@
<string name="startup_failed_notification_title">Briar could not start</string>
<string name="startup_failed_notification_text">Tap for more information.</string>
<string name="startup_failed_activity_title">Briar Startup Failure</string>
<string name="startup_failed_db_error">For some reason, your Briar database is corrupted beyond repair. Your account, your data and all your contacts are lost. Unfortunately, you need to reinstall Briar and set up a new account by choosing \'I have forgotten my password\' at the password prompt.</string>
<string name="startup_failed_data_too_old_error">Your account was created with an old version of this app and cannot be opened with this version. You must either reinstall the old version or delete your old account by choosing \'I have forgotten my password\' at the password prompt.</string>
<string name="startup_failed_db_error">For some reason, your Briar database is corrupted beyond repair. Your account, your data and all your contacts are lost. Unfortunately, you need to reinstall Briar or set up a new account by choosing \'I have forgotten my password\' at the password prompt.</string>
<string name="startup_failed_data_too_old_error">Your account was created with an old version of this app and cannot be opened with this version. You must either reinstall the old version or set up a new account by choosing \'I have forgotten my password\' at the password prompt.</string>
<string name="startup_failed_data_too_new_error">This version of the app is too old. Please upgrade to the latest version and try again.</string>
<string name="startup_failed_service_error">Briar was unable to start a required plugin. Reinstalling Briar usually solves this problem. However, please note that you will then lose your account and all data associated with it since Briar is not using central servers to store your data on.</string>
<plurals name="expiry_warning">
@@ -108,9 +108,9 @@
<string name="sorry">Sorry</string>
<!-- Contacts and Private Conversations-->
<string name="no_contacts">It seems that you are new here and have no contacts yet.\n\nTap the + icon at the top and follow the instructions to add some friends to your list.\n\nPlease remember: You can only add new contacts face-to-face to prevent anyone from impersonating you or reading your messages in the future.</string>
<string name="no_contacts">No contacts to show\n\nTap the + icon to add a contact</string>
<string name="date_no_private_messages">No messages.</string>
<string name="no_private_messages">This is the conversation view.\n\nThere seems to be a lack of conversation.\n\nJust tap the input field at the bottom to start a conversation.</string>
<string name="no_private_messages">No messages to show</string>
<string name="message_hint">Type message</string>
<string name="delete_contact">Delete contact</string>
<string name="dialog_title_delete_contact">Confirm Contact Deletion</string>
@@ -160,7 +160,7 @@
</plurals>
<!-- Private Groups -->
<string name="groups_list_empty">You are not participating in any groups.\n\nTap the + icon at the top to create a group yourself or ask your contacts to get invited into one of their groups.</string>
<string name="groups_list_empty">No groups to show\n\nTap the + icon to create a group, or ask your contacts to share groups with you</string>
<string name="groups_created_by">Created by %s</string>
<plurals name="messages">
<item quantity="one">%d message</item>
@@ -216,12 +216,12 @@
<string name="groups_reveal_invisible">Contact relationship is not visible to the group</string>
<!-- Forums -->
<string name="no_forums">You don\'t have any forums yet.\n\nWhy don\'t you create a new one yourself by tapping the + icon at the top?\n\nYou can also ask your contacts to share forums with you.</string>
<string name="no_forums">No forums to show\n\nTap the + icon to create a forum, or ask your contacts to share forums with you</string>
<string name="create_forum_title">Create Forum</string>
<string name="choose_forum_hint">Choose a name for your forum</string>
<string name="create_forum_button">Create Forum</string>
<string name="forum_created_toast">Forum created</string>
<string name="no_forum_posts">This forum is empty.\n\nUse the pen icon at the top to compose the first post.\n\nFeeling lonely here? Share this forum with more of your contacts!</string>
<string name="no_forum_posts">No posts to show</string>
<string name="no_posts">No posts</string>
<plurals name="posts">
<item quantity="one">%d post</item>
@@ -233,24 +233,24 @@
<string name="btn_reply">Reply</string>
<string name="forum_leave">Leave Forum</string>
<string name="dialog_title_leave_forum">Confirm Leaving Forum</string>
<string name="dialog_message_leave_forum">Are you sure that you want to leave this forum? Contacts you have shared this forum with might get cut off from receiving updates for this forum.</string>
<string name="dialog_message_leave_forum">Are you sure that you want to leave this forum?\n\nAny contacts you\'ve shared this forum with might stop receiving updates.</string>
<string name="dialog_button_leave">Leave</string>
<string name="forum_left_toast">Left Forum</string>
<string name="forum_left_toast">Left forum</string>
<!-- Forum Sharing -->
<string name="forum_share_button">Share Forum</string>
<string name="contacts_selected">Contacts selected</string>
<string name="activity_share_toolbar_header">Choose Contacts</string>
<string name="no_contacts_selector">It seems that you are new here and have no contacts yet.\n\nPlease come back here after you added your first contact.</string>
<string name="no_contacts_selector">No contacts to show\n\nPlease come back here after adding a contact</string>
<string name="forum_shared_snackbar">Forum shared with chosen contacts</string>
<string name="forum_share_message">Add a message (optional)</string>
<string name="forum_share_error">There was an error sharing this forum.</string>
<string name="forum_invitation_received">%1$s has shared the forum \"%2$s\" with you.</string>
<string name="forum_invitation_sent">You have shared the forum \"%1$s\" with %2$s.</string>
<string name="forum_invitations_title">Forum Invitations</string>
<string name="forum_invitation_exists">You accepted an invitation to this forum already. Accepting more invitations will grow and strengthen the communication in the forum.</string>
<string name="forum_joined_toast">Joined Forum</string>
<string name="forum_declined_toast">Forum Invitation Declined</string>
<string name="forum_invitation_exists">You accepted an invitation to this forum already.\n\nAccepting more invitations will make your connection to the forum faster and more reliable.</string>
<string name="forum_joined_toast">Joined forum</string>
<string name="forum_declined_toast">Invitation declined</string>
<string name="shared_by_format">Shared by %s</string>
<string name="forum_invitation_already_sharing">Already sharing</string>
<string name="forum_invitation_response_accepted_sent">You accepted the forum invitation from %s.</string>
@@ -268,19 +268,19 @@
<string name="nobody">Nobody</string>
<!-- Blogs -->
<string name="blogs_other_blog_empty_state">This blog is currently empty.\n\nEither the author hasn\'t written anything yet, or the person who shared this blog with you needs to come online, so posts can be synchronized.</string>
<string name="blogs_other_blog_empty_state">No posts to show</string>
<string name="read_more">read more</string>
<string name="blogs_write_blog_post">Write Blog Post</string>
<string name="blogs_write_blog_post_body_hint">Type your blog post here</string>
<string name="blogs_write_blog_post_body_hint">Type your blog post</string>
<string name="blogs_publish_blog_post">Publish</string>
<string name="blogs_blog_post_created">Blog Post Created</string>
<string name="blogs_blog_post_received">New Blog Post Received</string>
<string name="blogs_blog_post_scroll_to">Scroll To</string>
<string name="blogs_feed_empty_state">This is the global blog feed.\n\nIt looks like nobody blogged anything, yet.\n\nBe the first and tap the pen icon to write a new blog post.</string>
<string name="blogs_feed_empty_state">No posts to show\n\nPosts from your contacts and blogs you subscribe to will appear here\n\nTap the pen icon to write a post</string>
<string name="blogs_remove_blog">Remove Blog</string>
<string name="blogs_remove_blog_dialog_message">Are you sure that you want to remove this blog and all posts?\nNote that this will not remove the blog from other people\'s devices.</string>
<string name="blogs_remove_blog_ok">Remove Blog</string>
<string name="blogs_blog_removed">Blog Removed</string>
<string name="blogs_remove_blog_dialog_message">Are you sure that you want to remove this blog?\n\nPosts will be removed from your device but not from other people\'s devices.\n\nAny contacts you\'ve shared this blog with might stop receiving updates.</string>
<string name="blogs_remove_blog_ok">Remove</string>
<string name="blogs_blog_removed">Blog removed</string>
<string name="blogs_reblog_comment_hint">Add a comment (optional)</string>
<string name="blogs_reblog_button">Reblog</string>
@@ -296,8 +296,8 @@
<string name="blogs_sharing_invitation_received">%1$s has shared the blog \"%2$s\" with you.</string>
<string name="blogs_sharing_invitation_sent">You have shared the blog \"%1$s\" with %2$s.</string>
<string name="blogs_sharing_invitations_title">Blog Invitations</string>
<string name="blogs_sharing_joined_toast">Subscribed to Blog</string>
<string name="blogs_sharing_declined_toast">Blog Invitation Declined</string>
<string name="blogs_sharing_joined_toast">Subscribed to blog</string>
<string name="blogs_sharing_declined_toast">Invitation declined</string>
<string name="sharing_status_blog">Anyone who subscribes to a blog can share it with their contacts. You are sharing this blog with the following contacts. There may also be other subscribers who you can\'t see.</string>
<!-- RSS Feeds -->
@@ -310,10 +310,10 @@
<string name="blogs_rss_feeds_manage_author">Author:</string>
<string name="blogs_rss_feeds_manage_updated">Last Updated:</string>
<string name="blogs_rss_remove_feed">Remove Feed</string>
<string name="blogs_rss_remove_feed_dialog_message">Are you sure you want to remove this feed and all its posts?\nAny posts you have shared will not be removed from other people\'s devices.</string>
<string name="blogs_rss_remove_feed_ok">Remove Feed</string>
<string name="blogs_rss_remove_feed_dialog_message">Are you sure that you want to remove this feed?\n\nPosts will be removed from your device but not from other people\'s devices.\n\nAny contacts you\'ve shared this feed with might stop receiving updates.</string>
<string name="blogs_rss_remove_feed_ok">Remove</string>
<string name="blogs_rss_feeds_manage_delete_error">The feed could not be deleted!</string>
<string name="blogs_rss_feeds_manage_empty_state">You haven\'t imported any RSS feeds.\n\nWhy don\'t you click the plus in the top right screen corner to add your first?</string>
<string name="blogs_rss_feeds_manage_empty_state">No RSS feeds to show\n\nTap the + icon to import a feed</string>
<string name="blogs_rss_feeds_manage_error">There was a problem loading your feeds. Please try again later.</string>
<!-- Settings Network -->
@@ -353,12 +353,16 @@
<string name="notification_settings_title">Notifications</string>
<string name="notify_private_messages_setting_title">Private messages</string>
<string name="notify_private_messages_setting_summary">Show alerts for private messages</string>
<string name="notify_private_messages_setting_summary_26">Configure alerts for private messages</string>
<string name="notify_group_messages_setting_title">Group messages</string>
<string name="notify_group_messages_setting_summary">Show alerts for group messages</string>
<string name="notify_group_messages_setting_summary_26">Configure alerts for group messages</string>
<string name="notify_forum_posts_setting_title">Forum posts</string>
<string name="notify_forum_posts_setting_summary">Show alerts for forum posts</string>
<string name="notify_forum_posts_setting_summary_26">Configure alerts for forum posts</string>
<string name="notify_blog_posts_setting_title">Blog posts</string>
<string name="notify_blog_posts_setting_summary">Show alerts for blog posts</string>
<string name="notify_blog_posts_setting_summary_26">Configure alerts for blog posts</string>
<string name="notify_vibration_setting">Vibrate</string>
<string name="notify_lock_screen_setting_title">Lock Screen</string>
<string name="notify_lock_screen_setting_summary">Show notifications on the lock screen</string>
@@ -408,5 +412,10 @@
<string name="permission_camera_request_body">To scan the QR code, Briar needs access to the camera.</string>
<string name="permission_camera_denied_body">You have denied access to the camera, but adding contacts requires using the camera.\n\nPlease consider granting access.</string>
<string name="permission_camera_denied_toast">Camera permission was not granted</string>
<string name="qr_code">QR code</string>
<string name="show_qr_code_fullscreen">Show QR code fullscreen</string>
<!-- Low Memory Notification -->
<string name="low_memory_shutdown_notification_title">Signed out of Briar</string>
<string name="low_memory_shutdown_notification_text">Signed out due to lack of memory.</string>
</resources>