Compare commits

..

51 Commits

Author SHA1 Message Date
akwizgran
a44a68f231 Bump version numbers for 1.0.8 release. 2018-06-08 13:00:39 +01:00
akwizgran
4ac6baa23d Update translations. 2018-06-08 13:00:39 +01:00
akwizgran
4cde50b7f5 Merge branch '1293-cookie-file-polling' into 'master'
Poll for creation of Tor auth cookie file

Closes #1293

See merge request akwizgran/briar!828
2018-06-08 11:30:12 +00:00
akwizgran
da40eca80b Merge branch '1160-language-setting' into 'master'
Add language setting

Closes #1160 and #1222

See merge request akwizgran/briar!679
2018-06-08 11:20:39 +00:00
akwizgran
fa267d38af Filter out RTL languages on API < 17. 2018-06-08 13:07:30 +02:00
akwizgran
ba20fbeb47 Poll for creation of cookie file. 2018-06-08 10:40:38 +01:00
akwizgran
196df05df9 Bump version numbers for 1.0.7 release. 2018-06-07 12:12:39 +01:00
akwizgran
44f07c8d76 Merge branch '1293-tor-cookie-file' into 'master'
Watch for creation rather than updating of Tor cookie file

See merge request akwizgran/briar!825
2018-06-07 11:07:30 +00:00
goapunk
18c4195115 fix region and title 2018-06-07 11:42:31 +02:00
akwizgran
d4a9c41cf5 Watch for creation rather than updating of Tor cookie file.
Tor writes to a temporary file and then renames it over the old
file, if any, so CLOSE_WRITE never occurs. The old code was
working in most cases because it received IGNORED when the old
file was unlinked and didn't check the event type.
2018-06-07 09:19:52 +01:00
goapunk
8bc28f99c1 Improvements:
* Force LTR by prefixing language names with the LRM marker
* Add Polish
* Cleanup
2018-06-07 10:19:17 +02:00
goapunk
1834146ad0 fix hebrew 2018-06-07 10:19:17 +02:00
akwizgran
624e03a2c9 Merge branch 'default-build-timestamp' into 'master'
Add default build timestamp in case Git command fails

See merge request akwizgran/briar!826
2018-06-07 08:15:08 +00:00
akwizgran
a24e0482c9 Add default build timestamp in case Git command fails. 2018-06-06 14:34:40 +01:00
goapunk
695b543ba9 fix review 2018-06-06 11:16:24 +02:00
goapunk
75e910e1d9 Add a language setting 2018-06-06 11:16:21 +02:00
Torsten Grote
8fc8333451 Merge branch '1294-log-stack-traces' into 'master'
Log exception stacktraces

Closes #1294

See merge request akwizgran/briar!824
2018-06-03 02:28:17 +00:00
akwizgran
c2154c81f4 Log exception stacktraces. 2018-06-01 16:43:10 +01:00
akwizgran
5cd5fc7e43 Bump version numbers for 1.0.6 release. 2018-06-01 10:20:32 +01:00
akwizgran
abd9db70b9 Update translations, add Polish translation. 2018-06-01 10:18:42 +01:00
akwizgran
5025cf1e40 Merge branch 'remove-removable-drive-plugin' into 'master'
Remove RemovableDrivePlugin, refactor plugin interface

Closes #25

See merge request akwizgran/briar!817
2018-05-31 08:49:32 +00:00
akwizgran
834342fd3a Merge branch 'remove-reblog-scene-transition' into 'master'
Disable reblog scene transition as it even crashes my Android 7.1 device

Closes #785

See merge request akwizgran/briar!821
2018-05-29 15:56:07 +00:00
akwizgran
3028b236e1 Merge branch 'disable-prefetching' into 'master'
Disable pre-fetching in Threaded RecyclerView as a workaround for #1289

See merge request akwizgran/briar!820
2018-05-29 15:47:15 +00:00
Torsten Grote
254422bc02 Disable reblog scene transition as it even crashes my Android 7.1 device
Closes #785
2018-05-29 12:44:41 -03:00
Torsten Grote
c7949d6e00 Disable pre-fetching in Threaded RecyclerView as a workaround for #1289 2018-05-29 12:29:40 -03:00
Torsten Grote
0187264da7 Merge branch '1219-remove-debug-logging' into 'master'
Remove debug logging from setup process

See merge request akwizgran/briar!819
2018-05-28 13:59:22 +00:00
akwizgran
85a18cf53f Remove debug logging from setup process. 2018-05-28 14:34:20 +01:00
akwizgran
3181b695df Remove RemovableDrivePlugin, refactor plugin interface. 2018-05-25 13:57:38 +01:00
akwizgran
b2ac210586 Merge branch 'factor_out_plugin_conf' into 'master'
Make plugins and polling configurable

See merge request akwizgran/briar!814
2018-05-24 16:34:05 +00:00
Torsten Grote
d20340416d Merge branch 'jcenter-tor-binaries' into 'master'
Download Tor binaries from JCenter

See merge request akwizgran/briar!816
2018-05-24 12:21:14 +00:00
akwizgran
9da871718c Download Tor binaries from JCenter. 2018-05-24 10:54:34 +01:00
goapunk
3793cb841b Fix test and poller instantiation 2018-05-23 14:39:01 +02:00
goapunk
c6b88b51f0 Make plugins and polling configurable
* Move PluginConfig out of bramble-android. Projects using bramble now need to provide it.
* Add a PluginConfig#shouldPoll() method which can be used to disable polling altogether.
* Move Poller instantiation to the PluginManager.
2018-05-23 14:39:00 +02:00
Torsten Grote
2f00215a44 Merge branch 'remove-jtorctl-jar' into 'master'
Replace jtorctl jar with JCenter dependency

See merge request akwizgran/briar!815
2018-05-23 11:22:15 +00:00
akwizgran
183f0c5f31 Bump version numbers for 1.0.5 release. 2018-05-22 15:30:33 +01:00
Torsten Grote
34c5aaae0a Update translations (Farsi and Chinese complete now) 2018-05-22 11:17:51 -03:00
Torsten Grote
5531355ebd Merge branch '1219-store-db-key-in-file' into 'master'
Store database key in a file

Closes #1219

See merge request akwizgran/briar!810
2018-05-22 12:24:13 +00:00
akwizgran
b9e607744a Store second copy of DB key in backup file. 2018-05-22 12:07:07 +01:00
akwizgran
def62bce5a Replace jtorctl jar with JCenter dependency. 2018-05-22 11:32:19 +01:00
akwizgran
9dae3d191a Merge branch '1281-introduction-bug' into 'master'
Introduction: Reset session information for removed introducees

Closes #1281

See merge request akwizgran/briar!813
2018-05-22 09:09:13 +00:00
Torsten Grote
20422edf78 Introduction: Reset session information for removed introducees 2018-05-21 16:26:11 -03:00
Torsten Grote
f8bc5f08bf Merge branch 'unicode-escapes-for-test-data' into 'master'
Escape Unicode characters in test data

See merge request akwizgran/briar!812
2018-05-21 16:21:29 +00:00
akwizgran
9434495d70 Escape Unicode characters in test data.
This enables reproducible builds with non-Unicode locales.
2018-05-21 17:02:09 +01:00
Torsten Grote
bf9e91fcf5 Merge branch 'fix-build-timestamp' into 'master'
Make build timestamp command compatible with old versions of Git

See merge request akwizgran/briar!811
2018-05-21 12:47:47 +00:00
akwizgran
d9d86206a6 Make build timestamp command compatible with old versions of Git. 2018-05-21 13:40:14 +01:00
akwizgran
b410b8efcc Don't overwrite the backup if it's our only copy. 2018-05-18 15:17:43 +01:00
akwizgran
39aa2d96b3 Unit tests for DB key storage and retrieval. 2018-05-18 15:11:28 +01:00
akwizgran
21dae824a6 Store database key in a file rather than shared prefs. 2018-05-18 14:47:53 +01:00
akwizgran
cfdbd29cb4 Remove unused logging methods. 2018-05-18 14:47:20 +01:00
akwizgran
4df335ebd3 Merge branch 'own-ci' into 'master'
Switch to our own CI image

See merge request akwizgran/briar!809
2018-05-18 10:01:04 +00:00
Torsten Grote
682bee1486 Switch to our own CI image 2018-05-17 18:52:49 -03:00
107 changed files with 2372 additions and 2338 deletions

View File

@@ -1,15 +1,15 @@
image: registry.gitlab.com/fdroid/ci-images-base:latest
cache:
paths:
- .gradle/wrapper
- .gradle/caches
before_script:
- set -e
- export GRADLE_USER_HOME=$PWD/.gradle
image: briar/ci-image-android:latest
test:
before_script:
- set -e
- export GRADLE_USER_HOME=$PWD/.gradle
cache:
paths:
- .gradle/wrapper
- .gradle/caches
script:
- ./gradlew --no-daemon animalSnifferMain animalSnifferTest
- ./gradlew --no-daemon test
@@ -19,12 +19,13 @@ test:
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
test_reproducible:
image: briar/reproducer:latest
script:
- cd .. && mv briar /opt/briar-reproducer/
- cd /opt/briar-reproducer
- ./reproduce.py ${CI_COMMIT_REF_NAME}
only:
- tags

View File

@@ -1,11 +1,5 @@
import de.undercouch.gradle.tasks.download.Download
import de.undercouch.gradle.tasks.download.Verify
import java.security.NoSuchAlgorithmException
apply plugin: 'com.android.library'
apply plugin: 'witness'
apply plugin: 'de.undercouch.download'
android {
compileSdkVersion 27
@@ -14,8 +8,8 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion 26
versionCode 10004
versionName "1.0.4"
versionCode 10008
versionName "1.0.8"
consumerProguardFiles 'proguard-rules.txt'
}
@@ -25,9 +19,14 @@ android {
}
}
configurations {
tor
}
dependencies {
implementation project(path: ':bramble-core', configuration: 'default')
implementation fileTree(dir: 'libs', include: '*.jar')
implementation 'org.briarproject:jtorctl:0.3'
tor 'org.briarproject:tor-android:0.2.9.15@zip'
annotationProcessor 'com.google.dagger:dagger-compiler:2.0.2'
@@ -92,6 +91,8 @@ dependencyVerification {
'org.apache.httpcomponents:httpmime:4.1:httpmime-4.1.jar:31629566148e8a47688ae43b420abc3ecd783ed15b33bebc00824bf24c9b15aa',
'org.bouncycastle:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca',
'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349',
'org.briarproject:jtorctl:0.3:jtorctl-0.3.jar:f2939238a097898998432effe93b0334d97a787972ab3a91a8973a1d309fc864',
'org.briarproject:tor-android:0.2.9.15:tor-android-0.2.9.15.zip:34a6474ee219ffa52e0f3393e917dda6ed03d320b02247d4fa5075aa4094ee6d',
'org.codehaus.groovy:groovy-all:2.4.12:groovy-all-2.4.12.jar:6a56af4bd48903d56bec62821876cadefafd007360cc6bd0d8f7aa8d72b38be4',
'org.codehaus.mojo:animal-sniffer-annotations:1.14:animal-sniffer-annotations-1.14.jar:2068320bd6bad744c3673ab048f67e30bef8f518996fa380033556600669905d',
'org.glassfish.jaxb:jaxb-core:2.2.11:jaxb-core-2.2.11.jar:37bcaee8ebb04362c8352a5bf6221b86967ecdab5164c696b10b9a2bb587b2aa',
@@ -112,81 +113,9 @@ dependencyVerification {
]
}
ext.torBinaryDir = 'src/main/res/raw'
ext.torVersion = '0.2.9.14'
ext.geoipVersion = '2017-11-06'
ext.torDownloadUrl = 'https://briarproject.org/build/'
def torBinaries = [
"tor_arm" : '1710ea6c47b7f4c1a88bdf4858c7893837635db10e8866854eed8d61629f50e8',
"tor_arm_pie": '974e6949507db8fa2ea45231817c2c3677ed4ccf5488a2252317d744b0be1917',
"tor_x86" : '3a5e45b3f051fcda9353b098b7086e762ffe7ba9242f7d7c8bf6523faaa8b1e9',
"tor_x86_pie": 'd1d96d8ce1a4b68accf04850185780d10cd5563d3552f7e1f040f8ca32cb4e51',
"geoip" : '8239b98374493529a29096e45fc5877d4d6fdad0146ad8380b291f90d61484ea'
]
def verifyOrDeleteBinary(name, chksum, alreadyVerified) {
return tasks.create("verifyOrDeleteBinary${name}", VerifyOrDelete) {
src "${torBinaryDir}/${name}.zip"
algorithm 'SHA-256'
checksum chksum
result alreadyVerified
onlyIf {
src.exists()
}
}
}
def downloadBinary(name, chksum, alreadyVerified) {
return tasks.create([
name: "downloadBinary${name}",
type: Download,
dependsOn: verifyOrDeleteBinary(name, chksum, alreadyVerified)]) {
src "${torDownloadUrl}${name}.zip"
.replace('tor_', "tor-${torVersion}-")
.replace('geoip', "geoip-${geoipVersion}")
.replaceAll('_', '-')
dest "${torBinaryDir}/${name}.zip"
onlyIf {
!dest.exists()
}
}
}
def verifyBinary(name, chksum) {
boolean[] alreadyVerified = [false]
return tasks.create([
name : "verifyBinary${name}",
type : Verify,
dependsOn: downloadBinary(name, chksum, alreadyVerified)]) {
src "${torBinaryDir}/${name}.zip"
algorithm 'SHA-256'
checksum chksum
onlyIf {
!alreadyVerified[0]
}
}
}
project.afterEvaluate {
torBinaries.every { name, checksum ->
preBuild.dependsOn.add(verifyBinary(name, checksum))
}
}
class VerifyOrDelete extends Verify {
boolean[] result
@TaskAction
@Override
void verify() throws IOException, NoSuchAlgorithmException {
try {
super.verify()
result[0] = true
} catch (Exception e) {
println "${src} failed verification - deleting"
src.delete()
}
copy {
from configurations.tor.collect { zipTree(it) }
into 'src/main/res/raw'
}
}

View File

@@ -1,12 +1,10 @@
package org.briarproject.bramble;
import org.briarproject.bramble.plugin.AndroidPluginModule;
import org.briarproject.bramble.system.AndroidSystemModule;
import dagger.Module;
@Module(includes = {
AndroidPluginModule.class,
AndroidSystemModule.class
})
public class BrambleAndroidModule {

View File

@@ -1,67 +0,0 @@
package org.briarproject.bramble.plugin;
import android.app.Application;
import android.content.Context;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.BackoffFactory;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.Scheduler;
import org.briarproject.bramble.plugin.bluetooth.AndroidBluetoothPluginFactory;
import org.briarproject.bramble.plugin.tcp.AndroidLanTcpPluginFactory;
import org.briarproject.bramble.plugin.tor.TorPluginFactory;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import javax.net.SocketFactory;
import dagger.Module;
import dagger.Provides;
@Module
public class AndroidPluginModule {
@Provides
PluginConfig providePluginConfig(@IoExecutor Executor ioExecutor,
@Scheduler ScheduledExecutorService scheduler,
AndroidExecutor androidExecutor, SecureRandom random,
SocketFactory torSocketFactory, BackoffFactory backoffFactory,
Application app, LocationUtils locationUtils, EventBus eventBus) {
Context appContext = app.getApplicationContext();
DuplexPluginFactory bluetooth =
new AndroidBluetoothPluginFactory(ioExecutor, androidExecutor,
appContext, random, eventBus, backoffFactory);
DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, scheduler,
appContext, locationUtils, eventBus, torSocketFactory,
backoffFactory);
DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
scheduler, backoffFactory, appContext);
Collection<DuplexPluginFactory> duplex =
Arrays.asList(bluetooth, tor, lan);
@NotNullByDefault
PluginConfig pluginConfig = new PluginConfig() {
@Override
public Collection<DuplexPluginFactory> getDuplexFactories() {
return duplex;
}
@Override
public Collection<SimplexPluginFactory> getSimplexFactories() {
return Collections.emptyList();
}
};
return pluginConfig;
}
}

View File

@@ -10,7 +10,6 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.FileObserver;
import android.os.PowerManager;
import net.freehaven.tor.control.EventHandler;
@@ -34,6 +33,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.util.IoUtils;
import org.briarproject.bramble.util.StringUtils;
@@ -50,13 +50,11 @@ import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
@@ -79,7 +77,6 @@ import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
import static android.os.PowerManager.PARTIAL_WAKE_LOCK;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@@ -103,7 +100,8 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
"CIRC", "ORCONN", "HS_DESC", "NOTICE", "WARN", "ERR"
};
private static final String OWNER = "__OwningControllerProcess";
private static final int COOKIE_TIMEOUT = 3000; // Milliseconds
private static final int COOKIE_TIMEOUT_MS = 3000;
private static final int COOKIE_POLLING_INTERVAL_MS = 200;
private static final Pattern ONION = Pattern.compile("[a-z2-7]{16}");
private static final Logger LOG =
Logger.getLogger(TorPlugin.class.getName());
@@ -113,6 +111,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private final Context appContext;
private final LocationUtils locationUtils;
private final SocketFactory torSocketFactory;
private final Clock clock;
private final Backoff backoff;
private final DuplexPluginCallback callback;
private final String architecture;
@@ -133,7 +132,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
TorPlugin(Executor ioExecutor, ScheduledExecutorService scheduler,
Context appContext, LocationUtils locationUtils,
SocketFactory torSocketFactory, Backoff backoff,
SocketFactory torSocketFactory, Clock clock, Backoff backoff,
DuplexPluginCallback callback, String architecture,
int maxLatency, int maxIdleTime) {
this.ioExecutor = ioExecutor;
@@ -141,6 +140,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
this.appContext = appContext;
this.locationUtils = locationUtils;
this.torSocketFactory = torSocketFactory;
this.clock = clock;
this.backoff = backoff;
this.callback = callback;
this.architecture = architecture;
@@ -186,18 +186,8 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (used.getAndSet(true)) throw new IllegalStateException();
// Install or update the assets if necessary
if (!assetsAreUpToDate()) installAssets();
LOG.info("Starting Tor");
// Watch for the auth cookie file being updated
try {
cookieFile.getParentFile().mkdirs();
cookieFile.createNewFile();
} catch (IOException e) {
throw new PluginException(e);
}
CountDownLatch latch = new CountDownLatch(1);
FileObserver obs = new WriteObserver(cookieFile, latch);
obs.startWatching();
// Start a new Tor process
LOG.info("Starting Tor");
String torPath = torFile.getAbsolutePath();
String configPath = configFile.getAbsolutePath();
String pid = String.valueOf(android.os.Process.myPid());
@@ -236,11 +226,16 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
throw new PluginException();
}
// Wait for the auth cookie file to be created/updated
if (!latch.await(COOKIE_TIMEOUT, MILLISECONDS)) {
LOG.warning("Auth cookie not created");
if (LOG.isLoggable(INFO)) listFiles(torDirectory);
throw new PluginException();
long start = clock.currentTimeMillis();
while (cookieFile.length() < 32) {
if (clock.currentTimeMillis() - start > COOKIE_TIMEOUT_MS) {
LOG.warning("Auth cookie not created");
if (LOG.isLoggable(INFO)) listFiles(torDirectory);
throw new PluginException();
}
Thread.sleep(COOKIE_POLLING_INTERVAL_MS);
}
LOG.info("Auth cookie created");
} catch (InterruptedException e) {
LOG.warning("Interrupted while starting Tor");
Thread.currentThread().interrupt();
@@ -334,7 +329,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
return zin;
}
private InputStream getConfigInputStream() throws IOException {
private InputStream getConfigInputStream() {
int resId = getResourceId("torrc");
return appContext.getResources().openRawResource(resId);
}
@@ -365,7 +360,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
File[] children = f.listFiles();
if (children != null) for (File child : children) listFiles(child);
} else {
LOG.info(f.getAbsolutePath());
LOG.info(f.getAbsolutePath() + " " + f.length());
}
}
@@ -499,7 +494,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
@Override
public void stop() throws PluginException {
public void stop() {
running = false;
tryToClose(socket);
if (networkStateReceiver != null)
@@ -533,20 +528,16 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
@Override
public void poll(Collection<ContactId> connected) {
public void poll(Map<ContactId, TransportProperties> contacts) {
if (!isRunning()) return;
backoff.increment();
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
ContactId c = e.getKey();
if (!connected.contains(c)) connectAndCallBack(c, e.getValue());
for (Entry<ContactId, TransportProperties> e : contacts.entrySet()) {
connectAndCallBack(e.getKey(), e.getValue());
}
}
private void connectAndCallBack(ContactId c, TransportProperties p) {
ioExecutor.execute(() -> {
if (!isRunning()) return;
DuplexTransportConnection d = createConnection(p);
if (d != null) {
backoff.reset();
@@ -556,13 +547,8 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
@Override
public DuplexTransportConnection createConnection(ContactId c) {
public DuplexTransportConnection createConnection(TransportProperties p) {
if (!isRunning()) return null;
return createConnection(callback.getRemoteProperties(c));
}
@Nullable
private DuplexTransportConnection createConnection(TransportProperties p) {
String onion = p.get(PROP_ONION);
if (StringUtils.isNullOrEmpty(onion)) return null;
if (!ONION.matcher(onion).matches()) {
@@ -651,22 +637,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
LOG.info("Descriptor uploaded");
}
private static class WriteObserver extends FileObserver {
private final CountDownLatch latch;
private WriteObserver(File file, CountDownLatch latch) {
super(file.getAbsolutePath(), CLOSE_WRITE);
this.latch = latch;
}
@Override
public void onEvent(int event, @Nullable String path) {
stopWatching();
latch.countDown();
}
}
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {

View File

@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.util.AndroidUtils;
@@ -42,11 +43,13 @@ public class TorPluginFactory implements DuplexPluginFactory {
private final EventBus eventBus;
private final SocketFactory torSocketFactory;
private final BackoffFactory backoffFactory;
private final Clock clock;
public TorPluginFactory(Executor ioExecutor,
ScheduledExecutorService scheduler, Context appContext,
LocationUtils locationUtils, EventBus eventBus,
SocketFactory torSocketFactory, BackoffFactory backoffFactory) {
SocketFactory torSocketFactory, BackoffFactory backoffFactory,
Clock clock) {
this.ioExecutor = ioExecutor;
this.scheduler = scheduler;
this.appContext = appContext;
@@ -54,6 +57,7 @@ public class TorPluginFactory implements DuplexPluginFactory {
this.eventBus = eventBus;
this.torSocketFactory = torSocketFactory;
this.backoffFactory = backoffFactory;
this.clock = clock;
}
@Override
@@ -90,7 +94,7 @@ public class TorPluginFactory implements DuplexPluginFactory {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);
TorPlugin plugin = new TorPlugin(ioExecutor, scheduler, appContext,
locationUtils, torSocketFactory, backoff, callback,
locationUtils, torSocketFactory, clock, backoff, callback,
architecture, MAX_LATENCY, MAX_IDLE_TIME);
eventBus.addListener(plugin);
return plugin;

View File

@@ -8,16 +8,13 @@ import android.os.Build;
import android.provider.Settings;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Scanner;
import java.util.logging.Logger;
import static android.content.Context.MODE_PRIVATE;
import static java.util.logging.Level.INFO;
public class AndroidUtils {
@@ -61,77 +58,28 @@ public class AndroidUtils {
&& !address.equals(FAKE_BLUETOOTH_ADDRESS);
}
public static void logDataDirContents(Context ctx) {
if (LOG.isLoggable(INFO)) {
LOG.info("Contents of data directory:");
logFileOrDir(new File(ctx.getApplicationInfo().dataDir));
}
}
private static void logFileOrDir(File f) {
LOG.info(f.getAbsolutePath() + " " + f.length());
if (f.isDirectory()) {
File[] children = f.listFiles();
if (children == null) {
LOG.info("Could not list files in " + f.getAbsolutePath());
} else {
for (File child : children) logFileOrDir(child);
}
}
}
public static File getSharedPrefsFile(Context ctx, String name) {
File dataDir = new File(ctx.getApplicationInfo().dataDir);
File prefsDir = new File(dataDir, "shared_prefs");
return new File(prefsDir, name + ".xml");
}
public static void logFileContents(File f) {
if (LOG.isLoggable(INFO)) {
LOG.info("Contents of " + f.getAbsolutePath() + ":");
try {
Scanner s = new Scanner(f);
while (s.hasNextLine()) LOG.info(s.nextLine());
s.close();
} catch (FileNotFoundException e) {
LOG.info(f.getAbsolutePath() + " not found");
}
}
}
@SuppressLint("ApplySharedPref")
public static void deleteAppData(Context ctx, SharedPreferences... clear) {
// Clear and commit shared preferences
for (SharedPreferences prefs : clear) {
boolean cleared = prefs.edit().clear().commit();
if (LOG.isLoggable(INFO)) {
if (cleared) LOG.info("Cleared shared preferences");
else LOG.info("Could not clear shared preferences");
}
if (!prefs.edit().clear().commit())
LOG.warning("Could not clear shared preferences");
}
// Delete files, except lib and shared_prefs directories
File dataDir = new File(ctx.getApplicationInfo().dataDir);
if (LOG.isLoggable(INFO))
LOG.info("Deleting app data from " + dataDir.getAbsolutePath());
File[] children = dataDir.listFiles();
if (children != null) {
if (children == null) {
LOG.warning("Could not list files in app data dir");
} else {
for (File child : children) {
String name = child.getName();
if (!name.equals("lib") && !name.equals("shared_prefs")) {
if (LOG.isLoggable(INFO))
LOG.info("Deleting " + child.getAbsolutePath());
IoUtils.deleteFileOrDir(child);
}
}
} else if (LOG.isLoggable(INFO)) {
LOG.info("Could not list files in " + dataDir.getAbsolutePath());
}
// Recreate the cache dir as some OpenGL drivers expect it to exist
boolean recreated = new File(dataDir, "cache").mkdir();
if (LOG.isLoggable(INFO)) {
if (recreated) LOG.info("Recreated cache dir");
else LOG.info("Could not recreate cache dir");
}
if (!new File(dataDir, "cache").mkdir())
LOG.warning("Could not recreate cache dir");
}
public static File getReportDir(Context ctx) {

View File

@@ -14,6 +14,8 @@ public interface DatabaseConfig {
File getDatabaseDirectory();
File getDatabaseKeyDirectory();
void setEncryptionKey(SecretKey key);
@Nullable

View File

@@ -0,0 +1,6 @@
package org.briarproject.bramble.api.plugin;
public interface FileConstants {
String PROP_PATH = "path";
}

View File

@@ -2,8 +2,9 @@ package org.briarproject.bramble.api.plugin;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.util.Collection;
import java.util.Map;
@NotNullByDefault
public interface Plugin {
@@ -39,21 +40,19 @@ public interface Plugin {
boolean isRunning();
/**
* Returns true if the plugin's {@link #poll(Collection)} method should be
* called periodically to attempt to establish connections.
* Returns true if the plugin should be polled periodically to attempt to
* establish connections.
*/
boolean shouldPoll();
/**
* Returns the desired interval in milliseconds between calls to the
* plugin's {@link #poll(Collection)} method.
* Returns the desired interval in milliseconds between polling attempts.
*/
int getPollingInterval();
/**
* Attempts to establish connections to contacts, passing any created
* connections to the callback. To avoid creating redundant connections,
* the plugin may exclude the given contacts from polling.
* Attempts to establish connections to the given contacts, passing any
* created connections to the callback.
*/
void poll(Collection<ContactId> connected);
void poll(Map<ContactId, TransportProperties> contacts);
}

View File

@@ -1,12 +1,9 @@
package org.briarproject.bramble.api.plugin;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.settings.Settings;
import java.util.Map;
/**
* An interface through which a transport plugin interacts with the rest of
* the application.
@@ -25,17 +22,7 @@ public interface PluginCallback {
TransportProperties getLocalProperties();
/**
* Returns the plugin's remote transport properties.
*/
Map<ContactId, TransportProperties> getRemoteProperties();
/**
* Returns the plugin's remote transport properties for the given contact.
*/
TransportProperties getRemoteProperties(ContactId c);
/**
* Merges the given settings with the namespaced settings
* Merges the given settings with the plugin's settings
*/
void mergeSettings(Settings s);
@@ -45,34 +32,12 @@ public interface PluginCallback {
void mergeLocalProperties(TransportProperties p);
/**
* Presents the user with a choice among two or more named options and
* returns the user's response. The message may consist of a translatable
* format string and arguments.
*
* @return an index into the array of options indicating the user's choice,
* or -1 if the user cancelled the choice.
*/
int showChoice(String[] options, String... message);
/**
* Asks the user to confirm an action and returns the user's response. The
* message may consist of a translatable format string and arguments.
*/
boolean showConfirmationMessage(String... message);
/**
* Shows a message to the user. The message may consist of a translatable
* format string and arguments.
*/
void showMessage(String... message);
/**
* Signal that the transport got enabled.
* Signals that the transport is enabled.
*/
void transportEnabled();
/**
* Signal that the transport got disabled.
* Signals that the transport is disabled.
*/
void transportDisabled();
}

View File

@@ -12,4 +12,6 @@ public interface PluginConfig {
Collection<DuplexPluginFactory> getDuplexFactories();
Collection<SimplexPluginFactory> getSimplexFactories();
boolean shouldPoll();
}

View File

@@ -22,11 +22,6 @@ public interface TransportConnectionWriter {
*/
int getMaxIdleTime();
/**
* Returns the capacity of the transport connection in bytes.
*/
long getCapacity();
/**
* Returns an output stream for writing to the transport connection.
*/

View File

@@ -71,11 +71,6 @@ public abstract class AbstractDuplexTransportConnection
return plugin.getMaxIdleTime();
}
@Override
public long getCapacity() {
return Long.MAX_VALUE;
}
@Override
public OutputStream getOutputStream() throws IOException {
return AbstractDuplexTransportConnection.this.getOutputStream();

View File

@@ -1,10 +1,10 @@
package org.briarproject.bramble.api.plugin.duplex;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.properties.TransportProperties;
import javax.annotation.Nullable;
@@ -15,12 +15,11 @@ import javax.annotation.Nullable;
public interface DuplexPlugin extends Plugin {
/**
* Attempts to create and return a connection to the given contact using
* the current transport and configuration properties. Returns null if a
* connection cannot be created.
* Attempts to create and return a connection using the given transport
* properties. Returns null if a connection cannot be created.
*/
@Nullable
DuplexTransportConnection createConnection(ContactId c);
DuplexTransportConnection createConnection(TransportProperties p);
/**
* Returns true if the plugin supports short-range key agreement.

View File

@@ -5,7 +5,8 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginCallback;
/**
* An interface for handling connections created by a duplex transport plugin.
* An interface through which a duplex plugin interacts with the rest of the
* application.
*/
@NotNullByDefault
public interface DuplexPluginCallback extends PluginCallback {

View File

@@ -1,10 +1,10 @@
package org.briarproject.bramble.api.plugin.simplex;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.properties.TransportProperties;
import javax.annotation.Nullable;
@@ -15,18 +15,16 @@ import javax.annotation.Nullable;
public interface SimplexPlugin extends Plugin {
/**
* Attempts to create and return a reader for the given contact using the
* current transport and configuration properties. Returns null if a reader
* cannot be created.
* Attempts to create and return a reader for the given transport
* properties. Returns null if a reader cannot be created.
*/
@Nullable
TransportConnectionReader createReader(ContactId c);
TransportConnectionReader createReader(TransportProperties p);
/**
* Attempts to create and return a writer for the given contact using the
* current transport and configuration properties. Returns null if a writer
* cannot be created.
* Attempts to create and return a writer for the given transport
* properties. Returns null if a writer cannot be created.
*/
@Nullable
TransportConnectionWriter createWriter(ContactId c);
TransportConnectionWriter createWriter(TransportProperties p);
}

View File

@@ -7,8 +7,8 @@ import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
/**
* An interface for handling readers and writers created by a simplex transport
* plugin.
* An interface through which a simplex plugin interacts with the rest of the
* application.
*/
@NotNullByDefault
public interface SimplexPluginCallback extends PluginCallback {

View File

@@ -1,26 +0,0 @@
package org.briarproject.bramble.api.ui;
public interface UiCallback {
/**
* Presents the user with a choice among two or more named options and
* returns the user's response. The message may consist of a translatable
* format string and arguments.
*
* @return an index into the array of options indicating the user's choice,
* or -1 if the user cancelled the choice.
*/
int showChoice(String[] options, String... message);
/**
* Asks the user to confirm an action and returns the user's response. The
* message may consist of a translatable format string and arguments.
*/
boolean showConfirmationMessage(String... message);
/**
* Shows a message to the user. The message may consist of a translatable
* format string and arguments.
*/
void showMessage(String... message);
}

View File

@@ -13,7 +13,7 @@ import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@NotNullByDefault
public class IoUtils {
@@ -25,18 +25,21 @@ public class IoUtils {
delete(f);
} else if (f.isDirectory()) {
File[] children = f.listFiles();
if (children != null)
if (children == null) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("Could not list files in "
+ f.getAbsolutePath());
}
} else {
for (File child : children) deleteFileOrDir(child);
}
delete(f);
}
}
private static void delete(File f) {
boolean deleted = f.delete();
if (LOG.isLoggable(INFO)) {
if (deleted) LOG.info("Deleted " + f.getAbsolutePath());
else LOG.info("Could not delete " + f.getAbsolutePath());
}
if (!f.delete() && LOG.isLoggable(WARNING))
LOG.warning("Could not delete " + f.getAbsolutePath());
}
public static void copyAndClose(InputStream in, OutputStream out) {

View File

@@ -22,19 +22,6 @@ public class OsUtils {
return os != null && os.contains("Mac OS");
}
public static boolean isMacLeopardOrNewer() {
if (!isMac() || version == null) return false;
try {
String[] v = version.split("\\.");
if (v.length != 3) return false;
int major = Integer.parseInt(v[0]);
int minor = Integer.parseInt(v[1]);
return major >= 10 && minor >= 5;
} catch (NumberFormatException e) {
return false;
}
}
public static boolean isLinux() {
return os != null && os.contains("Linux") && !isAndroid();
}

View File

@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.lifecycle.Service;
import org.briarproject.bramble.api.lifecycle.ServiceException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.PluginConfig;
@@ -29,17 +30,19 @@ import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.ui.UiCallback;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
@@ -57,12 +60,15 @@ class PluginManagerImpl implements PluginManager, Service {
Logger.getLogger(PluginManagerImpl.class.getName());
private final Executor ioExecutor;
private final ScheduledExecutorService scheduler;
private final EventBus eventBus;
private final PluginConfig pluginConfig;
private final ConnectionManager connectionManager;
private final ConnectionRegistry connectionRegistry;
private final SettingsManager settingsManager;
private final TransportPropertyManager transportPropertyManager;
private final UiCallback uiCallback;
private final SecureRandom random;
private final Clock clock;
private final Map<TransportId, Plugin> plugins;
private final List<SimplexPlugin> simplexPlugins;
private final List<DuplexPlugin> duplexPlugins;
@@ -70,27 +76,41 @@ class PluginManagerImpl implements PluginManager, Service {
private final AtomicBoolean used = new AtomicBoolean(false);
@Inject
PluginManagerImpl(@IoExecutor Executor ioExecutor, EventBus eventBus,
PluginManagerImpl(@IoExecutor Executor ioExecutor,
@Scheduler ScheduledExecutorService scheduler, EventBus eventBus,
PluginConfig pluginConfig, ConnectionManager connectionManager,
ConnectionRegistry connectionRegistry,
SettingsManager settingsManager,
TransportPropertyManager transportPropertyManager,
UiCallback uiCallback) {
SecureRandom random, Clock clock) {
this.ioExecutor = ioExecutor;
this.scheduler = scheduler;
this.eventBus = eventBus;
this.pluginConfig = pluginConfig;
this.connectionManager = connectionManager;
this.connectionRegistry = connectionRegistry;
this.settingsManager = settingsManager;
this.transportPropertyManager = transportPropertyManager;
this.uiCallback = uiCallback;
this.random = random;
this.clock = clock;
plugins = new ConcurrentHashMap<>();
simplexPlugins = new CopyOnWriteArrayList<>();
duplexPlugins = new CopyOnWriteArrayList<>();
startLatches = new ConcurrentHashMap<>();
}
@Override
public void startService() throws ServiceException {
public void startService() {
if (used.getAndSet(true)) throw new IllegalStateException();
// Instantiate the poller
if (pluginConfig.shouldPoll()) {
LOG.info("Starting poller");
Poller poller = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, this, transportPropertyManager, random,
clock);
eventBus.addListener(poller);
}
// Instantiate the simplex plugins and start them asynchronously
LOG.info("Starting simplex plugins");
for (SimplexPluginFactory f : pluginConfig.getSimplexFactories()) {
@@ -273,26 +293,6 @@ class PluginManagerImpl implements PluginManager, Service {
}
}
@Override
public Map<ContactId, TransportProperties> getRemoteProperties() {
try {
return transportPropertyManager.getRemoteProperties(id);
} catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return Collections.emptyMap();
}
}
@Override
public TransportProperties getRemoteProperties(ContactId c) {
try {
return transportPropertyManager.getRemoteProperties(c, id);
} catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return new TransportProperties();
}
}
@Override
public void mergeSettings(Settings s) {
try {
@@ -311,21 +311,6 @@ class PluginManagerImpl implements PluginManager, Service {
}
}
@Override
public int showChoice(String[] options, String... message) {
return uiCallback.showChoice(options, message);
}
@Override
public boolean showConfirmationMessage(String... message) {
return uiCallback.showConfirmationMessage(message);
}
@Override
public void showMessage(String... message) {
uiCallback.showMessage(message);
}
@Override
public void transportEnabled() {
eventBus.broadcast(new TransportEnabledEvent(id));

View File

@@ -1,18 +1,10 @@
package org.briarproject.bramble.plugin;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.plugin.BackoffFactory;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler;
import java.security.SecureRandom;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -26,8 +18,6 @@ public class PluginModule {
public static class EagerSingletons {
@Inject
PluginManager pluginManager;
@Inject
Poller poller;
}
@Provides
@@ -35,19 +25,6 @@ public class PluginModule {
return new BackoffFactoryImpl();
}
@Provides
@Singleton
Poller providePoller(@IoExecutor Executor ioExecutor,
@Scheduler ScheduledExecutorService scheduler,
ConnectionManager connectionManager,
ConnectionRegistry connectionRegistry, PluginManager pluginManager,
SecureRandom random, Clock clock, EventBus eventBus) {
Poller poller = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, random, clock);
eventBus.addListener(poller);
return poller;
}
@Provides
@Singleton
ConnectionManager provideConnectionManager(

View File

@@ -2,6 +2,7 @@ package org.briarproject.bramble.plugin;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.event.ContactStatusChangedEvent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
@@ -19,10 +20,13 @@ import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
@@ -33,10 +37,10 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@ThreadSafe
@NotNullByDefault
@@ -49,22 +53,24 @@ class Poller implements EventListener {
private final ConnectionManager connectionManager;
private final ConnectionRegistry connectionRegistry;
private final PluginManager pluginManager;
private final TransportPropertyManager transportPropertyManager;
private final SecureRandom random;
private final Clock clock;
private final Lock lock;
private final Map<TransportId, ScheduledPollTask> tasks; // Locking: lock
@Inject
Poller(@IoExecutor Executor ioExecutor,
@Scheduler ScheduledExecutorService scheduler,
ConnectionManager connectionManager,
ConnectionRegistry connectionRegistry, PluginManager pluginManager,
TransportPropertyManager transportPropertyManager,
SecureRandom random, Clock clock) {
this.ioExecutor = ioExecutor;
this.scheduler = scheduler;
this.connectionManager = connectionManager;
this.connectionRegistry = connectionRegistry;
this.pluginManager = pluginManager;
this.transportPropertyManager = transportPropertyManager;
this.random = random;
this.clock = clock;
lock = new ReentrantLock();
@@ -120,10 +126,15 @@ class Poller implements EventListener {
private void connectToContact(ContactId c, SimplexPlugin p) {
ioExecutor.execute(() -> {
TransportId t = p.getId();
if (!connectionRegistry.isConnected(c, t)) {
TransportConnectionWriter w = p.createWriter(c);
if (connectionRegistry.isConnected(c, t)) return;
try {
TransportProperties props =
transportPropertyManager.getRemoteProperties(c, t);
TransportConnectionWriter w = p.createWriter(props);
if (w != null)
connectionManager.manageOutgoingConnection(c, t, w);
} catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
});
}
@@ -131,10 +142,15 @@ class Poller implements EventListener {
private void connectToContact(ContactId c, DuplexPlugin p) {
ioExecutor.execute(() -> {
TransportId t = p.getId();
if (!connectionRegistry.isConnected(c, t)) {
DuplexTransportConnection d = p.createConnection(c);
if (connectionRegistry.isConnected(c, t)) return;
try {
TransportProperties props =
transportPropertyManager.getRemoteProperties(c, t);
DuplexTransportConnection d = p.createConnection(props);
if (d != null)
connectionManager.manageOutgoingConnection(c, t, d);
} catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
});
}
@@ -186,7 +202,17 @@ class Poller implements EventListener {
private void poll(Plugin p) {
TransportId t = p.getId();
if (LOG.isLoggable(INFO)) LOG.info("Polling plugin " + t);
p.poll(connectionRegistry.getConnectedContacts(t));
try {
Map<ContactId, TransportProperties> remote =
transportPropertyManager.getRemoteProperties(t);
Collection<ContactId> connected =
connectionRegistry.getConnectedContacts(t);
remote = new HashMap<>(remote);
remote.keySet().removeAll(connected);
if (!remote.isEmpty()) p.poll(remote);
} catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
private class ScheduledPollTask {

View File

@@ -26,7 +26,6 @@ import org.briarproject.bramble.util.StringUtils;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
@@ -250,19 +249,16 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
}
@Override
public void poll(Collection<ContactId> connected) {
public void poll(Map<ContactId, TransportProperties> contacts) {
if (!isRunning() || !shouldAllowContactConnections()) return;
backoff.increment();
// Try to connect to known devices in parallel
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
ContactId c = e.getKey();
if (connected.contains(c)) continue;
for (Entry<ContactId, TransportProperties> e : contacts.entrySet()) {
String address = e.getValue().get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) continue;
String uuid = e.getValue().get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) continue;
ContactId c = e.getKey();
ioExecutor.execute(() -> {
if (!isRunning() || !shouldAllowContactConnections()) return;
if (!connectionLimiter.canOpenContactConnection()) return;
@@ -308,10 +304,9 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
}
@Override
public DuplexTransportConnection createConnection(ContactId c) {
public DuplexTransportConnection createConnection(TransportProperties p) {
if (!isRunning() || !shouldAllowContactConnections()) return null;
if (!connectionLimiter.canOpenContactConnection()) return null;
TransportProperties p = callback.getRemoteProperties(c);
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID);

View File

@@ -1,27 +1,21 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginCallback;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Locale;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.transport.TransportConstants.MIN_STREAM_LENGTH;
import static org.briarproject.bramble.api.plugin.FileConstants.PROP_PATH;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@NotNullByDefault
abstract class FilePlugin implements SimplexPlugin {
@@ -29,25 +23,15 @@ abstract class FilePlugin implements SimplexPlugin {
private static final Logger LOG =
Logger.getLogger(FilePlugin.class.getName());
protected final Executor ioExecutor;
protected final SimplexPluginCallback callback;
protected final int maxLatency;
protected final AtomicBoolean used = new AtomicBoolean(false);
protected volatile boolean running = false;
protected abstract void writerFinished(File f, boolean exception);
@Nullable
protected abstract File chooseOutputDirectory();
protected abstract void readerFinished(File f, boolean exception,
boolean recognised);
protected abstract Collection<File> findFilesByName(String filename);
protected abstract void writerFinished(File f);
protected abstract void readerFinished(File f);
protected FilePlugin(Executor ioExecutor, SimplexPluginCallback callback,
int maxLatency) {
this.ioExecutor = ioExecutor;
FilePlugin(SimplexPluginCallback callback, int maxLatency) {
this.callback = callback;
this.maxLatency = maxLatency;
}
@@ -58,81 +42,36 @@ abstract class FilePlugin implements SimplexPlugin {
}
@Override
public int getMaxIdleTime() {
return Integer.MAX_VALUE; // We don't need keepalives
}
@Override
public boolean isRunning() {
return running;
}
@Override
public TransportConnectionReader createReader(ContactId c) {
return null;
}
@Override
public TransportConnectionWriter createWriter(ContactId c) {
if (!running) return null;
return createWriter(createConnectionFilename());
}
private String createConnectionFilename() {
StringBuilder s = new StringBuilder(12);
for (int i = 0; i < 8; i++) s.append((char) ('a' + Math.random() * 26));
s.append(".dat");
return s.toString();
}
// Package access for testing
boolean isPossibleConnectionFilename(String filename) {
return filename.toLowerCase(Locale.US).matches("[a-z]{8}\\.dat");
}
@Nullable
private TransportConnectionWriter createWriter(String filename) {
if (!running) return null;
File dir = chooseOutputDirectory();
if (dir == null || !dir.exists() || !dir.isDirectory()) return null;
File f = new File(dir, filename);
public TransportConnectionReader createReader(TransportProperties p) {
if (!isRunning()) return null;
String path = p.get(PROP_PATH);
if (isNullOrEmpty(path)) return null;
try {
long capacity = dir.getFreeSpace();
if (capacity < MIN_STREAM_LENGTH) return null;
OutputStream out = new FileOutputStream(f);
return new FileTransportWriter(f, out, capacity, this);
File file = new File(path);
FileInputStream in = new FileInputStream(file);
return new FileTransportReader(file, in, this);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
f.delete();
return null;
}
}
protected void createReaderFromFile(File f) {
if (!running) return;
ioExecutor.execute(new ReaderCreator(f));
}
private class ReaderCreator implements Runnable {
private final File file;
private ReaderCreator(File file) {
this.file = file;
}
@Override
public void run() {
if (isPossibleConnectionFilename(file.getName())) {
try {
FileInputStream in = new FileInputStream(file);
callback.readerCreated(new FileTransportReader(file, in,
FilePlugin.this));
} catch (IOException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
@Override
public TransportConnectionWriter createWriter(TransportProperties p) {
if (!isRunning()) return null;
String path = p.get(PROP_PATH);
if (isNullOrEmpty(path)) return null;
try {
File file = new File(path);
if (!file.exists() && !file.createNewFile()) {
LOG.info("Failed to create file");
return null;
}
FileOutputStream out = new FileOutputStream(file);
return new FileTransportWriter(file, out, this);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
}
}
}

View File

@@ -38,9 +38,6 @@ class FileTransportReader implements TransportConnectionReader {
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
if (recognised) {
file.delete();
plugin.readerFinished(file);
}
plugin.readerFinished(file, exception, recognised);
}
}

View File

@@ -18,14 +18,11 @@ class FileTransportWriter implements TransportConnectionWriter {
private final File file;
private final OutputStream out;
private final long capacity;
private final FilePlugin plugin;
FileTransportWriter(File file, OutputStream out, long capacity,
FilePlugin plugin) {
FileTransportWriter(File file, OutputStream out, FilePlugin plugin) {
this.file = file;
this.out = out;
this.capacity = capacity;
this.plugin = plugin;
}
@@ -39,11 +36,6 @@ class FileTransportWriter implements TransportConnectionWriter {
return plugin.getMaxIdleTime();
}
@Override
public long getCapacity() {
return capacity;
}
@Override
public OutputStream getOutputStream() {
return out;
@@ -56,7 +48,6 @@ class FileTransportWriter implements TransportConnectionWriter {
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
if (exception) file.delete();
else plugin.writerFinished(file);
plugin.writerFinished(file, exception);
}
}

View File

@@ -207,20 +207,16 @@ abstract class TcpPlugin implements DuplexPlugin {
}
@Override
public void poll(Collection<ContactId> connected) {
public void poll(Map<ContactId, TransportProperties> contacts) {
if (!isRunning()) return;
backoff.increment();
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
ContactId c = e.getKey();
if (!connected.contains(c)) connectAndCallBack(c, e.getValue());
for (Entry<ContactId, TransportProperties> e : contacts.entrySet()) {
connectAndCallBack(e.getKey(), e.getValue());
}
}
private void connectAndCallBack(ContactId c, TransportProperties p) {
ioExecutor.execute(() -> {
if (!isRunning()) return;
DuplexTransportConnection d = createConnection(p);
if (d != null) {
backoff.reset();
@@ -230,13 +226,8 @@ abstract class TcpPlugin implements DuplexPlugin {
}
@Override
public DuplexTransportConnection createConnection(ContactId c) {
public DuplexTransportConnection createConnection(TransportProperties p) {
if (!isRunning()) return null;
return createConnection(callback.getRemoteProperties(c));
}
@Nullable
private DuplexTransportConnection createConnection(TransportProperties p) {
for (InetSocketAddress remote : getRemoteSocketAddresses(p)) {
if (!isConnectable(remote)) {
if (LOG.isLoggable(INFO)) {

View File

@@ -2,6 +2,7 @@ package org.briarproject.bramble.plugin;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId;
@@ -13,16 +14,18 @@ import org.briarproject.bramble.api.plugin.simplex.SimplexPluginCallback;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.ui.UiCallback;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.test.BrambleTestCase;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.lib.concurrent.Synchroniser;
import org.junit.Test;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
@@ -34,15 +37,20 @@ public class PluginManagerImplTest extends BrambleTestCase {
setThreadingPolicy(new Synchroniser());
}};
Executor ioExecutor = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduler =
context.mock(ScheduledExecutorService.class);
SecureRandom random = new SecureRandom();
Clock clock = context.mock(Clock.class);
EventBus eventBus = context.mock(EventBus.class);
PluginConfig pluginConfig = context.mock(PluginConfig.class);
ConnectionManager connectionManager =
context.mock(ConnectionManager.class);
ConnectionRegistry connectionRegistry =
context.mock(ConnectionRegistry.class);
SettingsManager settingsManager =
context.mock(SettingsManager.class);
TransportPropertyManager transportPropertyManager =
context.mock(TransportPropertyManager.class);
UiCallback uiCallback = context.mock(UiCallback.class);
// Two simplex plugin factories: both create plugins, one fails to start
SimplexPluginFactory simplexFactory =
@@ -71,6 +79,8 @@ public class PluginManagerImplTest extends BrambleTestCase {
will(returnValue(simplexFailId));
allowing(duplexPlugin).getId();
will(returnValue(duplexId));
allowing(pluginConfig).shouldPoll();
will(returnValue(false));
// start()
// First simplex plugin
oneOf(pluginConfig).getSimplexFactories();
@@ -112,9 +122,9 @@ public class PluginManagerImplTest extends BrambleTestCase {
oneOf(duplexPlugin).stop();
}});
PluginManagerImpl p = new PluginManagerImpl(ioExecutor, eventBus,
pluginConfig, connectionManager, settingsManager,
transportPropertyManager, uiCallback);
PluginManagerImpl p = new PluginManagerImpl(ioExecutor, scheduler,
eventBus, pluginConfig, connectionManager, connectionRegistry,
settingsManager, transportPropertyManager, random, clock);
// Two plugins should be started and stopped
p.startService();

View File

@@ -15,6 +15,8 @@ import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.ImmediateExecutor;
@@ -24,13 +26,15 @@ import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.Test;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
@@ -44,6 +48,8 @@ public class PollerTest extends BrambleMockTestCase {
context.mock(ConnectionRegistry.class);
private final PluginManager pluginManager =
context.mock(PluginManager.class);
private final TransportPropertyManager transportPropertyManager =
context.mock(TransportPropertyManager.class);
private final Clock clock = context.mock(Clock.class);
private final ScheduledFuture future = context.mock(ScheduledFuture.class);
private final SecureRandom random;
@@ -51,6 +57,7 @@ public class PollerTest extends BrambleMockTestCase {
private final Executor ioExecutor = new ImmediateExecutor();
private final TransportId transportId = getTransportId();
private final ContactId contactId = new ContactId(234);
private final TransportProperties properties = new TransportProperties();
private final int pollingInterval = 60 * 1000;
private final long now = System.currentTimeMillis();
@@ -66,8 +73,8 @@ public class PollerTest extends BrambleMockTestCase {
SimplexPlugin simplexPlugin1 =
context.mock(SimplexPlugin.class, "simplexPlugin1");
TransportId simplexId1 = getTransportId();
List<SimplexPlugin> simplexPlugins = Arrays.asList(simplexPlugin,
simplexPlugin1);
List<SimplexPlugin> simplexPlugins =
asList(simplexPlugin, simplexPlugin1);
TransportConnectionWriter simplexWriter =
context.mock(TransportConnectionWriter.class);
@@ -76,8 +83,8 @@ public class PollerTest extends BrambleMockTestCase {
TransportId duplexId = getTransportId();
DuplexPlugin duplexPlugin1 =
context.mock(DuplexPlugin.class, "duplexPlugin1");
List<DuplexPlugin> duplexPlugins = Arrays.asList(duplexPlugin,
duplexPlugin1);
List<DuplexPlugin> duplexPlugins =
asList(duplexPlugin, duplexPlugin1);
DuplexTransportConnection duplexConnection =
context.mock(DuplexTransportConnection.class);
@@ -96,8 +103,12 @@ public class PollerTest extends BrambleMockTestCase {
will(returnValue(simplexId1));
oneOf(connectionRegistry).isConnected(contactId, simplexId1);
will(returnValue(false));
// Get the transport properties
oneOf(transportPropertyManager).getRemoteProperties(contactId,
simplexId1);
will(returnValue(properties));
// Connect to the contact
oneOf(simplexPlugin1).createWriter(contactId);
oneOf(simplexPlugin1).createWriter(properties);
will(returnValue(simplexWriter));
// Pass the connection to the connection manager
oneOf(connectionManager).manageOutgoingConnection(contactId,
@@ -105,7 +116,7 @@ public class PollerTest extends BrambleMockTestCase {
// Get the duplex plugins
oneOf(pluginManager).getDuplexPlugins();
will(returnValue(duplexPlugins));
// The first plugin supports polling
// The duplex plugin supports polling
oneOf(duplexPlugin).shouldPoll();
will(returnValue(true));
// Check whether the contact is already connected
@@ -113,8 +124,12 @@ public class PollerTest extends BrambleMockTestCase {
will(returnValue(duplexId));
oneOf(connectionRegistry).isConnected(contactId, duplexId);
will(returnValue(false));
// Get the transport properties
oneOf(transportPropertyManager).getRemoteProperties(contactId,
duplexId);
will(returnValue(properties));
// Connect to the contact
oneOf(duplexPlugin).createConnection(contactId);
oneOf(duplexPlugin).createConnection(properties);
will(returnValue(duplexConnection));
// Pass the connection to the connection manager
oneOf(connectionManager).manageOutgoingConnection(contactId,
@@ -125,7 +140,8 @@ public class PollerTest extends BrambleMockTestCase {
}});
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, random, clock);
connectionRegistry, pluginManager, transportPropertyManager,
random, clock);
p.eventOccurred(new ContactStatusChangedEvent(contactId, true));
}
@@ -165,8 +181,12 @@ public class PollerTest extends BrambleMockTestCase {
// Check whether the contact is already connected
oneOf(connectionRegistry).isConnected(contactId, transportId);
will(returnValue(false));
// Get the transport properties
oneOf(transportPropertyManager).getRemoteProperties(contactId,
transportId);
will(returnValue(properties));
// Connect to the contact
oneOf(plugin).createConnection(contactId);
oneOf(plugin).createConnection(properties);
will(returnValue(duplexConnection));
// Pass the connection to the connection manager
oneOf(connectionManager).manageOutgoingConnection(contactId,
@@ -174,15 +194,15 @@ public class PollerTest extends BrambleMockTestCase {
}});
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, random, clock);
connectionRegistry, pluginManager, transportPropertyManager,
random, clock);
p.eventOccurred(new ConnectionClosedEvent(contactId, transportId,
false));
}
@Test
public void testRescheduleOnConnectionOpened() throws Exception {
public void testRescheduleOnConnectionOpened() {
Plugin plugin = context.mock(Plugin.class);
context.checking(new Expectations() {{
@@ -205,14 +225,15 @@ public class PollerTest extends BrambleMockTestCase {
}});
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, random, clock);
connectionRegistry, pluginManager, transportPropertyManager,
random, clock);
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
false));
}
@Test
public void testRescheduleDoesNotReplaceEarlierTask() throws Exception {
public void testRescheduleDoesNotReplaceEarlierTask() {
Plugin plugin = context.mock(Plugin.class);
context.checking(new Expectations() {{
@@ -248,7 +269,8 @@ public class PollerTest extends BrambleMockTestCase {
}});
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, random, clock);
connectionRegistry, pluginManager, transportPropertyManager,
random, clock);
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
false));
@@ -257,7 +279,7 @@ public class PollerTest extends BrambleMockTestCase {
}
@Test
public void testRescheduleReplacesLaterTask() throws Exception {
public void testRescheduleReplacesLaterTask() {
Plugin plugin = context.mock(Plugin.class);
context.checking(new Expectations() {{
@@ -296,7 +318,8 @@ public class PollerTest extends BrambleMockTestCase {
}});
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, random, clock);
connectionRegistry, pluginManager, transportPropertyManager,
random, clock);
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
false));
@@ -306,8 +329,7 @@ public class PollerTest extends BrambleMockTestCase {
@Test
public void testPollsOnTransportEnabled() throws Exception {
Plugin plugin = context.mock(Plugin.class);
List<ContactId> connected = Collections.singletonList(contactId);
DuplexPlugin plugin = context.mock(DuplexPlugin.class);
context.checking(new Expectations() {{
allowing(plugin).getId();
@@ -335,20 +357,69 @@ public class PollerTest extends BrambleMockTestCase {
oneOf(scheduler).schedule(with(any(Runnable.class)),
with((long) (pollingInterval * 0.5)), with(MILLISECONDS));
will(returnValue(future));
// Poll the plugin
// Get the transport properties and connected contacts
oneOf(transportPropertyManager).getRemoteProperties(transportId);
will(returnValue(singletonMap(contactId, properties)));
oneOf(connectionRegistry).getConnectedContacts(transportId);
will(returnValue(connected));
oneOf(plugin).poll(connected);
will(returnValue(emptyList()));
// Poll the plugin
oneOf(plugin).poll(singletonMap(contactId, properties));
}});
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, random, clock);
connectionRegistry, pluginManager, transportPropertyManager,
random, clock);
p.eventOccurred(new TransportEnabledEvent(transportId));
}
@Test
public void testCancelsPollingOnTransportDisabled() throws Exception {
public void testDoesNotPollIfAllContactsAreConnected() throws Exception {
DuplexPlugin plugin = context.mock(DuplexPlugin.class);
context.checking(new Expectations() {{
allowing(plugin).getId();
will(returnValue(transportId));
// Get the plugin
oneOf(pluginManager).getPlugin(transportId);
will(returnValue(plugin));
// The plugin supports polling
oneOf(plugin).shouldPoll();
will(returnValue(true));
// Schedule a polling task immediately
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(scheduler).schedule(with(any(Runnable.class)), with(0L),
with(MILLISECONDS));
will(returnValue(future));
will(new RunAction());
// Running the polling task schedules the next polling task
oneOf(plugin).getPollingInterval();
will(returnValue(pollingInterval));
oneOf(random).nextDouble();
will(returnValue(0.5));
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(scheduler).schedule(with(any(Runnable.class)),
with((long) (pollingInterval * 0.5)), with(MILLISECONDS));
will(returnValue(future));
// Get the transport properties and connected contacts
oneOf(transportPropertyManager).getRemoteProperties(transportId);
will(returnValue(singletonMap(contactId, properties)));
oneOf(connectionRegistry).getConnectedContacts(transportId);
will(returnValue(singletonList(contactId)));
// All contacts are connected, so don't poll the plugin
}});
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, transportPropertyManager,
random, clock);
p.eventOccurred(new TransportEnabledEvent(transportId));
}
@Test
public void testCancelsPollingOnTransportDisabled() {
Plugin plugin = context.mock(Plugin.class);
context.checking(new Expectations() {{
@@ -371,7 +442,8 @@ public class PollerTest extends BrambleMockTestCase {
}});
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, random, clock);
connectionRegistry, pluginManager, transportPropertyManager,
random, clock);
p.eventOccurred(new TransportEnabledEvent(transportId));
p.eventOccurred(new TransportDisabledEvent(transportId));

View File

@@ -23,8 +23,6 @@ import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -40,7 +38,6 @@ import static org.junit.Assert.assertTrue;
public class LanTcpPluginTest extends BrambleTestCase {
private final ContactId contactId = new ContactId(234);
private final Backoff backoff = new TestBackoff();
@Test
@@ -160,12 +157,10 @@ public class LanTcpPluginTest extends BrambleTestCase {
error.set(true);
}
}).start();
// Tell the plugin about the port
// Connect to the port
TransportProperties p = new TransportProperties();
p.put("ipPorts", addrString + ":" + port);
callback.remote.put(contactId, p);
// Connect to the port
DuplexTransportConnection d = plugin.createConnection(contactId);
DuplexTransportConnection d = plugin.createConnection(p);
assertNotNull(d);
// Check that the connection was accepted
assertTrue(latch.await(5, SECONDS));
@@ -281,7 +276,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
}
@Test
public void testComparatorPrefersNonZeroPorts() throws Exception {
public void testComparatorPrefersNonZeroPorts() {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress nonZero = new InetSocketAddress("1.2.3.4", 1234);
InetSocketAddress zero = new InetSocketAddress("1.2.3.4", 0);
@@ -294,7 +289,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
}
@Test
public void testComparatorPrefersLongerPrefixes() throws Exception {
public void testComparatorPrefersLongerPrefixes() {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress prefix192 = new InetSocketAddress("192.168.0.1", 0);
InetSocketAddress prefix172 = new InetSocketAddress("172.16.0.1", 0);
@@ -314,7 +309,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
}
@Test
public void testComparatorPrefersSiteLocalToLinkLocal() throws Exception {
public void testComparatorPrefersSiteLocalToLinkLocal() {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress prefix192 = new InetSocketAddress("192.168.0.1", 0);
InetSocketAddress prefix172 = new InetSocketAddress("172.16.0.1", 0);
@@ -345,8 +340,6 @@ public class LanTcpPluginTest extends BrambleTestCase {
@NotNullByDefault
private static class Callback implements DuplexPluginCallback {
private final Map<ContactId, TransportProperties> remote =
new Hashtable<>();
private final CountDownLatch propertiesLatch = new CountDownLatch(1);
private final CountDownLatch connectionsLatch = new CountDownLatch(1);
private final TransportProperties local = new TransportProperties();
@@ -361,16 +354,6 @@ public class LanTcpPluginTest extends BrambleTestCase {
return local;
}
@Override
public Map<ContactId, TransportProperties> getRemoteProperties() {
return remote;
}
@Override
public TransportProperties getRemoteProperties(ContactId c) {
return remote.get(c);
}
@Override
public void mergeSettings(Settings s) {
}
@@ -381,20 +364,6 @@ public class LanTcpPluginTest extends BrambleTestCase {
propertiesLatch.countDown();
}
@Override
public int showChoice(String[] options, String... message) {
return -1;
}
@Override
public boolean showConfirmationMessage(String... message) {
return false;
}
@Override
public void showMessage(String... message) {
}
@Override
public void incomingConnectionCreated(DuplexTransportConnection d) {
connectionsLatch.countDown();

View File

@@ -20,7 +20,7 @@ public class CaptureArgumentAction<T> implements Action {
}
@Override
public Object invoke(Invocation invocation) throws Throwable {
public Object invoke(Invocation invocation) {
captured.set(capturedClass.cast(invocation.getParameter(index)));
return null;
}

View File

@@ -9,25 +9,31 @@ import java.io.File;
@NotNullByDefault
public class TestDatabaseConfig implements DatabaseConfig {
private final File dir;
private final File dbDir, keyDir;
private final long maxSize;
private volatile SecretKey key = new SecretKey(new byte[SecretKey.LENGTH]);
public TestDatabaseConfig(File dir, long maxSize) {
this.dir = dir;
public TestDatabaseConfig(File testDir, long maxSize) {
dbDir = new File(testDir, "db");
keyDir = new File(testDir, "key");
this.maxSize = maxSize;
}
@Override
public boolean databaseExists() {
if (!dir.isDirectory()) return false;
File[] files = dir.listFiles();
if (!dbDir.isDirectory()) return false;
File[] files = dbDir.listFiles();
return files != null && files.length > 0;
}
@Override
public File getDatabaseDirectory() {
return dir;
return dbDir;
}
@Override
public File getDatabaseKeyDirectory() {
return keyDir;
}
@Override

View File

@@ -9,13 +9,14 @@ import org.briarproject.bramble.api.plugin.simplex.SimplexPluginCallback;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import java.util.Collection;
import java.util.Collections;
import javax.annotation.Nullable;
import dagger.Module;
import dagger.Provides;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
@Module
@@ -51,12 +52,17 @@ public class TestPluginConfigModule {
@Override
public Collection<DuplexPluginFactory> getDuplexFactories() {
return Collections.emptyList();
return emptyList();
}
@Override
public Collection<SimplexPluginFactory> getSimplexFactories() {
return Collections.singletonList(simplex);
return singletonList(simplex);
}
@Override
public boolean shouldPoll() {
return false;
}
};
return pluginConfig;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -10,20 +10,20 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.reliability.ReliabilityLayerFactory;
import org.briarproject.bramble.plugin.bluetooth.JavaBluetoothPluginFactory;
import org.briarproject.bramble.plugin.file.RemovableDrivePluginFactory;
import org.briarproject.bramble.plugin.modem.ModemPluginFactory;
import org.briarproject.bramble.plugin.tcp.LanTcpPluginFactory;
import org.briarproject.bramble.plugin.tcp.WanTcpPluginFactory;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Executor;
import dagger.Module;
import dagger.Provides;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
@Module
public class DesktopPluginModule extends PluginModule {
@@ -41,12 +41,8 @@ public class DesktopPluginModule extends PluginModule {
backoffFactory);
DuplexPluginFactory wan = new WanTcpPluginFactory(ioExecutor,
backoffFactory, shutdownManager);
SimplexPluginFactory removable =
new RemovableDrivePluginFactory(ioExecutor);
Collection<SimplexPluginFactory> simplex =
Collections.singletonList(removable);
Collection<DuplexPluginFactory> duplex =
Arrays.asList(bluetooth, modem, lan, wan);
asList(bluetooth, modem, lan, wan);
@NotNullByDefault
PluginConfig pluginConfig = new PluginConfig() {
@@ -57,7 +53,12 @@ public class DesktopPluginModule extends PluginModule {
@Override
public Collection<SimplexPluginFactory> getSimplexFactories() {
return simplex;
return emptyList();
}
@Override
public boolean shouldPoll() {
return true;
}
};
return pluginConfig;

View File

@@ -1,28 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
@NotNullByDefault
class LinuxRemovableDriveFinder extends UnixRemovableDriveFinder {
@Override
protected String getMountCommand() {
return "/bin/mount";
}
@Override
@Nullable
protected String parseMountPoint(String line) {
// The format is "/dev/foo on /bar/baz type bam (opt1,opt2)"
String pattern = "^/dev/[^ ]+ on (.*) type [^ ]+ \\([^)]+\\)$";
String path = line.replaceFirst(pattern, "$1");
return path.equals(line) ? null : path;
}
@Override
protected boolean isRemovableDriveMountPoint(String path) {
return path.startsWith("/mnt/") || path.startsWith("/media/");
}
}

View File

@@ -1,12 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
class LinuxRemovableDriveMonitor extends UnixRemovableDriveMonitor {
@Override
protected String[] getPathsToWatch() {
return new String[] {"/mnt", "/media"};
}
}

View File

@@ -1,28 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
@NotNullByDefault
class MacRemovableDriveFinder extends UnixRemovableDriveFinder {
@Override
protected String getMountCommand() {
return "/sbin/mount";
}
@Override
@Nullable
protected String parseMountPoint(String line) {
// The format is "/dev/foo on /bar/baz (opt1, opt2)"
String pattern = "^/dev/[^ ]+ on (.*) \\([^)]+\\)$";
String path = line.replaceFirst(pattern, "$1");
return path.equals(line) ? null : path;
}
@Override
protected boolean isRemovableDriveMountPoint(String path) {
return path.startsWith("/Volumes/");
}
}

View File

@@ -1,12 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
class MacRemovableDriveMonitor extends UnixRemovableDriveMonitor {
@Override
protected String[] getPathsToWatch() {
return new String[] {"/Volumes"};
}
}

View File

@@ -1,84 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class PollingRemovableDriveMonitor implements RemovableDriveMonitor, Runnable {
private static final Logger LOG =
Logger.getLogger(PollingRemovableDriveMonitor.class.getName());
private final Executor ioExecutor;
private final RemovableDriveFinder finder;
private final int pollingInterval;
private final Lock pollingLock = new ReentrantLock();
private final Condition stopPolling = pollingLock.newCondition();
private volatile boolean running = false;
private volatile Callback callback = null;
PollingRemovableDriveMonitor(Executor ioExecutor,
RemovableDriveFinder finder, int pollingInterval) {
this.ioExecutor = ioExecutor;
this.finder = finder;
this.pollingInterval = pollingInterval;
}
@Override
public void start(Callback callback) throws IOException {
this.callback = callback;
running = true;
ioExecutor.execute(this);
}
@Override
public void stop() throws IOException {
running = false;
pollingLock.lock();
try {
stopPolling.signalAll();
} finally {
pollingLock.unlock();
}
}
@Override
public void run() {
try {
Collection<File> drives = finder.findRemovableDrives();
while (running) {
pollingLock.lock();
try {
stopPolling.await(pollingInterval, MILLISECONDS);
} finally {
pollingLock.unlock();
}
if (!running) return;
Collection<File> newDrives = finder.findRemovableDrives();
for (File f : newDrives) {
if (!drives.contains(f)) callback.driveInserted(f);
}
drives = newDrives;
}
} catch (InterruptedException e) {
LOG.warning("Interrupted while waiting to poll");
Thread.currentThread().interrupt();
} catch (IOException e) {
callback.exceptionThrown(e);
}
}
}

View File

@@ -1,13 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
@NotNullByDefault
interface RemovableDriveFinder {
Collection<File> findRemovableDrives() throws IOException;
}

View File

@@ -1,21 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.File;
import java.io.IOException;
@NotNullByDefault
interface RemovableDriveMonitor {
void start(Callback c) throws IOException;
void stop() throws IOException;
interface Callback {
void driveInserted(File root);
void exceptionThrown(IOException e);
}
}

View File

@@ -1,140 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginCallback;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
@NotNullByDefault
class RemovableDrivePlugin extends FilePlugin
implements RemovableDriveMonitor.Callback {
static final TransportId ID =
new TransportId("org.briarproject.bramble.file");
private static final Logger LOG =
Logger.getLogger(RemovableDrivePlugin.class.getName());
private final RemovableDriveFinder finder;
private final RemovableDriveMonitor monitor;
RemovableDrivePlugin(Executor ioExecutor, SimplexPluginCallback callback,
RemovableDriveFinder finder, RemovableDriveMonitor monitor,
int maxLatency) {
super(ioExecutor, callback, maxLatency);
this.finder = finder;
this.monitor = monitor;
}
@Override
public TransportId getId() {
return ID;
}
@Override
public void start() throws PluginException {
if (used.getAndSet(true)) throw new IllegalStateException();
running = true;
try {
monitor.start(this);
} catch (IOException e) {
throw new PluginException(e);
}
}
@Override
public void stop() throws PluginException {
running = false;
try {
monitor.stop();
} catch (IOException e) {
throw new PluginException(e);
}
}
@Override
public boolean shouldPoll() {
return false;
}
@Override
public int getPollingInterval() {
throw new UnsupportedOperationException();
}
@Override
public void poll(Collection<ContactId> connected) {
throw new UnsupportedOperationException();
}
@Override
protected File chooseOutputDirectory() {
try {
List<File> drives = new ArrayList<>(finder.findRemovableDrives());
if (drives.isEmpty()) return null;
String[] paths = new String[drives.size()];
for (int i = 0; i < paths.length; i++) {
paths[i] = drives.get(i).getPath();
}
int i = callback.showChoice(paths, "REMOVABLE_DRIVE_CHOOSE_DRIVE");
if (i == -1) return null;
return drives.get(i);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
}
}
@Override
protected void readerFinished(File f) {
callback.showMessage("REMOVABLE_DRIVE_READ_FINISHED");
}
@Override
protected void writerFinished(File f) {
callback.showMessage("REMOVABLE_DRIVE_WRITE_FINISHED");
}
@Override
protected Collection<File> findFilesByName(String filename) {
List<File> matches = new ArrayList<>();
try {
for (File drive : finder.findRemovableDrives()) {
File[] files = drive.listFiles();
if (files != null) {
for (File f : files) {
if (f.isFile() && filename.equals(f.getName()))
matches.add(f);
}
}
}
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
return matches;
}
@Override
public void driveInserted(File root) {
File[] files = root.listFiles();
if (files != null) {
for (File f : files) if (f.isFile()) createReaderFromFile(f);
}
}
@Override
public void exceptionThrown(IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}

View File

@@ -1,63 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginCallback;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.util.OsUtils;
import java.util.concurrent.Executor;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class RemovableDrivePluginFactory implements SimplexPluginFactory {
// Maximum latency 14 days (Royal Mail or lackadaisical carrier pigeon)
private static final int MAX_LATENCY = 14 * 24 * 60 * 60 * 1000;
private static final int POLLING_INTERVAL = 10 * 1000; // 10 seconds
private final Executor ioExecutor;
public RemovableDrivePluginFactory(Executor ioExecutor) {
this.ioExecutor = ioExecutor;
}
@Override
public TransportId getId() {
return RemovableDrivePlugin.ID;
}
@Override
public int getMaxLatency() {
return MAX_LATENCY;
}
@Override
public SimplexPlugin createPlugin(SimplexPluginCallback callback) {
RemovableDriveFinder finder;
RemovableDriveMonitor monitor;
if (OsUtils.isLinux()) {
finder = new LinuxRemovableDriveFinder();
monitor = new LinuxRemovableDriveMonitor();
} else if (OsUtils.isMacLeopardOrNewer()) {
finder = new MacRemovableDriveFinder();
monitor = new MacRemovableDriveMonitor();
} else if (OsUtils.isMac()) {
// JNotify requires OS X 10.5 or newer, so we have to poll
finder = new MacRemovableDriveFinder();
monitor = new PollingRemovableDriveMonitor(ioExecutor, finder,
POLLING_INTERVAL);
} else if (OsUtils.isWindows()) {
finder = new WindowsRemovableDriveFinder();
monitor = new PollingRemovableDriveMonitor(ioExecutor, finder,
POLLING_INTERVAL);
} else {
return null;
}
return new RemovableDrivePlugin(ioExecutor, callback, finder, monitor,
MAX_LATENCY);
}
}

View File

@@ -1,48 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import javax.annotation.Nullable;
@NotNullByDefault
abstract class UnixRemovableDriveFinder implements RemovableDriveFinder {
protected abstract String getMountCommand();
@Nullable
protected abstract String parseMountPoint(String line);
protected abstract boolean isRemovableDriveMountPoint(String path);
@Override
public List<File> findRemovableDrives() throws IOException {
List<File> drives = new ArrayList<>();
Process p = new ProcessBuilder(getMountCommand()).start();
Scanner s = new Scanner(p.getInputStream(), "UTF-8");
try {
while (s.hasNextLine()) {
String line = s.nextLine();
String[] tokens = line.split(" ");
if (tokens.length < 3) continue;
// The general format is "/dev/foo on /bar/baz ..."
if (tokens[0].startsWith("/dev/") && tokens[1].equals("on")) {
// The path may contain spaces so we can't use tokens[2]
String path = parseMountPoint(line);
if (path != null && isRemovableDriveMountPoint(path)) {
File f = new File(path);
if (f.exists() && f.isDirectory()) drives.add(f);
}
}
}
} finally {
s.close();
}
return drives;
}
}

View File

@@ -1,129 +0,0 @@
package org.briarproject.bramble.plugin.file;
import net.contentobjects.jnotify.JNotify;
import net.contentobjects.jnotify.JNotifyListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Nullable;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class UnixRemovableDriveMonitor implements RemovableDriveMonitor,
JNotifyListener {
//TODO: rationalise this in a further refactor
private static final Lock staticLock = new ReentrantLock();
// The following are locking: staticLock
private static boolean triedLoad = false;
private static Throwable loadError = null;
private final Lock lock = new ReentrantLock();
// The following are locking: lock
private final List<Integer> watches = new ArrayList<>();
private boolean started = false;
private Callback callback = null;
protected abstract String[] getPathsToWatch();
@Nullable
private static Throwable tryLoad() {
try {
Class.forName("net.contentobjects.jnotify.JNotify");
return null;
} catch (UnsatisfiedLinkError | ClassNotFoundException e) {
return e;
}
}
private static void checkEnabled() throws IOException {
staticLock.lock();
try {
if (!triedLoad) {
loadError = tryLoad();
triedLoad = true;
}
if (loadError != null) throw new IOException(loadError.toString());
} finally {
staticLock.unlock();
}
}
@Override
public void start(Callback callback) throws IOException {
checkEnabled();
List<Integer> watches = new ArrayList<>();
int mask = JNotify.FILE_CREATED;
for (String path : getPathsToWatch()) {
if (new File(path).exists())
watches.add(JNotify.addWatch(path, mask, false, this));
}
lock.lock();
try {
if (started) throw new AssertionError();
if (this.callback != null) throw new AssertionError();
started = true;
this.callback = callback;
this.watches.addAll(watches);
} finally {
lock.unlock();
}
}
@Override
public void stop() throws IOException {
checkEnabled();
List<Integer> watches;
lock.lock();
try {
if (!started) throw new AssertionError();
if (callback == null) throw new AssertionError();
started = false;
callback = null;
watches = new ArrayList<>(this.watches);
this.watches.clear();
} finally {
lock.unlock();
}
for (Integer w : watches) JNotify.removeWatch(w);
}
@Override
public void fileCreated(int wd, String rootPath, String name) {
Callback callback;
lock.lock();
try {
callback = this.callback;
} finally {
lock.unlock();
}
if (callback != null)
callback.driveInserted(new File(rootPath + "/" + name));
}
@Override
public void fileDeleted(int wd, String rootPath, String name) {
throw new UnsupportedOperationException();
}
@Override
public void fileModified(int wd, String rootPath, String name) {
throw new UnsupportedOperationException();
}
@Override
public void fileRenamed(int wd, String rootPath, String oldName,
String newName) {
throw new UnsupportedOperationException();
}
}

View File

@@ -1,34 +0,0 @@
package org.briarproject.bramble.plugin.file;
import com.sun.jna.platform.win32.Kernel32;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@NotNullByDefault
class WindowsRemovableDriveFinder implements RemovableDriveFinder {
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa364939.aspx
private static final int DRIVE_REMOVABLE = 2;
@Override
public Collection<File> findRemovableDrives() throws IOException {
File[] roots = File.listRoots();
if (roots == null) throw new IOException();
List<File> drives = new ArrayList<>();
for (File root : roots) {
try {
int type = Kernel32.INSTANCE.GetDriveType(root.getPath());
if (type == DRIVE_REMOVABLE) drives.add(root);
} catch (RuntimeException e) {
throw new IOException(e);
}
}
return drives;
}
}

View File

@@ -17,7 +17,7 @@ import org.briarproject.bramble.util.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
@@ -115,7 +115,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
}
@Override
public void poll(Collection<ContactId> connected) {
public void poll(Map<ContactId, TransportProperties> contacts) {
throw new UnsupportedOperationException();
}
@@ -139,17 +139,16 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
}
@Override
public DuplexTransportConnection createConnection(ContactId c) {
public DuplexTransportConnection createConnection(TransportProperties p) {
if (!running) return null;
// Get the ISO 3166 code for the caller's country
String fromIso = callback.getLocalProperties().get("iso3166");
if (StringUtils.isNullOrEmpty(fromIso)) return null;
// Get the ISO 3166 code for the callee's country
TransportProperties properties = callback.getRemoteProperties(c);
String toIso = properties.get("iso3166");
String toIso = p.get("iso3166");
if (StringUtils.isNullOrEmpty(toIso)) return null;
// Get the callee's phone number
String number = properties.get("number");
String number = p.get("number");
if (StringUtils.isNullOrEmpty(number)) return null;
// Convert the number into direct dialling form
number = CountryCodes.translate(number, fromIso, toIso);

View File

@@ -1,26 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class LinuxRemovableDriveFinderTest extends BrambleTestCase {
@Test
public void testParseMountPoint() {
LinuxRemovableDriveFinder f = new LinuxRemovableDriveFinder();
String line = "/dev/sda3 on / type ext3"
+ " (rw,errors=remount-ro,commit=0)";
assertEquals("/", f.parseMountPoint(line));
line = "gvfs-fuse-daemon on /home/alice/.gvfs"
+ " type fuse.gvfs-fuse-daemon (rw,nosuid,nodev,user=alice)";
assertEquals(null, f.parseMountPoint(line)); // Can't be parsed
line = "fusectl on /sys/fs/fuse/connections type fusectl (rw)";
assertEquals(null, f.parseMountPoint(line)); // Can't be parsed
line = "/dev/sdd1 on /media/HAZ SPACE(!) type vfat"
+ " (rw,nosuid,nodev,uhelper=udisks,uid=1000,gid=1000,"
+ "shortname=mixed,dmask=0077,utf8=1,showexec,flush)";
assertEquals("/media/HAZ SPACE(!)", f.parseMountPoint(line));
}
}

View File

@@ -1,24 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MacRemovableDriveFinderTest extends BrambleTestCase {
@Test
public void testParseMountPoint() {
MacRemovableDriveFinder f = new MacRemovableDriveFinder();
String line = "/dev/disk0s3 on / (local, journaled)";
assertEquals("/", f.parseMountPoint(line));
line = "devfs on /dev (local)";
assertEquals(null, f.parseMountPoint(line)); // Can't be parsed
line = "<volfs> on /.vol";
assertEquals(null, f.parseMountPoint(line)); // Can't be parsed
line = "automount -nsl [117] on /Network (automounted)";
assertEquals(null, f.parseMountPoint(line)); // Can't be parsed
line = "/dev/disk1s1 on /Volumes/HAZ SPACE(!) (local, nodev, nosuid)";
assertEquals("/Volumes/HAZ SPACE(!)", f.parseMountPoint(line));
}
}

View File

@@ -1,107 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.plugin.file.RemovableDriveMonitor.Callback;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class PollingRemovableDriveMonitorTest extends BrambleTestCase {
@Test
public void testOneCallbackPerFile() throws Exception {
// Create a finder that returns no files the first time, then two files
File file1 = new File("foo");
File file2 = new File("bar");
@NotNullByDefault
RemovableDriveFinder finder = new RemovableDriveFinder() {
private AtomicBoolean firstCall = new AtomicBoolean(true);
@Override
public Collection<File> findRemovableDrives() throws IOException {
if (firstCall.getAndSet(false)) return Collections.emptyList();
else return Arrays.asList(file1, file2);
}
};
// Create a callback that waits for two files
CountDownLatch latch = new CountDownLatch(2);
List<File> detected = new ArrayList<>();
@NotNullByDefault
Callback callback = new Callback() {
@Override
public void driveInserted(File f) {
detected.add(f);
latch.countDown();
}
@Override
public void exceptionThrown(IOException e) {
fail();
}
};
// Create the monitor and start it
RemovableDriveMonitor monitor = new PollingRemovableDriveMonitor(
Executors.newCachedThreadPool(), finder, 1);
monitor.start(callback);
// Wait for the monitor to detect the files
assertTrue(latch.await(10, SECONDS));
monitor.stop();
// Check that both files were detected
assertEquals(2, detected.size());
assertTrue(detected.contains(file1));
assertTrue(detected.contains(file2));
}
@Test
public void testExceptionCallback() throws Exception {
// Create a finder that throws an exception the second time it's polled
RemovableDriveFinder finder = new RemovableDriveFinder() {
private AtomicBoolean firstCall = new AtomicBoolean(true);
@Override
public Collection<File> findRemovableDrives() throws IOException {
if (firstCall.getAndSet(false)) return Collections.emptyList();
else throw new IOException();
}
};
// Create a callback that waits for an exception
CountDownLatch latch = new CountDownLatch(1);
@NotNullByDefault
Callback callback = new Callback() {
@Override
public void driveInserted(File root) {
fail();
}
@Override
public void exceptionThrown(IOException e) {
latch.countDown();
}
};
// Create the monitor and start it
RemovableDriveMonitor monitor = new PollingRemovableDriveMonitor(
Executors.newCachedThreadPool(), finder, 1);
monitor.start(callback);
assertTrue(latch.await(10, SECONDS));
monitor.stop();
}
}

View File

@@ -1,378 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginCallback;
import org.briarproject.bramble.plugin.file.RemovableDriveMonitor.Callback;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.ImmediateExecutor;
import org.briarproject.bramble.test.TestUtils;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.lib.concurrent.Synchroniser;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import static org.briarproject.bramble.api.transport.TransportConstants.MIN_STREAM_LENGTH;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class RemovableDrivePluginTest extends BrambleTestCase {
private final File testDir = TestUtils.getTestDirectory();
private final ContactId contactId = new ContactId(234);
@Before
public void setUp() {
testDir.mkdirs();
}
@Test
public void testWriterIsNullIfNoDrivesAreFound() throws Exception {
List<File> drives = Collections.emptyList();
Mockery context = new Mockery() {{
setThreadingPolicy(new Synchroniser());
}};
Executor executor = context.mock(Executor.class);
SimplexPluginCallback callback =
context.mock(SimplexPluginCallback.class);
RemovableDriveFinder finder =
context.mock(RemovableDriveFinder.class);
RemovableDriveMonitor monitor =
context.mock(RemovableDriveMonitor.class);
context.checking(new Expectations() {{
oneOf(monitor).start(with(any(Callback.class)));
oneOf(finder).findRemovableDrives();
will(returnValue(drives));
}});
RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
callback, finder, monitor, 0);
plugin.start();
assertNull(plugin.createWriter(contactId));
context.assertIsSatisfied();
}
@Test
public void testWriterIsNullIfNoDriveIsChosen() throws Exception {
File drive1 = new File(testDir, "1");
File drive2 = new File(testDir, "2");
List<File> drives = new ArrayList<>();
drives.add(drive1);
drives.add(drive2);
Mockery context = new Mockery() {{
setThreadingPolicy(new Synchroniser());
}};
Executor executor = context.mock(Executor.class);
SimplexPluginCallback callback =
context.mock(SimplexPluginCallback.class);
RemovableDriveFinder finder =
context.mock(RemovableDriveFinder.class);
RemovableDriveMonitor monitor =
context.mock(RemovableDriveMonitor.class);
context.checking(new Expectations() {{
oneOf(monitor).start(with(any(Callback.class)));
oneOf(finder).findRemovableDrives();
will(returnValue(drives));
oneOf(callback).showChoice(with(any(String[].class)),
with(any(String[].class)));
will(returnValue(-1)); // The user cancelled the choice
}});
RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
callback, finder, monitor, 0);
plugin.start();
assertNull(plugin.createWriter(contactId));
File[] files = drive1.listFiles();
assertTrue(files == null || files.length == 0);
context.assertIsSatisfied();
}
@Test
public void testWriterIsNullIfOutputDirDoesNotExist() throws Exception {
File drive1 = new File(testDir, "1");
File drive2 = new File(testDir, "2");
List<File> drives = new ArrayList<>();
drives.add(drive1);
drives.add(drive2);
Mockery context = new Mockery() {{
setThreadingPolicy(new Synchroniser());
}};
Executor executor = context.mock(Executor.class);
SimplexPluginCallback callback =
context.mock(SimplexPluginCallback.class);
RemovableDriveFinder finder =
context.mock(RemovableDriveFinder.class);
RemovableDriveMonitor monitor =
context.mock(RemovableDriveMonitor.class);
context.checking(new Expectations() {{
oneOf(monitor).start(with(any(Callback.class)));
oneOf(finder).findRemovableDrives();
will(returnValue(drives));
oneOf(callback).showChoice(with(any(String[].class)),
with(any(String[].class)));
will(returnValue(0)); // The user chose drive1 but it doesn't exist
}});
RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
callback, finder, monitor, 0);
plugin.start();
assertNull(plugin.createWriter(contactId));
File[] files = drive1.listFiles();
assertTrue(files == null || files.length == 0);
context.assertIsSatisfied();
}
@Test
public void testWriterIsNullIfOutputDirIsAFile() throws Exception {
File drive1 = new File(testDir, "1");
File drive2 = new File(testDir, "2");
List<File> drives = new ArrayList<>();
drives.add(drive1);
drives.add(drive2);
// Create drive1 as a file rather than a directory
assertTrue(drive1.createNewFile());
Mockery context = new Mockery() {{
setThreadingPolicy(new Synchroniser());
}};
Executor executor = context.mock(Executor.class);
SimplexPluginCallback callback =
context.mock(SimplexPluginCallback.class);
RemovableDriveFinder finder =
context.mock(RemovableDriveFinder.class);
RemovableDriveMonitor monitor =
context.mock(RemovableDriveMonitor.class);
context.checking(new Expectations() {{
oneOf(monitor).start(with(any(Callback.class)));
oneOf(finder).findRemovableDrives();
will(returnValue(drives));
oneOf(callback).showChoice(with(any(String[].class)),
with(any(String[].class)));
will(returnValue(0)); // The user chose drive1 but it's not a dir
}});
RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
callback, finder, monitor, 0);
plugin.start();
assertNull(plugin.createWriter(contactId));
File[] files = drive1.listFiles();
assertTrue(files == null || files.length == 0);
context.assertIsSatisfied();
}
@Test
public void testWriterIsNotNullIfOutputDirIsADir() throws Exception {
File drive1 = new File(testDir, "1");
File drive2 = new File(testDir, "2");
List<File> drives = new ArrayList<>();
drives.add(drive1);
drives.add(drive2);
// Create drive1 as a directory
assertTrue(drive1.mkdir());
Mockery context = new Mockery() {{
setThreadingPolicy(new Synchroniser());
}};
Executor executor = context.mock(Executor.class);
SimplexPluginCallback callback =
context.mock(SimplexPluginCallback.class);
RemovableDriveFinder finder =
context.mock(RemovableDriveFinder.class);
RemovableDriveMonitor monitor =
context.mock(RemovableDriveMonitor.class);
context.checking(new Expectations() {{
oneOf(monitor).start(with(any(Callback.class)));
oneOf(finder).findRemovableDrives();
will(returnValue(drives));
oneOf(callback).showChoice(with(any(String[].class)),
with(any(String[].class)));
will(returnValue(0)); // The user chose drive1
}});
RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
callback, finder, monitor, 0);
plugin.start();
assertNotNull(plugin.createWriter(contactId));
// The output file should exist and should be empty
File[] files = drive1.listFiles();
assertNotNull(files);
assertEquals(1, files.length);
assertEquals(0, files[0].length());
context.assertIsSatisfied();
}
@Test
public void testWritingToWriter() throws Exception {
File drive1 = new File(testDir, "1");
File drive2 = new File(testDir, "2");
List<File> drives = new ArrayList<>();
drives.add(drive1);
drives.add(drive2);
// Create drive1 as a directory
assertTrue(drive1.mkdir());
Mockery context = new Mockery() {{
setThreadingPolicy(new Synchroniser());
}};
Executor executor = context.mock(Executor.class);
SimplexPluginCallback callback =
context.mock(SimplexPluginCallback.class);
RemovableDriveFinder finder =
context.mock(RemovableDriveFinder.class);
RemovableDriveMonitor monitor =
context.mock(RemovableDriveMonitor.class);
context.checking(new Expectations() {{
oneOf(monitor).start(with(any(Callback.class)));
oneOf(finder).findRemovableDrives();
will(returnValue(drives));
oneOf(callback).showChoice(with(any(String[].class)),
with(any(String[].class)));
will(returnValue(0)); // The user chose drive1
oneOf(callback).showMessage(with(any(String[].class)));
}});
RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
callback, finder, monitor, 0);
plugin.start();
TransportConnectionWriter writer = plugin.createWriter(contactId);
assertNotNull(writer);
// The output file should exist and should be empty
File[] files = drive1.listFiles();
assertNotNull(files);
assertEquals(1, files.length);
assertEquals(0, files[0].length());
// Writing to the output stream should increase the size of the file
OutputStream out = writer.getOutputStream();
out.write(new byte[1234]);
out.flush();
out.close();
// Disposing of the writer should not delete the file
writer.dispose(false);
assertTrue(files[0].exists());
assertEquals(1234, files[0].length());
context.assertIsSatisfied();
}
@Test
public void testEmptyDriveIsIgnored() throws Exception {
Mockery context = new Mockery() {{
setThreadingPolicy(new Synchroniser());
}};
Executor executor = context.mock(Executor.class);
SimplexPluginCallback callback =
context.mock(SimplexPluginCallback.class);
RemovableDriveFinder finder =
context.mock(RemovableDriveFinder.class);
RemovableDriveMonitor monitor =
context.mock(RemovableDriveMonitor.class);
context.checking(new Expectations() {{
oneOf(monitor).start(with(any(Callback.class)));
}});
RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
callback, finder, monitor, 0);
plugin.start();
plugin.driveInserted(testDir);
context.assertIsSatisfied();
}
@Test
public void testFilenames() {
Mockery context = new Mockery() {{
setThreadingPolicy(new Synchroniser());
}};
Executor executor = context.mock(Executor.class);
SimplexPluginCallback callback =
context.mock(SimplexPluginCallback.class);
RemovableDriveFinder finder =
context.mock(RemovableDriveFinder.class);
RemovableDriveMonitor monitor =
context.mock(RemovableDriveMonitor.class);
RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
callback, finder, monitor, 0);
assertFalse(plugin.isPossibleConnectionFilename("abcdefg.dat"));
assertFalse(plugin.isPossibleConnectionFilename("abcdefghi.dat"));
assertFalse(plugin.isPossibleConnectionFilename("abcdefgh_dat"));
assertFalse(plugin.isPossibleConnectionFilename("abcdefgh.rat"));
assertTrue(plugin.isPossibleConnectionFilename("abcdefgh.dat"));
assertTrue(plugin.isPossibleConnectionFilename("ABCDEFGH.DAT"));
context.assertIsSatisfied();
}
@Test
public void testReaderIsCreated() throws Exception {
Mockery context = new Mockery() {{
setThreadingPolicy(new Synchroniser());
}};
SimplexPluginCallback callback =
context.mock(SimplexPluginCallback.class);
RemovableDriveFinder finder =
context.mock(RemovableDriveFinder.class);
RemovableDriveMonitor monitor =
context.mock(RemovableDriveMonitor.class);
context.checking(new Expectations() {{
oneOf(monitor).start(with(any(Callback.class)));
oneOf(callback).readerCreated(with(any(FileTransportReader.class)));
}});
RemovableDrivePlugin plugin = new RemovableDrivePlugin(
new ImmediateExecutor(), callback, finder, monitor, 0);
plugin.start();
File f = new File(testDir, "abcdefgh.dat");
OutputStream out = new FileOutputStream(f);
out.write(new byte[MIN_STREAM_LENGTH]);
out.flush();
out.close();
assertEquals(MIN_STREAM_LENGTH, f.length());
plugin.driveInserted(testDir);
context.assertIsSatisfied();
}
@After
public void tearDown() {
TestUtils.deleteTestDirectory(testDir);
}
}

View File

@@ -1,112 +0,0 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.plugin.file.RemovableDriveMonitor.Callback;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.OsUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class UnixRemovableDriveMonitorTest extends BrambleTestCase {
private final File testDir = TestUtils.getTestDirectory();
@Before
public void setUp() {
testDir.mkdirs();
}
@Test
public void testNonexistentDir() throws Exception {
if (!(OsUtils.isLinux() || OsUtils.isMacLeopardOrNewer())) {
System.err.println("WARNING: Skipping test, can't run on this OS");
return;
}
File doesNotExist = new File(testDir, "doesNotExist");
RemovableDriveMonitor monitor = createMonitor(doesNotExist);
@NotNullByDefault
Callback callback = new Callback() {
@Override
public void driveInserted(File root) {
fail();
}
@Override
public void exceptionThrown(IOException e) {
fail();
}
};
monitor.start(callback);
monitor.stop();
}
@Test
public void testOneCallbackPerFile() throws Exception {
if (!(OsUtils.isLinux() || OsUtils.isMacLeopardOrNewer())) {
System.err.println("WARNING: Skipping test, can't run on this OS");
return;
}
// Create a callback that will wait for two files before stopping
List<File> detected = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(2);
@NotNullByDefault
Callback callback = new Callback() {
@Override
public void driveInserted(File f) {
detected.add(f);
latch.countDown();
}
@Override
public void exceptionThrown(IOException e) {
fail();
}
};
// Create the monitor and start it
RemovableDriveMonitor monitor = createMonitor(testDir);
monitor.start(callback);
// Create two files in the test directory
File file1 = new File(testDir, "1");
File file2 = new File(testDir, "2");
assertTrue(file1.createNewFile());
assertTrue(file2.createNewFile());
// Wait for the monitor to detect the files
assertTrue(latch.await(5, SECONDS));
monitor.stop();
// Check that both files were detected
assertEquals(2, detected.size());
assertTrue(detected.contains(file1));
assertTrue(detected.contains(file2));
}
@After
public void tearDown() {
TestUtils.deleteTestDirectory(testDir);
}
private RemovableDriveMonitor createMonitor(File dir) {
@NotNullByDefault
RemovableDriveMonitor monitor = new UnixRemovableDriveMonitor() {
@Override
protected String[] getPathsToWatch() {
return new String[] {dir.getPath()};
}
};
return monitor;
}
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.bramble.plugin.modem;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.test.BrambleTestCase;
@@ -66,7 +65,6 @@ public class ModemPluginTest extends BrambleTestCase {
TransportProperties remote = new TransportProperties();
remote.put("iso3166", ISO_1336);
remote.put("number", NUMBER);
ContactId contactId = new ContactId(234);
context.checking(new Expectations() {{
// start()
oneOf(serialPortList).getPortNames();
@@ -78,14 +76,12 @@ public class ModemPluginTest extends BrambleTestCase {
// createConnection()
oneOf(callback).getLocalProperties();
will(returnValue(local));
oneOf(callback).getRemoteProperties(contactId);
will(returnValue(remote));
oneOf(modem).dial(NUMBER);
will(returnValue(true));
}});
plugin.start();
// A connection should be returned
assertNotNull(plugin.createConnection(contactId));
assertNotNull(plugin.createConnection(remote));
context.assertIsSatisfied();
}
@@ -105,7 +101,6 @@ public class ModemPluginTest extends BrambleTestCase {
TransportProperties remote = new TransportProperties();
remote.put("iso3166", ISO_1336);
remote.put("number", NUMBER);
ContactId contactId = new ContactId(234);
context.checking(new Expectations() {{
// start()
oneOf(serialPortList).getPortNames();
@@ -117,14 +112,12 @@ public class ModemPluginTest extends BrambleTestCase {
// createConnection()
oneOf(callback).getLocalProperties();
will(returnValue(local));
oneOf(callback).getRemoteProperties(contactId);
will(returnValue(remote));
oneOf(modem).dial(NUMBER);
will(returnValue(false));
}});
plugin.start();
// No connection should be returned
assertNull(plugin.createConnection(contactId));
assertNull(plugin.createConnection(remote));
context.assertIsSatisfied();
}
@@ -144,7 +137,6 @@ public class ModemPluginTest extends BrambleTestCase {
TransportProperties remote = new TransportProperties();
remote.put("iso3166", ISO_1336);
remote.put("number", NUMBER);
ContactId contactId = new ContactId(234);
context.checking(new Expectations() {{
// start()
oneOf(serialPortList).getPortNames();
@@ -156,8 +148,6 @@ public class ModemPluginTest extends BrambleTestCase {
// createConnection()
oneOf(callback).getLocalProperties();
will(returnValue(local));
oneOf(callback).getRemoteProperties(contactId);
will(returnValue(remote));
oneOf(modem).dial(NUMBER);
will(throwException(new IOException()));
// resetModem()
@@ -170,7 +160,7 @@ public class ModemPluginTest extends BrambleTestCase {
}});
plugin.start();
// No connection should be returned
assertNull(plugin.createConnection(contactId));
assertNull(plugin.createConnection(remote));
context.assertIsSatisfied();
}
}

View File

@@ -4,3 +4,4 @@ build
local.properties
.settings
src/main/assets/*.zip
src/main/res/values-iw

View File

@@ -238,15 +238,16 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion 26
versionCode 10004
versionName "1.0.4"
versionCode 10008
versionName "1.0.8"
applicationId "org.briarproject.briar.android"
resValue "string", "app_package", "org.briarproject.briar.android"
resValue "string", "app_name", "Briar"
buildConfigField "String", "GitHash",
"\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\""
def now = (long) (System.currentTimeMillis() / 1000)
buildConfigField "Long", "BuildTimestamp",
"${getStdout(['git', 'log', '-n', '1', '--date=unix', '--format=%cd'], 0)}000L"
"${getStdout(['git', 'log', '-n', '1', '--format=%ct'], now)}000L"
}
buildTypes {
@@ -256,11 +257,13 @@ android {
resValue "string", "app_name", "Briar Debug"
shrinkResources false
minifyEnabled true
crunchPngs false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
release {
shrinkResources true
minifyEnabled true
crunchPngs false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
@@ -276,13 +279,52 @@ android {
}
}
aaptOptions {
cruncherEnabled = false
}
lintOptions {
warning 'MissingTranslation'
warning 'ImpliedQuantity'
warning 'ExtraTranslation'
}
}
task verifyTranslations {
doLast {
def file = "briar-android/src/main/res/values/arrays.xml"
def arrays = new XmlParser().parse(file)
def lc = arrays.children().find { it.@name == "pref_language_values" }
def translations = []
lc.children().each { value -> translations.add(value.text()) }
def folders = ["default", "en-US"]
new File("briar-android/src/main/res").eachDir { dir ->
if (dir.name.startsWith("values-") && !dir.name.endsWith("night")) {
folders.add(dir.name.substring(7).replace("-r", "-"))
}
}
folders.each { n ->
if (!translations.remove(n) && n != 'iw') {
throw new GradleException("Translation " + n + " is missing in $file")
}
}
if (translations.size() != 0)
throw new GradleException("Translations\n" + translations.join("\n")
+ "\nhave no matching value folder")
// Some devices use iw instead of he for hebrew
def hebrew_legacy = new File("briar-android/src/main/res/values-iw")
def hebrew = new File("briar-android/src/main/res/values-he")
// Copy values-he to values-iw
if (hebrew.exists()) {
hebrew_legacy.mkdir()
copy {
from 'src/main/res/values-he'
into 'src/main/res/values-iw'
}
}
}
}
project.afterEvaluate {
preBuild.dependsOn.add(verifyTranslations)
}

View File

@@ -17,31 +17,32 @@ class AndroidDatabaseConfig implements DatabaseConfig {
private static final Logger LOG =
Logger.getLogger(AndroidDatabaseConfig.class.getName());
private final File dir;
private final File dbDir, keyDir;
@Nullable
private volatile SecretKey key = null;
@Nullable
private volatile String nickname = null;
AndroidDatabaseConfig(File dir) {
this.dir = dir;
AndroidDatabaseConfig(File dbDir, File keyDir) {
this.dbDir = dbDir;
this.keyDir = keyDir;
}
@Override
public boolean databaseExists() {
// FIXME should not run on UiThread #620
if (!dir.isDirectory()) {
if (!dbDir.isDirectory()) {
if (LOG.isLoggable(INFO))
LOG.info(dir.getAbsolutePath() + " is not a directory");
LOG.info(dbDir.getAbsolutePath() + " is not a directory");
return false;
}
File[] files = dir.listFiles();
File[] files = dbDir.listFiles();
if (LOG.isLoggable(INFO)) {
if (files == null) {
LOG.info("Could not list files in " + dir.getAbsolutePath());
LOG.info("Could not list files in " + dbDir.getAbsolutePath());
} else {
LOG.info("Files in " + dir.getAbsolutePath() + ":");
LOG.info("Files in " + dbDir.getAbsolutePath() + ":");
for (File f : files) LOG.info(f.getName());
}
LOG.info("Database exists: " + (files != null && files.length > 0));
@@ -51,10 +52,16 @@ class AndroidDatabaseConfig implements DatabaseConfig {
@Override
public File getDatabaseDirectory() {
File dir = this.dir;
if (LOG.isLoggable(INFO))
LOG.info("Database directory: " + dir.getAbsolutePath());
return dir;
LOG.info("Database directory: " + dbDir.getAbsolutePath());
return dbDir;
}
@Override
public File getDatabaseKeyDirectory() {
if (LOG.isLoggable(INFO))
LOG.info("Database key directory: " + keyDir.getAbsolutePath());
return keyDir;
}
@Override

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.StrictMode;
@@ -8,12 +9,23 @@ import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.BackoffFactory;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.reporting.DevConfig;
import org.briarproject.bramble.api.ui.UiCallback;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.Scheduler;
import org.briarproject.bramble.plugin.bluetooth.AndroidBluetoothPluginFactory;
import org.briarproject.bramble.plugin.tcp.AndroidLanTcpPluginFactory;
import org.briarproject.bramble.plugin.tor.TorPluginFactory;
import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.api.android.AndroidNotificationManager;
@@ -23,14 +35,21 @@ import org.briarproject.briar.api.android.ScreenFilterMonitor;
import java.io.File;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.net.SocketFactory;
import dagger.Module;
import dagger.Provides;
import static android.content.Context.MODE_PRIVATE;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX;
@@ -47,27 +66,9 @@ public class AppModule {
}
private final Application application;
private final UiCallback uiCallback;
public AppModule(Application application) {
this.application = application;
uiCallback = new UiCallback() {
@Override
public int showChoice(String[] options, String... message) {
throw new UnsupportedOperationException();
}
@Override
public boolean showConfirmationMessage(String... message) {
throw new UnsupportedOperationException();
}
@Override
public void showMessage(String... message) {
throw new UnsupportedOperationException();
}
};
}
@Provides
@@ -76,25 +77,60 @@ public class AppModule {
return application;
}
@Provides
UiCallback provideUICallback() {
return uiCallback;
}
@Provides
@Singleton
DatabaseConfig provideDatabaseConfig(Application app) {
//FIXME: StrictMode
StrictMode.ThreadPolicy tp = StrictMode.allowThreadDiskReads();
StrictMode.allowThreadDiskWrites();
File dir = app.getApplicationContext().getDir("db", MODE_PRIVATE);
File dbDir = app.getApplicationContext().getDir("db", MODE_PRIVATE);
File keyDir = app.getApplicationContext().getDir("key", MODE_PRIVATE);
StrictMode.setThreadPolicy(tp);
@MethodsNotNullByDefault
@ParametersNotNullByDefault
DatabaseConfig databaseConfig = new AndroidDatabaseConfig(dir);
DatabaseConfig databaseConfig =
new AndroidDatabaseConfig(dbDir, keyDir);
return databaseConfig;
}
@Provides
PluginConfig providePluginConfig(@IoExecutor Executor ioExecutor,
@Scheduler ScheduledExecutorService scheduler,
AndroidExecutor androidExecutor, SecureRandom random,
SocketFactory torSocketFactory, BackoffFactory backoffFactory,
Application app, LocationUtils locationUtils, EventBus eventBus,
Clock clock) {
Context appContext = app.getApplicationContext();
DuplexPluginFactory bluetooth =
new AndroidBluetoothPluginFactory(ioExecutor, androidExecutor,
appContext, random, eventBus, backoffFactory);
DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, scheduler,
appContext, locationUtils, eventBus, torSocketFactory,
backoffFactory, clock);
DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
scheduler, backoffFactory, appContext);
Collection<DuplexPluginFactory> duplex = asList(bluetooth, tor, lan);
@NotNullByDefault
PluginConfig pluginConfig = new PluginConfig() {
@Override
public Collection<DuplexPluginFactory> getDuplexFactories() {
return duplex;
}
@Override
public Collection<SimplexPluginFactory> getSimplexFactories() {
return emptyList();
}
@Override
public boolean shouldPoll() {
return true;
}
};
return pluginConfig;
}
@Provides
@Singleton
DevConfig provideDevConfig(Application app, CryptoComponent crypto) {

View File

@@ -2,9 +2,12 @@ package org.briarproject.briar.android;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy;
import android.os.StrictMode.VmPolicy;
import android.preference.PreferenceManager;
import org.acra.ACRA;
import org.acra.ReportingInteractionMode;
@@ -75,7 +78,12 @@ public class BriarApplicationImpl extends Application
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(base);
// Loading the language needs to be done here.
Localizer.initialize(prefs);
super.attachBaseContext(
Localizer.getInstance().setLocale(base));
ACRA.init(this);
}
@@ -108,6 +116,12 @@ public class BriarApplicationImpl extends Application
AndroidEagerSingletons.initEagerSingletons(applicationComponent);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Localizer.getInstance().setLocale(this);
}
private void enableStrictMode() {
ThreadPolicy.Builder threadPolicy = new ThreadPolicy.Builder();
threadPolicy.detectAll();

View File

@@ -168,6 +168,11 @@ public class BriarService extends Service {
registerReceiver(receiver, filter);
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(Localizer.getInstance().setLocale(base));
}
private void showStartupFailureNotification(StartResult result) {
androidExecutor.runOnUiThread(() -> {
NotificationCompat.Builder b = new NotificationCompat.Builder(

View File

@@ -0,0 +1,79 @@
package org.briarproject.briar.android;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Locale;
import javax.annotation.Nullable;
import static android.os.Build.VERSION.SDK_INT;
import static org.briarproject.briar.android.settings.SettingsFragment.LANGUAGE;
@NotNullByDefault
public class Localizer {
// Locking: class
@Nullable
private static Localizer INSTANCE;
@Nullable
private final Locale locale;
private Localizer(SharedPreferences sharedPreferences) {
locale = getLocaleFromTag(
sharedPreferences.getString(LANGUAGE, "default"));
}
public static synchronized void initialize(SharedPreferences prefs) {
if (INSTANCE == null)
INSTANCE = new Localizer(prefs);
}
public static synchronized Localizer getInstance() {
if (INSTANCE == null)
throw new IllegalStateException("Localizer not initialized");
return INSTANCE;
}
// Get Locale from BCP-47 tag
@Nullable
public static Locale getLocaleFromTag(String tag) {
if (tag.equals("default"))
return null;
if (SDK_INT >= 21) {
return Locale.forLanguageTag(tag);
}
if (tag.contains("-")) {
String[] langArray = tag.split("-");
return new Locale(langArray[0], langArray[1]);
} else
return new Locale(tag);
}
public Context setLocale(Context context) {
if (locale == null)
return context;
Resources res = context.getResources();
Configuration conf = res.getConfiguration();
Locale currentLocale;
if (SDK_INT >= 24) {
currentLocale = conf.getLocales().get(0);
} else
currentLocale = conf.locale;
if (locale.equals(currentLocale))
return context;
Locale.setDefault(locale);
if (SDK_INT >= 17) {
conf.setLocale(locale);
context.createConfigurationContext(conf);
} else
conf.locale = locale;
//noinspection deprecation
res.updateConfiguration(conf, res.getDisplayMetrics());
return context;
}
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.activity;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.LayoutRes;
@@ -18,6 +19,7 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.AndroidComponent;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.DestroyableContext;
import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.fragment.BaseFragment;
@@ -84,6 +86,12 @@ public abstract class BaseActivity extends AppCompatActivity
}
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(
Localizer.getInstance().setLocale(base));
}
public ActivityComponent getActivityComponent() {
return activityComponent;
}

View File

@@ -1,13 +1,9 @@
package org.briarproject.briar.android.blog;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
@@ -27,7 +23,6 @@ import org.briarproject.briar.api.blog.BlogPostHeader;
import javax.annotation.Nullable;
import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@@ -35,7 +30,6 @@ import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
import static org.briarproject.briar.android.util.UiUtils.TEASER_LENGTH;
import static org.briarproject.briar.android.util.UiUtils.getSpanned;
import static org.briarproject.briar.android.util.UiUtils.getTeaser;
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
import static org.briarproject.briar.api.blog.MessageType.POST;
@@ -135,18 +129,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
Intent i = new Intent(ctx, ReblogActivity.class);
i.putExtra(GROUP_ID, item.getGroupId().getBytes());
i.putExtra(POST_ID, item.getId().getBytes());
if (Build.VERSION.SDK_INT >= 23 && !isSamsung7()) {
ActivityOptionsCompat options =
makeSceneTransitionAnimation((Activity) ctx, layout,
getTransitionName(item.getId()));
ActivityCompat.startActivity(ctx, i,
options.toBundle());
} else {
// work-around for android bug #224270
// work-around for Samsung Android 7 bug #1007
ctx.startActivity(i);
}
ctx.startActivity(i);
});
// comments

View File

@@ -12,7 +12,7 @@ public interface ConfigController {
@Nullable
String getEncryptedDatabaseKey();
void storeEncryptedDatabaseKey(String hex);
boolean storeEncryptedDatabaseKey(String hex);
void deleteAccount(Context ctx);

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.controller;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.v7.preference.PreferenceManager;
@@ -9,12 +8,18 @@ import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.util.AndroidUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@NotNullByDefault
public class ConfigControllerImpl implements ConfigController {
@@ -23,8 +28,11 @@ public class ConfigControllerImpl implements ConfigController {
Logger.getLogger(ConfigControllerImpl.class.getName());
private static final String PREF_DB_KEY = "key";
private static final String DB_KEY_FILENAME = "db.key";
private static final String DB_KEY_BACKUP_FILENAME = "db.key.bak";
private final SharedPreferences briarPrefs;
private final File dbKeyFile, dbKeyBackupFile;
protected final DatabaseConfig databaseConfig;
@Inject
@@ -32,22 +40,113 @@ public class ConfigControllerImpl implements ConfigController {
DatabaseConfig databaseConfig) {
this.briarPrefs = briarPrefs;
this.databaseConfig = databaseConfig;
File keyDir = databaseConfig.getDatabaseKeyDirectory();
dbKeyFile = new File(keyDir, DB_KEY_FILENAME);
dbKeyBackupFile = new File(keyDir, DB_KEY_BACKUP_FILENAME);
}
@Override
@Nullable
public String getEncryptedDatabaseKey() {
String key = briarPrefs.getString(PREF_DB_KEY, null);
if (LOG.isLoggable(INFO))
LOG.info("Got database key from preferences: " + (key != null));
String key = getDatabaseKeyFromPreferences();
if (key == null) key = getDatabaseKeyFromFile();
else migrateDatabaseKeyToFile(key);
return key;
}
@Nullable
private String getDatabaseKeyFromPreferences() {
String key = briarPrefs.getString(PREF_DB_KEY, null);
if (key == null) LOG.info("No database key in preferences");
else LOG.info("Found database key in preferences");
return key;
}
@Nullable
private String getDatabaseKeyFromFile() {
String key = readDbKeyFromFile(dbKeyFile);
if (key == null) {
LOG.info("No database key in primary file");
key = readDbKeyFromFile(dbKeyBackupFile);
if (key == null) LOG.info("No database key in backup file");
else LOG.warning("Found database key in backup file");
} else {
LOG.info("Found database key in primary file");
}
return key;
}
@Nullable
private String readDbKeyFromFile(File f) {
if (!f.exists()) {
LOG.info("Key file does not exist");
return null;
}
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream(f), "UTF-8"));
String key = reader.readLine();
reader.close();
return key;
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
}
}
private void migrateDatabaseKeyToFile(String key) {
if (storeEncryptedDatabaseKey(key)) {
if (briarPrefs.edit().remove(PREF_DB_KEY).commit())
LOG.info("Database key migrated to file");
else LOG.warning("Database key not removed from preferences");
} else {
LOG.warning("Database key not migrated to file");
}
}
@Override
@SuppressLint("ApplySharedPref")
public void storeEncryptedDatabaseKey(String hex) {
LOG.info("Storing database key in preferences");
briarPrefs.edit().putString(PREF_DB_KEY, hex).commit();
public boolean storeEncryptedDatabaseKey(String hex) {
LOG.info("Storing database key in file");
// Create the directory if necessary
if (databaseConfig.getDatabaseKeyDirectory().mkdirs())
LOG.info("Created database key directory");
// If only the backup file exists, rename it so we don't overwrite it
if (dbKeyBackupFile.exists() && !dbKeyFile.exists()) {
if (dbKeyBackupFile.renameTo(dbKeyFile))
LOG.info("Renamed old backup");
else LOG.warning("Failed to rename old backup");
}
try {
// Write to the backup file
writeDbKeyToFile(hex, dbKeyBackupFile);
LOG.info("Stored database key in backup file");
// Delete the old primary file, if it exists
if (dbKeyFile.exists()) {
if (dbKeyFile.delete()) LOG.info("Deleted primary file");
else LOG.warning("Failed to delete primary file");
}
// The backup file becomes the new primary
if (dbKeyBackupFile.renameTo(dbKeyFile)) {
LOG.info("Renamed backup file to primary");
} else {
LOG.warning("Failed to rename backup file to primary");
return false; // Don't overwrite our only copy
}
// Write a second copy to the backup file
writeDbKeyToFile(hex, dbKeyBackupFile);
LOG.info("Stored second copy of database key in backup file");
return true;
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return false;
}
}
private void writeDbKeyToFile(String key, File f) throws IOException {
FileOutputStream out = new FileOutputStream(f);
out.write(key.getBytes("UTF-8"));
out.flush();
out.close();
}
@Override
@@ -56,22 +155,16 @@ public class ConfigControllerImpl implements ConfigController {
SharedPreferences defaultPrefs =
PreferenceManager.getDefaultSharedPreferences(ctx);
AndroidUtils.deleteAppData(ctx, briarPrefs, defaultPrefs);
AndroidUtils.logDataDirContents(ctx);
}
@Override
public boolean accountExists() {
String hex = getEncryptedDatabaseKey();
boolean exists = hex != null && databaseConfig.databaseExists();
if (LOG.isLoggable(INFO)) LOG.info("Account exists: " + exists);
return exists;
return hex != null && databaseConfig.databaseExists();
}
@Override
public boolean accountSignedIn() {
boolean signedIn = databaseConfig.getEncryptionKey() != null;
if (LOG.isLoggable(INFO)) LOG.info("Signed in: " + signedIn);
return signedIn;
return databaseConfig.getEncryptionKey() != null;
}
}

View File

@@ -42,6 +42,22 @@ public class BriefLogFormatter extends Formatter {
tag = tag.substring(tag.lastIndexOf('.') + 1);
sb.append(tag).append(": ");
sb.append(record.getMessage());
Throwable t = record.getThrown();
if (t != null) {
sb.append('\n');
appendThrowable(sb, t);
}
return sb.toString();
}
private void appendThrowable(StringBuilder sb, Throwable t) {
sb.append(t);
for (StackTraceElement e : t.getStackTrace())
sb.append("\n at ").append(e);
Throwable cause = t.getCause();
if (cause != null) {
sb.append("\n Caused by: ");
appendThrowable(sb, cause);
}
}
}

View File

@@ -72,8 +72,7 @@ public class PasswordControllerImpl extends ConfigControllerImpl
} else {
String hex =
encryptDatabaseKey(new SecretKey(key), newPassword);
storeEncryptedDatabaseKey(hex);
resultHandler.onResult(true);
resultHandler.onResult(storeEncryptedDatabaseKey(hex));
}
});
}

View File

@@ -9,7 +9,6 @@ import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.briar.android.controller.handler.ResultHandler;
import org.briarproject.briar.android.controller.handler.UiResultHandler;
@@ -103,14 +102,11 @@ public class SetupControllerImpl extends PasswordControllerImpl
if (password == null) throw new IllegalStateException();
cryptoExecutor.execute(() -> {
LOG.info("Creating account");
AndroidUtils.logDataDirContents(setupActivity);
databaseConfig.setLocalAuthorName(authorName);
SecretKey key = crypto.generateSecretKey();
databaseConfig.setEncryptionKey(key);
String hex = encryptDatabaseKey(key, password);
storeEncryptedDatabaseKey(hex);
LOG.info("Created account");
AndroidUtils.logDataDirContents(setupActivity);
resultHandler.onResult(null);
});
}

View File

@@ -68,6 +68,7 @@ public class NavDrawerActivity extends BriarActivity implements
public static final String INTENT_GROUPS = "intent_groups";
public static final String INTENT_FORUMS = "intent_forums";
public static final String INTENT_BLOGS = "intent_blogs";
public static final String INTENT_SIGN_OUT = "intent_sign_out";
private static final Logger LOG =
Logger.getLogger(NavDrawerActivity.class.getName());
@@ -99,6 +100,8 @@ public class NavDrawerActivity extends BriarActivity implements
R.id.nav_btn_contacts);
} else if (intent.getBooleanExtra(INTENT_BLOGS, false)) {
startFragment(FeedFragment.newInstance(), R.id.nav_btn_blogs);
} else if (intent.getBooleanExtra(INTENT_SIGN_OUT, false)) {
signOut(false);
}
setIntent(null);
}
@@ -225,12 +228,12 @@ public class NavDrawerActivity extends BriarActivity implements
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.
*/
/*
* 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 {

View File

@@ -36,5 +36,4 @@ public class SettingsActivity extends BriarActivity {
}
return false;
}
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.settings;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.media.Ringtone;
@@ -8,6 +9,7 @@ import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.support.v4.text.TextUtilsCompat;
import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
@@ -30,8 +32,13 @@ import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.util.UserFeedback;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import javax.inject.Inject;
@@ -50,6 +57,7 @@ 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.support.v4.view.ViewCompat.LAYOUT_DIRECTION_LTR;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@@ -58,6 +66,7 @@ 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.android.navdrawer.NavDrawerActivity.INTENT_SIGN_OUT;
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;
@@ -80,11 +89,13 @@ public class SettingsFragment extends PreferenceFragmentCompat
public static final String SETTINGS_NAMESPACE = "android-ui";
public static final String BT_NAMESPACE = BluetoothConstants.ID.getString();
public static final String TOR_NAMESPACE = TorConstants.ID.getString();
public static final String LANGUAGE = "pref_key_language";
private static final Logger LOG =
Logger.getLogger(SettingsFragment.class.getName());
private SettingsActivity listener;
private ListPreference language;
private ListPreference enableBluetooth;
private ListPreference torNetwork;
private CheckBoxPreference notifyPrivateMessages;
@@ -119,6 +130,8 @@ public class SettingsFragment extends PreferenceFragmentCompat
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings);
language = (ListPreference) findPreference(LANGUAGE);
setLanguageEntries();
enableBluetooth = (ListPreference) findPreference("pref_key_bluetooth");
torNetwork = (ListPreference) findPreference("pref_key_tor_network");
notifyPrivateMessages = (CheckBoxPreference) findPreference(
@@ -137,6 +150,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
setSettingsEnabled(false);
language.setOnPreferenceChangeListener(this);
enableBluetooth.setOnPreferenceChangeListener(this);
torNetwork.setOnPreferenceChangeListener(this);
if (SDK_INT >= 21) {
@@ -180,6 +194,51 @@ public class SettingsFragment extends PreferenceFragmentCompat
eventBus.removeListener(this);
}
private void setLanguageEntries() {
CharSequence[] tags = language.getEntryValues();
List<CharSequence> entries = new ArrayList<>(tags.length);
List<CharSequence> entryValues = new ArrayList<>(tags.length);
for (CharSequence cs : tags) {
String tag = cs.toString();
if (tag.equals("default")) {
entries.add(getString(R.string.pref_language_default));
entryValues.add(tag);
continue;
}
Locale locale = Localizer.getLocaleFromTag(tag);
if (locale == null)
throw new IllegalStateException();
// Exclude RTL locales on API < 17, they won't be laid out correctly
if (SDK_INT < 17 && !isLeftToRight(locale)) {
if (LOG.isLoggable(INFO))
LOG.info("Skipping RTL locale " + tag);
continue;
}
String nativeName = locale.getDisplayName(locale);
// Fallback to English if the name is unknown in both native and
// current locale.
if (nativeName.equals(tag)) {
String tmp = locale.getDisplayLanguage(Locale.ENGLISH);
if (!tmp.isEmpty() && !tmp.equals(nativeName))
nativeName = tmp;
}
// Prefix with LRM marker to prevent any RTL direction
entries.add("\u200E" + nativeName.substring(0, 1).toUpperCase()
+ nativeName.substring(1));
entryValues.add(tag);
}
language.setEntries(entries.toArray(new CharSequence[0]));
language.setEntryValues(entryValues.toArray(new CharSequence[0]));
}
private boolean isLeftToRight(Locale locale) {
// TextUtilsCompat returns the wrong direction for Hebrew on some phones
String language = locale.getLanguage();
if (language.equals("iw") || language.equals("he")) return false;
int direction = TextUtilsCompat.getLayoutDirectionFromLocale(locale);
return direction == LAYOUT_DIRECTION_LTR;
}
private void loadSettings() {
listener.runOnDbThread(() -> {
try {
@@ -312,41 +371,65 @@ public class SettingsFragment extends PreferenceFragmentCompat
}
@Override
public boolean onPreferenceChange(Preference preference, Object o) {
if (preference == enableBluetooth) {
boolean btSetting = Boolean.valueOf((String) o);
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (preference == language) {
if (!language.getValue().equals(newValue))
languageChanged((String) newValue);
return false;
} else if (preference == enableBluetooth) {
boolean btSetting = Boolean.valueOf((String) newValue);
storeBluetoothSettings(btSetting);
} else if (preference == torNetwork) {
int torSetting = Integer.valueOf((String) o);
int torSetting = Integer.valueOf((String) newValue);
storeTorSettings(torSetting);
} else if (preference == notifyPrivateMessages) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_PRIVATE, (Boolean) o);
s.putBoolean(PREF_NOTIFY_PRIVATE, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyGroupMessages) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_GROUP, (Boolean) o);
s.putBoolean(PREF_NOTIFY_GROUP, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyForumPosts) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_FORUM, (Boolean) o);
s.putBoolean(PREF_NOTIFY_FORUM, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyBlogPosts) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_BLOG, (Boolean) o);
s.putBoolean(PREF_NOTIFY_BLOG, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyVibration) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_VIBRATION, (Boolean) o);
s.putBoolean(PREF_NOTIFY_VIBRATION, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyLockscreen) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_LOCK_SCREEN, (Boolean) o);
s.putBoolean(PREF_NOTIFY_LOCK_SCREEN, (Boolean) newValue);
storeSettings(s);
}
return true;
}
private void languageChanged(String newValue) {
AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.pref_language_title);
builder.setMessage(R.string.pref_language_changed);
builder.setPositiveButton(R.string.sign_out_button,
(dialogInterface, i) -> {
language.setValue(newValue);
Intent intent = new Intent(getContext(),
NavDrawerActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(INTENT_SIGN_OUT, true);
getActivity().startActivity(intent);
getActivity().finish();
});
builder.setNegativeButton(R.string.cancel, null);
builder.setCancelable(false);
builder.show();
}
private void storeTorSettings(int torSetting) {
listener.runOnDbThread(() -> {
try {

View File

@@ -8,7 +8,6 @@ import android.support.v7.preference.PreferenceManager;
import android.transition.Fade;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
@@ -45,11 +44,9 @@ public class SplashScreenActivity extends BaseActivity {
setContentView(R.layout.splash);
if (configController.accountSignedIn()) {
LOG.info("Already signed in, not showing splash screen");
startActivity(new Intent(this, OpenDatabaseActivity.class));
finish();
} else {
LOG.info("Showing splash screen");
new Handler().postDelayed(() -> {
startNextActivity();
supportFinishAfterTransition();
@@ -67,7 +64,6 @@ public class SplashScreenActivity extends BaseActivity {
LOG.info("Expired");
startActivity(new Intent(this, ExpiredActivity.class));
} else {
AndroidUtils.logDataDirContents(this);
if (configController.accountExists()) {
LOG.info("Account exists");
startActivity(new Intent(this, OpenDatabaseActivity.class));
@@ -80,10 +76,8 @@ public class SplashScreenActivity extends BaseActivity {
}
private void setPreferencesDefaults() {
androidExecutor.runOnBackgroundThread(() -> {
PreferenceManager.setDefaultValues(SplashScreenActivity.this,
R.xml.panic_preferences, false);
LOG.info("Finished setting panic preference defaults");
});
androidExecutor.runOnBackgroundThread(() ->
PreferenceManager.setDefaultValues(SplashScreenActivity.this,
R.xml.panic_preferences, false));
}
}

View File

@@ -89,6 +89,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
textInput.setListener(this);
list = findViewById(R.id.list);
layoutManager = new LinearLayoutManager(this);
// FIXME pre-fetching messes with read state, find better solution #1289
layoutManager.setItemPrefetchEnabled(false);
list.setLayoutManager(layoutManager);
adapter = createAdapter(layoutManager);
list.setAdapter(adapter);

View File

@@ -158,8 +158,12 @@
<string name="blogs_rss_remove_feed_ok">Dilemel</string>
<!--Settings Network-->
<!--Settings Security and Panic-->
<string name="security_settings_title">Surentezh</string>
<string name="change_password">Cheñch ar ger-tremen</string>
<string name="panic_app_setting_none">Hini ebet</string>
<string name="lock_setting_title">Digevreañ</string>
<!--Settings Notifications-->
<string name="notify_sound_setting_disabled">Hini ebet</string>
<!--Settings Feedback-->
<!--Link Warning-->
<!--Crash Reporter-->

View File

@@ -2,50 +2,50 @@
<resources>
<!--Setup-->
<string name="setup_title">Benvingut a Briar</string>
<string name="setup_name_explanation">El vostre sobrenom es mostrarà al costat de qualsevol contingut que publiqueu. No podeu canviar-lo després de crear el compte.</string>
<string name="setup_name_explanation">El vostre sobrenom etiquetarà tot el que publiqueu. Després de crear el compte ja no podreu canviar el sobrenom.</string>
<string name="setup_next">Següent</string>
<string name="setup_password_intro">Trieu una contrasenya</string>
<string name="setup_password_explanation">El vostre compte Briar s\'emmagatzema encriptada al vostre dispositiu, no al núvol. Si oblideu la vostra contrasenya o desinstal·leu Briar, no hi ha forma de recuperar el vostre compte.\n\nTrieu una contrasenya llarga que sigui difícil d\'endevinar, com ara quatre paraules aleatòries o deu lletres, números i símbols aleatoris.</string>
<string name="setup_doze_title">Connexions de fons</string>
<string name="setup_doze_intro">Per rebre missatges, Briar necessita estar connectat en segon pla.</string>
<string name="setup_doze_explanation">Per rebre missatges, Briar necessita estar connectat en segon pla. Desactiveu les optimitzacions de la bateria perquè Briar es mantingui connectat.</string>
<string name="setup_password_intro">Establiu una contrasenya</string>
<string name="setup_password_explanation">El compte de Briar s\'emmagatzema xifrat en el vostre dispositiu, no en el núvol. Si oblideu la contrasenya o desinstal·leu Briar no podreu recuperar el vostre compte ni les dades associades.\n\nTrieu una contrasenya llarga que sigui difícil d\'endevinar, com ara quatre paraules aleatòries o deu lletres, números i símbols aleatoris.</string>
<string name="setup_doze_title">Connexions en segon pla</string>
<string name="setup_doze_intro">Per rebre missatges Briar necessita estar connectat en segon pla.</string>
<string name="setup_doze_explanation">Per rebre missatges Briar necessita estar connectat en segon pla. Desactiveu les optimitzacions de la bateria per permetre que Briar resti sempre connectat.</string>
<string name="setup_doze_button">Permet connexions</string>
<string name="choose_nickname">Trieu el sobrenom</string>
<string name="choose_password">Trieu la contrasenya</string>
<string name="confirm_password">Confirmeu la contrasenya</string>
<string name="name_too_long">El nom és massa llarg</string>
<string name="password_too_weak">La contrasenya és dèbil</string>
<string name="password_too_weak">La contrasenya és massa feble</string>
<string name="passwords_do_not_match">Les contrasenyes no coincideixen</string>
<string name="create_account_button">Crear compte</string>
<string name="create_account_button">Crea el compte</string>
<string name="more_info">Més informació</string>
<string name="don_t_ask_again">No ho tornis a preguntar</string>
<string name="setup_huawei_text">Feu clic al botó següent i assegureu-vos que Briar està protegit a la pantalla «Aplicacions protegides».</string>
<string name="setup_huawei_button">Protegeix el Briar</string>
<string name="setup_huawei_help">Si Briar no s\'afegeix a la llista d\'aplicacions protegides, no podrà executar-se en segon pla.</string>
<string name="warning_dozed">%s no ha pogut executar-se en segon pla</string>
<string name="don_t_ask_again">No tornis a preguntar-ho</string>
<string name="setup_huawei_text">Feu un toc sobre el botó següent i assegureu-vos de que Briar consta com a protegit a la pantalla «Aplicacions protegides».</string>
<string name="setup_huawei_button">Protegeix Briar</string>
<string name="setup_huawei_help">Si no afegiu Briar a la llista d\'aplicacions protegides, s\'evitarà que Briar s\'executi en segon pla.</string>
<string name="warning_dozed">%s no s\'ha pogut executar en segon pla</string>
<!--Login-->
<string name="enter_password">Introdueix la teva contrasenya:</string>
<string name="try_again">La contrasenya és incorrecta, torna-ho a provar</string>
<string name="enter_password">Escriviu la vostra contrasenya:</string>
<string name="try_again">La contrasenya és incorrecta, torneu a escriure-la</string>
<string name="sign_in_button">Inicia la sessió</string>
<string name="forgotten_password">He oblidat la contrasenya</string>
<string name="forgotten_password">No recordo la contrasenya</string>
<string name="dialog_title_lost_password">Contrasenya perduda</string>
<string name="dialog_message_lost_password">El vostre compte de Briar s\'emmagatzema xifrat al vostre dispositiu, no al núvol; per tant no podem restaurar-ne la contrasenya. Voleu esborrar el compte i tornar a començar?\n\nAlerta: les vostres identitats, contactes i missatges es perdran per sempre.</string>
<string name="dialog_message_lost_password">El vostre compte de Briar s\'emmagatzema només en el vostre dispositiu i xifrat. La contrasenya, doncs, no es pot restablir. Voleu esborrar el compte i crear-ne un de nou?\n\nAtenció! Si esborreu el compte la vostra identitat, els contactes i els missatges antics es perdran per sempre.</string>
<string name="startup_failed_notification_title">Briar no s\'ha pogut iniciar</string>
<string name="startup_failed_notification_text">Toqueu per obtenir més informació.</string>
<string name="startup_failed_notification_text">Feu un toc per obtenir més informació.</string>
<string name="startup_failed_activity_title">Error iniciant Briar</string>
<string name="startup_failed_db_error">Per alguna raó, la vostra base de dades de Briar ha estat corrompuda i no es pot reparar. El vostre compte, les dades i els contactes s\'han perdut. Malhauradament, has de reinstal·lar Briar o crear un nou compte triant \"He oblidat la contrasenya\" en la pantalla on poses la contrasenya.</string>
<string name="startup_failed_data_too_old_error">El teu compte es va crear amb una versió antiga d\'aquesta app i no es pot obrir amb aquesta versió. O bé reinstal·les la versió antiga o crea un nou compte triant \"Ho oblidat la contrasenya\" en la pantalla on poses la contrasenya.</string>
<string name="startup_failed_data_too_new_error">Aquesta versió de l\'aplicació és massa antiga. Actualitzeu a la versió més recent i torneu-ho a provar.</string>
<string name="startup_failed_service_error">Briar no ha pogut engegar un connector necessari. La solució sol ser reinstal·lar Briar. De tota manera, tingueu en compte que si ho feu perdreu el vostre compte i totes les dades associades, doncs Briar no usa servidors centrals per desar les vostres dades.</string>
<string name="startup_failed_db_error">Per alguna raó, la base de dades de Briar s\'ha corromput i no es pot adobar. El vostre compte, les dades i els contactes s\'han perdut. Malauradament, heu de reinstal·lar Briar o crear un nou compte triant l\'opció «No recordo la contrasenya» quan se us demani la contrasenya.</string>
<string name="startup_failed_data_too_old_error">El vostre compte fou creat amb una versió antiga de Briar i no es pot obrir amb la versió actual. O bé reinstal·leu la versió antiga o creeu un nou compte triant l\'opció «No recordo la contrasenya» quan se us demani la contrasenya.</string>
<string name="startup_failed_data_too_new_error">Aquesta versió de Briar és massa antiga. Actualitzeu Briar a la darrera versió i torneu a provar-ho.</string>
<string name="startup_failed_service_error">Briar no ha pogut engegar un connector imprescindible. La reinstal·lació de Briar acostuma a resoldre aquest problema. Tingueu en compte que si reinstal·leu, perdreu el vostre compte i les dades associades doncs Briar no usa servidors centrals per desar-les.</string>
<plurals name="expiry_warning">
<item quantity="one">Aquesta és una versió de prova de Briar. El vostre compte expira en %d dia i no es pot renovar.</item>
<item quantity="other">Aquesta és una versió de prova de Briar. El vostre compte expira en %d dies i no es pot renovar.</item>
<item quantity="other">Aquesta és una versió de prova de Briar. El vostre compte caducarà en %d dies i no es podrà renovar.</item>
</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="download_briar">Per continuar utilitzant Briar, baixeu la versió 1.0.</string>
<string name="expiry_update">S\'ha allargat la data de caducitat d\'aquesta versió de test de Briar. Ara el vostre compte caducarà d\'aquí a %d dies.</string>
<string name="expiry_date_reached">Aquesta versió de Briar ha caducat.\nGràcies per haver-lo provat!</string>
<string name="download_briar">Per continuar fent servir Briar, descarregueu-vos la versió 1.0.</string>
<string name="create_new_account">Haureu de crear un compte nou, però podeu utilitzar el mateix sobrenom.</string>
<string name="download_briar_button">Baixa Briar 1.0</string>
<string name="download_briar_button">Descarrega Briar 1.0</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-->
@@ -66,30 +66,30 @@
<string name="ongoing_notification_text">Toca per a obrir Briar.</string>
<plurals name="private_message_notification_text">
<item quantity="one">Missatge privat nou.</item>
<item quantity="other">1%d missatges privats nous.</item>
<item quantity="other">%d missatges privats nous.</item>
</plurals>
<plurals name="group_message_notification_text">
<item quantity="one">Missatge de grup nou.</item>
<item quantity="other">1%d missatges de grup nous</item>
<item quantity="other">%d missatges de grup nous</item>
</plurals>
<plurals name="forum_post_notification_text">
<item quantity="one"> Un nou missatge del fòrum.</item>
<item quantity="other">%d nous missatges del fòrum.</item>
<item quantity="other">%d apunts nous al fòrum.</item>
</plurals>
<plurals name="blog_post_notification_text">
<item quantity="one">Una nova publicacions al bloc.</item>
<item quantity="other">%d noves publicacions al blog.</item>
<item quantity="other">%d apunts nous al blog.</item>
</plurals>
<!--Misc-->
<string name="now">Ara</string>
<string name="show">Mostrar</string>
<string name="hide">Ocultar</string>
<string name="show">Mostra</string>
<string name="hide">Oculta</string>
<string name="ok">D\'acord</string>
<string name="cancel">Cancel·la</string>
<string name="got_it">D\'acord</string>
<string name="delete">Suprimeix</string>
<string name="accept">Accepta</string>
<string name="decline">Rebutja</string>
<string name="decline">Refusa</string>
<string name="options">Opcions</string>
<string name="online">En línia</string>
<string name="offline">Fora de línia</string>
@@ -98,159 +98,159 @@
<string name="open">Obre</string>
<string name="no_data">Sense dades</string>
<string name="ellipsis">...</string>
<string name="text_too_long">El text introduït és massa llarg</string>
<string name="text_too_long">El text és massa llarg</string>
<string name="show_onboarding">Mostra el diàleg d\'ajuda.</string>
<string name="fix">Corregeix</string>
<string name="help">Ajuda</string>
<string name="sorry">Ens sap greu</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">No hi ha contactes\n\nPulsa l\'icona + per afegir un contacte</string>
<string name="no_contacts">No hi ha cap contacte\n\nFeu un toc sobre la icona + per afegir un nou contacte</string>
<string name="date_no_private_messages">Sense missatges.</string>
<string name="no_private_messages">No hi ha missatges</string>
<string name="message_hint">Escriu el missatge.</string>
<string name="delete_contact">Suprimeix contacte</string>
<string name="dialog_title_delete_contact">Confirma la supressió del contacte</string>
<string name="dialog_message_delete_contact">Estàs segur que vols esborrar aquest contacte i tots els missatges que hi has intercanviat?</string>
<string name="contact_deleted_toast">Contacte suprimit</string>
<string name="no_private_messages">No hi ha cap missatge</string>
<string name="message_hint">Escriviu un missatge</string>
<string name="delete_contact">Suprimeix el contacte</string>
<string name="dialog_title_delete_contact">Confirmeu la supressió del contacte</string>
<string name="dialog_message_delete_contact">Segur que voleu suprimir aquest contacte i tots els missatges que us heu intercanviat?</string>
<string name="contact_deleted_toast">S\'ha suprimit el contacte</string>
<!--Adding Contacts-->
<string name="add_contact_title">Afegir contacte</string>
<string name="face_to_face">T\'has de trobar amb la persona que vols afegir com a contacte.\n\nAixò evitarà que algú suplanti la teva identitat o llegeixi els teus missatges.</string>
<string name="add_contact_title">Afegiu un contacte</string>
<string name="face_to_face">Heu de coincidir en el mateix lloc amb la persona que voleu afegir com a contacte.\n\nD\'aquesta manera evitareu que algú suplanti les vostres identitats o pugui llegir els vostres missatges en el futur.</string>
<string name="continue_button">Continua</string>
<string name="connection_failed">La connexió ha fallat</string>
<string name="try_again_button">Torna-ho a provar</string>
<string name="waiting_for_contact_to_scan">Esperant que el teu contacte escanegi i connecti\u2026</string>
<string name="waiting_for_contact_to_scan">Esperant que el vostre contacte escanegi i es connecti\u2026</string>
<string name="exchanging_contact_details">Intercanviant els detalls del contacte\u2026</string>
<string name="contact_added_toast">Contacte afegit: %s</string>
<string name="contact_already_exists">El contacte %s ja existeix</string>
<string name="contact_added_toast">S\'ha afegit el contacte %s</string>
<string name="contact_already_exists">El contacte %s ja existia</string>
<string name="contact_exchange_failed">L\'intercanvi de contactes ha fallat</string>
<string name="qr_code_invalid">El codi QR és invàlid</string>
<string name="qr_code_unsupported">El codi QR que intenteu escanejar pertany a una versió antiga de %s que ja no és compatible.\n\nAssegureu-vos que tots dos estiguin executant la versió més recent i torneu-ho a provar.</string>
<string name="qr_code_unsupported">El codi QR que intenteu escanejar pertany a una versió antiga de %s que ja no és compatible.\n\nAssegureu-vos que tothom està executant la darrera versió i torneu a provar-ho.</string>
<string name="camera_error">Error de la càmera</string>
<string name="connecting_to_device">Connectant al dispositiu\u2026</string>
<string name="connecting_to_device">Connectant-se al dispositiu\u2026</string>
<string name="authenticating_with_device">Autenticant-se amb el dispositiu\u2026</string>
<string name="connection_aborted_local">S\'ha avortat la connexió! Això podria significar que algú intenta interferir amb la vostra connexió</string>
<string name="connection_aborted_remote">El teu contacte ha avortat la connexió! Això podria significar que algú està provant d\'interferir la vostra connexió</string>
<string name="connection_aborted_local">S\'ha avortat la connexió! Podria ser que algú estigués provant d\'interferir la vostra connexió</string>
<string name="connection_aborted_remote">El vostre contacte ha avortat la connexió! Podria ser que algú estigués provant d\'interferir la vostra connexió</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Introdueix els teus contactes</string>
<string name="introduction_onboarding_text">Pots presentar els teus contactes entre si, de manera que no necessiten trobar-se en persona per a donar-se d\'alta com a contactes a Briar.</string>
<string name="introduction_activity_title">Seleccionar contacte</string>
<string name="introduction_not_possible">Ja teniu una introducció en progrés amb aquests contactes. Permeteu que això finalitzi primer. Si vostè o els vostres contactes poques vegades són en línia, això pot trigar un temps.</string>
<string name="introduction_message_title">Introduir contacte</string>
<string name="introduction_message_hint">Afegir un missatge (opcional)</string>
<string name="introduction_button">Presentar contactes entre si</string>
<string name="introduction_sent">S\'ha enviat la teva presentació.</string>
<string name="introduction_error">Hi ha hagut un error en fer la presentació de contactes.</string>
<string name="introduction_onboarding_title">Presenteu als vostres contactes</string>
<string name="introduction_onboarding_text">Podeu presentar als vostres contactes entre si, de manera que no necessitin trobar-se en persona per a relacionar-se a través de Briar.</string>
<string name="introduction_activity_title">Trieu un contacte</string>
<string name="introduction_not_possible">Ja teniu una presentació en marxa entre aquests contactes. Si us plau, primer deixeu que aquesta presentació acabi. Si vós o els contactes presentats sovint esteu desconnectats, la presentació pot trigar temps.</string>
<string name="introduction_message_title">Presenteu contactes entre si</string>
<string name="introduction_message_hint">Afegiu una nota (opcional)</string>
<string name="introduction_button">Presenta els contactes</string>
<string name="introduction_sent">S\'ha enviat la vostra presentació.</string>
<string name="introduction_error">Hi ha hagut un error en presentar els contactes.</string>
<string name="introduction_response_error">Error en respondre a la presentació</string>
<string name="introduction_request_sent">Heu demanat que introduïu %1$s a %2$s.</string>
<string name="introduction_request_received">%1$s ha demanat que us presenteu a %2$s. Voleu afegir a%2$s a la vostra llista de contactes?</string>
<string name="introduction_request_exists_received">%1$s ha demanat que us presenteu a %2$s, però %2$s ja està a la vostra llista de contactes. Com que %1$s no sap això, encara podeu respondre:</string>
<string name="introduction_request_answered_received">%1$s ha demanat que us presenteu a %2$s.</string>
<string name="introduction_response_accepted_sent">Has acceptat la presentació amb el contacte %1$s.</string>
<string name="introduction_response_accepted_sent_info">Abans que %1$s s\'afegeixi als vostres contactes, també heu d\'acceptar la introducció. Això pot trigar un temps.</string>
<string name="introduction_response_declined_sent">Heu rebutjat la presentació a %1$s.</string>
<string name="introduction_response_accepted_received">%1$s va acceptar la presentació de %2$s.</string>
<string name="introduction_response_declined_received">%1$s va rebutjar la presentació a %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s diu que %2$s va rebutjar la presentació.</string>
<string name="introduction_request_sent">Heu demanat fer les presentacions per a que %1$s i %2$s es coneguin.</string>
<string name="introduction_request_received">%1$s us vol presentar a %2$s. Voleu afegir a%2$s a la vostra llista de contactes?</string>
<string name="introduction_request_exists_received">%1$s us vol presentar a %2$s, però ja teniu a %2$s a la llista de contactes. Atès que segurament %1$s no ho sabia, encara el podeu contestar:</string>
<string name="introduction_request_answered_received">%1$s us vol presentar a %2$s.</string>
<string name="introduction_response_accepted_sent">Heu acceptat conèixer a %1$s.</string>
<string name="introduction_response_accepted_sent_info">Quan %1$s també hagi acceptat la presentació, s\'afegirà als vostres contactes. Això pot trigar un temps.</string>
<string name="introduction_response_declined_sent">Heu refusat conèixer a %1$s.</string>
<string name="introduction_response_accepted_received">%1$s ha acceptat conèixer a %2$s.</string>
<string name="introduction_response_declined_received">%1$s ha refusat conèixer a %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s diu que %2$s ha refusat conèixer-lo.</string>
<plurals name="introduction_notification_text">
<item quantity="one">S\'ha afegit un nou contacte.</item>
<item quantity="other">S\'ha afegit %d nous contactes.</item>
<item quantity="other">S\'han afegit %d nous contactes.</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">No hi ha grups\n\nApreta l\'icona de + per crear un grup, o demana als teus contactes de compartir els seus grups amb tu</string>
<string name="groups_created_by">Creat per 1%s</string>
<string name="groups_list_empty">No hi ha cap grup\n\nFeu un toc sobre la icona + per crear un nou grup, o demaneu als vostres contactes que comparteixin els seus grups amb vós.</string>
<string name="groups_created_by">Creat per %s</string>
<plurals name="messages">
<item quantity="one">1%d missatge</item>
<item quantity="other">1%d missatges</item>
<item quantity="other">%d missatges</item>
</plurals>
<string name="groups_group_is_empty">Aquest grup està buit.</string>
<string name="groups_group_is_dissolved">Aquest grup s\'ha dissolt.</string>
<string name="groups_remove">Suprimeix</string>
<string name="groups_create_group_title">Crea un Grup Privat</string>
<string name="groups_create_group_button">Crea un Grup</string>
<string name="groups_create_group_title">Creeu un grup privat</string>
<string name="groups_create_group_button">Crea el grup</string>
<string name="groups_create_group_invitation_button">Envia una invitació</string>
<string name="groups_create_group_hint">Tria un nom per al teu grup privat</string>
<string name="groups_create_group_hint">Trieu un nom per al vostre grup privat</string>
<string name="groups_invitation_sent">S\'ha enviat la invitació del grup</string>
<string name="groups_message_sent">Missatge enviat</string>
<string name="groups_member_list">Llistat de membres</string>
<string name="groups_invite_members">Convida membres</string>
<string name="groups_member_created_you">Has creat el grup</string>
<string name="groups_member_list">Llista de membres</string>
<string name="groups_invite_members">Convida a nous membres</string>
<string name="groups_member_created_you">Heu creat el grup</string>
<string name="groups_member_created">%s ha creat el grup</string>
<string name="groups_member_joined_you">T\'has unit al grup</string>
<string name="groups_member_joined">%s s\'ha unit al grup</string>
<string name="groups_leave">Deixar el Grup</string>
<string name="groups_leave_dialog_title">Confirma que deixes el Grup</string>
<string name="groups_leave_dialog_message">Estàs segur que vols deixar aquest grup?</string>
<string name="groups_dissolve">Dissoldre el grup</string>
<string name="groups_dissolve_dialog_title">Confirma la dissolució del grup</string>
<string name="groups_dissolve_dialog_message">Esteu segur que voleu dissoldre aquest grup?\n\nCap altre membre podrà continuar la conversa i podrien no rebre els darrers missatges.</string>
<string name="groups_dissolve_button">Dissoldre</string>
<string name="groups_dissolved_dialog_title">S\'ha dissolt el Grup</string>
<string name="groups_dissolved_dialog_message">El creador del grup l\'ha dissolt.\n\nJa no hi podeu escriure missatges i podríeu no rebre tots els missatges que s\'hi havien escrit.</string>
<string name="groups_member_joined_you">Us heu afegit al grup</string>
<string name="groups_member_joined">%s s\'ha afegit al grup</string>
<string name="groups_leave">Abandona el grup</string>
<string name="groups_leave_dialog_title">Confirmeu que abandoneu el grup</string>
<string name="groups_leave_dialog_message">Segur que voleu abandonar aquest grup?</string>
<string name="groups_dissolve">Dissol el grup</string>
<string name="groups_dissolve_dialog_title">Confirmeu la dissolució del grup</string>
<string name="groups_dissolve_dialog_message">Esteu segur que voleu dissoldre aquest grup?\n\nSi el dissoleu cap altre membre podrà continuar la conversa i alguns membres pot ser que no rebin els darrers missatges.</string>
<string name="groups_dissolve_button">Dissol</string>
<string name="groups_dissolved_dialog_title">S\'ha dissolt el grup</string>
<string name="groups_dissolved_dialog_message">El creador del grup l\'ha dissolt.\n\nJa no hi podeu escriure més. És possible que no hagueu rebut algun dels darrers missatges que s\'hi havien escrit.</string>
<!--Private Group Invitations-->
<string name="groups_invitations_title">Invitacions al grup</string>
<string name="groups_invitations_invitation_sent">Heu convidat a 1%1$s al grup \"%2$s \".</string>
<string name="groups_invitations_invitation_received">1%1$s us ha convidat a unir-vos al grup \" 2%2$s \".</string>
<string name="groups_invitations_joined">Us heu unit al grup</string>
<string name="groups_invitations_declined">S\'ha rebutjat la invitació al grup</string>
<string name="groups_invitations_title">Convideu a formar part del grup</string>
<string name="groups_invitations_invitation_sent">Heu convidat a %1$s a afegir-se al grup «%2$s».</string>
<string name="groups_invitations_invitation_received">%1$s us ha convidat a afegir-vos al grup «%2$s».</string>
<string name="groups_invitations_joined">Us heu afegit al grup</string>
<string name="groups_invitations_declined">Ha refusat afegir-se al grup</string>
<plurals name="groups_invitations_open">
<item quantity="one">%d invitació a un grup obert</item>
<item quantity="other">%d invitacions a grups oberts</item>
</plurals>
<string name="groups_invitations_response_accepted_sent">Heu acceptat la invitació del grup de %s.</string>
<string name="groups_invitations_response_declined_sent">Heu rebutjat la invitació del grup de %s.</string>
<string name="groups_invitations_response_accepted_received">%s va acceptar la invitació del grup.</string>
<string name="groups_invitations_response_declined_received">%s va rebutjar la invitació del grup.</string>
<string name="groups_invitations_response_accepted_sent">Heu acceptat afegir-vos al grup per invitació de %s.</string>
<string name="groups_invitations_response_declined_sent">Heu refusat afegir-vos al grup per invitació de %s.</string>
<string name="groups_invitations_response_accepted_received">%s ha acceptat afegir-se al grup.</string>
<string name="groups_invitations_response_declined_received">%s ha refusat afegir-se al grup.</string>
<string name="sharing_status_groups">Només el creador del grup pot convidar a nous membres. Tot seguit hi ha la llista dels membres del grup.</string>
<!--Private Groups Revealing Contacts-->
<string name="groups_reveal_contacts">Revela els contactes</string>
<string name="groups_reveal_dialog_message">Podeu triar si voleu que es mostrin contactes a tots els membres actuals i futurs d\'aquest grup.\n\nLa revelació dels contactes fa que la vostra connexió al grup sigui més ràpida i més fiable, ja que podeu comunicar-vos amb els contactes revelats, fins i tot quan el creador del grup està fora de línia.</string>
<string name="groups_reveal_visible">La relació del contacte és visible per al grup</string>
<string name="groups_reveal_visible_revealed_by_us">La relació del contacte és visible per al grup (revelat per vós)</string>
<string name="groups_reveal_visible_revealed_by_contact">La relació del contacte és visible per al grup (revelat per %s)</string>
<string name="groups_reveal_invisible">La relació del contacte no és visible per al grup</string>
<string name="groups_reveal_dialog_message">Podeu triar si voleu que es mostrin els contactes a tots els membres actuals i futurs d\'aquest grup.\n\nLa revelació dels contactes fa que la vostra connexió al grup sigui més ràpida i més fiable, ja que podeu comunicar-vos amb els contactes revelats, fins i tot quan el creador del grup està fora de línia.</string>
<string name="groups_reveal_visible">La relació de contactes és visible pel grup</string>
<string name="groups_reveal_visible_revealed_by_us">La relació de contactes és visible pel grup (revelada per vós)</string>
<string name="groups_reveal_visible_revealed_by_contact">La relació de contactes és visible pel grup (revelada per %s)</string>
<string name="groups_reveal_invisible">La relació de contactes no és visible pel grup</string>
<!--Forums-->
<string name="no_forums">No hi ha fòrums\n\nApreta l\'icona + per crear un fòrum, o demana els teus contactes que comparteixin els seus fòrums amb tu</string>
<string name="create_forum_title">Crea un fòrum</string>
<string name="no_forums">No hi ha cap fòrum\n\nFeu un toc sobre la icona + per crear un nou fòrum, o demaneu als vostres contactes que comparteixin els seus fòrums amb vós.</string>
<string name="create_forum_title">Creeu un fòrum</string>
<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">No hi ha publicacions per mostrar</string>
<string name="no_posts">No hi ha publicacions</string>
<string name="no_forum_posts">No hi ha cap apunt per mostrar</string>
<string name="no_posts">No hi ha cap apunt</string>
<plurals name="posts">
<item quantity="one">%d publicacio</item>
<item quantity="other">%d publicacions</item>
<item quantity="other">%d apunts</item>
</plurals>
<string name="forum_new_entry_posted">Entrada publicada al fòrum</string>
<string name="forum_new_message_hint">Entrada nova</string>
<string name="forum_new_entry_posted">S\'ha publicat un nou apunt al fòrum</string>
<string name="forum_new_message_hint">Apunt nou</string>
<string name="forum_message_reply_hint">Resposta nova</string>
<string name="btn_reply">Respon</string>
<string name="forum_leave">Abandona el fòrum</string>
<string name="dialog_title_leave_forum">Confirmeu la sortida del Forum</string>
<string name="dialog_message_leave_forum">Estàs segur/a que vols marxar d\'aquest fòrum?\n\nTots els contactes que tinguis compartits en aquest fòrum poden deixar de rebre actualitzacions</string>
<string name="dialog_title_leave_forum">Confirmeu l\'abandonament del Forum</string>
<string name="dialog_message_leave_forum">Segur que voleu abandonar aquest fòrum?\n\nEls contactes amb els que heu compartit aquest fòrum poden deixar de rebre les actualitzacions</string>
<string name="dialog_button_leave">Abandona</string>
<string name="forum_left_toast">Has deixat el fòrum</string>
<string name="forum_left_toast">Heu abandonat el fòrum</string>
<!--Forum Sharing-->
<string name="forum_share_button">Comparteix el fòrum</string>
<string name="contacts_selected">Contactes seleccionats</string>
<string name="activity_share_toolbar_header">Tria contactes</string>
<string name="no_contacts_selector">No hi ha contactes\n\nSi us plau torna quan hagis afegit algun contacte</string>
<string name="activity_share_toolbar_header">Trieu els contactes</string>
<string name="no_contacts_selector">No hi ha cap contacte\n\nSi us plau, reintenteu-ho després d\'haver afegit algun contacte</string>
<string name="forum_shared_snackbar">Fòrum compartit amb els contactes seleccionats</string>
<string name="forum_share_message">Afegiu un missatge (opcional)</string>
<string name="forum_share_message">Afegiu una nota (opcional)</string>
<string name="forum_share_error">S\'ha produït un error en compartir aquest fòrum.</string>
<string name="forum_invitation_received">%1$s ha compartit el fòrum «%2$s» amb vós.</string>
<string name="forum_invitation_sent">Heu compartit el fòrum «%1$s» amb %2$s.</string>
<string name="forum_invitations_title">Invitacions al fòrum</string>
<string name="forum_invitation_exists">Ja has acceptat una invitació a aquest fòrum.\n\nAcceptant més invitacions farà que la teva connexió amb el fòrum sigui més ràpida i fiable</string>
<string name="forum_joined_toast">T\'has unit al fòrum</string>
<string name="forum_declined_toast">Has declinat la invitació</string>
<string name="shared_by_format">Compartit per 1%s</string>
<string name="forum_invitation_already_sharing">Ja esteu compartint</string>
<string name="forum_invitation_response_accepted_sent">Heu acceptat la invitació del fòrum de %s.</string>
<string name="forum_invitation_response_declined_sent">Heu rebutjat la invitació del fòrum de %s.</string>
<string name="forum_invitation_response_accepted_received">%s va acceptar la invitació del fòrum.</string>
<string name="forum_invitation_response_declined_received">%s va rebutjar la invitació del fòrum.</string>
<string name="forum_invitation_received">%1$s vol compartir el fòrum «%2$s» amb vós.</string>
<string name="forum_invitation_sent">Heu convidat a%2$s a compartit el fòrum «%1$s».</string>
<string name="forum_invitations_title">Convideu a participar al fòrum</string>
<string name="forum_invitation_exists">Ja heu acceptat una invitació a aquest fòrum.\n\nAcceptar més invitacions farà que la vostra connexió amb el fòrum sigui més ràpida i fiable.</string>
<string name="forum_joined_toast">Us heu afegit al fòrum</string>
<string name="forum_declined_toast">Heu refusat la invitació</string>
<string name="shared_by_format">Compartit per %s</string>
<string name="forum_invitation_already_sharing">Ja l\'esteu compartint</string>
<string name="forum_invitation_response_accepted_sent">Heu acceptat la invitació al fòrum enviada per %s.</string>
<string name="forum_invitation_response_declined_sent">Heu refusat la invitació al fòrum enviada per %s.</string>
<string name="forum_invitation_response_accepted_received">%s ha acceptat la invitació al fòrum.</string>
<string name="forum_invitation_response_declined_received">%s ha rebutjat la invitació al fòrum.</string>
<string name="sharing_status">Estat de la compartició</string>
<string name="sharing_status_forum">Qualsevol membre d\'un fòrum pot compartir-lo amb els seus contactes. Esteu compartint aquest fòrum amb els següents contactes. També hi pot haver altres membres que no pugueu veure.</string>
<string name="sharing_status_forum">Qualsevol membre d\'un fòrum pot compartir-lo amb els seus contactes. Esteu compartint aquest fòrum amb els següents contactes. Poden haver-hi membres del fòrum que no pugueu veure.</string>
<string name="shared_with">Compartit amb %1$d (%2$d en línia)</string>
<plurals name="forums_shared">
<item quantity="one">%d fòrum compartit per contactes</item>
@@ -258,20 +258,20 @@
</plurals>
<string name="nobody">Ningú</string>
<!--Blogs-->
<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">Escriu el que vulguis publicar</string>
<string name="blogs_other_blog_empty_state">No hi ha cap apunt per mostrar</string>
<string name="read_more">llegeix-ne més</string>
<string name="blogs_write_blog_post">Escriviu un apunt al blog</string>
<string name="blogs_write_blog_post_body_hint">Escriviu el vostre apunt al blog</string>
<string name="blogs_publish_blog_post">Publica</string>
<string name="blogs_blog_post_created">S\'ha creat la publicació al blog</string>
<string name="blogs_blog_post_received">S\'ha rebut una nova publicació al blog</string>
<string name="blogs_blog_post_created">S\'ha publicat l\'apunt al blog</string>
<string name="blogs_blog_post_received">S\'ha rebut un nou apunt al blog</string>
<string name="blogs_blog_post_scroll_to">Desplaça</string>
<string name="blogs_feed_empty_state">No hi ha publicacions\n\nPublicacions dels teus contactes i blogs als quals et subscriguis apareixeran aquí\n\nPulsa la icona del bolígraf per escriure un post</string>
<string name="blogs_remove_blog">Elimina el blog</string>
<string name="blogs_remove_blog_dialog_message">Estàs segur/a que vols eliminar aquest blog?\n\nLes publicacions seran eliminades del teu dispositiu però no del d\'altres persones.\n\nEls contactes amb els quals hagis compartit aquest blog poden deixar de rebre actualitzacions.</string>
<string name="blogs_remove_blog_ok">Eliminar</string>
<string name="blogs_blog_removed">Blog eliminat</string>
<string name="blogs_reblog_comment_hint">Afegiu un comentari (opcional)</string>
<string name="blogs_feed_empty_state">No hi ha cap apunt\n\nEls apunts dels vostres contactes i dels blogs als que esteu subscrit es mostraran aquí\n\nFeu un toc sobre la icona del bolígraf per escriure un nou apunt</string>
<string name="blogs_remove_blog">Suprimeix el blog</string>
<string name="blogs_remove_blog_dialog_message">Segur que voleu suprimir aquest blog?\n\nEls apunts publicats s\'esborraran del vostre dispositiu però no del d\'altres persones.\n\nEls contactes amb els que hagueu compartit aquest blog poden deixar de rebre les actualitzacions.</string>
<string name="blogs_remove_blog_ok">Suprimeix</string>
<string name="blogs_blog_removed">S\'ha suprimit el blog</string>
<string name="blogs_reblog_comment_hint">Afegiu una nota (opcional)</string>
<string name="blogs_reblog_button">Rebloga</string>
<!--Blog Sharing-->
<string name="blogs_sharing_share">Compartiu el blog</string>
@@ -282,27 +282,27 @@
<string name="blogs_sharing_response_declined_sent">Heu refusat la invitació al blog de %s.</string>
<string name="blogs_sharing_response_accepted_received">%s ha acceptat la invitació al blog.</string>
<string name="blogs_sharing_response_declined_received">%s ha refusat la invitació al blog.</string>
<string name="blogs_sharing_invitation_received">%1$s us ha compartit el blog \"%2$s\".</string>
<string name="blogs_sharing_invitation_sent">Heu compartit el blog \"%1$s\" amb %2$s.</string>
<string name="blogs_sharing_invitations_title">Invitacions al blog</string>
<string name="blogs_sharing_joined_toast">T\'has subscrit al blog</string>
<string name="blogs_sharing_declined_toast">Has declinat la invitació</string>
<string name="sharing_status_blog">Qualsevol subscrit a un blog el pot compartir amb els seus contactes. Esteu compartint aquest blog amb els següents contactes. També hi pot haver altres subscrits que no veieu.</string>
<string name="blogs_sharing_invitation_received">%1$s ha compartit el blog «%2$s» amb vós.</string>
<string name="blogs_sharing_invitation_sent">Heu compartit el blog «%1$s» amb %2$s.</string>
<string name="blogs_sharing_invitations_title">Convideu veure el blog</string>
<string name="blogs_sharing_joined_toast">Us heu subscrit al blog</string>
<string name="blogs_sharing_declined_toast">Heu refusat la invitació</string>
<string name="sharing_status_blog">Qualsevol membre subscrit a un blog el pot compartir amb els seus contactes. Esteu compartint aquest blog amb els següents contactes. Poden haver-hi altres membres subscrits que no veieu.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Importa canal RSS</string>
<string name="blogs_rss_feeds_import_button">Importa</string>
<string name="blogs_rss_feeds_import_hint">Introduïu l\'URL del canal RSS</string>
<string name="blogs_rss_feeds_import_error">Ens sap greu! S\'ha produït un error en importar el vostre canal.</string>
<string name="blogs_rss_feeds_manage">Gestiona els canals RSS</string>
<string name="blogs_rss_feeds_import">Subscriure\'s al canal de notícies RSS</string>
<string name="blogs_rss_feeds_import_button">Subscriu-me</string>
<string name="blogs_rss_feeds_import_hint">Escriviu l\'URL del canal de notícies RSS</string>
<string name="blogs_rss_feeds_import_error">Ens sap greu! S\'ha produït un error en subscriure-us al vostre canal de notícies.</string>
<string name="blogs_rss_feeds_manage">Gestiona els canals de notícies 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">Darrera actualització:</string>
<string name="blogs_rss_remove_feed">Elimina el canal</string>
<string name="blogs_rss_remove_feed_dialog_message">Estàs segur/a que vols eliminar aquest noticiari?\n\nLes publicacions seran eliminades del teu dispositiu però no del d\'altres persones.\n\nEls contactes amb els que hagis compartit aquest noticiari poden deixar de rebre actualitzacions.</string>
<string name="blogs_rss_remove_feed_ok">Eliminar</string>
<string name="blogs_rss_feeds_manage_delete_error">El canal no s\'ha pogut esborrar.</string>
<string name="blogs_rss_feeds_manage_empty_state">No hi ha notícies a mostrar\n\nPulsa l\'icona + per importar un noticiari</string>
<string name="blogs_rss_feeds_manage_error">S\'ha produït un problema en carregar els vostres canals. Torneu-ho a provar més tard.</string>
<string name="blogs_rss_remove_feed">Suprimeix la subscripció al canal de notícies</string>
<string name="blogs_rss_remove_feed_dialog_message">Segur que voleu suprimir la subscripció a aquest canal de notícies?\n\nLes notícies d\'aquest canal s\'eliminaran del vostre dispositiu però no del d\'altres persones.\n\nEls contactes amb els que hagueu compartit aquest canal poden deixar de rebre les actualitzacions.</string>
<string name="blogs_rss_remove_feed_ok">Suprimeix la subscripció</string>
<string name="blogs_rss_feeds_manage_delete_error">La subscripció al canal de notícies no s\'ha pogut suprimir.</string>
<string name="blogs_rss_feeds_manage_empty_state">No hi ha cap notícia per mostrar\n\nFeu un toc sobre la icona + per subscriure-us a un canal de notícies</string>
<string name="blogs_rss_feeds_manage_error">S\'ha produït un problema en actualitzar els vostres canals de notícies. Torneu-ho a provar més endavant.</string>
<!--Settings Network-->
<string name="network_settings_title">Xarxes</string>
<string name="bluetooth_setting">Connecta via bluetooth</string>
@@ -310,87 +310,87 @@
<string name="bluetooth_setting_disabled">Només quan s\'afegeixen contactes</string>
<string name="tor_network_setting">Connecta via Tor</string>
<string name="tor_network_setting_never">Mai</string>
<string name="tor_network_setting_wifi">Només amb WiFi</string>
<string name="tor_network_setting_always">Quan s\'utilitzi la WiFi o les dades mòbils</string>
<string name="tor_network_setting_wifi">Només quan s\'utilitzi WiFi</string>
<string name="tor_network_setting_always">Quan s\'utilitzi WiFi o les dades mòbils</string>
<!--Settings Security and Panic-->
<string name="security_settings_title">Seguretat</string>
<string name="change_password">Canvia la contrasenya</string>
<string name="current_password">Introduïu la contrasenya actual:</string>
<string name="current_password">Escriviu la contrasenya actual:</string>
<string name="choose_new_password">Escriviu la contrasenya nova:</string>
<string name="confirm_new_password">Confirmeu la contrasenya nova:</string>
<string name="password_changed">Heu canviat la contrasenya.</string>
<string name="panic_setting">Configuració del botó del pànic</string>
<string name="panic_setting_title">Botó de pànic</string>
<string name="panic_setting_hint">Configureu com reaccionarà Briar quan feu servir una aplicació del pànic</string>
<string name="panic_setting_hint">Configureu com reaccionarà Briar quan feu servir el botó de pànic</string>
<string name="panic_app_setting_title">Aplicació de botó de pànic</string>
<string name="unknown_app">una aplicació desconeguda</string>
<string name="panic_app_setting_summary">No s\'ha definit cap aplicació</string>
<string name="panic_app_setting_none">Cap</string>
<string name="dialog_title_connect_panic_app">Confirmeu l\'aplicació del pànic</string>
<string name="dialog_message_connect_panic_app">Esteu segurs que voleu permetre que %1$s activi accions destructives del botó del pànic?</string>
<string name="dialog_title_connect_panic_app">Confirmeu l\'aplicació de pànic</string>
<string name="dialog_message_connect_panic_app">Segur que voleu permetre que %1$s desencadeni accions destructives a conseqüència del botó de pànic?</string>
<string name="lock_setting_title">Tanca la sessió</string>
<string name="lock_setting_summary">Tanca la sessió de Briar si es prem un botó del pànic</string>
<string name="purge_setting_title">Esborra el compte</string>
<string name="purge_setting_summary">Suprimeix el compte de Briar si es prem un botó del pànic. Precaució: s\'eliminarà permanentment les vostres identitats, contactes i missatges</string>
<string name="uninstall_setting_title">Desinstal·la Briar</string>
<string name="uninstall_setting_summary">Això requereix confirmació manual en una situació de pànic</string>
<string name="lock_setting_summary">Tanca la sessió de Briar si es prem un botó de pànic</string>
<string name="purge_setting_title">Esborreu el compte</string>
<string name="purge_setting_summary">Suprimeix el compte de Briar si es prem el botó de pànic. Atenció: En aquest cas, s\'eliminarien permanentment les vostres identitats, contactes i missatges</string>
<string name="uninstall_setting_title">Desinstal·leu Briar</string>
<string name="uninstall_setting_summary">Això requeria la confirmació manual malgrat ser en una situació de pànic</string>
<!--Settings Notifications-->
<string name="notification_settings_title">Notificacions</string>
<string name="notify_private_messages_setting_title">Missatges privats</string>
<string name="notify_private_messages_setting_summary">Mostra els avisos per als missatges privats</string>
<string name="notify_private_messages_setting_summary_26">Configura les alertes per missatges privats</string>
<string name="notify_group_messages_setting_title">Missatges grupals</string>
<string name="notify_group_messages_setting_summary">Mostra alertes per als missatges grupals</string>
<string name="notify_group_messages_setting_summary_26">Configura les alertes per missatges de grup</string>
<string name="notify_forum_posts_setting_title">Publicacions del fòrum</string>
<string name="notify_forum_posts_setting_summary">Mostra alertes per a les publicacions del fòrum</string>
<string name="notify_forum_posts_setting_summary_26">Configura les alertes per publicacions als fòrums</string>
<string name="notify_blog_posts_setting_title">Publicacions al blog</string>
<string name="notify_blog_posts_setting_summary">Mostra alertes per les publicacions al blog</string>
<string name="notify_blog_posts_setting_summary_26">Configura les alertes per publicacions als blogs</string>
<string name="notify_private_messages_setting_summary">Mostra avisos pels missatges privats</string>
<string name="notify_private_messages_setting_summary_26">Configura els avisos pels missatges privats</string>
<string name="notify_group_messages_setting_title">Missatges dels grups</string>
<string name="notify_group_messages_setting_summary">Mostra avisos pels missatges dels grups</string>
<string name="notify_group_messages_setting_summary_26">Configura els avisos pels missatges dels grups</string>
<string name="notify_forum_posts_setting_title">Apunts del fòrum</string>
<string name="notify_forum_posts_setting_summary">Mostra avisos pels apunts del fòrum</string>
<string name="notify_forum_posts_setting_summary_26">Configura els avisos pels apunts dels fòrums</string>
<string name="notify_blog_posts_setting_title">Apunts als blogs</string>
<string name="notify_blog_posts_setting_summary">Mostra avisos pels apunts als blogs</string>
<string name="notify_blog_posts_setting_summary_26">Configura els avisos pels apunts als blogs</string>
<string name="notify_vibration_setting">Vibra</string>
<string name="notify_lock_screen_setting_title">Bloca la pantalla</string>
<string name="notify_lock_screen_setting_summary">Mostra notificacions a la pantalla de bloqueig</string>
<string name="notify_lock_screen_setting_summary">Mostra les notificacions a la pantalla de bloqueig</string>
<string name="notify_sound_setting">So</string>
<string name="notify_sound_setting_default">To de trucada predeterminat</string>
<string name="notify_sound_setting_default">So d\'avís predeterminat</string>
<string name="notify_sound_setting_disabled">Cap</string>
<string name="choose_ringtone_title">Trieu el to de trucada</string>
<string name="cannot_load_ringtone">No s\'ha pogut carregar el to de trucada</string>
<string name="choose_ringtone_title">Trieu el so d\'avís</string>
<string name="cannot_load_ringtone">No s\'ha pogut carregar el so d\'avís</string>
<!--Settings Feedback-->
<string name="feedback_settings_title">Comentaris</string>
<string name="send_feedback">Envieu comentaris</string>
<!--Link Warning-->
<string name="link_warning_title">Avís d\'enllaç</string>
<string name="link_warning_intro">L\'enllaç s\'obrirà amb una aplicació externa.</string>
<string name="link_warning_text">Açò podria usar-se per a identificar-vos. Penseu si confieu prou en la persona que us ha enviat l\'enllaç i si convindria obrir-lo amb Orfox.</string>
<string name="link_warning_open_link">Obri l\'enllaç</string>
<string name="link_warning_text">Això podria usar-se per a identificar-vos. Penseu si us en refieu prou de la persona que us ha enviat l\'enllaç. Avalueu si us convindria obrir-lo amb un navegador que faciliti l\'anonimat com Orfox.</string>
<string name="link_warning_open_link">Obre l\'enllaç</string>
<!--Crash Reporter-->
<string name="crash_report_title">Informe de bloqueig de Briar</string>
<string name="briar_crashed">Ens sap greu, el Briar s\'ha tancat inesperadament.</string>
<string name="crash_report_title">Informe de fallida de Briar</string>
<string name="briar_crashed">Ens sap greu, Briar s\'ha tancat inesperadament.</string>
<string name="not_your_fault">Això no és culpa vostra.</string>
<string name="please_send_report">Ajuda\'ns a construir un Briar millor enviant-nos un informe de fallida.</string>
<string name="report_is_encrypted">Ens comprometem a què l\'informe es xifra i s\'envia de manera segura.</string>
<string name="please_send_report">Ajudi\'ns a construir un Briar millor enviant-nos un informe de fallida.</string>
<string name="report_is_encrypted">Us garantim que l\'informe es xifra i s\'envia de manera segura.</string>
<string name="feedback_title">Comentaris</string>
<string name="describe_crash">Descriu el que hi ha succeït (opcional)</string>
<string name="enter_feedback">Introduïu els vostres comentaris</string>
<string name="describe_crash">Descriviu el que hi ha succeït (opcional)</string>
<string name="enter_feedback">Escriviu els vostres comentaris</string>
<string name="optional_contact_email">La vostra adreça de correu (opcional)</string>
<string name="include_debug_report_crash">Inclou dades anònimes sobre el bloqueig</string>
<string name="include_debug_report_crash">Inclou dades anònimes sobre la fallida</string>
<string name="include_debug_report_feedback">Inclou dades anònimes sobre el dispositiu</string>
<string name="could_not_load_report_data">No s\'han pogut carregar les dades de l\'informe.</string>
<string name="send_report">Envia l\'informe</string>
<string name="close">Tanca</string>
<string name="dev_report_saved">S\'ha desat l\'informe. Se us enviarà la propera vegada que inicieu sessió a Briar.</string>
<!--Sign Out-->
<string name="progress_title_logout">S\'està sortint del Briar...</string>
<string name="progress_title_logout">S\'està tancant la sessió de Briar...</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">S\'ha detectat superposició de la pantalla</string>
<string name="screen_filter_body">Una altra aplicació es troba damunt de Briar. Per protegir la vostra seguretat, Briar no respondrà als tocs quan s\'aprovi una altra aplicació.\n\nLes següents aplicacions poden estar dibuixant a la part superior:\n\n%1$s</string>
<string name="screen_filter_allow">Permet que aquestes aplicacions dibuixin a la part superior</string>
<string name="screen_filter_body">Una altra aplicació es troba damunt de Briar. Per protegir la vostra seguretat, Briar no respondrà a les pulsacions quan una altra aplicació s\'hi hagi sobreposat.\n\nLes següents aplicacions poden estar sobreposades a Briar:\n\n%1$s</string>
<string name="screen_filter_allow">Permet que aquestes aplicacions se sobreposin a Briar</string>
<!--Permission Requests-->
<string name="permission_camera_title">Permís de la càmera</string>
<string name="permission_camera_request_body">Per escanejar el codi QR, Briar necessita accés a la càmera.</string>
<string name="permission_camera_denied_body">Heu denegat l\'accés a la càmera, però l\'addició de contactes requereix utilitzar la càmera.\n\nTingueu en compte permetre l\'accés.</string>
<string name="permission_camera_denied_toast">No s\'ha concedit el permís de la càmera</string>
<string name="permission_camera_request_body">Per escanejar el codi QR, Briar necessita accedir a la càmera.</string>
<string name="permission_camera_denied_body">Heu denegat l\'accés a la càmera però per afegir contactes cal utilitzar la càmera.\n\nRecomanem que permeteu l\'accés a la càmera.</string>
<string name="permission_camera_denied_toast">No s\'ha concedit el permís per accedir a la càmera</string>
<string name="qr_code">Codi QR</string>
<string name="show_qr_code_fullscreen">Mostra el codi QR a pantalla completa</string>
</resources>

View File

@@ -102,9 +102,11 @@
<string name="show_onboarding">Hilfe anzeigen</string>
<string name="fix">Behoben</string>
<string name="help">Hilfe</string>
<string name="sorry">Entschuldigung</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Du hast noch keine Kontakte\n\nTippe auf das +-Symbol um einen Kontakt hinzuzufügen</string>
<string name="date_no_private_messages">Keine Nachrichten.</string>
<string name="no_private_messages">Keine Nachrichten zum Anzeigen</string>
<string name="message_hint">Nachricht eingeben</string>
<string name="delete_contact">Kontakt löschen</string>
<string name="dialog_title_delete_contact">Löschen des Kontakts bestätigen</string>
@@ -208,6 +210,7 @@
<string name="choose_forum_hint">Wähle einen Namen für dein Forum</string>
<string name="create_forum_button">Forum erstellen</string>
<string name="forum_created_toast">Forum wurde erstellt</string>
<string name="no_forum_posts">Keine Beiträge zum Anzeigen</string>
<string name="no_posts">Keine Beiträge</string>
<plurals name="posts">
<item quantity="one">%d Beitrag</item>
@@ -250,6 +253,7 @@
</plurals>
<string name="nobody">Niemand</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Keine Blogbeiträge zum Anzeigen</string>
<string name="read_more">weiterlesen</string>
<string name="blogs_write_blog_post">Blogbeitrag erstellen</string>
<string name="blogs_write_blog_post_body_hint">Gib deinen Blogbeitrag ein</string>
@@ -325,12 +329,16 @@
<string name="notification_settings_title">Benachrichtigungen</string>
<string name="notify_private_messages_setting_title">Private Nachrichten</string>
<string name="notify_private_messages_setting_summary">Zeige Benachrichtigungen für private Nachrichten</string>
<string name="notify_private_messages_setting_summary_26">Benachrichtigungen für private Nachrichten konfigurieren</string>
<string name="notify_group_messages_setting_title">Gruppennachrichten</string>
<string name="notify_group_messages_setting_summary">Benachrichtigungen für Gruppennachrichten anzeigen</string>
<string name="notify_group_messages_setting_summary_26">Benachrichtigungen für Gruppennachrichten konfigurieren</string>
<string name="notify_forum_posts_setting_title">Forenbeiträge</string>
<string name="notify_forum_posts_setting_summary">Benachrichtigungen für Forenbeiträge anzeigen</string>
<string name="notify_forum_posts_setting_summary_26">Benachrichtigungen für Forenbeiträge konfigurieren</string>
<string name="notify_blog_posts_setting_title">Blogbeiträge</string>
<string name="notify_blog_posts_setting_summary">Benachrichtigungen für Blogbeiträge anzeigen</string>
<string name="notify_blog_posts_setting_summary_26">Benachrichtigungen für Blogbeiträge konfigurieren</string>
<string name="notify_vibration_setting">Vibration</string>
<string name="notify_lock_screen_setting_title">Sperrbildschirm</string>
<string name="notify_lock_screen_setting_summary">Zeigt Benachrichtigungen auf dem Sperrbildschirm an</string>
@@ -375,4 +383,5 @@
<string name="permission_camera_denied_body">Du hast den Zugriff auf die Kamera verweigert, aber das Hinzufügen von Kontakten erfordert die Verwendung der Kamera.\n\nBitte gewähre den Zugriff.</string>
<string name="permission_camera_denied_toast">Berechtigung für Kamera wurde nicht gewährt</string>
<string name="qr_code">QR-Code</string>
<string name="show_qr_code_fullscreen">QR-Code im Vollbildmodus anzeigen</string>
</resources>

View File

@@ -5,9 +5,13 @@
<string name="setup_name_explanation">نام مستعارتان کنار هر مطلب شما قرار خواهد گرفت و بعد از ایجاد حساب کاربری امکان تغییر آن وجود ندارد.</string>
<string name="setup_next">بعدی</string>
<string name="setup_password_intro">یک رمز عبور انتخاب کنید</string>
<string name="setup_password_explanation">حساب کاربری برایر شما به صورت رمزگذاری شده روی وسیله شما به جای حافظه ابری ذخیره شده است. اگر شما رمز عبور خود را فراموش کنید یا برایر را پاک کنید، راهی برای بازیابی حساب کاربری شما وجود نخواهد داشت.
یک رمز عبور طولانی انتخاب کنید که حدس آن سخت باشد، مثل چهار عبارت تصادفی یا ده لغت تصادفی با اعداد و نماد ها.</string>
<string name="setup_doze_title">اتصال های پس زمینه</string>
<string name="setup_doze_intro">برای دریافت پیام ها، برایر نیاز دارد تا در پس زمینه اتصال داشته باشد.</string>
<string name="setup_doze_button">اجازه دادن به اتصالات</string>
<string name="setup_doze_intro">برای دریافت پیام، برایر نیاز دارد تا در پس زمینه اتصال داشته باشد.</string>
<string name="setup_doze_explanation">برای دریافت پیام، برایر نیاز دارد تا در پس زمینه اتصال داشته باشد. لطفا بهینه سازی باتری را غیر فعال کنید تا برایر بتواند به اتصال خود ادامه دهد.</string>
<string name="setup_doze_button">دادن اجازه به اتصالات</string>
<string name="choose_nickname">نام مستعار خود را انتخاب کنید</string>
<string name="choose_password">رمز عبور خود را انتخاب کنید</string>
<string name="confirm_password">رمز عبور خود را تایید کنید</string>
@@ -20,19 +24,31 @@
<string name="setup_huawei_text">لطفا روی دکمه زیر کلیک کنید و مطمئن شوید که از برایر در صفحه \"برنامه های محافظت شده\" محافظت می شود.</string>
<string name="setup_huawei_button">حفاظت از برایر</string>
<string name="setup_huawei_help">اگر برایر به فهرست برنامه های محافظت شده اضافه نشده، نمی تواند در پس زمینه مشغول به کار باشد.</string>
<string name="warning_dozed">ناتوانی %s برای اجراء در پس زمینه</string>
<!--Login-->
<string name="enter_password">رمز عبور خود را وارد کنید:</string>
<string name="try_again">رمز عبور اشتباه است، لطفا دوباره سعی کنید</string>
<string name="sign_in_button">ورود</string>
<string name="forgotten_password">رمز عبور را فراموش کرده ام</string>
<string name="dialog_title_lost_password">رمز عبور گمشده</string>
<string name="dialog_message_lost_password">حساب کاربری برایر شما به صورت رمزنگاری شده روی سیستم شما به جای حافظه ابری ذخیره شده است برای همین ما نمی توانیم رمز عبور شما را به صورت مجدد تنظیم کنیم. آیا مایل هستید تا حساب کاربری شما را پاک کنیم و دوباره از ابتدا شروع کنیم؟
اخطار: هویت های شما، مخاطبان شما و پیام های شما برای همیشه از بین خواهند رفت.</string>
<string name="startup_failed_notification_title">برایر نمی تواند شروع به کار کند.</string>
<string name="startup_failed_notification_text">برای اطلاعات بیشتر کلیک کنید</string>
<string name="startup_failed_activity_title">خطا در شروع برایر</string>
<string name="startup_failed_db_error">به دلایلی، دیتابیس برایر شما خراب شده و قابل اصلاح نیست. حساب کاربری شما، داده های شما و تمام مخاطبان شما از بین رفته اند. متاسفانه، شما یا باید برایر را دوباره نصب کنید یا یک حساب کاربری جدید با انتخاب \'رمز عبور ام را فراموش کرده ام\' انتخاب کنید.</string>
<string name="startup_failed_data_too_old_error">حساب کاربری شما با یک نسخه قدیمی از این برنامه ایجاد شده است و به همین خاطربا این نسخه نمی تواند باز شود. شما باید نسخه قدیمی را دوباره نصب کنید یا یک حساب کاربری جدید با \"رمز عبور ام را فراموش کرده ام\" در پرامپت رمز عبور ایجاد کنید.</string>
<string name="startup_failed_data_too_new_error">این نسخه از برنامه قدیمی می باشد. لطفا به آخرین نسخه از برنامه ارتقاء داده و دوباره سعی کنید.</string>
<string name="startup_failed_service_error">برایر نمی تواند یک پلاگین ضروری را اجراء کند. نصب دوباره برایر معمولا این مشکل را حل میکند. هرچند، توجه داشته باشید که حساب کاربری و تمام داده های مرتبط با آن را از دست خواهید داد از آنجایی که برایر از هیچ سرور مرکزی برای ذخیره داده های شما استفاده نمی کند.</string>
<plurals name="expiry_warning">
<item quantity="one">این یک نسخه آزمایشی از برایر می باشد. حساب کاربری شما در %d روز آینده به پایان می رسد و امکان تمدید آن وجود نخواهد داشت.</item>
<item quantity="other">این یک نسخه آزمایشی از برایر می باشد. حساب کاربری شما در %d روز آینده به پایان می رسد و امکان تمدید آن وجود نخواهد داشت.</item>
</plurals>
<string name="expiry_update">تاریخ اتمام آزمایش افزایش یافته است. حساب کاربری شما در %d روز آینده به پایان می رسد.</string>
<string name="expiry_date_reached">این نرم افزار منقضی شده است.
بابت تست از شما سپاسگزاریم.</string>
<string name="download_briar">برای اینکه به استفاده خود از برایر ادامه دهید، لطفا نسخه ۱.۰ را دانلود کنید.</string>
<string name="create_new_account">لازم است تا یک حساب کاربری جدید ایجاد کنید، می توانید از نام مستعار یکسان برای حساب های کاربری استفاده کنید.</string>
<string name="download_briar_button">دانلود برایر 1.0</string>
@@ -52,6 +68,7 @@
<string name="transport_bt">بلوتوث</string>
<string name="transport_lan">وای فای</string>
<!--Notifications-->
<string name="ongoing_notification_title">وارد برایر شد</string>
<string name="ongoing_notification_text">برای باز کردن برایر کلیک کنید.</string>
<plurals name="private_message_notification_text">
<item quantity="one">%dپیام خصوصی جدید</item>
@@ -88,10 +105,14 @@
<string name="no_data">داده ای موجود نمی باشد</string>
<string name="ellipsis"></string>
<string name="text_too_long">متن وارد شده بیش از حد طولانی می باشد</string>
<string name="show_onboarding">نمایش پنجره راهنما</string>
<string name="fix">اصلاح</string>
<string name="help">راهنما</string>
<string name="sorry">ببخشید</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">مخاطبی برای نشان دادن وجود ندارد
روی علامت + برای افزودن مخاطب کلیک کنید</string>
<string name="date_no_private_messages">هیچ پیامی موجود نیست</string>
<string name="no_private_messages">هیچ پیامی برای نشان دادن وجود ندارد</string>
<string name="message_hint">نوشتن پیام</string>
@@ -106,9 +127,22 @@
<string name="continue_button">ادامه</string>
<string name="connection_failed">اتصال ناموفق بود</string>
<string name="try_again_button">دوباره سعی کنید</string>
<string name="waiting_for_contact_to_scan">انتظار برای اسکن و اتصال مخاطبu2026\</string>
<string name="exchanging_contact_details">تبادیل جزییات مخاطبu2026\</string>
<string name="contact_added_toast">مخاطب اضافه شد: %s</string>
<string name="contact_already_exists">مخاطب %s از قبل وجود دارد</string>
<string name="contact_exchange_failed">تبادل مخاطب با خطا مواجه شد</string>
<string name="qr_code_invalid">کد QR نامعتبر می باشد</string>
<string name="qr_code_unsupported">کد QR که شما سعی دارید اسکن کنید متعلق به یک نسخه قدیمی از %s میباشد که دیگر پشتیبانی نمی شود.
لطفا مطمئن شوید که هردوی شما از آخرین نسخه استفاده میکنید و دوباره امتحان کنید.</string>
<string name="camera_error">خطای دوربین</string>
<string name="connecting_to_device">اتصال به دستگاهu2026\</string>
<string name="authenticating_with_device">تصدیق سازی با دستگاه u2026\</string>
<string name="connection_aborted_local">اتصال قطع شد! این میتواند نشان دهنده این باشد که شخصی قصد دارد در اتصال شما اختلال ایجاد کند</string>
<string name="connection_aborted_remote">اتصال توسط مخاطب شما بی نتیجه ماند! این ممکن است نشان دهنده این باشد که شخصی سعی دارد تا در اتصال شما اختلال ایجاد کند</string>
<!--Introductions-->
<string name="introduction_onboarding_title">معرفی مخاطبان</string>
<string name="introduction_onboarding_text">شما می توانید مخاطبان خود را به یکدیگر معرفی کنید، در این صورت دیگر نیازی به دیدار حضوری برای اتصال روی برایر نمی باشد.</string>
<string name="introduction_activity_title">انتخاب مخاطب</string>
<string name="introduction_not_possible">شما همین الان یک معرفی در مرحله انجام با این مخاطبان دارید. لطفا اجازه دهید تا این معرفی به پایان برسد. اگر شما و یا مخاطبانتان به ندرت آنلاین هستید، ممکن است کمی زمان ببرد.</string>
@@ -118,6 +152,9 @@
<string name="introduction_sent">معرفی شما فرستاده شد.</string>
<string name="introduction_error">خطایی در معرفی کردن رخ داده است.</string>
<string name="introduction_response_error">خطا در هنگام پاسخ به معرفی</string>
<string name="introduction_request_sent">شما میخواهید %1$s را به %2$s معرفی کنید.</string>
<string name="introduction_request_received">%1$s مایل است شما را به %2$s کند. آیا میخواهید %2$s را به لیست مخاطبانتان اضافه کنید؟</string>
<string name="introduction_request_exists_received">%1$s مایل است شما را به %2$s معرفی کند، اما %2$s از قبل جزء لیست مخاطبان شما می باشد. از آنجایی که %1$s ممکن است از این موضوع خبر نداشته باشد، شما هم چنان میتوانید پاسخ دهید:</string>
<string name="introduction_request_answered_received">%1$s میخواهد شما را به %2$s معرفی کند.</string>
<string name="introduction_response_accepted_sent">شما معرفی به %1$sرا پذیرفتید.</string>
<string name="introduction_response_accepted_sent_info">قبل از اضافه شدن %1$s به مخاطبان شما، آن ها باید معرفی را بپذیرند. این شاید کمی زمان ببرد.</string>
@@ -130,6 +167,9 @@
<item quantity="other">%d مخاطب جدید افزوده شد.</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">هیچ گروهی برای نمایش وجود ندارد
روی آیکون + برای ایجاد یک گروه کلیک کنید، یا از مخاطبان خود بخواهید تا گروهی را با شما به اشتراک بگذارند</string>
<string name="groups_created_by">ایجاد شده توسط %s</string>
<plurals name="messages">
<item quantity="one">%d پیام</item>
@@ -155,13 +195,24 @@
<string name="groups_leave_dialog_message">مطمئن هستید که میخواهید این گروه را ترک کنید؟</string>
<string name="groups_dissolve">انحلال گروه</string>
<string name="groups_dissolve_dialog_title">تایید انحلال گروه</string>
<string name="groups_dissolve_dialog_message">آیا از انحلال این گروه اطمینان دارید؟
سایر اعضاء قادر نخواهند بود تا مکالمات خود را ادامه دهند و ممکن است آخرین پیام ها را دریافت نکنند.</string>
<string name="groups_dissolve_button">انحلال</string>
<string name="groups_dissolved_dialog_title">گروه انحلال یافت</string>
<string name="groups_dissolved_dialog_message">ایجاد کننده این گروه آن را منحل کرده است.
شما از این به بعد قادر نخواهید بود پیام های دیگری در گروه بنویسید و ممکن است تمام پست هایی که نوشته شده اند را دریافت نکنید.</string>
<!--Private Group Invitations-->
<string name="groups_invitations_title">دعوت نامه های گروه</string>
<string name="groups_invitations_invitation_sent">شما %1$s را برای عضویت به گروه \"%2$s\" دعوت کردید.</string>
<string name="groups_invitations_invitation_received">%1$s شما را دعوت کرده تا عضو گروه \"%2$s\" شوید.</string>
<string name="groups_invitations_joined">عضو گروه شد</string>
<string name="groups_invitations_declined">دعوت نامه گروه رد شد</string>
<plurals name="groups_invitations_open">
<item quantity="one">%d دعوتنامه سرگشاده</item>
<item quantity="other">%d دعوت نامه سرگشاده</item>
</plurals>
<string name="groups_invitations_response_accepted_sent">شما دعوت نامه گروه از %s را پذیرفتید.</string>
<string name="groups_invitations_response_declined_sent">شما دعوت نامه گروه از %s را رد کردید.</string>
<string name="groups_invitations_response_accepted_received">%sدعوت نامه گروه را پذیرفت.</string>
@@ -169,7 +220,17 @@
<string name="sharing_status_groups">فقط سازنده گروه می تواند اعضای جدید را به گروه دعوت کند. در پایین تمام اعضای فعلی گروه آمده است.</string>
<!--Private Groups Revealing Contacts-->
<string name="groups_reveal_contacts">نشان دادن مخاطبان</string>
<string name="groups_reveal_dialog_message">شما میتوانید مخاطبان را به تمام اعضای فعلی و آینده این گروه نشان دهید.
نشان دادن مخاطبان اتصال شما به گروه را سریع تر و قابل اطمینان تر میکند، چراکه میتوانید با مخاطبان نشان داده شده ارتباط برقرار کنید حتی در موقعی که گروه آفلاین می باشد.</string>
<string name="groups_reveal_visible">ارتباط مخاطب برای گروه قابل دیدن می باشد</string>
<string name="groups_reveal_visible_revealed_by_us">ارتباط مخاطب برای گروه قابل دیدن می باشد (آشکار شده توسط شما)</string>
<string name="groups_reveal_visible_revealed_by_contact">ارتباط مخاطب برای گروه قابل یدن می باشد (آشکار شده توسط %s)</string>
<string name="groups_reveal_invisible">ارتباط مخاطب برای گروه قابل دیدن نمی باشد</string>
<!--Forums-->
<string name="no_forums">هیچ فرومی برای نمایش وجود ندارد
روی آیکون + برای ایجاد فروم کلیک کنید یا از مخاطبانتان بخواهید تا با شما فروم به اشتراک بگذارند</string>
<string name="create_forum_title">ایجاد فروم</string>
<string name="choose_forum_hint">انتخاب یک نام برای فروم</string>
<string name="create_forum_button">ایجاد فروم</string>
@@ -180,28 +241,48 @@
<item quantity="one">%d پست</item>
<item quantity="other">%d پست</item>
</plurals>
<string name="forum_new_entry_posted">ثبت فروم پست شد</string>
<string name="forum_new_message_hint">ثبت جدید</string>
<string name="forum_message_reply_hint">پاسخ جدید</string>
<string name="btn_reply">پاسخ</string>
<string name="forum_leave">ترک فروم</string>
<string name="dialog_title_leave_forum">تأیید ترک فروم</string>
<string name="dialog_message_leave_forum">آیا اطمینان دارید که میخواهید این فروم را ترک کنید؟
هر مخاطبی که این فروم را با آنها به اشتراک گذاشته اید ممکن است آپدیت دیگری دریافت نکنند.</string>
<string name="dialog_button_leave">ترک</string>
<string name="forum_left_toast">ترک فروم</string>
<!--Forum Sharing-->
<string name="forum_share_button">اشتراک گذاری فروم</string>
<string name="contacts_selected">مخاطبان انتخاب شدند</string>
<string name="activity_share_toolbar_header">انتخاب مخاطبان</string>
<string name="no_contacts_selector">هیچ مخاطبی برای نمایش دادن وجود ندارد
لطفا بعد از افزودن مخاطب برگردید.</string>
<string name="forum_shared_snackbar">فروم با مخاطبان انتخاب شده به اشتراک گذاشته شد</string>
<string name="forum_share_message">افزودن یک پیام (اختیاری)</string>
<string name="forum_share_error">خطایی با اشتراک گذاری این فروم رخ داد</string>
<string name="forum_invitation_received">%1$s فروم \"%2$s\" را با شما به اشتراک گذاشته است.</string>
<string name="forum_invitation_sent">شما فروم \"%1$s\" را با %2$s به اشتراک گذاشتید.</string>
<string name="forum_invitations_title">دعوت نامه های فروم</string>
<string name="forum_invitation_exists">شما دعوت به این فروم را پذیرفته بودید.
پذیرفتن دعوت نامه های بیشتر باعث می شود تا اتصال شما به فروم سریع تر و قابل اطمینان تر شود.</string>
<string name="forum_joined_toast">عضو فروم شد</string>
<string name="forum_declined_toast">دعوت نامه رد شد</string>
<string name="shared_by_format">به اشتراک گذاشته شده توسط %s</string>
<string name="forum_invitation_already_sharing">در حال به اشتراک گذاری</string>
<string name="forum_invitation_response_accepted_sent">شما دعوت نامه فروم از %s را پذیرفتید.</string>
<string name="forum_invitation_response_declined_sent">شما دعوت نامه فروم از %s را رد کردید.</string>
<string name="forum_invitation_response_accepted_received">%s دعوت نامه فروم را پذیرفت.</string>
<string name="forum_invitation_response_declined_received">%s دعوت نامه فروم را رد کرد.</string>
<string name="sharing_status">به اشتراک گذاری وضعیت</string>
<string name="sharing_status_forum">هر عضو فروم می تواند آن را با دیگر مخاطبان خود به اشتراک بگذارد. شما این فروم را با مخاطبان زیر به اشتراک می گذارید. ممکن است اعضای دیگری هم باشند که شما نمی توانید آن ها را ببینید.</string>
<string name="shared_with">به اشتراک گذاشته شده با %1$d (%2$d نفر آنلاین)</string>
<plurals name="forums_shared">
<item quantity="one"> %d فروم به اشتراک گذاشته شده توسط مخاطبان</item>
<item quantity="other">%d فروم به اشتراک گذاشته شده توسط مخاطبان</item>
</plurals>
<string name="nobody">هیچکس</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">هیچ پستی برای نشان دادن وجود ندارد</string>
@@ -211,12 +292,25 @@
<string name="blogs_publish_blog_post">انتشار</string>
<string name="blogs_blog_post_created">پست بلاگ ایجاد شد</string>
<string name="blogs_blog_post_received">پست جدید بلاگ دریافت شد</string>
<string name="blogs_blog_post_scroll_to">حرکت به</string>
<string name="blogs_feed_empty_state">هیچ پستی برای نشان دادن وجود ندارد
پست های مخاطبان و بلاگ هایی که مشترک آن ها هستید اینجا نشان داده خواهند شد
روی آیکون خودکار برای ایجاد یک پست کلیک کنید</string>
<string name="blogs_remove_blog">حذف بلاگ</string>
<string name="blogs_remove_blog_dialog_message">آیا اطمینان دارید که میخواهید این بلاگ را حذف کنید؟
پست ها از روی دستگاه شما حذف خواهند شد اما از روی دستگاه سایر افراد حذف نخواهد شد.
هر مخاطبی که شما این بلاگ را با آن ها به اشتراک گذاشته اید ممکن است از این به بعد آپدیتی دریافت نکند.</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>
<!--Blog Sharing-->
<string name="blogs_sharing_share">به اشتراک گذاری بلاگ</string>
<string name="blogs_sharing_error">خطایی با اشتراک گذاری این بلاگ وجود داشت.</string>
<string name="blogs_sharing_button">به اشتراک گذاری بلاگ</string>
<string name="blogs_sharing_snackbar">بلاگ با مخاطبان انتخاب شده به اشتراک گذاشته شد</string>
<string name="blogs_sharing_response_accepted_sent">شما دعوت نامه بلاگ از طرف %s را پذیرفتید.</string>
@@ -226,8 +320,12 @@
<string name="blogs_sharing_invitation_received">%1$s بلاگ \"%2$s\" را با شما به اشتراک گذاشت.</string>
<string name="blogs_sharing_invitation_sent">شما بلاگ \"%1$s\" را با %2$s به اشتراک گذاشته اید.</string>
<string name="blogs_sharing_invitations_title">دعوت نامه های بلاگ</string>
<string name="blogs_sharing_joined_toast">به اشتراک بلاگ درآمد</string>
<string name="blogs_sharing_declined_toast">دعوت نامه رد شد</string>
<string name="sharing_status_blog">هرکسی که به اشتراک یک بلاگ در بیاید می تواند آن را با مخاطبان خود به اشتراک بگذارد. شما این بلاگ را با مخاطبان زیر به اشتراک میگذارید. ممکن است مشترکان دیگری هم باشند که شما نتوانید آن ها را ببینید.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">وارد کردن خوراک RSS</string>
<string name="blogs_rss_feeds_import_button">وارد کردن</string>
<string name="blogs_rss_feeds_import_hint">آدرس خوراک RSS را وارد کنید</string>
<string name="blogs_rss_feeds_import_error">متاسفیم! وارد کردن خوراک شما با خطا مواجه شده است.</string>
<string name="blogs_rss_feeds_manage">مدیریت خوراک های RSS</string>
@@ -235,8 +333,16 @@
<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_dialog_message">آیا مطمئن هستید که میخواهید این خوراک را حذف کنید؟
پست ها از دستگاه شما پاک خواهند شد اما روی دستگاه سایر افراد باقی خواهند ماند
هر مخاطبی که با آن این خوراک را به اشتراک گذاشته اید ممکن است دیگر آپدیت دریافت نکند.</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 برای نمایش وجود ندارد
برای وارد کردن خوراک روی آیکون + کلیک کنید</string>
<string name="blogs_rss_feeds_manage_error">مشکلی با بارگذاری فیدهای شما وجود داشت. لطفا بعدا امتحان کنید.</string>
<!--Settings Network-->
<string name="network_settings_title">شبکه ها</string>
@@ -254,24 +360,36 @@
<string name="choose_new_password">رمز عبور جدید خود را انتخاب کنید:</string>
<string name="confirm_new_password">رمز عبور جدید خود را تأیید کنید:</string>
<string name="password_changed">رمز عبور تغییر کرده است</string>
<string name="panic_setting">برپایی دکمه هراس</string>
<string name="panic_setting_title">دکمه هراس</string>
<string name="panic_setting_hint">تنظیم نحوه واکنش برایر هنگام فعال سازی دکمه هراس</string>
<string name="panic_app_setting_title">برنامه دکمه هراس</string>
<string name="unknown_app">یک برنامه ناشناخته</string>
<string name="panic_app_setting_summary">هیچ برنامه تنظیم نشده است</string>
<string name="panic_app_setting_none">هیچکدام</string>
<string name="dialog_title_connect_panic_app">تایید برنامه هراس</string>
<string name="dialog_message_connect_panic_app">آیا مطمئن هستید که میخواهید به %1$s اجازه دهید تا باعث عملیات مخرب دکمه هراس بشود؟</string>
<string name="lock_setting_title">خروج</string>
<string name="lock_setting_summary">در صورت کلیک بر روی کلید هراس از برایر خارج شو</string>
<string name="purge_setting_title">حذف حساب کاربری</string>
<string name="purge_setting_summary">پاک کردن حساب کاربری برایر شما در صورتی که دکمه هراس فشار داده شود. اخطار: این باعث پاک شدن دائمی تمام هویت ها، مخاطبان و پیام های شما خواهد شد</string>
<string name="uninstall_setting_title">پاک کردن برایر</string>
<string name="uninstall_setting_summary">این نیازمند تایید دستی در موعد هراس میباشد</string>
<!--Settings Notifications-->
<string name="notification_settings_title">نوتیفیکیشن ها</string>
<string name="notify_private_messages_setting_title">پیام های خصوصی</string>
<string name="notify_private_messages_setting_summary">نمایش هشدار برای پیام های خصوصی</string>
<string name="notify_private_messages_setting_summary_26">تنظیم هشدار برای پیام های شخصی</string>
<string name="notify_group_messages_setting_title">پیام های گروه</string>
<string name="notify_group_messages_setting_summary">نمایش هشدار برای پیام های گروه</string>
<string name="notify_forum_posts_setting_title">پست های فروم</string>
<string name="notify_forum_posts_setting_summary">نمایش هشدار برای پست های فروم</string>
<string name="notify_group_messages_setting_summary_26">تنظیم هشدار برای پیام های گروه</string>
<string name="notify_forum_posts_setting_title">پست های تالار گفتمان</string>
<string name="notify_forum_posts_setting_summary">نمایش هشدار برای پست های تالار گفتمان</string>
<string name="notify_forum_posts_setting_summary_26">تنظیم هشدار برای پست های تالار گفتمان</string>
<string name="notify_blog_posts_setting_title">پست های بلاگ</string>
<string name="notify_blog_posts_setting_summary">نمایش هشدار برای پست های بلاگ</string>
<string name="notify_blog_posts_setting_summary_26">تنظیم هشدار برای پست های بلاگ</string>
<string name="notify_vibration_setting">لرزش</string>
<string name="notify_lock_screen_setting_title">قفل صفحه</string>
<string name="notify_lock_screen_setting_summary">نمایش نوتیفیکشن روی صفحه قفل</string>
<string name="notify_sound_setting">صدا</string>
@@ -297,6 +415,8 @@
<string name="describe_crash">توضیح دهید چه اتفاقی افتاد (اختیاری)</string>
<string name="enter_feedback">بازخورد خود را وارد کنید</string>
<string name="optional_contact_email">آدرس ایمیل شما (اختیاری)</string>
<string name="include_debug_report_crash">قرار دادن داده های ناشناس مربوط به خرابی</string>
<string name="include_debug_report_feedback">قرار دادن داده های ناشناس درباره این دستگاه</string>
<string name="could_not_load_report_data">امکان بارگذاری داده های گزارش وجود ندارد.</string>
<string name="send_report">ارسال گزارش</string>
<string name="close">بستن</string>
@@ -304,9 +424,20 @@
<!--Sign Out-->
<string name="progress_title_logout">خروج از برایر...</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">قرارگیری صفحه شناسایی شد</string>
<string name="screen_filter_body">برنامه ای دیگری روی برایر قرار گرفته است. برای حفاظت از امنیت شما، برایر پاسخگو به لمس های صفحه نمایش تا زمانی که برنامه دیگری بالای آن قرار دارد نخواهد بود.
این برنامه ها ممکن است روی برایر قرار گرفته باشند:
%1$s</string>
<string name="screen_filter_allow">به این برنامه ها اجازه بده تا روی برایر قرار بگیرند</string>
<!--Permission Requests-->
<string name="permission_camera_title">دسترسی به دوربین</string>
<string name="permission_camera_request_body">برای اسکن کردن کد QR دسترسی به دوربین لازم است.</string>
<string name="permission_camera_denied_body">شما دسترسی به دوربین را رد کرده اید، اما افزودن مخاطب نیاز به دوربین دارد.
لطفا اجازه دسترسی را بدهید.</string>
<string name="permission_camera_denied_toast">اجازه دسترسی به دوربین پذیرفته نشد</string>
<string name="qr_code">کد QR</string>
<string name="show_qr_code_fullscreen">نمایش کد QR به صورت فول اسکرین</string>
</resources>

View File

@@ -124,6 +124,7 @@
<string name="contact_already_exists">Yhteystieto %s on jo olemassa</string>
<string name="contact_exchange_failed">Yhteystietojen vaihto epäonnistui</string>
<string name="qr_code_invalid">QR koodi on virheellinen</string>
<string name="qr_code_unsupported">Skannaamasi QR-koodi kuuluu %s:in vanhaan versioon, jonka tuki on loppunut.\n\nVarmista että kumpikin teistä käyttää uusinta versiota ja yritä uudelleen.</string>
<string name="camera_error">Kameravirhe</string>
<string name="connecting_to_device">Yhdistetään laitteeseen\u2026</string>
<string name="authenticating_with_device">Tunnistaudutaan laitteen kanssa\u2026</string>
@@ -144,6 +145,7 @@
<string name="introduction_request_exists_received">%1$s on pyytänyt, että sinut esitellään käyttäjälle %2$s, mutta %2$s on jo yhteystiedoissasi. %1$s ei välttämättä tiedä tätä, joten voit silti vastata:</string>
<string name="introduction_request_answered_received">%1$s on pyytänyt, että sinut esitellään käyttäjälle %2$s.</string>
<string name="introduction_response_accepted_sent">Olet hyväksynyt esittelyn käyttäjälle %1$s.</string>
<string name="introduction_response_accepted_sent_info">Ennen kuin %1$s voidaan lisätä sinun yhteystietoihin, hänen täytyy myös hyväksyä esittely. Tämä voi viedä vähän aikaa.</string>
<string name="introduction_response_declined_sent">Olet kieltäytynyt esittelystä käyttäjälle %1$s.</string>
<string name="introduction_response_accepted_received">%1$s hyväksyi esittelyn käyttäjälle %2$s.</string>
<string name="introduction_response_declined_received">%1$s kieltäytyi esittelystä käyttäjälle %2$s.</string>
@@ -263,6 +265,7 @@
<string name="blogs_blog_post_created">Blogikirjoitus julkaistu</string>
<string name="blogs_blog_post_received">Uusi blogikirjoitus vastaanotettu</string>
<string name="blogs_blog_post_scroll_to">Vieritä kohtaan</string>
<string name="blogs_feed_empty_state">Ei kirjoituksia\n\nYhteyshenkilöitesi ja seuraamiesi blogien kirjoitukset tulevat näkymään tässä\n\nNapauta kynää kirjoittaaksesi jotain</string>
<string name="blogs_remove_blog">Poista blogi</string>
<string name="blogs_remove_blog_dialog_message">Oletko varma, että haluat poistaa tämän blogin?\n\nKirjoitukset poistuvat sinun laitteelta, mutta ei muiden laitteilta.\n\nKäyttäjät joiden kanssa olet jakanut tämän blogin eivät välttämättä saa uusia päivityksiä.</string>
<string name="blogs_remove_blog_ok">Poista</string>

View File

@@ -1,7 +1,15 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<!--Setup-->
<string name="setup_title">Benvida a Briar</string>
<string name="setup_name_explanation">O seu alcume mostrarase xunto a todas as mensaxes que publique. Pode cambialo tras crear a súa conta.</string>
<string name="setup_next">Seguinte</string>
<string name="setup_password_intro">Escolla un Contrasinal</string>
<string name="setup_password_explanation">A súa conta en Briar gárdase cifrada no seu dispositivo, non na nube. Si esquece o contrasinal ou desinstala Briar, non haberá xeito de recuperar a súa conta.\n\nEscolla un contrasinal longo que sexa difícil de supoñer, algo como catro palabras ao chou, ou dez letras aleatorias, números e símbolos.</string>
<string name="setup_doze_title">Conexións en segundo plano</string>
<string name="setup_doze_intro">Para recibir mensaxes, Briar precisa estar conectada en segundo plano.</string>
<string name="setup_doze_explanation">Para recibir mensaxes, Briar precisa estar conectada en segundo plano. Por favor desactive as optimizacións de batería para que Briar poida permanecer conectada.</string>
<string name="setup_doze_button">Permitir conexións</string>
<string name="choose_nickname">Escolle o teu alcume</string>
<string name="choose_password">Escolle a túa clave</string>
<string name="confirm_password">Confirma a túa clave</string>
@@ -9,6 +17,12 @@
<string name="password_too_weak">A clave é demasiado débil</string>
<string name="passwords_do_not_match">As claves non coinciden</string>
<string name="create_account_button">Crea a conta</string>
<string name="more_info">Máis información</string>
<string name="don_t_ask_again">Non preguntar de novo</string>
<string name="setup_huawei_text">Por favor toque o botón inferior e asegúrese de que Briar está protexida na pantalla \"Apps Protexidas\"</string>
<string name="setup_huawei_button">Protexer Briar</string>
<string name="setup_huawei_help">Si Briar non se engade ao listado de apps protexidas, non poderá funcionar en segundo plano.</string>
<string name="warning_dozed">%s non foi quen de funcionar en segundo plano</string>
<!--Login-->
<string name="enter_password">Introduce a túa clave:</string>
<string name="try_again">Clave incorrecta, tenteo de novo</string>
@@ -17,9 +31,23 @@
<string name="dialog_title_lost_password">Clave perdida</string>
<string name="dialog_message_lost_password">Briar almacena a súa configuración encriptada no dispositivo, non na nube, así que non podemos restabelecer a súa clave. Querrías borrar a túa conta e empezar de novo?\n\nPrecaución: As túas identidades, contactos e mensaxes serán eliminadas de forma permanente.</string>
<string name="startup_failed_notification_title">Briar non puido iniciarse</string>
<string name="startup_failed_notification_text">Toque para máis información.</string>
<string name="startup_failed_activity_title">Fallo de Inicio de Briar</string>
<string name="startup_failed_db_error">Por algún motivo, a súa base de datos de Briar está defectuosa sen remedio. A súa conta, os seus datos e contactos perdéronse. Desgraciadamente, debe reinstalar Briar ou crear unha nova conta escollendo \'Esquecín o meu contrasinal\' cando se lle solicite o contrasinal.</string>
<string name="startup_failed_data_too_old_error">A súa conta foi creada con unha versión anterior da aplicación e non se pode abrir con esta versión. Deberá reinstalar a versión anterior ou ben crear unha nova conta escollendo \'Esquecín o meu contrasinal\' cando se lle solicite o contrasinal.</string>
<string name="startup_failed_data_too_new_error">Esta versión da app é moi antiga. Actualice por favor a última versión e inténteo de novo.</string>
<string name="startup_failed_service_error"> Briar non puido iniciar un complemento necesario. Xeralmente reinstalar Briar resolve este problema. Teña en conta que entón perderá a súa conta e todos os datos asociados a esta pois Briar non está a utilizar servidores centrais para almacenar os seus datos.</string>
<plurals name="expiry_warning">
<item quantity="one">Esta versión de Briar é para probas. A conta caducará en %d día e non se pode anovar.</item>
<item quantity="other">Esta versión de Briar é para probas. A conta caducará en %d días e non se pode anovar.</item>
</plurals>
<string name="expiry_update">A data de caducidade de probas foi alongada. Agora a súa conta caduca en %d días.</string>
<string name="expiry_date_reached">Este software caducou.\nGrazas por probalo!</string>
<string name="download_briar">Para continuar utilizando Briar, descargue por favor a versión 1.0.</string>
<string name="create_new_account">Precisa crear unha nova conta, pero pode utilizar o mesmo alcume.</string>
<string name="download_briar_button">Descargar Briar 1.0</string>
<string name="startup_open_database">Descifrando a Base de datos...</string>
<string name="startup_migrate_database">Actualizando a Base de datos...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Abra a gaveta de navegación</string>
<string name="nav_drawer_close_description">Peche a gaveta de navegación</string>
@@ -36,6 +64,22 @@
<!--Notifications-->
<string name="ongoing_notification_title">Conectado a Briar</string>
<string name="ongoing_notification_text">Toque para abrir Briar</string>
<plurals name="private_message_notification_text">
<item quantity="one">Nova mensaxe privada.</item>
<item quantity="other">%d novas mensaxes privadas.</item>
</plurals>
<plurals name="group_message_notification_text">
<item quantity="one">Nova mensaxe de grupo.</item>
<item quantity="other">%d novas mensaxes de grupo.</item>
</plurals>
<plurals name="forum_post_notification_text">
<item quantity="one">Nova publicación de foro.</item>
<item quantity="other">%d nova publicación de foro.</item>
</plurals>
<plurals name="blog_post_notification_text">
<item quantity="one">Nova publicación de blog.</item>
<item quantity="other">%d novas publicacións de blog.</item>
</plurals>
<!--Misc-->
<string name="now">agora</string>
<string name="show">Amosar</string>
@@ -56,24 +100,68 @@
<string name="ellipsis">...</string>
<string name="text_too_long">O texto inserido e demasiado longo</string>
<string name="show_onboarding">Amosar xanela de axuda</string>
<string name="fix">Arranxar</string>
<string name="help">Axuda</string>
<string name="sorry">Desculpe</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Sen contactos a mostrar\n\nToque na icona + para engadir contactos</string>
<string name="date_no_private_messages">Sen mensaxes</string>
<string name="no_private_messages">Sen mensaxes que mostrar</string>
<string name="message_hint">Esciba unha mensaxe</string>
<string name="delete_contact">Eliminar contacto</string>
<string name="dialog_title_delete_contact">Confirme a eliminación do contacto</string>
<string name="dialog_message_delete_contact">Segura de querer eliminar este contacto e todas as mensaxes que intercambiaron?</string>
<string name="contact_deleted_toast">Contacto eliminado</string>
<!--Adding Contacts-->
<string name="add_contact_title">Engada un contacto</string>
<string name="face_to_face">Debe encontrarse coa persoa que quere engadir como contacto.\n\nEsto evitará que calquera poida suplantala ou ler as súas mensaxes no futuro.</string>
<string name="continue_button">Continuar</string>
<string name="connection_failed">Fallou a conexión</string>
<string name="try_again_button">Tenteo de novo</string>
<string name="waiting_for_contact_to_scan">Agardando polo contacto para escanear e conectar\u2026</string>
<string name="exchanging_contact_details">Intercambiando detalles do contacto\u2026</string>
<string name="contact_added_toast">Contacto engadido: %s</string>
<string name="contact_already_exists">O contacto %s xa existe</string>
<string name="contact_exchange_failed">Fallo no intercambio de contacto</string>
<string name="qr_code_invalid">O código QR non é válido</string>
<string name="qr_code_unsupported">O código QR que intenta escanear pertence a unha versión antiga de %s que xa non está soportada.\n\nPor favor, asegúrese que ambas utilizan a última versión e inténteno de novo.</string>
<string name="camera_error">Fallo na cámara</string>
<string name="connecting_to_device">Conectando co dispositivo\u2026</string>
<string name="authenticating_with_device">Autenticándose co dispositivo\u2026</string>
<string name="connection_aborted_local">Conexión cancelada! Esto podería significar que alguén está intentando entremeterse na súa conexión.</string>
<string name="connection_aborted_remote">Conexión rexeitada polo seu contacto! Esto podería indicar que alguén está intentando interferir na súa conexión</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Presente os seus contactos</string>
<string name="introduction_onboarding_text">Pode presentar aos seus contactos, así non precisan encontrarse en persoa para conectar a través de Briar.</string>
<string name="introduction_activity_title">Escoller contacto</string>
<string name="introduction_not_possible">Xa ten unha presentación en progreso con estes contactos. Por favor, deixe que remate o proceso. Si vostede ou os contactos se conectan con pouca asiduidade esto podería levar algún tempo.</string>
<string name="introduction_message_title">Introducir Contactos</string>
<string name="introduction_message_hint">Engadir unha mensaxe (opcional)</string>
<string name="introduction_button">Preséntese</string>
<string name="introduction_sent">Enviouse a súa presentación.</string>
<string name="introduction_error">Algo fallou ao enviar a presentación.</string>
<string name="introduction_response_error">Fallo respondendo a presentación</string>
<string name="introduction_request_sent">Solicitou presentar %1$s a %2$s.</string>
<string name="introduction_request_received">%1$s solicitou presentala a %2$s. Quere engadir a %2$s ao seu listado de contactos?</string>
<string name="introduction_request_exists_received">%1$s solicitou presentala a %2$s, pero %2$s xa está no seu listado de contactos. Xa que %1$s podería non sabelo, pode responder igualmente:</string>
<string name="introduction_request_answered_received">%1$s solicitou presentala a %2$s.</string>
<string name="introduction_response_accepted_sent">Aceptou a presentación a %1$s.</string>
<string name="introduction_response_accepted_sent_info">Antes de engadir %1$s aos seus contactos, eles precisan aceptar a presentación tamén. Esto podería levar algún tempo.</string>
<string name="introduction_response_declined_sent">Vostede rexeitou a presentación a %1$s.</string>
<string name="introduction_response_accepted_received">%1$s aceptou a presentación a %2$s.</string>
<string name="introduction_response_declined_received">%1$s rexeitou a presentación a %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s di que %2$srexeitou a presentación.</string>
<plurals name="introduction_notification_text">
<item quantity="one">Novo contacto engadido.</item>
<item quantity="other">%d novos contactos engadidos.</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">Sen grupos para mostrar\n\nToque na icona + para crear un grupo, ou pida aos seus contactos que compartan grupos con vostede</string>
<string name="groups_created_by">Creado por %s</string>
<plurals name="messages">
<item quantity="one">%d mensaxe</item>
<item quantity="other">%d mensaxes</item>
</plurals>
<string name="groups_group_is_empty">Este grupo está valeiro</string>
<string name="groups_group_is_dissolved">Este grupo foi disolto</string>
<string name="groups_remove">Eliminar</string>
@@ -81,51 +169,228 @@
<string name="groups_create_group_button">Crear Grupo</string>
<string name="groups_create_group_invitation_button">Enviar Convite</string>
<string name="groups_create_group_hint">Escolla un nome para o seu grupo privado</string>
<string name="groups_invitation_sent">Enviouse o convite de grupo</string>
<string name="groups_message_sent">Mensaxe enviada</string>
<string name="groups_member_list">Lista de Membros</string>
<string name="groups_invite_members">Convidar a Membros</string>
<string name="groups_member_created_you">Vostede creou o grupo</string>
<string name="groups_member_created">%s creou o grupo</string>
<string name="groups_member_joined_you">Vostede ingresou no grupo</string>
<string name="groups_member_joined">%s uníuse ao grupo</string>
<string name="groups_leave">Deixar Grupo</string>
<string name="groups_leave_dialog_title">Confirme que deixa o Grupo</string>
<string name="groups_leave_dialog_message">Está certo de que quere deixar este grupo?</string>
<string name="groups_dissolve">Desfacer o grupo</string>
<string name="groups_dissolve_dialog_title">Confirme a disolución do grupo</string>
<string name="groups_dissolve_dialog_message">Segura de querer desfacer o grupo?\n\nO resto da membresía non poderá continuar a conversa e podería non recibir as últimas mensaxes.</string>
<string name="groups_dissolve_button">Desfacer</string>
<string name="groups_dissolved_dialog_title">O grupo foi desfeito</string>
<string name="groups_dissolved_dialog_message">A persoa creadora do grupo desfíxoo.\n\nXa non poderá escribir mensaxes ao grupo e podería non ter recibido todas as mensaxes que foran escritas.</string>
<!--Private Group Invitations-->
<string name="groups_invitations_title">Convites de Grupo</string>
<string name="groups_invitations_invitation_sent">Convidou a %1$s a unirse ao grupo \"%2$s\".</string>
<string name="groups_invitations_invitation_received">%1$s convidouna a unirse ao grupo \"%2$s\".</string>
<string name="groups_invitations_joined">Xa está no grupo</string>
<string name="groups_invitations_declined">Rexeitou o convite ao grupo</string>
<plurals name="groups_invitations_open">
<item quantity="one">%d convite de grupo aberto</item>
<item quantity="other">%d convites de grupo abertos</item>
</plurals>
<string name="groups_invitations_response_accepted_sent">Aceptou o convite de grupo de %s.</string>
<string name="groups_invitations_response_declined_sent">Rexeitou o convite de grupo de %s.</string>
<string name="groups_invitations_response_accepted_received">%s aceptou o convite de grupo.</string>
<string name="groups_invitations_response_declined_received">%s rexeitou o convite de grupo.</string>
<string name="sharing_status_groups">Só a persoa creadora poder convidar a novos membros ao grupo. Abaixo está a membresía do grupo.</string>
<!--Private Groups Revealing Contacts-->
<string name="groups_reveal_contacts">Revelar contactos</string>
<string name="groups_reveal_dialog_message">Pode escoller si revela os contactos a todos os compoñentes actuais e futuros do grupo.\n\Si revela os contactos a conexión do grupo será máis rápida e fiable, xa que poderá comunicarse cos contactos revelados incluso si a creadora do grupo non está en liña.</string>
<string name="groups_reveal_visible">A relación do contacto é visible para o grupo</string>
<string name="groups_reveal_visible_revealed_by_us">A relación co contacto é visible para o grupo (revelada por vostede)</string>
<string name="groups_reveal_visible_revealed_by_contact">A relación con contacto é visible para o grupo (revelada por %s)</string>
<string name="groups_reveal_invisible">A relación co contacto non é visible para o grupo</string>
<!--Forums-->
<string name="no_forums">Si foros que mostrar\n\nToque na icona + para crear un foro, ou solicite aos contactos que compartan foros con vostede</string>
<string name="create_forum_title">Crear Foro</string>
<string name="choose_forum_hint">Escolla un nome para o seu foro</string>
<string name="create_forum_button">Crear Foro</string>
<string name="forum_created_toast">Foro creado</string>
<string name="no_forum_posts">Sen publicacións que mostrar</string>
<string name="no_posts">Non hai mensaxes</string>
<plurals name="posts">
<item quantity="one">%d publicación</item>
<item quantity="other">%d publicacións</item>
</plurals>
<string name="forum_new_entry_posted">Entrada ao foro enviada</string>
<string name="forum_new_message_hint">Nova Entrada</string>
<string name="forum_message_reply_hint">Nova Resposta</string>
<string name="btn_reply">Respostar</string>
<string name="forum_leave">Deixar foro</string>
<string name="dialog_title_leave_forum">Confirme a saída do foro</string>
<string name="dialog_message_leave_forum">Segura de querer deixar este foro?\n\nTodos os contactos cos que comparteu este foro poderían deixar de recibir actualizacións.</string>
<string name="dialog_button_leave">Saír</string>
<string name="forum_left_toast">Saír do foro</string>
<!--Forum Sharing-->
<string name="forum_share_button">Compartir Foro</string>
<string name="contacts_selected">Contactos selecionados</string>
<string name="activity_share_toolbar_header">Escolla Contactos</string>
<string name="no_contacts_selector">Sen contactos que mostrar\n\nPor favor, volte aquí tras engadir un contacto</string>
<string name="forum_shared_snackbar">Foro compartido cos contactos escollidos</string>
<string name="forum_share_message">Engadir unha mensaxe (opcional)</string>
<string name="forum_share_error">Algo fallou ao compartir este foro.</string>
<string name="forum_invitation_received">%1$s compartiu este foro \"%2$s\" con vostede.</string>
<string name="forum_invitation_sent">Compartiu este foro \"%1$s\" con %2$s.</string>
<string name="forum_invitations_title">Convites a foros</string>
<string name="forum_invitation_exists">Xa aceptara o convite a este foro\n\nAceptando máis convites fará a súa conexión ao foro máis rápida e fiable.</string>
<string name="forum_joined_toast">Uniuse ao foro</string>
<string name="forum_declined_toast">Rexeitou o convite</string>
<string name="shared_by_format">Compartido por %s</string>
<string name="forum_invitation_already_sharing">Xa compartindo</string>
<string name="forum_invitation_response_accepted_sent">Aceptou o convite de %s ao foro.</string>
<string name="forum_invitation_response_declined_sent">Rexeitou o convite de %s ao foro.</string>
<string name="forum_invitation_response_accepted_received">%s aceptou o convite ao foro.</string>
<string name="forum_invitation_response_declined_received">%s rexeitou o convite ao foro.</string>
<string name="sharing_status">Estado do compartido</string>
<string name="sharing_status_forum">Calquera compoñente do foro pode compartilo cos seus contactos. Vostede pode compartir este foro cos seguintes contactos. Pode haber outras persoas que vostede non pode ver.</string>
<string name="shared_with">Compartido con %1$d (%2$d en liña)</string>
<plurals name="forums_shared">
<item quantity="one">%d foro compartido por contactos</item>
<item quantity="other">%d foros compartidos por contactos</item>
</plurals>
<string name="nobody">Ninguén</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Sen publicacións que mostrar</string>
<string name="read_more">ler mais</string>
<string name="blogs_write_blog_post">Escribir entrada de Blog</string>
<string name="blogs_write_blog_post_body_hint">Escriba a súa entrada no blog</string>
<string name="blogs_publish_blog_post">Publicar</string>
<string name="blogs_blog_post_created">Entrada no blog creada</string>
<string name="blogs_blog_post_received">Nova entrada de blog recibida</string>
<string name="blogs_blog_post_scroll_to">Desplazarse a</string>
<string name="blogs_feed_empty_state">Sen entradas que mostrar\n\nAs entradas dos seus contactos e blogs aos que está subscrita aparecerán aquí\n\nToque na icona do lapis para escribir unha entrada</string>
<string name="blogs_remove_blog">Eliminar Blog</string>
<string name="blogs_remove_blog_dialog_message">Segura de que quere eliminar este blog?\n\nAs entradas eliminaranse do seu dispositivo pero non dos dispositivos de outras persoas.\n\nCalquera contacto co que compartira este blog podería deixar de recibir actualizacións.</string>
<string name="blogs_remove_blog_ok">Eliminar</string>
<string name="blogs_blog_removed">Blog eliminado</string>
<string name="blogs_reblog_comment_hint">Engadir un comentario (optativo)</string>
<string name="blogs_reblog_button">Compartir</string>
<!--Blog Sharing-->
<string name="blogs_sharing_share">Compartir blog</string>
<string name="blogs_sharing_error">Algo fallou ao compartir o blog</string>
<string name="blogs_sharing_button">Compartir blog</string>
<string name="blogs_sharing_snackbar">Blog compartido cos contactos escollidos</string>
<string name="blogs_sharing_response_accepted_sent">Aceptou o convite ao blog de %s.</string>
<string name="blogs_sharing_response_declined_sent">Rexeitou o convite ao blog de %s.</string>
<string name="blogs_sharing_response_accepted_received">%s aceptou o convite ao blog.</string>
<string name="blogs_sharing_response_declined_received">%s rexeitou o convite ao blog.</string>
<string name="blogs_sharing_invitation_received">%1$s compartiu o blog \"%2$s\" con vostede.</string>
<string name="blogs_sharing_invitation_sent">Vostede compartiu o blog \"%1$s\" con %2$s.</string>
<string name="blogs_sharing_invitations_title">Convites a Blog</string>
<string name="blogs_sharing_joined_toast">Subscrita ao blog</string>
<string name="blogs_sharing_declined_toast">Rexeitou o convite</string>
<string name="sharing_status_blog">Calquera que se subscribe a un blog pode compartilo cos seus contactos. Pode compartir este blog cos seguintes contactos. Podería haber outras subscritoras que vostede non pode ver.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Importar fonte RSS</string>
<string name="blogs_rss_feeds_import_button">Importar</string>
<string name="blogs_rss_feeds_import_hint">Introduza o URL da fonte RSS</string>
<string name="blogs_rss_feeds_import_error">Lamentámolo! Algo fallou ao importar a fonte.</string>
<string name="blogs_rss_feeds_manage">Xestionar Fontes RSS</string>
<string name="blogs_rss_feeds_manage_imported">Importado:</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">Eliminar fonte</string>
<string name="blogs_rss_remove_feed_dialog_message">Está segura de que quere eliminar esta fonte?\n\nAs entradas eliminaranse do seu dispositivo pero non dos dispositivos de outras persoas\n\nTodas as persoas coas que compartiu esta fonte poderían deixar de recibir actualizacións.</string>
<string name="blogs_rss_remove_feed_ok">Eliminar</string>
<string name="blogs_rss_feeds_manage_delete_error">Non se puido eliminar a fonte!</string>
<string name="blogs_rss_feeds_manage_empty_state">Sen fontes RSS que mostrar\n\nToque na icona + para importar unha fonte</string>
<string name="blogs_rss_feeds_manage_error">Aconteceu un problema ao cargar as súas fontes. Por favor, inténteo máis tarde.</string>
<!--Settings Network-->
<string name="network_settings_title">Redes</string>
<string name="bluetooth_setting">Conectar vía Bluetooth</string>
<string name="bluetooth_setting_enabled">En claquera momento que un contacto estea preto</string>
<string name="bluetooth_setting_disabled">Só ao engadir contactos</string>
<string name="tor_network_setting">Conectar vía Tor</string>
<string name="tor_network_setting_never">Nunca</string>
<string name="tor_network_setting_wifi">Só utilizando Wi-Fi</string>
<string name="tor_network_setting_always">Utilizando Wi-Fi ou datos móbiles</string>
<!--Settings Security and Panic-->
<string name="security_settings_title">Seguridade</string>
<string name="change_password">Cambiar contrasinal</string>
<string name="current_password">Introduza o contrasinal actual:</string>
<string name="choose_new_password">Novo contrasinal:</string>
<string name="confirm_new_password">Confirme o contrasinal:</string>
<string name="password_changed">Cambiou o contrasinal</string>
<string name="panic_setting">Axustes do botón do pánico</string>
<string name="panic_setting_title">Botón do pánico</string>
<string name="panic_setting_hint">Axuste como debe reaccionar Briar cando utilice a app botón do pánico</string>
<string name="panic_app_setting_title">App Botón do pánico</string>
<string name="unknown_app">unha aplicación descoñecida</string>
<string name="panic_app_setting_summary">Non se estableceu unha app</string>
<string name="panic_app_setting_none">Ningún</string>
<string name="dialog_title_connect_panic_app">Confirme a App do pánico</string>
<string name="dialog_message_connect_panic_app">Está segura de querer permitir a %1$s activar accións destrutivas do botón do pánico?</string>
<string name="lock_setting_title">Finalizar sesión</string>
<string name="lock_setting_summary">Desconecte de Briar si o botón do pánico se preme</string>
<string name="purge_setting_title">Eliminar conta</string>
<string name="purge_setting_summary">Elimina a súa conta en Briar si se preme o botón do pánico. Coidado: Esto eliminará permanentemente as súas identidade, contactos e mensaxes</string>
<string name="uninstall_setting_title">Desinstalar Briar</string>
<string name="uninstall_setting_summary">Esto require confirmación manual no evento do pánico</string>
<!--Settings Notifications-->
<string name="notification_settings_title">Notificacións</string>
<string name="notify_private_messages_setting_title">Mensaxes privadas</string>
<string name="notify_private_messages_setting_summary">Mostra alertas para mensaxes privadas</string>
<string name="notify_private_messages_setting_summary_26">Configurar alertas para mensaxes privadas</string>
<string name="notify_group_messages_setting_title">Mensaxes de grupo</string>
<string name="notify_group_messages_setting_summary">Mostra alertas para mensaxes de grupo</string>
<string name="notify_group_messages_setting_summary_26">Configurar alertas para mensaxes de grupo</string>
<string name="notify_forum_posts_setting_title">Entradas no foro</string>
<string name="notify_forum_posts_setting_summary">Mostra alertas para mensaxes nos foros</string>
<string name="notify_forum_posts_setting_summary_26">Configurar alertas para mensaxes nos foros</string>
<string name="notify_blog_posts_setting_title">Entradas no Blog</string>
<string name="notify_blog_posts_setting_summary">Mostra alertas para entradas no blog</string>
<string name="notify_blog_posts_setting_summary_26">Configurar alertas para entradas no blog</string>
<string name="notify_vibration_setting">Vibrar</string>
<string name="notify_lock_screen_setting_title">Bloquear pantalla</string>
<string name="notify_lock_screen_setting_summary">Mostra notificacións na pantalla bloqueada</string>
<string name="notify_sound_setting">Son</string>
<string name="notify_sound_setting_default">Son por omisión</string>
<string name="notify_sound_setting_disabled">Ningún</string>
<string name="choose_ringtone_title">Escolla son</string>
<string name="cannot_load_ringtone">Non se cargou o son</string>
<!--Settings Feedback-->
<string name="feedback_settings_title">Comente</string>
<string name="send_feedback">Envíe comentario</string>
<!--Link Warning-->
<string name="link_warning_title">Aviso de ligazón</string>
<string name="link_warning_intro">Vai abrir a seguinte ligazón nunha aplicación externa.</string>
<string name="link_warning_text">Esto pode utilizarse para identificala. Pense si confía na persoa que lle enviou a ligazón e considere abrila con Orfox.</string>
<string name="link_warning_open_link">Abrir ligazón</string>
<!--Crash Reporter-->
<string name="crash_report_title">Informe de fallo de Briar</string>
<string name="briar_crashed">Lamentámolo, Briar fallou.</string>
<string name="not_your_fault">Non é culpa súa.</string>
<string name="please_send_report">Axúdenos por favor a mellorar Briar enviándonos un informe do fallo.</string>
<string name="report_is_encrypted">Prometemos que o informe está cifrado e enviado con seguridade.</string>
<string name="feedback_title">Comente</string>
<string name="describe_crash">Describa que aconteceu (optativo)</string>
<string name="enter_feedback">Escriba o seu comentario</string>
<string name="optional_contact_email">O seu enderezo e-mail (optativo)</string>
<string name="include_debug_report_crash">Incluír datos anónimos sobre o fallo</string>
<string name="include_debug_report_feedback">Incluír datos anónimos sobre este dispositivo</string>
<string name="could_not_load_report_data">Non se puideron cargar os datos do informe.</string>
<string name="send_report">Enviar informe</string>
<string name="close">Pechar</string>
<string name="dev_report_saved">Informe gardado. Enviarase a seguinte vez que se conecte con Briar.</string>
<!--Sign Out-->
<string name="progress_title_logout">Desconectando de Briar...</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Detectouse unha sobreescrita da pantalla</string>
<string name="screen_filter_body">Outra aplicación estase mostrando enriba de Briar. Para protexer a súa seguridade, Briar non responderá a toques cando outra aplicación está debuxando enriba.\n\nAs seguintes aplicacións poderían estar debuxando enriba:\n\n%1$s</string>
<string name="screen_filter_allow">Permitir a estas aplicación mostrarse enriba</string>
<!--Permission Requests-->
<string name="permission_camera_title">Permiso da cámara</string>
<string name="permission_camera_request_body">Para escanear códigos QR, Briar precisa acceso a cámara.</string>
<string name="permission_camera_denied_body">Denegou o permiso de acceso a cámara, pero é necesario para engadir contactos.\n\nPor favor, considere conceder o permiso.</string>
<string name="permission_camera_denied_toast">Non concedeu o acceso a cámara</string>
<string name="qr_code">Código QR</string>
<string name="show_qr_code_fullscreen">Mostrar o código QR a pantalla completa</string>
</resources>

View File

@@ -42,25 +42,25 @@
<plurals name="private_message_notification_text">
<item quantity="one">הודעה פרטית חדשה.</item>
<item quantity="two">%d הודעות פריטות חדשות.</item>
<item quantity="many"></item>
<item quantity="many">%d הודעות פריטות חדשות.</item>
<item quantity="other">%d הודעות פריטות חדשות.</item>
</plurals>
<plurals name="group_message_notification_text">
<item quantity="one">הודעה קבוצתית חדשה.</item>
<item quantity="two">%d הודעות קבוצתיות חדשות.</item>
<item quantity="many"></item>
<item quantity="many">%d הודעות קבוצתיות חדשות.</item>
<item quantity="other">%d הודעות קבוצתיות חדשות.</item>
</plurals>
<plurals name="forum_post_notification_text">
<item quantity="one">פוסט חדש בפורומים.</item>
<item quantity="two">%d פוסטים חדשים בפורומים.</item>
<item quantity="many"></item>
<item quantity="many">%d פוסטים חדשים בפורומים.</item>
<item quantity="other">%d פוסטים חדשים בפורומים.</item>
</plurals>
<plurals name="blog_post_notification_text">
<item quantity="one">פוסט חדש בבלוגים.</item>
<item quantity="two">%d פוסטים חדשים בבלוגים.</item>
<item quantity="many"></item>
<item quantity="many">%d פוסטים חדשים בבלוגים.</item>
<item quantity="other">%d פוסטים חדשים בבלוגים.</item>
</plurals>
<!--Misc-->
@@ -130,7 +130,7 @@
<plurals name="introduction_notification_text">
<item quantity="one">נוסף איש קשר חדש.</item>
<item quantity="two">נוספו %d אנשי קשר חדשים.</item>
<item quantity="many"></item>
<item quantity="many">נוספו %d אנשי קשר חדשים.</item>
<item quantity="other">נוספו %d אנשי קשר חדשים.</item>
</plurals>
<!--Private Groups-->
@@ -138,7 +138,7 @@
<plurals name="messages">
<item quantity="one">הודעה %d</item>
<item quantity="two">%d הודעות</item>
<item quantity="many"></item>
<item quantity="many">%d הודעות</item>
<item quantity="other">%d הודעות</item>
</plurals>
<string name="groups_group_is_empty">קבוצה זו הינה ריקה</string>
@@ -178,7 +178,7 @@
<plurals name="groups_invitations_open">
<item quantity="one">הזמנה קבוצתית פתוחה %d</item>
<item quantity="two">%dהזמנות קבוצתית פתוחות </item>
<item quantity="many"></item>
<item quantity="many">%dהזמנות קבוצתית פתוחות </item>
<item quantity="other">%dהזמנות קבוצתית פתוחות </item>
</plurals>
<string name="groups_invitations_response_accepted_sent">הסכמתם להזמנה הקבוצתית מ- %s.</string>
@@ -204,7 +204,7 @@
<plurals name="posts">
<item quantity="one">פוסט %d</item>
<item quantity="two">%d פוסטים</item>
<item quantity="many"></item>
<item quantity="many">%d פוסטים</item>
<item quantity="other">%d פוסטים</item>
</plurals>
<string name="forum_new_entry_posted">הפוסט נוצר</string>

View File

@@ -31,7 +31,11 @@
<string name="dialog_title_lost_password">पासवर्ड खो गया</string>
<string name="dialog_message_lost_password">आपका ब्रियर खाता आपके डिवाइस पर एन्क्रिप्ट किया गया है, बादल में नहीं, इसलिए हम आपका पासवर्ड रीसेट नहीं कर सकते। क्या आप अपना खाता हटाना चाहते हैं और फिर से शुरू करना चाहते हैं? \ N \ n सावधानी: आपकी पहचान, संपर्क और संदेश स्थायी रूप से खो जाएंगे</string>
<string name="startup_failed_notification_title">बियर शुरू नहीं हो सका</string>
<string name="startup_failed_notification_text">अधिक जानकारी के लिए टैप करें।</string>
<string name="startup_failed_activity_title">ब्रियर स्टार्टअप विफलता</string>
<string name="startup_failed_db_error">किसी कारण से, आपका ब्रियर डेटाबेस मरम्मत से परे दूषित हो गया है। आपका खाता, आपका डेटा और आपके सभी संपर्क खो गए हैं। दुर्भाग्यवश, आपको पासवर्ड प्रॉम्प्ट पर \'मेरा पासवर्ड भूल गया है\' चुनकर ब्रियर को पुनर्स्थापित करने या एक नया खाता सेट अप करने की आवश्यकता है।</string>
<string name="startup_failed_data_too_old_error">आपका खाता इस ऐप के पुराने संस्करण के साथ बनाया गया था और इस संस्करण के साथ खोला नहीं जा सकता है। आपको या तो पुराने संस्करण को पुनर्स्थापित करना होगा या पासवर्ड प्रॉम्प्ट पर \'मैं अपना पासवर्ड भूल गया हूं\' चुनकर एक नया खाता सेट करना होगा।</string>
<string name="startup_failed_data_too_new_error">ऐप का यह संस्करण बहुत पुराना है। कृपया नवीनतम संस्करण में अपग्रेड करें और पुनः प्रयास करें।</string>
<string name="startup_failed_service_error">ब्रियर एक आवश्यक प्लगइन प्रारंभ करने में असमर्थ था बरिअर को पुनः स्थापित करना आमतौर पर इस समस्या को हल करता है हालांकि, कृपया ध्यान दें कि बियर आपके डेटा को स्टोर करने के लिए केंद्रीय सर्वर का उपयोग नहीं कर रहा है, इसके बाद आप अपने खाता और उसके साथ जुड़े सभी डेटा खो देंगे।</string>
<plurals name="expiry_warning">
<item quantity="one">यह Briar का एक परीक्षण संस्करण है आपका खाता %dदिनों में समाप्त हो जाएगा और नवीनीकरण नहीं किया जा सकता है</item>
@@ -39,6 +43,11 @@
</plurals>
<string name="expiry_update">परीक्षण समाप्ति तिथि बढ़ा दी गई है। आपका खाता अब %d दिनों में समाप्त हो जाएगा</string>
<string name="expiry_date_reached">यह सॉफ्टवेयर समाप्त हो गया है। \n परीक्षण के लिए धन्यवाद!</string>
<string name="download_briar">ब्रियर का उपयोग जारी रखने के लिए, कृपया संस्करण 1.0 डाउनलोड करें।</string>
<string name="create_new_account">आपको एक नया खाता बनाना होगा, लेकिन आप उसी उपनाम का उपयोग कर सकते हैं।</string>
<string name="download_briar_button">ब्रायर 1.0 डाउनलोड करें</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>
@@ -93,8 +102,11 @@
<string name="show_onboarding">सहायता संवाद दिखाएं</string>
<string name="fix">ठीक कर</string>
<string name="help">सहायता</string>
<string name="sorry">माफ़ कीजिये</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">कोई संपर्क दिखाने के लिए \ n \ n टैप + आइकन टैप करने के लिए कोई संपर्क नहीं है</string>
<string name="date_no_private_messages">कोई संदेश नहीं।</string>
<string name="no_private_messages">दिखाने के लिए कोई संदेश नहीं</string>
<string name="message_hint">संदेश लिखें</string>
<string name="delete_contact">संपर्क मिटा दें</string>
<string name="dialog_title_delete_contact">संपर्क हटाने की पुष्टि करें</string>
@@ -112,6 +124,7 @@
<string name="contact_already_exists">संपर्क%s पहले से मौजूद है</string>
<string name="contact_exchange_failed">संपर्क विनिमय विफल</string>
<string name="qr_code_invalid">QR कोड अमान्य है</string>
<string name="qr_code_unsupported">क्यूआर कोड जिसे आप स्कैन करने का प्रयास कर रहे हैं वह पुराने संस्करण से संबंधित है %sजिसका अब समर्थित नहीं है। \ N \ n कृपया सुनिश्चित करें कि आप दोनों नवीनतम संस्करण चला रहे हैं और फिर पुन: प्रयास करें।</string>
<string name="camera_error">कैमरा त्रुटि</string>
<string name="connecting_to_device">उपकरण \ u2026 से कनेक्ट हो रहा है</string>
<string name="authenticating_with_device">डिवाइस के साथ प्रमाणीकरण \ u2026</string>
@@ -121,6 +134,7 @@
<string name="introduction_onboarding_title">अपने संपर्कों का परिचय दें</string>
<string name="introduction_onboarding_text">आप अपने संपर्कों को एक दूसरे से जोड़ सकते हैं, इसलिए उन्हें ब्रियर से जुड़ने के लिए व्यक्तिगत रूप से मिलने की जरूरत नहीं है।</string>
<string name="introduction_activity_title">संपर्क का चयन करें</string>
<string name="introduction_not_possible">इन संपर्कों के साथ आपके पास पहले से ही एक परिचय प्रगति है। कृपया इसे पहले खत्म करने की अनुमति दें। यदि आप या आपके संपर्क शायद ही कभी ऑनलाइन हैं, तो इसमें कुछ समय लग सकता है।</string>
<string name="introduction_message_title">संपर्कों का परिचय</string>
<string name="introduction_message_hint">एक संदेश जोड़ें (वैकल्पिक)</string>
<string name="introduction_button">परिचय करें</string>
@@ -132,6 +146,7 @@
<string name="introduction_request_exists_received">%1$sने आपको%2$s में लाने के लिए कहा है, लेकिन%2$s आपकी संपर्क सूची में पहले से मौजूद है। चूंकि%1$s शायद यह नहीं जान पाए, आप फिर भी जवाब दे सकते हैं:</string>
<string name="introduction_request_answered_received">%1$sने आपको%2$s में पेश करने को कहा है</string>
<string name="introduction_response_accepted_sent">आपने%1$s की शुरूआत स्वीकार कर ली है</string>
<string name="introduction_response_accepted_sent_info">आपके %1$sसंपर्कों में शामिल होने से पहले, उन्हें भी परिचय स्वीकार करने की आवश्यकता है। इसमें कुछ समय लग सकता है।</string>
<string name="introduction_response_declined_sent">आपने%1$s की शुरुआत करने से मना कर दिया</string>
<string name="introduction_response_accepted_received">%1$s%2$s की शुरूआत स्वीकार कर ली</string>
<string name="introduction_response_declined_received">%1$s%2$sकी शुरूआत में गिरावट आई</string>
@@ -141,6 +156,7 @@
<item quantity="other">%dनया संपर्क जोड़ा।</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">कोई समूह दिखाने के लिए \ n \ n समूह बनाने के लिए + आइकन टैप करें, या अपने संपर्कों को समूहों के साथ साझा करने के लिए कहें</string>
<string name="groups_created_by">के द्वारा बनाई गई%s</string>
<plurals name="messages">
<item quantity="one">संदेशों%d</item>
@@ -193,10 +209,12 @@
<string name="groups_reveal_visible_revealed_by_contact">संपर्क संबंध समूह को दिखाई देता है (%s द्वारा पता चला है)</string>
<string name="groups_reveal_invisible">संपर्क संबंध समूह को दिखाई नहीं दे रहा है</string>
<!--Forums-->
<string name="no_forums">कोई फोरम दिखाने के लिए \ n \ n फोरम बनाने के लिए + आइकन टैप करें, या अपने संपर्कों को आपके साथ मंच साझा करने के लिए कहें</string>
<string name="create_forum_title">फोरम बनाएँ</string>
<string name="choose_forum_hint">अपने मंच का नाम चुनें</string>
<string name="create_forum_button">फोरम बनाएँ</string>
<string name="forum_created_toast">फोरम बनाया</string>
<string name="no_forum_posts">दिखाने के लिए कोई पोस्ट नहीं</string>
<string name="no_posts">कोई पोस्ट नहीं</string>
<plurals name="posts">
<item quantity="one">%dपदों</item>
@@ -208,17 +226,23 @@
<string name="btn_reply">जवाब दें</string>
<string name="forum_leave">फोरम छोड़ें</string>
<string name="dialog_title_leave_forum">फोरम छोड़ने की पुष्टि करें</string>
<string name="dialog_message_leave_forum">क्या आप वाकई इस मंच को छोड़ना चाहते हैं? \ N \ n आपके द्वारा इस मंच को साझा करने वाले किसी भी संपर्क के साथ अपडेट प्राप्त करना बंद हो सकता है।</string>
<string name="dialog_button_leave">छोड़ना</string>
<string name="forum_left_toast">वाम मंच</string>
<!--Forum Sharing-->
<string name="forum_share_button">शेयर फ़ोरम</string>
<string name="contacts_selected">संपर्क चयनित</string>
<string name="activity_share_toolbar_header">संपर्क चुनें</string>
<string name="no_contacts_selector">कोई संपर्क दिखाने के लिए \ n \ n संपर्क जोड़ने के बाद यहां वापस आएं</string>
<string name="forum_shared_snackbar">चयनित संपर्कों के साथ फ़ोरम साझा किया गया</string>
<string name="forum_share_message">एक संदेश जोड़ें (वैकल्पिक)</string>
<string name="forum_share_error">इस फ़ोरम को साझा करने में कोई त्रुटि थी।</string>
<string name="forum_invitation_received">%1$sने आपके साथ \"%2$s\" मंच साझा किया है</string>
<string name="forum_invitation_sent">आपने%2$s के साथ \"%1$s\" मंच साझा किया है</string>
<string name="forum_invitations_title">फोरम निमंत्रण</string>
<string name="forum_invitation_exists">आपने पहले से ही इस मंच पर एक निमंत्रण स्वीकार कर लिया है। \ N \ n अधिक आमंत्रण स्वीकार करने से मंच से आपका कनेक्शन तेजी से और अधिक विश्वसनीय हो जाएगा।</string>
<string name="forum_joined_toast">फोरम में शामिल</string>
<string name="forum_declined_toast">आमंत्रण में कमी आई</string>
<string name="shared_by_format">%s द्वारा साझा किया गया</string>
<string name="forum_invitation_already_sharing">पहले से ही साझा करना</string>
<string name="forum_invitation_response_accepted_sent">आपने%s से मंच निमंत्रण स्वीकार कर लिया है</string>
@@ -234,14 +258,19 @@
</plurals>
<string name="nobody">कोई भी नहीं</string>
<!--Blogs-->
<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>
<string name="blogs_publish_blog_post">प्रकाशित करना</string>
<string name="blogs_blog_post_created">ब्लॉग पोस्ट बनाया</string>
<string name="blogs_blog_post_received">नया ब्लॉग पोस्ट प्राप्त हुआ</string>
<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_dialog_message">क्या आप वाकई इस ब्लॉग को हटाना चाहते हैं? \ N \ n पोस्ट आपके डिवाइस से हटा दिए जाएंगे, लेकिन अन्य लोगों के डिवाइस से नहीं। \ N \ 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>
<!--Blog Sharing-->
@@ -256,6 +285,8 @@
<string name="blogs_sharing_invitation_received">%1$sने आपके साथ \"%2$s\" ब्लॉग को साझा किया है</string>
<string name="blogs_sharing_invitation_sent">आपने %1$s को%2$s के साथ साझा किया है</string>
<string name="blogs_sharing_invitations_title">ब्लॉग आमंत्रण</string>
<string name="blogs_sharing_joined_toast">ब्लॉग के लिए सदस्यता लें</string>
<string name="blogs_sharing_declined_toast">आमंत्रण में कमी आई</string>
<string name="sharing_status_blog">जो कोई भी ब्लॉग के लिए सदस्यता लेता है, उसे अपने संपर्कों के साथ साझा कर सकता है आप इस ब्लॉग को निम्नलिखित संपर्कों के साथ साझा कर रहे हैं। ऐसे अन्य सदस्य भी हो सकते हैं जिन्हें आप नहीं देख सकते हैं।</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">आरएसएस फ़ीड आयात करें</string>
@@ -267,8 +298,10 @@
<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_dialog_message">क्या आप वाकई इस फीड को हटाना चाहते हैं? \ N \ n पोस्ट आपके डिवाइस से हटा दिए जाएंगे, लेकिन अन्य लोगों के डिवाइस से नहीं। \ N \ 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">कोई आरएसएस फ़ीड दिखाने के लिए फ़ीड नहीं करता \ n \ n फ़ीड आयात करने के लिए + आइकन टैप करें</string>
<string name="blogs_rss_feeds_manage_error">आपकी फ़ीड लोड करने में एक समस्या थी बाद में पुन: प्रयास करें।</string>
<!--Settings Network-->
<string name="network_settings_title">नेटवर्क</string>
@@ -305,12 +338,16 @@
<string name="notification_settings_title">सूचनाएं</string>
<string name="notify_private_messages_setting_title">निजी संदेश</string>
<string name="notify_private_messages_setting_summary">निजी संदेशों के लिए अलर्ट दिखाएं</string>
<string name="notify_private_messages_setting_summary_26">निजी संदेशों के लिए अलर्ट कॉन्फ़िगर करें</string>
<string name="notify_group_messages_setting_title">समूह संदेश</string>
<string name="notify_group_messages_setting_summary">समूह संदेशों के लिए अलर्ट्स दिखाएं</string>
<string name="notify_group_messages_setting_summary_26">समूह संदेशों के लिए अलर्ट कॉन्फ़िगर करें</string>
<string name="notify_forum_posts_setting_title">फ़ोरम पोस्ट</string>
<string name="notify_forum_posts_setting_summary">फ़ोरम पोस्ट के लिए अलर्ट्स दिखाएं</string>
<string name="notify_forum_posts_setting_summary_26">फोरम पोस्ट के लिए अलर्ट कॉन्फ़िगर करें</string>
<string name="notify_blog_posts_setting_title">वेबदैनिकी डाक</string>
<string name="notify_blog_posts_setting_summary">ब्लॉग पोस्ट के लिए अलर्ट्स दिखाएं</string>
<string name="notify_blog_posts_setting_summary_26">ब्लॉग पोस्ट के लिए अलर्ट कॉन्फ़िगर करें</string>
<string name="notify_vibration_setting">कांपना</string>
<string name="notify_lock_screen_setting_title">लॉक स्क्रीन</string>
<string name="notify_lock_screen_setting_summary">लॉक स्क्रीन पर सूचनाएं दिखाएं</string>
@@ -354,4 +391,6 @@
<string name="permission_camera_request_body">QR कोड को स्कैन करने के लिए, Briar को कैमरे तक पहुंच की आवश्यकता है।</string>
<string name="permission_camera_denied_body">आपने कैमरे तक पहुंच से वंचित किया है, लेकिन संपर्क जोड़ने के लिए कैमरे का उपयोग करने की आवश्यकता है। \ N \ n कृपया पहुंच प्रदान करने पर विचार करें।</string>
<string name="permission_camera_denied_toast">कैमरा अनुमति नहीं दी गई थी</string>
<string name="qr_code">क्यूआर कोड</string>
<string name="show_qr_code_fullscreen">क्यूआर कोड पूर्णस्क्रीन दिखाएं</string>
</resources>

View File

@@ -0,0 +1,369 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<!--Setup-->
<string name="setup_title">Witaj w Briar</string>
<string name="setup_name_explanation">Twoja nazwa użytkownika będzie wyświetlana przy każdej zamieszczonej przez Ciebie treści. Nie można jej zmienić po tworzeniu konta.</string>
<string name="setup_next">Dalej</string>
<string name="setup_password_intro">Wybierz Hasło</string>
<string name="setup_password_explanation">Twoje konto Briar jest przechowywane i szyfrowane na Twoim urządzeniu nie w chmurze. Jeśli zapomnisz hasła lub odinstalujesz Briar, nie ma możliwości aby odzyskać Twoje konto.\n\nWybierz długie hasło, które jest trudne do odgadnięcia, czyli takie z losowymi literami numerami i symbolami.</string>
<string name="setup_doze_title">Połączenia w Tle</string>
<string name="setup_doze_intro">Aby otrzymywać wiadomości, Briar musi utrzymywać połączenie w tle.</string>
<string name="setup_doze_explanation">Aby otrzymywać wiadomości, Briar musi utrzymywać połączenie w tle. Wyłącz optymalizacje baterii aby Briar mógł zostać połączony.</string>
<string name="setup_doze_button">Pozwól na Połączenia</string>
<string name="choose_nickname">Wybierz nazwę użytkownika</string>
<string name="choose_password">Wybierz hasło</string>
<string name="confirm_password">Potwierdź hasło</string>
<string name="name_too_long">Nazwa użytkownika jest zbyt długa</string>
<string name="password_too_weak">Hasło jest zbyt długie</string>
<string name="passwords_do_not_match">Hasła się nie zgadzają</string>
<string name="create_account_button">Utwórz Konto</string>
<string name="more_info">Więcej Informacji</string>
<string name="don_t_ask_again">Nie pytaj ponownie</string>
<string name="setup_huawei_text">Proszę dotknąć przycisku poniżej i upewnić się, że Briar jest na liście chronionych aplikacji.</string>
<string name="setup_huawei_button">Chroń Briar</string>
<string name="setup_huawei_help">Jeśli Briar nie będzie na liście chronionych aplikacji nie będzie miał możliwości aby działać w tle.</string>
<string name="warning_dozed">%s nie był wstanie działać w tle</string>
<!--Login-->
<string name="enter_password">Wpisz swoje hasło:</string>
<string name="try_again">Złe hasło, spróbuj ponownie</string>
<string name="sign_in_button">Zaloguj Się</string>
<string name="forgotten_password">Przypomnij hasło</string>
<string name="dialog_title_lost_password">Nie pamiętam hasła</string>
<string name="dialog_message_lost_password">Twoje konto Briar jest zaszyfrowane na Twoim urządzeniu nie w chmurze, więc nie będzie można zresetować Twojego hasła. Czy chcesz usunąć swoje konto i stworzyć nowe?\n\nUwaga: Twoje hasła, kontakty i wiadomości będą utracone.</string>
<string name="startup_failed_notification_title">Briar nie mógł się uruchomić</string>
<string name="startup_failed_notification_text">Więcej informacji</string>
<string name="startup_failed_activity_title">Briar nie mógł się uruchomić</string>
<string name="startup_failed_db_error">Niestety baza danych Briar została uszkodzona i nie można jej naprawić. Twoje konto, dane i kontakty zostały utracone. Niestety musisz przeinstalować Briar lub utworzyć nowe konto wybierając opcje \"Zapomniałem hasła\".</string>
<string name="startup_failed_data_too_old_error">Twoje konto zostało stworzone przez starszą wersję aplikacji i nie może zostać otwarte w tej wersji. Musisz zainstalować starszą wersję aplikacji lub utworzyć nowe konto klikając \"Zapomniałem hasła\".</string>
<string name="startup_failed_data_too_new_error">Ta wersja aplikacji jest zbyt stara. Proszę zaktualizować program do najnowszej wersji i spróbować ponownie.</string>
<string name="startup_failed_service_error">Briar nie był w stanie uruchomić wybranego rozszerzenia. Przeinstalowanie Briar zwykle rozwiązuje ten problem. Pamiętaj jednak, że stracisz wtedy swoje konto i wszystkie powiązanie z nim dane, gdyż Briar nie używa centralnych serwerów by przechowywać dane.</string>
<plurals name="expiry_warning">
<item quantity="one">To jest testowa wersja Briar. Twoje konto wygaśnie za %d dzień i nie będzie odnowione.</item>
<item quantity="few">To jest testowa wersja Briar. Twoje konto wygaśnie za %d dni i nie będzie odnowione.</item>
<item quantity="many">To jest testowa wersja Briar. Twoje konto wygaśnie za %d dni i nie będzie odnowione.</item>
<item quantity="other">To jest testowa wersja Briar. Twoje konto wygaśnie za %d dni i nie będzie odnowione.</item>
</plurals>
<string name="expiry_update">Okres testowania został wydłużony. Twoje konto wygaśnie za %d dni.</string>
<string name="expiry_date_reached">Ten program wygasł.\nDziękujemy za testy!</string>
<string name="download_briar">Aby dalej korzystać z Briar, proszę pobrać wersję 1.0.</string>
<string name="create_new_account">Będzie potrzeba stworzenia nowego konta, ale możesz użyć takiej samej nazwy użytkownika.</string>
<string name="download_briar_button">Pobierz Briar 1.0</string>
<string name="startup_open_database">Deszyfruję Bazę Danych...</string>
<string name="startup_migrate_database">Aktualizuję Bazę Danych...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Otwórz panel nawigacji</string>
<string name="nav_drawer_close_description">Zamknij panel nawigacji</string>
<string name="contact_list_button">Kontakty</string>
<string name="groups_button">Grupy Prywatne</string>
<string name="forums_button">Fora</string>
<string name="blogs_button">Blogi</string>
<string name="settings_button">Ustawienia</string>
<string name="sign_out_button">Wyloguj się</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">Zalogowany w Briar</string>
<string name="ongoing_notification_text">Dotknij aby otworzyć Briar</string>
<plurals name="private_message_notification_text">
<item quantity="one">Nowa prywatna wiadomość</item>
<item quantity="few">%d prywatnych wiadomości.</item>
<item quantity="many">%d prywatnych wiadomości. </item>
<item quantity="other">%d prywatnych wiadomości.</item>
</plurals>
<plurals name="group_message_notification_text">
<item quantity="one">Nowa wiadomość grupowa.</item>
<item quantity="few">%dwiadomości grupowych. </item>
<item quantity="many">%dwiadomości grupowych. </item>
<item quantity="other">%d wiadomości grupowych.</item>
</plurals>
<plurals name="forum_post_notification_text">
<item quantity="one">Nowy post z forum.</item>
<item quantity="few">%d nowych postów z forum. </item>
<item quantity="many">%d nowych postów z forum. </item>
<item quantity="other">%d nowych postów z forum.</item>
</plurals>
<plurals name="blog_post_notification_text">
<item quantity="one">Nowy post z bloga.</item>
<item quantity="few">%d nowych postów z bloga. </item>
<item quantity="many">%d nowych postów z bloga. </item>
<item quantity="other">%d nowych postów z bloga.</item>
</plurals>
<!--Misc-->
<string name="now">teraz</string>
<string name="show">Pokaż</string>
<string name="hide">Ukryj</string>
<string name="ok">OK</string>
<string name="cancel">Anuluj</string>
<string name="got_it">Rozumiem</string>
<string name="delete">Usuń</string>
<string name="accept">Akceptuj</string>
<string name="decline">Odrzuć</string>
<string name="options">Opcje</string>
<string name="online">Online</string>
<string name="offline">Offline</string>
<string name="send">Wyślij</string>
<string name="allow">Pozwól</string>
<string name="open">Otwórz</string>
<string name="no_data">Brak danych</string>
<string name="ellipsis">...</string>
<string name="text_too_long">Wprowadzony tekst jest zbyt długi</string>
<string name="show_onboarding">Pokaż okno pomocy</string>
<string name="fix">Napraw</string>
<string name="help">Pomoc</string>
<string name="sorry">Przepraszam</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Brak kontaktów do wyświetlenia\n\nDotknij ikonkę + aby dodać kontakt</string>
<string name="date_no_private_messages">Brak wiadomości.</string>
<string name="no_private_messages">Brak wiadomości do pokazania</string>
<string name="message_hint">Typ wiadomości</string>
<string name="delete_contact">Usuń kontakt</string>
<string name="dialog_title_delete_contact">Potwierdź usunięcie kontaktu</string>
<string name="dialog_message_delete_contact">Czy na pewno chcesz usunąć ten kontakt i wszystkie wymienione z nim wiadomości?</string>
<string name="contact_deleted_toast">Kontakt usunięty</string>
<!--Adding Contacts-->
<string name="add_contact_title">Dodaj kontakt</string>
<string name="face_to_face">Musisz spotkać się z osobą którą chcesz dodać jako kontakt.\n\nTo uniemożliwi komukolwiek podszyć się pod Ciebie lub czytać wysyłane wiadomości.</string>
<string name="continue_button">Kontynuuj</string>
<string name="connection_failed">Połączenie nieudane</string>
<string name="try_again_button">Spróbuj ponownie</string>
<string name="waiting_for_contact_to_scan">Czekanie aż ktoś zeskanuje kod i połączy się\u2026</string>
<string name="exchanging_contact_details">Wymienianie szczegółów dotyczących kontatku\u2026</string>
<string name="contact_added_toast">Kontakt dodany: %s</string>
<string name="contact_already_exists">Kontakt %s już istnieje</string>
<string name="contact_exchange_failed">Wymiana kontaktów nie powiodła się</string>
<string name="qr_code_invalid">Kod QR jest nie prawidłowy</string>
<string name="qr_code_unsupported">Kod QR który chcecsz zeskanować jest z wersji %s która nie jest już wspierana.\n\nProszę upewnić się czy oboje używacie najnowszej wersji i spróbować ponownie.</string>
<string name="camera_error">Błąd aparatu</string>
<string name="connecting_to_device">Łączenie z urządzeniem\u2026</string>
<string name="authenticating_with_device">Autoryzowanie z urządzeniem\u2026</string>
<string name="connection_aborted_local">Połączenie przerwane! To może oznaczać że ktoś próbuje podszyć się pod Ciebie</string>
<string name="connection_aborted_remote">Połączenie przerwane przez kontakt! To może oznaczać, że ktoś próbuje zagłuszyć wasze połączenie</string>
<!--Introductions-->
<string name="introduction_activity_title">Wybierz kontakt</string>
<string name="introduction_message_hint">Dodaj wiadomość (opcjonalne)</string>
<plurals name="introduction_notification_text">
<item quantity="one">Nowy kontakt został dodany.</item>
<item quantity="few">%d nowych kontaktów zostało dodanych.</item>
<item quantity="many">%d nowych kontaktów zostało dodanych.</item>
<item quantity="other">%d nowych kontaktów zostało dodanych.</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">Brak grup do wyświetlenia\n\nDotknij ikonki + aby stworzyć grupę bądź poprosić kontakty o udostępnienie grup</string>
<string name="groups_created_by">Stworzone przez %s</string>
<plurals name="messages">
<item quantity="one">%d wiadomość</item>
<item quantity="few">%d wiadomości</item>
<item quantity="many">%d wiadomości</item>
<item quantity="other">%d wiadomości</item>
</plurals>
<string name="groups_group_is_empty">Grupa jest pusta</string>
<string name="groups_group_is_dissolved">Ta grupa została rozwiązana</string>
<string name="groups_remove">Usuń</string>
<string name="groups_create_group_title">Stwórz Grupę Prywatną</string>
<string name="groups_create_group_button">Stwórz Grupę</string>
<string name="groups_create_group_invitation_button">Wyślij Zaproszenie</string>
<string name="groups_create_group_hint">Wybierz nazwę dla twojej prywatnej grupy</string>
<string name="groups_invitation_sent">Zaproszenie do grupy zostało wysłane</string>
<string name="groups_message_sent">Wiadomość wysłana</string>
<string name="groups_member_list">Lista Użytkowników</string>
<string name="groups_invite_members">Zaproś Użytkowników</string>
<string name="groups_member_created_you">Stworzyłeś grupę</string>
<string name="groups_member_created">%s stworzył grupę</string>
<string name="groups_member_joined_you">Dołączyłeś do grupy</string>
<string name="groups_member_joined">%s dołączył/a do grupy</string>
<string name="groups_leave">Opuść Grupę</string>
<string name="groups_leave_dialog_title">Potwierdź Opuszczenie Grupy</string>
<string name="groups_leave_dialog_message">Jesteś pewny, że chcesz opuścić tą grupę?</string>
<string name="groups_dissolve">Rozwiąż Grupę</string>
<string name="groups_dissolve_dialog_title">Potwierdź Rozwiązanie Grupy</string>
<string name="groups_dissolve_dialog_message">Czy chcesz rozwiązać tą grupę?\n\nWszyscy członkowie nie będą mogli kontynuować swoich konwersacji ani otrzymywać wiadomości.</string>
<string name="groups_dissolve_button">Rozwiąż</string>
<string name="groups_dissolved_dialog_title">Grupa Została Rozwiązana</string>
<string name="groups_dissolved_dialog_message">Twórca tej grupy rozwiązał ją.\n\nNie możesz już więcej pisać z członkami grupy ani czytać postów które zostały już napisane.</string>
<!--Private Group Invitations-->
<string name="groups_invitations_title">Zaproszenia do Grup</string>
<string name="groups_invitations_invitation_sent">Zaprosiłeś %1$s aby dołączył do grupy \"%2$s\".</string>
<string name="groups_invitations_invitation_received">%1$s zaprosił cię abyś dołączył do grupy \"%2$s\".</string>
<string name="groups_invitations_joined">Dołączyłeś do grupy</string>
<string name="groups_invitations_declined">Zaproszenie do grupy odrzucone</string>
<plurals name="groups_invitations_open">
<item quantity="one">%d otwórz zaproszenie do grupy</item>
<item quantity="few">%dotwórz zaproszenia do grup </item>
<item quantity="many">%dotwórz zaproszenia do grup </item>
<item quantity="other">%d otwórz zaproszenia do grup</item>
</plurals>
<string name="groups_invitations_response_accepted_sent">Przyjąłeś zaproszenie do grupy od %s.</string>
<string name="groups_invitations_response_declined_sent">Odrzuciłeś zaproszenie do grupy od %s.</string>
<string name="groups_invitations_response_accepted_received">%s przyjął zaproszenie do grupy.</string>
<string name="groups_invitations_response_declined_received">%s odrzucił zaproszenie do grupy.</string>
<string name="sharing_status_groups">Tylko osoba która utworzyła grupę może zapraszać nowych użytkowników do grupy. Poniżej są osoby, które dołączyły do grupy.</string>
<!--Private Groups Revealing Contacts-->
<!--Forums-->
<string name="create_forum_title">Stwórz Forum</string>
<string name="choose_forum_hint">Wybierz nazwę dla swojego forum</string>
<string name="create_forum_button">Stwórz Forum</string>
<string name="forum_created_toast">Forum stworzone</string>
<string name="no_forum_posts">Brak postów do pokazania</string>
<string name="no_posts">Brak postów</string>
<plurals name="posts">
<item quantity="one">%d post</item>
<item quantity="few">%d postów</item>
<item quantity="many">%d postów</item>
<item quantity="other">%d postów</item>
</plurals>
<string name="forum_new_message_hint">Nowy Wpis</string>
<string name="forum_message_reply_hint">Nowa Odpowiedź</string>
<string name="btn_reply">Odpowiedz</string>
<string name="forum_leave">Opuść Forum</string>
<string name="dialog_title_leave_forum">Potwierdź Opuszczenie Forum</string>
<string name="dialog_button_leave">Opuść</string>
<string name="forum_left_toast">Opuścił/a Forum</string>
<!--Forum Sharing-->
<string name="forum_share_button">Udostępnij Forum</string>
<string name="contacts_selected">Zaznaczone kontakty</string>
<string name="activity_share_toolbar_header">Wybierz Kontakty</string>
<string name="forum_share_message">Dodaj wiadomość (opcjonalne)</string>
<string name="forum_invitations_title">Zaproszenia do for</string>
<string name="forum_joined_toast">Dołączył do forum</string>
<string name="forum_declined_toast">Zaproszenie odrzucone</string>
<string name="shared_by_format">Udostępnione przez %s</string>
<string name="forum_invitation_already_sharing">Już udostępnione</string>
<string name="forum_invitation_response_accepted_sent">Przyjąłeś zaproszenie do forum od %s</string>
<string name="forum_invitation_response_declined_sent">Odrzuciłeś zaproszenie do forum od %s</string>
<string name="forum_invitation_response_accepted_received">%s przyjął zaproszenie do forum.</string>
<string name="forum_invitation_response_declined_received">%s odrzucił zaproszenie do forum.</string>
<string name="sharing_status">Status Udostępniania</string>
<plurals name="forums_shared">
<item quantity="one">%d forum udostępnione przez kontakty </item>
<item quantity="few">%d for udostępnionych przez kontakty </item>
<item quantity="many">%d for udostępnionych przez kontakty </item>
<item quantity="other">%d for udostępnionych przez kontakty</item>
</plurals>
<string name="nobody">Nikt</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Brak postów do pokazania</string>
<string name="read_more">czytaj więcej</string>
<string name="blogs_write_blog_post">Napisz Post Bloga</string>
<string name="blogs_write_blog_post_body_hint">Napisz swój wpis na bloga</string>
<string name="blogs_publish_blog_post">Opublikuj</string>
<string name="blogs_blog_post_created">Wpis na Bloga Utworzony</string>
<string name="blogs_blog_post_received">Otrzymano Nowy Wpis z Bloga</string>
<string name="blogs_blog_post_scroll_to">Przewiń Do</string>
<string name="blogs_remove_blog">Usuń Blog</string>
<string name="blogs_remove_blog_ok">Usuń</string>
<string name="blogs_blog_removed">Blog usunięty</string>
<string name="blogs_reblog_comment_hint">Dodaj komentarz (opcjonalne)</string>
<string name="blogs_reblog_button">Podaj dalej</string>
<!--Blog Sharing-->
<string name="blogs_sharing_share">Udostępnij Blog</string>
<string name="blogs_sharing_error">Wystąpił błąd podczas udostępniania tego bloga.</string>
<string name="blogs_sharing_button">Udostępnij Blog</string>
<string name="blogs_sharing_snackbar">Blog udostępniony wybranym kontaktom</string>
<string name="blogs_sharing_response_accepted_sent">Zaakceptowano zaproszenie do bloga od %s</string>
<string name="blogs_sharing_response_declined_sent">Odrzucono zaproszenie do bloga od %s</string>
<string name="blogs_sharing_response_accepted_received">Użytkownik %s zaakceptował zaproszenie do bloga.</string>
<string name="blogs_sharing_response_declined_received">Użytkownik %s odrzucił zaproszenie do bloga.</string>
<string name="blogs_sharing_invitation_received">%1$s udostępnił/a Ci blog \"%2$s\"</string>
<string name="blogs_sharing_invitation_sent">Udostępniasz blog \"%1$s\" użytkownikowi %2$s.</string>
<string name="blogs_sharing_invitations_title">Zaproszenia do Blogów</string>
<string name="blogs_sharing_joined_toast">Zasubskrybowano blog</string>
<string name="blogs_sharing_declined_toast">Zaproszenie odrzucone</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Zaimportuj RSS</string>
<string name="blogs_rss_feeds_import_button">Zaimportuj</string>
<string name="blogs_rss_feeds_import_hint">Wprowadź adres URL do RSS</string>
<string name="blogs_rss_feeds_import_error">Wystąpił błąd podczas importowania.</string>
<string name="blogs_rss_feeds_manage">Zarządzaj RSS</string>
<string name="blogs_rss_feeds_manage_imported">Zaimportowane:</string>
<string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Ostatnio Zaktualizowane:</string>
<string name="blogs_rss_remove_feed">Usuń RSS</string>
<string name="blogs_rss_remove_feed_ok">Usuń</string>
<string name="blogs_rss_feeds_manage_delete_error">Kanał nie mógł zostać usunięty!</string>
<!--Settings Network-->
<string name="network_settings_title">Sieci</string>
<string name="bluetooth_setting">Połącz przez Bluetooth</string>
<string name="bluetooth_setting_enabled">Zawsze wtedy gdy kontakty są w pobliżu</string>
<string name="bluetooth_setting_disabled">Tylko gdy dodaję kontakty</string>
<string name="tor_network_setting">Połącz przez Tor</string>
<string name="tor_network_setting_never">Nigdy</string>
<string name="tor_network_setting_wifi">Tylko gdy używam Wi-Fi</string>
<string name="tor_network_setting_always">Gdy używam Wi-Fi lub sieci komórkowej</string>
<!--Settings Security and Panic-->
<string name="security_settings_title">Bezpieczeństwo</string>
<string name="change_password">Zmień hasło</string>
<string name="current_password">Wprowadź swoje aktualne hasło:</string>
<string name="choose_new_password">Wpisz swoje nowe hasło:</string>
<string name="confirm_new_password">Potwierdź swoje nowe hasło:</string>
<string name="password_changed">Hasło zostało zmienione.</string>
<string name="panic_setting">Konfiguracja przycisku wyjścia awaryjnego</string>
<string name="panic_setting_title">Przycisk wyjścia awaryjnego</string>
<string name="panic_setting_hint">Skonfiguruj jak Briar ma zareagować gdy wciśniesz przycisk wyjścia awaryjnego</string>
<string name="panic_app_setting_title">Aplikacja Przycisku Wyjścia Awaryjnego</string>
<string name="unknown_app">nie znana aplikacja</string>
<string name="panic_app_setting_summary">Żadna aplikacja nie została ustawiona</string>
<string name="panic_app_setting_none">Brak</string>
<string name="dialog_title_connect_panic_app">Potwierdź Awaryjną Aplikację</string>
<string name="dialog_message_connect_panic_app">Czy na pewno chcesz pozwolić %1$s aby działała jako przycisk wyjścia awaryjnego?</string>
<string name="lock_setting_title">Wyloguj się</string>
<string name="lock_setting_summary">Wyloguj się z Briar jeśli przycisk zostanie wciśnięty</string>
<string name="purge_setting_title">Skasuj Konto</string>
<string name="purge_setting_summary">Skasuj Twoje konto Briar jeśli przycisk wyjścia awaryjnego zostanie wciśnięty. Ostrzeżenie: Ta akcja usunie permanentnie Twoje konto, kontakty i wiadomości</string>
<string name="uninstall_setting_title">Odinstaluj Briar</string>
<string name="uninstall_setting_summary">To wymaga manualnego potwierdzenia w przypadku aktywowania</string>
<!--Settings Notifications-->
<string name="notification_settings_title">Powiadomienia</string>
<string name="notify_private_messages_setting_title">Prywatne wiadomości</string>
<string name="notify_private_messages_setting_summary">Pokaż powiadomienia dla prywatnych wiadomości</string>
<string name="notify_private_messages_setting_summary_26">Skonfiguruj powiadomienia dla prywatnych wiadomości</string>
<string name="notify_group_messages_setting_title">Wiadomości grupowe</string>
<string name="notify_group_messages_setting_summary">Pokaż powiadomienia dla aplikacji grupowych</string>
<string name="notify_group_messages_setting_summary_26">Skonfiguruj powiadomienia dla wiadomości grupowych</string>
<string name="notify_forum_posts_setting_title">Posty na forum</string>
<string name="notify_forum_posts_setting_summary">Pokazuj powiadomienia dla postów w forach</string>
<string name="notify_forum_posts_setting_summary_26">Skonfiguruj powiadomienia dla postów w forach</string>
<string name="notify_blog_posts_setting_title">Wpisy na blogach</string>
<string name="notify_blog_posts_setting_summary">Pokazuj powiadomienia dla wpisów na blogach</string>
<string name="notify_blog_posts_setting_summary_26">Skonfiguruj alerty dla wpisów na blogach</string>
<string name="notify_vibration_setting">Wibruj</string>
<string name="notify_lock_screen_setting_title">Ekran blokady</string>
<string name="notify_lock_screen_setting_summary">Pokaż powiadomienia na zablokowanym ekranie</string>
<string name="notify_sound_setting">Dźwięk</string>
<string name="notify_sound_setting_default">Domyślny dzwonek</string>
<string name="notify_sound_setting_disabled">Brak</string>
<string name="choose_ringtone_title">Wybierz dzwonek</string>
<string name="cannot_load_ringtone">Nie mogę załadować dzwonka</string>
<!--Settings Feedback-->
<string name="feedback_settings_title">Wsparcie</string>
<!--Link Warning-->
<string name="link_warning_open_link">Otwórz Link</string>
<!--Crash Reporter-->
<string name="crash_report_title">Zgłoś błąd w Briar</string>
<string name="briar_crashed">Przepraszamy, wystąpił błąd w Briar</string>
<string name="not_your_fault">To nie Twoja wina.</string>
<string name="please_send_report">Proszę pomóż nam ulepszać Briar wysyłając raport z usterki.</string>
<string name="report_is_encrypted">Raport z usterki jest zaszyfrowany i wysyłany bezpiecznie.</string>
<string name="feedback_title">Wsparcie</string>
<string name="describe_crash">Opisz co się stało (opcjonalne)</string>
<string name="enter_feedback">Wprowadź swoje uwagi</string>
<string name="optional_contact_email">Twój adres email (opcjonalne)</string>
<string name="include_debug_report_crash">Załącz anonimowe dane na temat usterki</string>
<string name="include_debug_report_feedback">Załącz anonimowe dane o tym urządzeniu</string>
<string name="could_not_load_report_data">Nie można było załadować danych.</string>
<string name="send_report">Wyślij raport</string>
<string name="close">Zamknij</string>
<string name="dev_report_saved">Raport zapisany. Zostanie wysłany następnym razem kiedy zalogujesz się do Briar.</string>
<!--Sign Out-->
<string name="progress_title_logout">Wylogowywanie z Briar...</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Wykryto nakładkę na ekran</string>
<string name="screen_filter_allow">Pozwól tym aplikacjom na pozostanie na pierwszym planie</string>
<!--Permission Requests-->
<string name="permission_camera_title">Dostęp do aparatu</string>
<string name="permission_camera_request_body">Aby zeskanować kod QR, Briar potrzebuje mieć dostęp do aparatu.</string>
<string name="permission_camera_denied_toast">Dostęp do aparatu nie został przyznany</string>
<string name="qr_code">Kod QR</string>
<string name="show_qr_code_fullscreen">Pokaż QR na pełnym ekranie</string>
</resources>

View File

@@ -5,6 +5,7 @@
<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_password_explanation">Contul dvs. Briar este stocat criptat pe dispozitiv, nu în cloud. Dacă vă uitați parola sau ștergeți Briar, nu veți putea să vă recuperați contul.\n\nAlegeți o parolă lungă greu de ghicit, de exemplu, patru cuvinte aleatorii sau zece litere, numere și simboluri aleatoare.</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_explanation">Pentru a primi mesaje, Briar are nevoie să stea conectat în fundal. Vă rugăm să dezactivați optimizarea bateriei ca Briar să rămână conectat.</string>
@@ -28,10 +29,14 @@
<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="dialog_message_lost_password">Contul dvs. Briar este stocat criptat pe dispozitiv, nu în cloud. Dacă vă uitați parola sau ștergeți Briar, nu veți putea să vă recuperați contul. Doriți să vă ștergeți contul și să începeți din nou?\n\nAtenție: identitățile, contactele și mesajele dvs. vor fi pierdute definitiv.</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_db_error">Din anumite motive, baza dvs. de date Briar este deteriorată fără vreo posibilitate de a o recupera. Contul dvs., datele dvs. și toate persoanele de contact sunt pierdute. Din nefericire, trebuie să reinstalați Briar sau să creați un nou cont, selectând \"Am uitat parola\" la promptul de parolă.</string>
<string name="startup_failed_data_too_old_error">Contul dvs. a fost creat cu o versiune veche a acestei aplicații și nu poate fi deschis cu această versiune. Trebuie fie să reinstalați versiunea veche, fie să configurați un nou cont, selectând \"Am uitat parola\" la solicitarea de a introduce parola.</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>
<string name="startup_failed_service_error">Briar nu a reușit să pornească un plugin necesar. Reinstalarea lui Briar rezolvă de obicei această problemă. Cu toate acestea, rețineți că după aceasta veți pierde contul și toate datele asociate, deoarece Briar nu utilizează serverele centrale pentru a stoca date.</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>
@@ -124,6 +129,7 @@
<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="qr_code_unsupported">Codul QR pe care încercați să îl scanați aparține unei versiuni vechi %s care nu mai este acceptată.\n\nVă rugăm să vă asigurați că amândoi executați cea mai recentă versiune și încercați din nou.</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>
@@ -133,6 +139,7 @@
<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_not_possible">Ați trimis deja o solicitare la contacte. Așteptați să răspundă. Dacă dvs. sau persoanele persoanele de contact sunteți rareori online, este posibil să dureze ceva timp.</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>
@@ -144,6 +151,7 @@
<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_accepted_sent_info">Înainte de a adăuga %1$s ca persoane de contact, este necesar ca ei tot să accepte solicitarea. Aceasta poate dura ceva timp.</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>
@@ -181,8 +189,10 @@
<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_dialog_message">Sunteți sigur că doriți să dizolvați acest grup?\n\nToți ceilalți membri nu vor putea continua conversația lor și s-ar putea să nu primească cele mai recente mesaje.</string>
<string name="groups_dissolve_button">Dizolvă</string>
<string name="groups_dissolved_dialog_title">Grupul a fost dizolvat</string>
<string name="groups_dissolved_dialog_message">Creatorul acestui grup a dizolvat-o.\n\nNu mai puteți scrie mesaje către grup și nu mai puteți primi postările scrise.</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>
@@ -201,6 +211,7 @@
<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_dialog_message">Puteți alege să dezvăluiți contacte tuturor membrilor actuali și viitori acestui grup.\n\nDeschiderea contactelor face conexiunea dvs. cu grupul mai rapidă și mai sigură, deoarece puteți comunica cu persoanele de contact dezvăluite chiar și atunci când creatorul grupului este offline.</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>
@@ -225,6 +236,7 @@
<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_message_leave_forum">Sunteți sigur că doriți să părăsiți acest forum?\n\nOrice persoane de contact cu care ați împărtășit acest forum s-ar putea să nu mai primească actualizări.</string>
<string name="dialog_button_leave">Părăsește</string>
<string name="forum_left_toast">Forum părăsit</string>
<!--Forum Sharing-->
@@ -238,6 +250,7 @@
<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="forum_invitation_exists">Ați acceptat deja o invitație la acest forum.\n\nAcceptarea mai multor invitații va face conexiunea dvs. la forum mai rapidă și mai sigură.</string>
<string name="forum_joined_toast">Alăturare forum</string>
<string name="forum_declined_toast">Invitația a fost refuzată</string>
<string name="shared_by_format">Partajat de %s</string>
@@ -247,6 +260,7 @@
<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="sharing_status_forum">Orice membru al unui forum poate să-l împărtășească cu contactele lor. Îți partajați acest forum cu următoarele persoane de contact. Pot exista și alți membri pe care nu îi puteți vedea.</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>
@@ -265,6 +279,7 @@
<string name="blogs_blog_post_scroll_to">Derulează la</string>
<string name="blogs_feed_empty_state">Nici un mesaj de arătat\n\nMesajele de la contactele dumneavoastră și de pe blog-urile la care sunteți abonați vor apărea aici\n\nAtingeți iconița cu creion pentru a scrie un mesaj</string>
<string name="blogs_remove_blog">Elimină blog</string>
<string name="blogs_remove_blog_dialog_message">Sunteți sigur că doriți să eliminați acest blog?\n\nPosturile vor fi eliminate de pe dispozitiv, dar nu și de dispozitivele altor persoane.\n\nOrice persoane de contact cu care ați partajat acest blog ar putea să nu mai primească actualizări.</string>
<string name="blogs_remove_blog_ok">Eliminare</string>
<string name="blogs_blog_removed">Blog eliminat</string>
<string name="blogs_reblog_comment_hint">Adaugă un comentariu (opțional)</string>
@@ -283,6 +298,7 @@
<string name="blogs_sharing_invitations_title">Invitații la blog-uri</string>
<string name="blogs_sharing_joined_toast">Abonare la blog</string>
<string name="blogs_sharing_declined_toast">Invitația a fost refuzată</string>
<string name="sharing_status_blog">Oricine abonat la un blog poate să-l împărtășească cu persoanele de contact. Partajați acest blog cu următoarele persoane de contact. Pot exista și alți abonați pe care nu îi puteți vedea.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Importă flux RSS</string>
<string name="blogs_rss_feeds_import_button">Importă</string>
@@ -293,6 +309,7 @@
<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_dialog_message">Sunteți sigur că doriți să eliminați acest feed?\n\nPosturile vor fi eliminate de pe dispozitiv, dar nu și de dispozitivele altor persoane.\n\nOrice persoane de contact cu care ați distribuit acest feed nu vor mai primi actualizări.</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_empty_state">Nici un flux RSS de arătat\n\nAtingeți iconița + pentru a adăuga un flux</string>
@@ -325,6 +342,7 @@
<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="purge_setting_summary">Ștergeți contul Briar dacă este apăsat buton de panică. Atenție: aceasta va șterge definitiv identitatea, contactele și mesajele dvs.</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-->
@@ -355,6 +373,7 @@
<!--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_text">Acest lucru poate fi folosit pentru a vă identifica. Gândiți-vă dacă aveți încredere în persoana care v-a trimis acest link și luați în considerare deschiderea acestuia cu Orfox.</string>
<string name="link_warning_open_link">Deschide adresă</string>
<!--Crash Reporter-->
<string name="crash_report_title">Raport de erori Briar</string>
@@ -376,6 +395,7 @@
<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_body">O altă aplicație este suprapusă pe Briar. Pentru a vă proteja securitatea, Briar nu va reacționa la atingere în timp ce există o suprapunere.\n\nAplicațiile următoare pot fi suprapuse:\n\n%1$s</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>

View File

@@ -238,8 +238,8 @@
<item quantity="many">%d постов</item>
<item quantity="other">%d постов</item>
</plurals>
<string name="forum_new_entry_posted">Опубликована запись форума</string>
<string name="forum_new_message_hint">Новая запись</string>
<string name="forum_new_entry_posted">Пост в форуме опубликован</string>
<string name="forum_new_message_hint">Новый пост</string>
<string name="forum_message_reply_hint">Новый ответ</string>
<string name="btn_reply">Ответ</string>
<string name="forum_leave">Покинуть форум</string>
@@ -268,7 +268,7 @@
<string name="forum_invitation_response_accepted_received">%sпринял приглашение на форум.</string>
<string name="forum_invitation_response_declined_received">%s отклонил приглашение на форум.</string>
<string name="sharing_status">Статус общего доступа</string>
<string name="sharing_status_forum">Любой участник форума может поделиться своими контактами. Вы делитесь этим форумом со следующими контактами. Могут быть и другие участники, которых вы не видите.</string>
<string name="sharing_status_forum">Любой участник форума может поделиться им со своими контактами. Вы делитесь этим форумом со следующими контактами. Могут быть и другие участники, которых вы не видите.</string>
<string name="shared_with">Совместно с %1$d (%2$d в сети)</string>
<plurals name="forums_shared">
<item quantity="one">%d форум, общий с контактами</item>

View File

@@ -141,6 +141,7 @@
<string name="introduction_request_exists_received">%1$s 想要将您介绍给 %2$s但是 %2$s 已经在您的联系人列表。%1$s 可能并不知情,但您仍可以做出回复:</string>
<string name="introduction_request_answered_received">%1$s 想要将您介绍给 %2$s。</string>
<string name="introduction_response_accepted_sent">您已接受与 %1$s 建立联系</string>
<string name="introduction_response_accepted_sent_info">在 %1$s添加到您通讯录之前他们需要接受您的名片。这可能要花点时间。</string>
<string name="introduction_response_declined_sent">您已谢绝与 %1$s 建立联系</string>
<string name="introduction_response_accepted_received">%1$s 接受与 %2$s 建立联系</string>
<string name="introduction_response_declined_received">%1$s 谢绝与 %2$s 建立联系</string>
@@ -149,6 +150,7 @@
<item quantity="other">已添加 %d 位新联系人。</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">没有群组\n\n点击 + 号创建群聊,或者让您的联系人分享群组给您</string>
<string name="groups_created_by">由 %s 创建</string>
<plurals name="messages">
<item quantity="other">%d 条消息。</item>
@@ -199,6 +201,7 @@
<string name="groups_reveal_visible_revealed_by_contact">联系人关系对群聊可见 (由 %s 设定)</string>
<string name="groups_reveal_invisible">联系人关系对群聊不可见</string>
<!--Forums-->
<string name="no_forums">没有论坛\n\n点击 + 号来创建一个,或者让您的联系人分享一个论坛给您</string>
<string name="create_forum_title">创建论坛</string>
<string name="choose_forum_hint">为论坛命名</string>
<string name="create_forum_button">创建论坛</string>
@@ -214,17 +217,23 @@
<string name="btn_reply">回复</string>
<string name="forum_leave">退出论坛</string>
<string name="dialog_title_leave_forum">确认退出论坛</string>
<string name="dialog_message_leave_forum">确定要退出论坛?\n\n那些您分享过论坛的联系人将不会再收到更新。</string>
<string name="dialog_button_leave">退出</string>
<string name="forum_left_toast">已退出论坛</string>
<!--Forum Sharing-->
<string name="forum_share_button">分享论坛</string>
<string name="contacts_selected">已选择联系人</string>
<string name="activity_share_toolbar_header">选择联系人</string>
<string name="no_contacts_selector">没有联系人\n\n请添加联系人后再来</string>
<string name="forum_shared_snackbar">论坛已分享给选中的联系人</string>
<string name="forum_share_message">添加一句话 (选填)</string>
<string name="forum_share_error">分享此论坛时发生错误。</string>
<string name="forum_invitation_received">%1$s 已将论坛 “%2$s” 分享给你。</string>
<string name="forum_invitation_sent">您已将论坛 “%1$s” 分享给 %2$s。</string>
<string name="forum_invitations_title">论坛邀请</string>
<string name="forum_invitation_exists">您已接受论坛邀请。\n\n接受更多的邀请会使论坛访问速度更快更可靠。</string>
<string name="forum_joined_toast">已加入论坛</string>
<string name="forum_declined_toast">邀请已拒绝</string>
<string name="shared_by_format">由 %s 分享</string>
<string name="forum_invitation_already_sharing">已在分享</string>
<string name="forum_invitation_response_accepted_sent">您接受了来自 %s的论坛邀请</string>
@@ -242,12 +251,16 @@
<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>
<string name="blogs_publish_blog_post">发布</string>
<string name="blogs_blog_post_created">博文已创建</string>
<string name="blogs_blog_post_received">收到新博文</string>
<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_dialog_message">确定要删除博文吗?\n\n文章将会从您的设备删除但仍存在于别人的设备上。\n\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>
<!--Blog Sharing-->
@@ -262,6 +275,8 @@
<string name="blogs_sharing_invitation_received">%1$s 向您分享博客 “%2$s”。</string>
<string name="blogs_sharing_invitation_sent">您已将博客 “%1$s” 分享给 %2$s。</string>
<string name="blogs_sharing_invitations_title">博客邀请</string>
<string name="blogs_sharing_joined_toast">订阅博客</string>
<string name="blogs_sharing_declined_toast">邀请已拒绝</string>
<string name="sharing_status_blog">任何订阅博客的人都可以将它分享给他的联系人。您正在将该博客分享给下列联系人。可能有其他您所不可见的订阅者存在。</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">导入 RSS 订阅</string>
@@ -273,8 +288,10 @@
<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_dialog_message">确定要删除信息流?\n\n文章将从您的设备上移除但仍存于别人的设备。\n\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">没有订阅内容\n\n点击 + 号导入一个 RSS 信息流</string>
<string name="blogs_rss_feeds_manage_error">加载订阅时出错。请稍候再试。</string>
<!--Settings Network-->
<string name="network_settings_title">网络</string>
@@ -311,12 +328,16 @@
<string name="notification_settings_title">消息提醒</string>
<string name="notify_private_messages_setting_title">私信</string>
<string name="notify_private_messages_setting_summary">显示私信通知</string>
<string name="notify_private_messages_setting_summary_26">配置私聊消息通知</string>
<string name="notify_group_messages_setting_title">群消息</string>
<string name="notify_group_messages_setting_summary">显示群聊消息通知</string>
<string name="notify_group_messages_setting_summary_26">配置群聊消息通知</string>
<string name="notify_forum_posts_setting_title">论坛帖子</string>
<string name="notify_forum_posts_setting_summary">显示论坛帖子通知</string>
<string name="notify_forum_posts_setting_summary_26">配置论坛文章通知</string>
<string name="notify_blog_posts_setting_title">博文</string>
<string name="notify_blog_posts_setting_summary">显示博文通知</string>
<string name="notify_blog_posts_setting_summary_26">配置博客文章通知</string>
<string name="notify_vibration_setting">震动</string>
<string name="notify_lock_screen_setting_title">锁定屏幕</string>
<string name="notify_lock_screen_setting_summary">在锁定屏幕上显示通知</string>
@@ -360,4 +381,6 @@
<string name="permission_camera_request_body">Briar 需要获得相机权限以扫描二维码。</string>
<string name="permission_camera_denied_body">您拒绝了相机权限,而添加联系人需要使用相机\n\n请考虑授予相机权限。</string>
<string name="permission_camera_denied_toast">未授予相机权限</string>
<string name="qr_code">二维码</string>
<string name="show_qr_code_fullscreen">全屏显示二维码</string>
</resources>

View File

@@ -18,4 +18,37 @@
<item>1</item>
<item>2</item>
</string-array>
</resources>
<string-array name="pref_language_values">
<item>default</item>
<item>en-US</item>
<item>ast</item>
<item>bg</item>
<item>br</item>
<item>ca</item>
<item>cs</item>
<item>de</item>
<item>es</item>
<item>eu</item>
<item>fa</item>
<item>fi</item>
<item>fr</item>
<item>gl</item>
<item>he</item>
<item>hi</item>
<item>it</item>
<item>ja</item>
<item>ms</item>
<item>nb</item>
<item>nl</item>
<item>oc</item>
<item>pl</item>
<item>pt-BR</item>
<item>ro</item>
<item>ru</item>
<item>sq</item>
<item>sr</item>
<item>sv</item>
<item>tr</item>
<item>zh-CN</item>
</string-array>
</resources>

View File

@@ -323,6 +323,12 @@
<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 Display -->
<string name="pref_language_title">Language &amp; region</string>
<string name="pref_language_changed">This setting will take effect when you restart Briar. Please sign out and restart Briar.</string>
<string name="pref_language_default">System default</string>
<string name="display_settings_title">Display</string>
<!-- Settings Network -->
<string name="network_settings_title">Networks</string>
<string name="bluetooth_setting">Connect via Bluetooth</string>

View File

@@ -1,6 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:layout="@layout/preferences_category"
android:title="@string/display_settings_title">
<ListPreference
android:defaultValue="default"
android:entryValues="@array/pref_language_values"
android:key="pref_key_language"
android:summary="%s"
android:title="@string/pref_language_title"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/preferences_category"

View File

@@ -1,6 +1,8 @@
package org.briarproject.briar.android;
import android.app.Application;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.briar.BriarCoreModule;
@@ -27,6 +29,8 @@ public class TestBriarApplication extends Application
super.onCreate();
LOG.info("Created");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Localizer.initialize(prefs);
applicationComponent = DaggerAndroidComponent.builder()
.appModule(new AppModule(this))
.build();

View File

@@ -0,0 +1,35 @@
package org.briarproject.briar.android;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.annotation.Nullable;
import static junit.framework.Assert.assertTrue;
@NotNullByDefault
public class TestDatabaseKeyUtils {
public static void storeDatabaseKey(File f, String hex) throws IOException {
f.getParentFile().mkdirs();
FileOutputStream out = new FileOutputStream(f);
out.write(hex.getBytes("UTF-8"));
out.flush();
out.close();
}
@Nullable
public static String loadDatabaseKey(File f) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream(f), "UTF-8"));
String hex = reader.readLine();
reader.close();
return hex;
}
}

View File

@@ -0,0 +1,205 @@
package org.briarproject.briar.android.controller;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations;
import org.junit.After;
import org.junit.Test;
import java.io.File;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
import static org.briarproject.bramble.util.StringUtils.toHexString;
import static org.briarproject.briar.android.TestDatabaseKeyUtils.loadDatabaseKey;
import static org.briarproject.briar.android.TestDatabaseKeyUtils.storeDatabaseKey;
public class ConfigControllerImplTest extends BrambleMockTestCase {
private final SharedPreferences prefs =
context.mock(SharedPreferences.class);
private final DatabaseConfig databaseConfig =
context.mock(DatabaseConfig.class);
private final Editor editor = context.mock(Editor.class);
private final byte[] encryptedKey = getRandomBytes(123);
private final String encryptedKeyHex = toHexString(encryptedKey);
private final String oldEncryptedKeyHex = toHexString(getRandomBytes(123));
private final File testDir = getTestDirectory();
private final File keyDir = new File(testDir, "key");
private final File keyFile = new File(keyDir, "db.key");
private final File keyBackupFile = new File(keyDir, "db.key.bak");
@Test
public void testDbKeyIsMigratedFromPreferencesToFile() throws Exception {
context.checking(new Expectations() {{
oneOf(prefs).getString("key", null);
will(returnValue(encryptedKeyHex));
allowing(databaseConfig).getDatabaseKeyDirectory();
will(returnValue(keyDir));
oneOf(prefs).edit();
will(returnValue(editor));
oneOf(editor).remove("key");
will(returnValue(editor));
oneOf(editor).commit();
will(returnValue(true));
}});
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
ConfigControllerImpl c = new ConfigControllerImpl(prefs,
databaseConfig);
assertEquals(encryptedKeyHex, c.getEncryptedDatabaseKey());
assertTrue(keyFile.exists());
assertTrue(keyBackupFile.exists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
}
@Test
public void testDbKeyIsLoadedFromPrimaryFile() throws Exception {
context.checking(new Expectations() {{
oneOf(prefs).getString("key", null);
will(returnValue(null));
allowing(databaseConfig).getDatabaseKeyDirectory();
will(returnValue(keyDir));
}});
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
storeDatabaseKey(keyFile, encryptedKeyHex);
assertTrue(keyFile.exists());
assertFalse(keyBackupFile.exists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
ConfigControllerImpl c = new ConfigControllerImpl(prefs,
databaseConfig);
assertEquals(encryptedKeyHex, c.getEncryptedDatabaseKey());
assertTrue(keyFile.exists());
assertFalse(keyBackupFile.exists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
}
@Test
public void testDbKeyIsLoadedFromBackupFile() throws Exception {
context.checking(new Expectations() {{
oneOf(prefs).getString("key", null);
will(returnValue(null));
allowing(databaseConfig).getDatabaseKeyDirectory();
will(returnValue(keyDir));
}});
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertFalse(keyFile.exists());
assertTrue(keyBackupFile.exists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
ConfigControllerImpl c = new ConfigControllerImpl(prefs,
databaseConfig);
assertEquals(encryptedKeyHex, c.getEncryptedDatabaseKey());
assertFalse(keyFile.exists());
assertTrue(keyBackupFile.exists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
}
@Test
public void testDbKeyIsNullIfNotFound() {
context.checking(new Expectations() {{
oneOf(prefs).getString("key", null);
will(returnValue(null));
allowing(databaseConfig).getDatabaseKeyDirectory();
will(returnValue(keyDir));
}});
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
ConfigControllerImpl c = new ConfigControllerImpl(prefs,
databaseConfig);
assertNull(c.getEncryptedDatabaseKey());
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
}
@Test
public void testStoringDbKeyOverwritesPrimary() throws Exception {
context.checking(new Expectations() {{
allowing(databaseConfig).getDatabaseKeyDirectory();
will(returnValue(keyDir));
}});
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
storeDatabaseKey(keyFile, oldEncryptedKeyHex);
assertTrue(keyFile.exists());
assertFalse(keyBackupFile.exists());
assertEquals(oldEncryptedKeyHex, loadDatabaseKey(keyFile));
ConfigController c = new ConfigControllerImpl(prefs,
databaseConfig);
assertTrue(c.storeEncryptedDatabaseKey(encryptedKeyHex));
assertTrue(keyFile.exists());
assertTrue(keyBackupFile.exists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
}
@Test
public void testStoringDbKeyOverwritesBackup() throws Exception {
context.checking(new Expectations() {{
allowing(databaseConfig).getDatabaseKeyDirectory();
will(returnValue(keyDir));
}});
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
storeDatabaseKey(keyBackupFile, oldEncryptedKeyHex);
assertFalse(keyFile.exists());
assertTrue(keyBackupFile.exists());
assertEquals(oldEncryptedKeyHex, loadDatabaseKey(keyBackupFile));
ConfigController c = new ConfigControllerImpl(prefs,
databaseConfig);
assertTrue(c.storeEncryptedDatabaseKey(encryptedKeyHex));
assertTrue(keyFile.exists());
assertTrue(keyBackupFile.exists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
}
@After
public void tearDown() {
deleteTestDirectory(testDir);
}
}

View File

@@ -8,14 +8,23 @@ import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.ImmediateExecutor;
import org.jmock.Expectations;
import org.junit.After;
import org.junit.Test;
import java.io.File;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
import static org.briarproject.bramble.util.StringUtils.toHexString;
import static org.briarproject.briar.android.TestDatabaseKeyUtils.loadDatabaseKey;
import static org.briarproject.briar.android.TestDatabaseKeyUtils.storeDatabaseKey;
public class PasswordControllerImplTest extends BrambleMockTestCase {
@@ -26,62 +35,90 @@ public class PasswordControllerImplTest extends BrambleMockTestCase {
private final CryptoComponent crypto = context.mock(CryptoComponent.class);
private final PasswordStrengthEstimator estimator =
context.mock(PasswordStrengthEstimator.class);
private final SharedPreferences.Editor editor =
context.mock(SharedPreferences.Editor.class);
private final Executor cryptoExecutor = new ImmediateExecutor();
private final String oldPassword = "some.old.pass";
private final String newPassword = "some.new.pass";
private final String oldEncryptedHex = "010203";
private final String newEncryptedHex = "020304";
private final byte[] oldEncryptedBytes = new byte[] {1, 2, 3};
private final byte[] newEncryptedBytes = new byte[] {2, 3, 4};
private final byte[] keyBytes = getSecretKey().getBytes();
private final byte[] oldEncryptedKey = getRandomBytes(123);
private final byte[] newEncryptedKey = getRandomBytes(123);
private final byte[] key = getSecretKey().getBytes();
private final File testDir = getTestDirectory();
private final File keyDir = new File(testDir, "key");
private final File keyFile = new File(keyDir, "db.key");
private final File keyBackupFile = new File(keyDir, "db.key.bak");
@Test
public void testChangePasswordReturnsTrue() {
public void testChangePasswordReturnsTrue() throws Exception {
context.checking(new Expectations() {{
// Look up the encrypted DB key
oneOf(briarPrefs).getString("key", null);
will(returnValue(oldEncryptedHex));
will(returnValue(null));
allowing(databaseConfig).getDatabaseKeyDirectory();
will(returnValue(keyDir));
// Decrypt and re-encrypt the key
oneOf(crypto).decryptWithPassword(oldEncryptedBytes, oldPassword);
will(returnValue(keyBytes));
oneOf(crypto).encryptWithPassword(keyBytes, newPassword);
will(returnValue(newEncryptedBytes));
// Store the re-encrypted key
oneOf(briarPrefs).edit();
will(returnValue(editor));
oneOf(editor).putString("key", newEncryptedHex);
will(returnValue(editor));
oneOf(editor).commit();
oneOf(crypto).decryptWithPassword(oldEncryptedKey, oldPassword);
will(returnValue(key));
oneOf(crypto).encryptWithPassword(key, newPassword);
will(returnValue(newEncryptedKey));
}});
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
storeDatabaseKey(keyFile, toHexString(oldEncryptedKey));
storeDatabaseKey(keyBackupFile, toHexString(oldEncryptedKey));
PasswordControllerImpl p = new PasswordControllerImpl(briarPrefs,
databaseConfig, cryptoExecutor, crypto, estimator);
AtomicBoolean capturedResult = new AtomicBoolean(false);
p.changePassword(oldPassword, newPassword, capturedResult::set);
assertTrue(capturedResult.get());
assertTrue(keyFile.exists());
assertTrue(keyBackupFile.exists());
assertEquals(toHexString(newEncryptedKey), loadDatabaseKey(keyFile));
assertEquals(toHexString(newEncryptedKey),
loadDatabaseKey(keyBackupFile));
}
@Test
public void testChangePasswordReturnsFalseIfOldPasswordIsWrong() {
public void testChangePasswordReturnsFalseIfOldPasswordIsWrong()
throws Exception {
context.checking(new Expectations() {{
// Look up the encrypted DB key
oneOf(briarPrefs).getString("key", null);
will(returnValue(oldEncryptedHex));
will(returnValue(null));
allowing(databaseConfig).getDatabaseKeyDirectory();
will(returnValue(keyDir));
// Try to decrypt the key - the password is wrong
oneOf(crypto).decryptWithPassword(oldEncryptedBytes, oldPassword);
oneOf(crypto).decryptWithPassword(oldEncryptedKey, oldPassword);
will(returnValue(null));
}});
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
storeDatabaseKey(keyFile, toHexString(oldEncryptedKey));
storeDatabaseKey(keyBackupFile, toHexString(oldEncryptedKey));
PasswordControllerImpl p = new PasswordControllerImpl(briarPrefs,
databaseConfig, cryptoExecutor, crypto, estimator);
AtomicBoolean capturedResult = new AtomicBoolean(true);
p.changePassword(oldPassword, newPassword, capturedResult::set);
assertFalse(capturedResult.get());
assertTrue(keyFile.exists());
assertTrue(keyBackupFile.exists());
assertEquals(toHexString(oldEncryptedKey), loadDatabaseKey(keyFile));
assertEquals(toHexString(oldEncryptedKey),
loadDatabaseKey(keyBackupFile));
}
@After
public void tearDown() {
deleteTestDirectory(testDir);
}
}

Some files were not shown because too many files have changed in this diff Show More