Compare commits

...

44 Commits

Author SHA1 Message Date
akwizgran
934997ed91 Check for partial setup of clients due to feature flags reverting. 2023-01-31 11:23:19 +00:00
Torsten Grote
0b94814620 Merge branch 'remove-migration-code' into 'master'
Remove various bits of code whose migration periods have passed

See merge request briar/briar!1750
2023-01-30 13:59:02 +00:00
akwizgran
e82e11acfa Merge branch '2384-mailbox-problem-notification' into 'master'
Clear mailbox problem notification after unlinking

Closes #2384

See merge request briar/briar!1764
2023-01-27 15:09:27 +00:00
Torsten Grote
795461d9a8 Clear mailbox problem notification after unlinking 2023-01-27 11:49:01 -03:00
Torsten Grote
7b8d01cfe0 Merge branch '1822-rss-feeds-backend' into 'master'
Resolve "Import RSS feeds shared by other apps"

See merge request briar/briar!1763
2023-01-25 11:39:34 +00:00
akwizgran
abd04ee7f5 Add tests for feed serialisation/deserialisation. 2023-01-24 17:27:52 +00:00
akwizgran
cc5365eaf0 Remove redundant comparison from test. 2023-01-24 17:27:34 +00:00
akwizgran
6b20b03698 Bump version numbers for 1.4.20 release. 2023-01-24 15:51:48 +00:00
akwizgran
9da7fbf4f6 Update translations. 2023-01-24 15:51:26 +00:00
akwizgran
f64f442fcf Merge branch 'add-comment-for-is-connected' into 'master'
Add comment about NetworkInfo#isConnected()

See merge request briar/briar!1762
2023-01-24 15:25:52 +00:00
akwizgran
6eda2f6d13 AnimalSniffer doesn't allow StandardCharsets in tests. 2023-01-24 14:50:40 +00:00
akwizgran
6faa095dfb FeedMatcher interface doesn't need to be public. 2023-01-24 14:48:55 +00:00
akwizgran
4007fca668 Add integration tests for importing an RSS feed from a file. 2023-01-24 14:15:03 +00:00
akwizgran
28a747f7f3 Add method for adding an RSS feed from an input stream. 2023-01-24 13:57:44 +00:00
Sebastian Kürten
fd2d5c9173 Add comment about NetworkInfo#isConnected() 2023-01-24 14:48:03 +01:00
akwizgran
8f7bb9d26b Don't overwrite the list of feeds after fetching. 2023-01-24 13:28:22 +00:00
akwizgran
dc220200b6 Match newly added RSS feeds to existing feeds. 2023-01-24 12:43:14 +00:00
Torsten Grote
0cea137d75 Merge branch 'update-tor-bridges' into 'master'
Update Tor bridges

See merge request briar/briar!1761
2023-01-23 15:06:28 +00:00
akwizgran
2eef34f424 Use new transaction wrappers. 2023-01-23 13:02:16 +00:00
akwizgran
a68fff9dd2 Merge branch 'tor-0.4.7.13' into 'master'
Upgrade Tor to 0.4.7.13

See merge request briar/briar!1760
2023-01-23 12:11:32 +00:00
akwizgran
ddc8f4a7d7 Add three non-default obfs4 bridges. 2023-01-20 16:12:47 +00:00
akwizgran
f961b6a80b Remove three failing bridges. 2023-01-20 16:11:11 +00:00
akwizgran
93439d9c17 Update translations. 2023-01-20 15:50:28 +00:00
akwizgran
f3ee884816 Upgrade Tor to 0.4.7.13. 2023-01-20 15:34:23 +00:00
akwizgran
8ca22043cf Merge branch '1897-sharing-status' into 'master'
Introduce SharingStatus to report more fine-grained status

Closes #1897

See merge request briar/briar!1758
2023-01-20 14:33:32 +00:00
Torsten Grote
9353b78da8 Clarify sharing state docs 2023-01-20 11:13:35 -03:00
Torsten Grote
429bbe1275 Introduce more sharing states 2023-01-20 11:13:35 -03:00
Torsten Grote
c5fb1416bd Update JavaDoc for SharingState change 2023-01-20 11:13:34 -03:00
Torsten Grote
e52cbd896e Introduce SharingStatus to report more fine-grained status 2023-01-20 11:13:34 -03:00
akwizgran
ab1b8784b7 Merge branch '90-clickable-links' into 'master'
Resolve "Handle Hyperlinks (Clickable Links)"

Closes #90

See merge request briar/briar!1757
2023-01-20 13:47:08 +00:00
akwizgran
55a4daa92f Merge branch 'progressbar-remove-contact' into 'master'
Show progress bar while removing contact

See merge request briar/briar!1759
2023-01-20 13:44:29 +00:00
akwizgran
e52250f1e4 Don't sort list of RSS feeds in UI. 2023-01-18 15:04:38 +00:00
akwizgran
33d01aac8c Add matcher for matching an imported feed against existing feeds. 2023-01-18 15:04:38 +00:00
akwizgran
b920382e44 Store additional properties of RSS feed in metadata. 2023-01-18 15:04:38 +00:00
akwizgran
1a2f85f701 Small code cleanups for feed manager, don't fetch new feeds twice. 2023-01-18 15:04:33 +00:00
Airplane Mode
186bcc0b47 Show progress bar while removing contact 2023-01-16 23:56:26 +00:00
Torsten Grote
8b9140f477 Merge branch 'tor-0.4.7.12' into 'master'
Upgrade Tor to 0.4.7.12

See merge request briar/briar!1755
2023-01-13 16:25:42 +00:00
Katelyn Dickey
f959c32935 Remove autoLink attribute which was causing warnings to show twice, and highlight links in comments before a blog is expanded 2023-01-05 17:20:40 -05:00
akwizgran
1c060bc6db Upgrade Tor to 0.4.7.12. 2023-01-04 17:51:46 +00:00
Katelyn Dickey
5e44e4d308 Add clickable links to blog comments 2023-01-03 20:52:36 -05:00
Katelyn Dickey
75d5dec45f Add clickable links to notices/requests 2023-01-03 18:38:13 -05:00
Katelyn Dickey
d825227eb5 Add clickable links to threads (forums/groups) 2023-01-03 18:38:07 -05:00
Katelyn Dickey
967dd1f18d Add clickable links for conversations 2023-01-03 18:37:40 -05:00
akwizgran
7a3ffcbae6 Remove various bits of code whose migration periods have passed. 2022-12-07 17:47:02 +00:00
93 changed files with 2505 additions and 615 deletions

View File

@@ -13,8 +13,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 31
versionCode 10419
versionName "1.4.19"
versionCode 10420
versionName "1.4.20"
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -118,6 +118,11 @@ class AndroidNetworkManager implements NetworkManager, Service {
try {
NetworkInfo net = connectivityManager.getActiveNetworkInfo();
boolean connected = net != null && net.isConnected();
// Research into Android's behavior to check network connectivity
// (https://code.briarproject.org/briar/public-mesh-research/-/issues/19)
// has shown that NetworkInfo#isConnected() returns true if the device
// is connected to any Wifi, independent of whether any specific IP
// address can be reached using it or any domain names can be resolved.
boolean wifi = false, ipv6Only = false;
if (connected) {
wifi = net.getType() == TYPE_WIFI;

View File

@@ -26,7 +26,7 @@ dependencyVerification {
'org.apache-extras.beanshell:bsh:2.0b6:bsh-2.0b6.jar:a17955976070c0573235ee662f2794a78082758b61accffce8d3f8aedcd91047',
'org.briarproject:obfs4proxy-android:0.0.14-tor1:obfs4proxy-android-0.0.14-tor1.jar:8b08068778b133484b17956d8f7a7710739c33f671a26a68156f4d34e6f28c30',
'org.briarproject:snowflake-android:2.3.1:snowflake-android-2.3.1.jar:1f83c9a070f87b7074af13627709a8b5aced5460104be7166af736b1bb73c293',
'org.briarproject:tor-android:0.4.5.14:tor-android-0.4.5.14.jar:7cf1beaa6c1db51fc8fac263aba9624ef289c3db29772509efcbc59f7057330a',
'org.briarproject:tor-android:0.4.7.13:tor-android-0.4.7.13.jar:7852aab7d2298b80878c7719f34ce665725b494d673ecf2e6f9e697564638cc6',
'org.checkerframework:checker-compat-qual:2.5.5:checker-compat-qual-2.5.5.jar:11d134b245e9cacc474514d2d66b5b8618f8039a1465cdc55bbc0b34e0008b7a',
'org.checkerframework:checker-qual:3.12.0:checker-qual-3.12.0.jar:ff10785ac2a357ec5de9c293cb982a2cbb605c0309ea4cc1cb9b9bc6dbe7f3cb',
'org.hamcrest:hamcrest-core:2.1:hamcrest-core-2.1.jar:e09109e54a289d88506b9bfec987ddd199f4217c9464132668351b9a4f00bee9',

View File

@@ -27,8 +27,6 @@ public interface TorConstants {
int PREF_TOR_NETWORK_AUTOMATIC = 0;
int PREF_TOR_NETWORK_WITHOUT_BRIDGES = 1;
int PREF_TOR_NETWORK_WITH_BRIDGES = 2;
// TODO: Remove when settings migration code is removed
int PREF_TOR_NETWORK_NEVER = 3;
// Default values for local settings
boolean DEFAULT_PREF_PLUGIN_ENABLE = true;

View File

@@ -33,7 +33,7 @@ dependencies {
testImplementation "org.jmock:jmock:$jmock_version"
testImplementation "org.jmock:jmock-junit4:$jmock_version"
testImplementation "org.jmock:jmock-imposters:$jmock_version"
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.3"
testImplementation "com.squareup.okhttp3:mockwebserver:$mockwebserver_version"
testAnnotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"

View File

@@ -140,7 +140,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private final long maxLatency;
private final int maxIdleTime;
private final int socketTimeout;
private final File torDirectory, geoIpFile, configFile;
private final File torDirectory;
private final File configFile;
private final int torSocksPort;
private final int torControlPort;
private final File doneFile, cookieFile;
@@ -195,7 +196,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
this.torDirectory = torDirectory;
this.torSocksPort = torSocksPort;
this.torControlPort = torControlPort;
geoIpFile = new File(torDirectory, "geoip");
configFile = new File(torDirectory, "torrc");
doneFile = new File(torDirectory, "done");
cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
@@ -332,12 +332,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
// The done file may already exist from a previous installation
//noinspection ResultOfMethodCallIgnored
doneFile.delete();
// The GeoIP file may exist from a previous installation - we can
// save some space by deleting it.
// TODO: Remove after a reasonable migration period
// (added 2022-03-29)
//noinspection ResultOfMethodCallIgnored
geoIpFile.delete();
installTorExecutable();
installObfs4Executable();
installSnowflakeExecutable();

View File

@@ -21,11 +21,11 @@ n Bridge obfs4 185.103.252.72:443 75F15E9339FF572F88F5588D429FEA379744BC53 cert=
n Bridge obfs4 76.255.201.112:8888 96CF36C2ECCFB7376AB6BE905BECD2C2AE8AEFCD cert=+q0pjaiM0JMqHL/BKqCRD+pjflaw/S406eUDF7CnFgamvQW3l2HVLJhQ6uX9P8zff0PLGg iat-mode=0
n Bridge obfs4 94.142.246.132:8088 135C158527AA9FE9A2F26EC515EB6999D813D347 cert=wTUz0/5FhAZRkitil5MprGbSF3JzjxjxI1kAmxAdSeDy98NgcLr11f/qUXWDC76Y97RiSg iat-mode=0
n Bridge obfs4 20.102.79.78:22022 B5705F7E616DAB0F477E3E1ADC23E40413F683FE cert=1Cc/hwPtPjzFKGHVOP0j/qmBgnvquRx8+im35/u5TIYjDQ3FlMfA5VvTrQ/hbX8BZZooLQ iat-mode=0
n Bridge obfs4 207.154.242.137:80 8E67A1B2A342652EE27376BD61BECF5806700E7F cert=qUrR9fan3XPNGNOwn9WGlXLJNZZx0grXH4AZXR+yoBbtbbj5Ak1n4a7TtjYgXcWcs/gHXw iat-mode=0
n Bridge obfs4 152.70.180.20:1993 3327C43587E66AD5F874C4234A1D72C938AD7318 cert=s7xLRUO2psaX7TMUP2YhXdxItR4U6K7D+E3gQaS/+yWUppevtazIibq4dN1g5Reu6dD2QQ iat-mode=0
n Bridge obfs4 144.202.12.254:10002 4E220F45CD404C8A3082A36326A5ED19BB8D4404 cert=iLz5YYWO4pUw7U7MRNOSvE0qO+IVeE4kVfFVWPO3coH3FmZtrkvlaTklfXxHZaCcXWBgaA iat-mode=0
n Bridge obfs4 15.235.47.204:42058 869133925B3CD07683BDF01805C36448D090CE88 cert=PFwh4mzZlSTUdcEskpe20t998n5jbr81s+XoX7gmazqzUGHNhkendK5K1j2gOxesz9AkBw iat-mode=0
n Bridge obfs4 51.75.93.136:45532 8402B84833527BC249B21AC885134197E624FB5A cert=LwXEf/Dgo0tKdMJByXdlvWiJqyyPw4T284Cg5qygDuIJJNFuz3ED9UhGil6H4Of3gM7wSg iat-mode=0
n Bridge obfs4 109.14.168.159:5082 BFE1416DEFFE969581F016A4A319A87FFB26BA91 cert=n3X1CDdKBPXPIzfKh83p3ydfMzb0AD9gKC+/gIpHb7+xjjAnYO9x3LT+T/MvOIfAXxYySg iat-mode=0
n Bridge obfs4 45.150.172.16:80 82849E69CBB25EA7F479155F7DCD89D85717FF47 cert=+Krdu1jmVQOxWkMj0mqJHgwbQV49eyD6mZDS+mRExssWNHosa60g4P5Gp81sBJKzN8NrSw iat-mode=0
n Bridge obfs4 185.177.207.132:8443 4FB781F7A9DD39DA53A7996907817FC479874D19 cert=UL2gCAXWW5kEWY4TQ0lNeu6OAmzh40bXYVhMnTWVG8USnyy/zEKGSIPgmwTDMumWr9c1Pg iat-mode=0
v Bridge 92.243.15.235:9001 477EAD3C04036B48235F1F27FC91420A286A4B7F
v Bridge 213.108.108.145:17674 A39C0FE47963B6E8CFE9815549864DE544935A31
v Bridge 213.196.191.96:9060 05E222E2A8C234234FE0CEB58B08A93B8FC360DB

View File

@@ -30,8 +30,8 @@ dependencyVerification {
'org.briarproject:obfs4proxy-windows:0.0.14-tor1:obfs4proxy-windows-0.0.14-tor1.jar:9dd122b31b3cd1616f168091dcdb01de049d1e052fe5c089b7627618a8a2694b',
'org.briarproject:snowflake-linux:2.3.1:snowflake-linux-2.3.1.jar:99ecf4546d8f79eb8408168c09380fec596558ac934554bf7d4247ea7ef2c9f3',
'org.briarproject:snowflake-windows:2.3.1:snowflake-windows-2.3.1.jar:d011f1a72c00a221f56380c19aad8ff11db8c2bb1adb0784125572d80b4d275a',
'org.briarproject:tor-linux:0.4.5.14:tor-linux-0.4.5.14.jar:1844e54cf6df0c85cec219381a3364c759ae444a6b63f7558b757becb7d41d08',
'org.briarproject:tor-windows:0.4.5.14:tor-windows-0.4.5.14.jar:d337afa1043f0cfa7e6e8c2473d682a5663a2c8052bb97a770450893c78c9b4f',
'org.briarproject:tor-linux:0.4.7.13:tor-linux-0.4.7.13.jar:9819ee973cbcdc133f7d04aef9d4b957a35087627a790e532142d15412a9636f',
'org.briarproject:tor-windows:0.4.7.13:tor-windows-0.4.7.13.jar:853d2769665614e26703cbe02e43b218b064c04a0bcd120fdc459cda45bd2606',
'org.checkerframework:checker-compat-qual:2.5.5:checker-compat-qual-2.5.5.jar:11d134b245e9cacc474514d2d66b5b8618f8039a1465cdc55bbc0b34e0008b7a',
'org.checkerframework:checker-qual:3.12.0:checker-qual-3.12.0.jar:ff10785ac2a357ec5de9c293cb982a2cbb605c0309ea4cc1cb9b9bc6dbe7f3cb',
'org.hamcrest:hamcrest-core:2.1:hamcrest-core-2.1.jar:e09109e54a289d88506b9bfec987ddd199f4217c9464132668351b9a4f00bee9',

View File

@@ -26,8 +26,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 31
versionCode 10419
versionName "1.4.19"
versionCode 10420
versionName "1.4.20"
applicationId "org.briarproject.briar.android"
buildConfigField "String", "TorVersion", "\"$tor_version\""

View File

@@ -52,7 +52,6 @@ import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResul
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SUCCESS;
import static org.briarproject.bramble.util.AndroidUtils.isUiThread;
import static org.briarproject.briar.android.BriarApplication.ENTRY_ACTIVITY;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FAILURE_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_CHANNEL_OLD_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_NOTIFICATION_ID;
@@ -141,11 +140,6 @@ public class BriarService extends Service {
ongoingChannel.setLockscreenVisibility(VISIBILITY_SECRET);
ongoingChannel.setShowBadge(false);
nm.createNotificationChannel(ongoingChannel);
// Delete the unused channel previously used for startup
// failure notifications
// TODO: Remove this ID after a reasonable upgrade period
// (added 2021-07-12)
nm.deleteNotificationChannel(FAILURE_CHANNEL_ID);
}
Notification foregroundNotification =
notificationManager.getForegroundNotification();

View File

@@ -41,7 +41,6 @@ import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
@NotNullByDefault
abstract class BaseViewModel extends DbViewModel implements EventListener {
@@ -115,7 +114,7 @@ abstract class BaseViewModel extends DbViewModel implements EventListener {
@DatabaseExecutor
private String getPostText(Transaction txn, MessageId m)
throws DbException {
return HtmlUtils.clean(blogManager.getPostText(txn, m), ARTICLE);
return HtmlUtils.cleanArticle(blogManager.getPostText(txn, m));
}
LiveData<LiveResult<BlogPostItem>> loadBlogPost(GroupId g, MessageId m) {

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.blog;
import android.content.Context;
import android.content.Intent;
import android.text.Spanned;
import android.text.util.Linkify;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -170,7 +171,12 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
// TODO make author clickable #624
text.setText(c.getComment());
if (fullText) text.setTextIsSelectable(true);
Linkify.addLinks(text, Linkify.WEB_URLS);
text.setMovementMethod(null);
if (fullText) {
text.setTextIsSelectable(true);
makeLinksClickable(text, listener::onLinkClick);
}
commentContainer.addView(v);
}

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.widget.Toast;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
@@ -16,10 +15,6 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.EXISTS;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.FAILED;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.IMPORTED;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedActivity extends BriarActivity
@@ -50,13 +45,13 @@ public class RssFeedActivity extends BriarActivity
viewModel.getImportResult().observeEvent(this, this::onImportResult);
}
private void onImportResult(RssFeedViewModel.ImportResult result) {
if (result == IMPORTED) {
private void onImportResult(boolean result) {
if (result) {
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag(RssFeedImportFragment.TAG) != null) {
onBackPressed();
}
} else if (result == FAILED) {
} else {
String url = viewModel.getUrlFailedImport();
if (url == null) {
throw new AssertionError();
@@ -65,9 +60,6 @@ public class RssFeedActivity extends BriarActivity
RssFeedImportFailedDialogFragment.newInstance(url);
dialog.show(getSupportFragmentManager(),
RssFeedImportFailedDialogFragment.TAG);
} else if (result == EXISTS) {
Toast.makeText(this, R.string.blogs_rss_feeds_import_exists,
Toast.LENGTH_LONG).show();
}
}
}

View File

@@ -28,8 +28,7 @@ class RssFeedAdapter extends ListAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
super(new DiffUtil.ItemCallback<Feed>() {
@Override
public boolean areItemsTheSame(Feed a, Feed b) {
return a.getUrl().equals(b.getUrl()) &&
a.getBlogId().equals(b.getBlogId()) &&
return a.getBlogId().equals(b.getBlogId()) &&
a.getAdded() == b.getAdded();
}
@@ -86,8 +85,8 @@ class RssFeedAdapter extends ListAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
delete.setOnClickListener(v -> listener.onDeleteClick(item));
// Author
if (item.getRssAuthor() != null) {
author.setText(item.getRssAuthor());
if (item.getProperties().getAuthor() != null) {
author.setText(item.getProperties().getAuthor());
author.setVisibility(VISIBLE);
authorLabel.setVisibility(VISIBLE);
} else {
@@ -100,8 +99,8 @@ class RssFeedAdapter extends ListAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
updated.setText(formatDate(ctx, item.getUpdated()));
// Description
if (item.getDescription() != null) {
description.setText(item.getDescription());
if (item.getProperties().getDescription() != null) {
description.setText(item.getProperties().getDescription());
description.setVisibility(VISIBLE);
} else {
description.setVisibility(GONE);

View File

@@ -22,7 +22,7 @@ import org.briarproject.nullsafety.NotNullByDefault;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -38,13 +38,9 @@ import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.EXISTS;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.FAILED;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.IMPORTED;
@NotNullByDefault
class RssFeedViewModel extends DbViewModel {
enum ImportResult {IMPORTED, FAILED, EXISTS}
private static final Logger LOG =
getLogger(RssFeedViewModel.class.getName());
@@ -60,7 +56,7 @@ class RssFeedViewModel extends DbViewModel {
private volatile String urlFailedImport = null;
private final MutableLiveData<Boolean> isImporting =
new MutableLiveData<>(false);
private final MutableLiveEvent<ImportResult> importResult =
private final MutableLiveEvent<Boolean> importResult =
new MutableLiveEvent<>();
@Inject
@@ -101,7 +97,6 @@ class RssFeedViewModel extends DbViewModel {
private List<Feed> loadFeeds(Transaction txn) throws DbException {
long start = now();
List<Feed> feeds = feedManager.getFeeds(txn);
Collections.sort(feeds);
logDuration(LOG, "Loading feeds", start);
return feeds;
}
@@ -125,7 +120,7 @@ class RssFeedViewModel extends DbViewModel {
});
}
LiveEvent<ImportResult> getImportResult() {
LiveEvent<Boolean> getImportResult() {
return importResult;
}
@@ -138,21 +133,23 @@ class RssFeedViewModel extends DbViewModel {
urlFailedImport = null;
ioExecutor.execute(() -> {
try {
if (exists(url)) {
importResult.postEvent(EXISTS);
return;
}
Feed feed = feedManager.addFeed(url);
List<Feed> updated = addListItem(getList(feeds), feed);
if (updated != null) {
Collections.sort(updated);
feeds.postValue(new LiveResult<>(updated));
// Update the feed if it was already present
List<Feed> feedList = getList(feeds);
if (feedList == null) feedList = new ArrayList<>();
List<Feed> updated = updateListItems(feedList,
f -> f.equals(feed), f -> feed);
// Add the feed if it wasn't already present
if (updated == null) {
feedList.add(feed);
updated = feedList;
}
importResult.postEvent(IMPORTED);
feeds.postValue(new LiveResult<>(updated));
importResult.postEvent(true);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
urlFailedImport = url;
importResult.postEvent(FAILED);
importResult.postEvent(false);
} finally {
isImporting.postValue(false);
}
@@ -163,16 +160,4 @@ class RssFeedViewModel extends DbViewModel {
String getUrlFailedImport() {
return urlFailedImport;
}
private boolean exists(String url) {
List<Feed> list = getList(feeds);
if (list != null) {
for (Feed feed : list) {
if (url.equals(feed.getUrl())) {
return true;
}
}
}
return false;
}
}

View File

@@ -12,7 +12,7 @@ import java.util.ArrayList;
import java.util.Collection;
@NotNullByDefault
public abstract class BaseContactSelectorAdapter<I extends SelectableContactItem, H extends ContactItemViewHolder<I>>
public abstract class BaseContactSelectorAdapter<I extends BaseSelectableContactItem, H extends ContactItemViewHolder<I>>
extends BaseContactListAdapter<I, H> {
public BaseContactSelectorAdapter(Context context, Class<I> c,
@@ -24,7 +24,7 @@ public abstract class BaseContactSelectorAdapter<I extends SelectableContactItem
Collection<ContactId> selected = new ArrayList<>();
for (int i = 0; i < items.size(); i++) {
SelectableContactItem item = items.get(i);
BaseSelectableContactItem item = items.get(i);
if (item.isSelected()) selected.add(item.getContact().getId());
}
return selected;

View File

@@ -33,7 +33,7 @@ import static org.briarproject.briar.android.contactselection.ContactSelectorAct
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class BaseContactSelectorFragment<I extends SelectableContactItem, A extends BaseContactSelectorAdapter<I, ? extends ContactItemViewHolder<I>>>
public abstract class BaseContactSelectorFragment<I extends BaseSelectableContactItem, A extends BaseContactSelectorAdapter<I, ? extends ContactItemViewHolder<I>>>
extends BaseFragment
implements OnContactClickListener<I> {

View File

@@ -17,7 +17,7 @@ import static org.briarproject.briar.android.util.UiUtils.GREY_OUT;
@UiThread
@NotNullByDefault
public class BaseSelectableContactHolder<I extends SelectableContactItem>
public abstract class BaseSelectableContactHolder<I extends BaseSelectableContactItem>
extends ContactItemViewHolder<I> {
private final CheckBox checkBox;

View File

@@ -0,0 +1,32 @@
package org.briarproject.briar.android.contactselection;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.briar.android.contact.ContactItem;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
@NotNullByDefault
public abstract class BaseSelectableContactItem extends ContactItem {
private boolean selected;
public BaseSelectableContactItem(Contact contact, AuthorInfo authorInfo,
boolean selected) {
super(contact, authorInfo);
this.selected = selected;
}
boolean isSelected() {
return selected;
}
void toggleSelected() {
selected = !selected;
}
public abstract boolean isDisabled();
}

View File

@@ -10,7 +10,7 @@ import org.briarproject.nullsafety.NotNullByDefault;
import java.util.Collection;
@NotNullByDefault
public interface ContactSelectorController<I extends SelectableContactItem>
public interface ContactSelectorController<I extends BaseSelectableContactItem>
extends DbController {
void loadContacts(GroupId g, Collection<ContactId> selection,

View File

@@ -11,6 +11,7 @@ import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.identity.AuthorManager;
import org.briarproject.briar.api.sharing.SharingManager.SharingStatus;
import org.briarproject.nullsafety.NotNullByDefault;
import java.util.ArrayList;
@@ -53,10 +54,8 @@ public abstract class ContactSelectorControllerImpl
AuthorInfo authorInfo = authorManager.getAuthorInfo(c);
// was this contact already selected?
boolean selected = selection.contains(c.getId());
// can this contact be selected?
boolean disabled = isDisabled(g, c);
contacts.add(new SelectableContactItem(c, authorInfo,
selected, disabled));
selected, getSharingStatus(g, c)));
}
handler.onResult(contacts);
} catch (DbException e) {
@@ -67,7 +66,7 @@ public abstract class ContactSelectorControllerImpl
}
@DatabaseExecutor
protected abstract boolean isDisabled(GroupId g, Contact c)
protected abstract SharingStatus getSharingStatus(GroupId g, Contact c)
throws DbException;
}

View File

@@ -2,15 +2,22 @@ package org.briarproject.briar.android.contactselection;
import android.view.View;
import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.OnContactClickListener;
import org.briarproject.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.ERROR;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.INVITE_RECEIVED;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.INVITE_SENT;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.NOT_SUPPORTED;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHARING;
@UiThread
@NotNullByDefault
@@ -27,6 +34,19 @@ class SelectableContactHolder
super.bind(item, listener);
if (item.isDisabled()) {
@StringRes int strRes;
if (item.getSharingStatus() == SHARING) {
strRes = R.string.forum_invitation_already_sharing;
} else if (item.getSharingStatus() == INVITE_SENT) {
strRes = R.string.forum_invitation_already_invited;
} else if (item.getSharingStatus() == INVITE_RECEIVED) {
strRes = R.string.forum_invitation_invite_received;
} else if (item.getSharingStatus() == NOT_SUPPORTED) {
strRes = R.string.forum_invitation_not_supported;
} else if (item.getSharingStatus() == ERROR) {
strRes = R.string.forum_invitation_error;
} else throw new AssertionError("Unhandled SharingStatus");
info.setText(strRes);
info.setVisibility(VISIBLE);
} else {
info.setVisibility(GONE);

View File

@@ -1,36 +1,33 @@
package org.briarproject.briar.android.contactselection;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.briar.android.contact.ContactItem;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.sharing.SharingManager.SharingStatus;
import org.briarproject.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.NotThreadSafe;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE;
@NotThreadSafe
@NotNullByDefault
public class SelectableContactItem extends ContactItem {
public class SelectableContactItem extends BaseSelectableContactItem {
private boolean selected;
private final boolean disabled;
private final SharingStatus sharingStatus;
public SelectableContactItem(Contact contact, AuthorInfo authorInfo,
boolean selected, boolean disabled) {
super(contact, authorInfo);
this.selected = selected;
this.disabled = disabled;
boolean selected, SharingStatus sharingStatus) {
super(contact, authorInfo, selected);
this.sharingStatus = sharingStatus;
}
boolean isSelected() {
return selected;
}
void toggleSelected() {
selected = !selected;
public SharingStatus getSharingStatus() {
return sharingStatus;
}
@Override
public boolean isDisabled() {
return disabled;
return sharingStatus != SHAREABLE;
}
}

View File

@@ -66,6 +66,7 @@ import org.briarproject.briar.android.view.TextAttachmentController.AttachmentLi
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent;
@@ -476,6 +477,12 @@ public class ConversationActivity extends BriarActivity
actionMode = null;
}
@Override
public void onLinkClick(String url) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(getSupportFragmentManager(), f.getUniqueTag());
}
private void addSelectionTracker() {
RecyclerView recyclerView = list.getRecyclerView();
if (recyclerView.getAdapter() != adapter)
@@ -925,6 +932,7 @@ public class ConversationActivity extends BriarActivity
}
private void removeContact() {
list.showProgressBar();
runOnDbThread(() -> {
try {
contactManager.removeContact(contactId);

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.text.util.Linkify;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
@@ -19,6 +20,7 @@ import static android.view.View.VISIBLE;
import static org.briarproject.bramble.util.StringUtils.trim;
import static org.briarproject.briar.android.util.UiUtils.formatDate;
import static org.briarproject.briar.android.util.UiUtils.formatDuration;
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@UiThread
@@ -58,6 +60,8 @@ abstract class ConversationItemViewHolder extends ViewHolder {
if (item.getText() != null) {
text.setText(trim(item.getText()));
Linkify.addLinks(text, Linkify.WEB_URLS);
makeLinksClickable(text, listener::onLinkClick);
}
long timestamp = item.getTime();

View File

@@ -20,4 +20,6 @@ interface ConversationListener {
void onAutoDeleteTimerNoticeClicked();
void onLinkClick(String url);
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.conversation;
import android.text.util.Linkify;
import android.view.View;
import android.widget.TextView;
@@ -13,6 +14,7 @@ import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.bramble.util.StringUtils.trim;
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
@UiThread
@NotNullByDefault
@@ -40,6 +42,8 @@ class ConversationNoticeViewHolder extends ConversationItemViewHolder {
} else {
msgText.setVisibility(VISIBLE);
msgText.setText(trim(text));
Linkify.addLinks(msgText, Linkify.WEB_URLS);
makeLinksClickable(msgText, listener::onLinkClick);
layout.setBackgroundResource(isIncoming() ?
R.drawable.notice_in_bottom : R.drawable.notice_out_bottom);
}

View File

@@ -245,6 +245,7 @@ public class MailboxActivity extends BriarActivity {
}
private void onUnPaired(boolean tellUserToWipeMailbox) {
viewModel.clearProblemNotification();
if (tellUserToWipeMailbox) {
showFragment(getSupportFragmentManager(), new BlankFragment(),
BlankFragment.TAG);

View File

@@ -13,6 +13,7 @@ import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListAct
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
import org.briarproject.briar.android.threaded.ThreadListActivity;
import org.briarproject.briar.android.threaded.ThreadListViewModel;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.nullsafety.MethodsNotNullByDefault;
import org.briarproject.nullsafety.ParametersNotNullByDefault;
@@ -158,6 +159,12 @@ public class GroupActivity extends
if (isDissolved != null && !isDissolved) super.onReplyClick(item);
}
@Override
public void onLinkClick(String url){
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(getSupportFragmentManager(), f.getUniqueTag());
}
private void setGroupEnabled(boolean enabled) {
sendController.setReady(enabled);
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f);

View File

@@ -26,6 +26,7 @@ import org.briarproject.briar.api.privategroup.PrivateGroupFactory;
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationFactory;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.briar.api.sharing.SharingManager.SharingStatus;
import org.briarproject.nullsafety.NotNullByDefault;
import java.util.ArrayList;
@@ -140,8 +141,8 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
}
@Override
protected boolean isDisabled(GroupId g, Contact c) throws DbException {
return !groupInvitationManager.isInvitationAllowed(c, g);
protected SharingStatus getSharingStatus(GroupId g, Contact c) throws DbException {
return groupInvitationManager.getSharingStatus(c, g);
}
@Override

View File

@@ -92,7 +92,7 @@ class RevealContactsControllerImpl extends DbControllerImpl
boolean selected =
disabled || selection.contains(c.getId());
items.add(new RevealableContactItem(c, authorInfo, selected,
disabled, m.getVisibility()));
m.getVisibility()));
}
}

View File

@@ -1,22 +1,24 @@
package org.briarproject.briar.android.privategroup.reveal;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.briar.android.contactselection.SelectableContactItem;
import org.briarproject.briar.android.contactselection.BaseSelectableContactItem;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.privategroup.Visibility;
import org.briarproject.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.NotThreadSafe;
import static org.briarproject.briar.api.privategroup.Visibility.INVISIBLE;
@NotThreadSafe
@NotNullByDefault
class RevealableContactItem extends SelectableContactItem {
class RevealableContactItem extends BaseSelectableContactItem {
private final Visibility visibility;
RevealableContactItem(Contact contact, AuthorInfo authorInfo,
boolean selected, boolean disabled, Visibility visibility) {
super(contact, authorInfo, selected, disabled);
boolean selected, Visibility visibility) {
super(contact, authorInfo, selected);
this.visibility = visibility;
}
@@ -24,4 +26,8 @@ class RevealableContactItem extends SelectableContactItem {
return visibility;
}
@Override
public boolean isDisabled() {
return visibility != INVISIBLE;
}
}

View File

@@ -18,7 +18,6 @@ import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.briar.android.settings.SettingsViewModel.BT_NAMESPACE;
import static org.briarproject.briar.android.settings.SettingsViewModel.TOR_NAMESPACE;
@@ -61,32 +60,18 @@ class ConnectionsManager {
}
void updateTorSettings(Settings settings) {
Settings torSettings = migrateTorSettings(settings);
torEnabled.postValue(torSettings.getBoolean(PREF_PLUGIN_ENABLE,
torEnabled.postValue(settings.getBoolean(PREF_PLUGIN_ENABLE,
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE));
int torNetworkSetting = torSettings.getInt(PREF_TOR_NETWORK,
int torNetworkSetting = settings.getInt(PREF_TOR_NETWORK,
DEFAULT_PREF_TOR_NETWORK);
torNetwork.postValue(Integer.toString(torNetworkSetting));
torMobile.postValue(torSettings.getBoolean(PREF_TOR_MOBILE,
torMobile.postValue(settings.getBoolean(PREF_TOR_MOBILE,
DEFAULT_PREF_TOR_MOBILE));
torCharging
.postValue(torSettings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING,
DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING));
}
// TODO: Remove after a reasonable migration period (added 2020-06-25)
private Settings migrateTorSettings(Settings s) {
int network = s.getInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
if (network == PREF_TOR_NETWORK_NEVER) {
s.putInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
s.putBoolean(PREF_PLUGIN_ENABLE, false);
// We don't need to save the migrated settings - the Tor plugin is
// responsible for that. This code just handles the case where the
// settings are loaded before the plugin migrates them.
}
return s;
torCharging.postValue(settings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING,
DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING));
}
LiveData<Boolean> btEnabled() {

View File

@@ -13,6 +13,7 @@ import org.briarproject.briar.android.contactselection.ContactSelectorController
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.identity.AuthorManager;
import org.briarproject.briar.api.sharing.SharingManager.SharingStatus;
import org.briarproject.nullsafety.NotNullByDefault;
import java.util.Collection;
@@ -47,8 +48,9 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
}
@Override
protected boolean isDisabled(GroupId g, Contact c) throws DbException {
return !blogSharingManager.canBeShared(g, c);
protected SharingStatus getSharingStatus(GroupId g, Contact c)
throws DbException {
return blogSharingManager.getSharingStatus(g, c);
}
@Override

View File

@@ -13,6 +13,7 @@ import org.briarproject.briar.android.contactselection.ContactSelectorController
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.api.identity.AuthorManager;
import org.briarproject.briar.api.sharing.SharingManager.SharingStatus;
import org.briarproject.nullsafety.NotNullByDefault;
import java.util.Collection;
@@ -47,8 +48,9 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
}
@Override
protected boolean isDisabled(GroupId g, Contact c) throws DbException {
return !forumSharingManager.canBeShared(g, c);
protected SharingStatus getSharingStatus(GroupId g, Contact c)
throws DbException {
return forumSharingManager.getSharingStatus(g, c);
}
@Override

View File

@@ -4,6 +4,7 @@ import android.animation.Animator;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.text.util.Linkify;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
@@ -20,6 +21,7 @@ import androidx.annotation.UiThread;
import androidx.recyclerview.widget.RecyclerView;
import static androidx.core.content.ContextCompat.getColor;
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
@UiThread
@NotNullByDefault
@@ -43,6 +45,8 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
@CallSuper
public void bind(I item, ThreadItemListener<I> listener) {
textView.setText(StringUtils.trim(item.getText()));
Linkify.addLinks(textView, Linkify.WEB_URLS);
makeLinksClickable(textView, listener::onLinkClick);
author.setAuthor(item.getAuthor(), item.getAuthorInfo());
author.setDate(item.getTimestamp());

View File

@@ -137,6 +137,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
public interface ThreadItemListener<I> {
void onReplyClick(I item);
void onLinkClick(String url);
}
}

View File

@@ -19,6 +19,7 @@ import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.android.view.UnreadMessageButton;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import org.briarproject.nullsafety.MethodsNotNullByDefault;
import org.briarproject.nullsafety.ParametersNotNullByDefault;
@@ -202,6 +203,12 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
}
}
@Override
public void onLinkClick(String url) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(getSupportFragmentManager(), f.getUniqueTag());
}
protected void setToolbarSubTitle(SharingInfo sharingInfo) {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {

View File

@@ -24,7 +24,6 @@ public interface AndroidNotificationManager {
// Notification IDs
int ONGOING_NOTIFICATION_ID = 1;
int FAILURE_NOTIFICATION_ID = 2;
int REMINDER_NOTIFICATION_ID = 3;
int PRIVATE_MESSAGE_NOTIFICATION_ID = 4;
int GROUP_MESSAGE_NOTIFICATION_ID = 5;
@@ -47,10 +46,6 @@ public interface AndroidNotificationManager {
String HOTSPOT_CHANNEL_ID = "zHotspot";
String MAILBOX_PROBLEM_CHANNEL_ID = "zMailboxProblem";
// This channel is no longer used - keep the ID so we can remove the
// channel from existing installations
String FAILURE_CHANNEL_ID = "zStartupFailure";
// Actions for pending intents
String ACTION_DISMISS_REMINDER = "dismissReminder";
String ACTION_STOP_HOTSPOT = "stopHotspot";

View File

@@ -33,6 +33,7 @@
android:paddingBottom="@dimen/listitem_vertical_margin"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_small"
android:textColorLink="@color/briar_text_link"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/authorView"

View File

@@ -41,6 +41,7 @@
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
android:textColor="?android:attr/textColorPrimary"
android:textColorLink="@color/briar_text_link"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/statusLayout"
app:layout_constraintEnd_toEndOf="parent"

View File

@@ -49,6 +49,7 @@
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
android:textColor="@color/briar_text_primary_inverse"
android:textColorLink="@color/briar_text_link_inverse"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/statusLayout"
app:layout_constraintEnd_toEndOf="parent"

View File

@@ -21,6 +21,7 @@
android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
android:background="@drawable/msg_in_top"
android:elevation="@dimen/message_bubble_elevation"
android:textColorLink="@color/briar_text_link"
tools:text="Short message"
tools:visibility="visible" />

View File

@@ -23,6 +23,7 @@
android:background="@drawable/msg_out_top"
android:elevation="@dimen/message_bubble_elevation"
android:textColor="@color/briar_text_primary_inverse"
android:textColorLink="@color/briar_text_link_inverse"
tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."
tools:visibility="visible" />

View File

@@ -22,6 +22,7 @@
android:background="@drawable/msg_in_top"
android:elevation="@dimen/message_bubble_elevation"
android:textColor="?android:attr/textColorPrimary"
android:textColorLink="@color/briar_text_link"
tools:text="Short message"
tools:visibility="visible" />

View File

@@ -114,6 +114,7 @@
android:textColor="?android:attr/textColorPrimary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textColorLink="@color/briar_text_link"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -1,36 +1,35 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<!--Setup-->
<string name="setup_title">Benvingut a Briar</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">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="dnkm_doze_title">Connexions en segon pla</string>
<string name="dnkm_doze_intro">Per rebre missatges Briar necessita estar connectat en segon pla.</string>
<string name="dnkm_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="dnkm_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 massa feble</string>
<string name="passwords_do_not_match">Les contrasenyes no coincideixen</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 tornis a preguntar-ho</string>
<string name="dnkm_huawei_protected_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="dnkm_huawei_protected_button">Protegeix Briar</string>
<string name="dnkm_huawei_protected_help">Si no afegiu Briar a la llista d\'aplicacions protegides, s\'evitarà que Briar s\'executi en segon pla.</string>
<string name="dnkm_huawei_app_launch_text">Premeu el botó, obriu la pantalla «Llença app» i assegureu-vos que Briar està configurat com «Gestiona a ma».</string>
<string name="dnkm_huawei_app_launch_button">Obriu la configuració de la bateria</string>
<string name="dnkm_huawei_app_launch_help">Si Briar no està configurat com a «Gestiona manualment» a la pantalla «Llença app», no podrà executar-se en segon pla.</string>
<string name="dnkm_xiaomi_text">Per executar-se en segon pla, Briar necessita ser blocat en la llista d\'aplicacions recents.</string>
<string name="dnkm_xiaomi_button">Protegeix Briar</string>
<string name="dnkm_xiaomi_help">Si Briar no està blocat a la llista d\'aplicacions recents, no podrà executar-se en segon pla.</string>
<string name="dnkm_xiaomi_dialog_body_old">1. Obriu la llista d\'aplicacions recents (de canvi d\'aplicació)\n\n2. Desplaceu avall la icona de Briar per mostrar la icona del cadenat\n\n3. Si el cadenat és obert, premeu per tancar-lo.</string>
<string name="dnkm_xiaomi_dialog_body_new">1. Obriu la llista d\'aplicacions recents (canvi d\'aplicació)\n\n2. Premeu la icona de Briar fins que es mostra la icona del cadenat\n\n3. Si el cadenat és obert, premeu per tancar-lo.</string>
<string name="dnkm_warning_dozed">%s no s\'ha pogut executar en segon pla</string>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Setup-->
<string name="setup_title">Benvingut a Briar</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">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="dnkm_doze_intro">Per rebre missatges Briar necessita estar connectat en segon pla.</string>
<string name="dnkm_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="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 massa feble</string>
<string name="passwords_do_not_match">Les contrasenyes no coincideixen</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 tornis a preguntar-ho</string>
<string name="dnkm_huawei_protected_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="dnkm_huawei_protected_button">Protegeix Briar</string>
<string name="dnkm_huawei_protected_help">Si no afegiu Briar a la llista d\'aplicacions protegides, s\'evitarà que Briar s\'executi en segon pla.</string>
<string name="dnkm_huawei_app_launch_text">Premeu el botó, obriu la pantalla «Llença app» i assegureu-vos que Briar està configurat com «Gestiona a ma».</string>
<string name="dnkm_huawei_app_launch_help">Si Briar no està configurat com a «Gestiona manualment» a la pantalla «Llença app», no podrà executar-se en segon pla.</string>
<string name="dnkm_xiaomi_text">Per executar-se en segon pla, Briar necessita ser blocat en la llista d\'aplicacions recents.</string>
<string name="dnkm_xiaomi_button">Protegeix Briar</string>
<string name="dnkm_xiaomi_help">Si Briar no està blocat a la llista d\'aplicacions recents, no podrà executar-se en segon pla.</string>
<string name="dnkm_xiaomi_dialog_body_old">1. Obriu la llista d\'aplicacions recents (de canvi d\'aplicació)\n\n2. Desplaceu avall la icona de Briar per mostrar la icona del cadenat\n\n3. Si el cadenat és obert, premeu per tancar-lo.</string>
<string name="dnkm_xiaomi_dialog_body_new">1. Obriu la llista d\'aplicacions recents (també anomenada commutador d\'aplicacions)\n\n2. Si el Briar té una petita imatge d\'un cadenat al costat del seu nom, no cal que feu res\n\n3. Si no hi ha cadenat, manteniu premuda la imatge de Briar fins que aparegui el botó del cadenat i, a continuació, toqueu-lo</string>
<string name="dnkm_xiaomi_lock_apps_text">Toqueu el botó següent per obrir la configuració de seguretat. Toqueu «Augmenta la velocitat» i, a continuació, toqueu «Bloca aplicacions» i assegureu-vos que el Briar estigui configurat a \"Blocat\".</string>
<string name="dnkm_xiaomi_lock_apps_help">Si el Briar no està configurat com a «Blocat» a la pantalla «Bloca aplicacions», no es podrà executar en segon pla.</string>
<string name="dnkm_warning_dozed_1">El Briar no va poder executar-se en segon pla</string>
<!--Login-->
<string name="enter_password">Contrasenya</string>
<string name="try_again">La contrasenya és incorrecta, torneu a escriure-la</string>
@@ -41,14 +40,25 @@
<string name="dialog_title_lost_password">Contrasenya perduda</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_activity_title">Error iniciant Briar</string>
<string name="startup_failed_clock_error">El Briar no s\'ha pogut iniciar perquè l\'hora del dispositiu és incorrecte.\n\nConfigureu l\'hora del dispositiu correctament i torneu-ho a provar.</string>
<string name="startup_failed_db_error">El Briar no ha pogut obrir la base de dades que conté el vostre compte, els vostres contactes i els vostres missatges.\n\nActualitzeu a la darrera versió de l\'aplicació i torneu-ho a provar, o configureu un compte nou seleccionant «He oblidat la meva contrasenya» a la sol·licitud de contrasenya.</string>
<string name="startup_failed_data_too_old_error">El vostre compte s\'ha creat amb una versió antiga d\'aquesta aplicació i no es pot obrir amb aquesta versió.\n\nHeu de tornar a instal·lar la versió antiga o configurar un compte nou escollint «He oblidat la meva contrasenya» a la sol·licitud de contrasenya.</string>
<string name="startup_failed_data_too_new_error">El vostre compte s\'ha creat amb una versió més recent d\'aquesta aplicació i no es pot obrir amb aquesta versió.\n\nActualitzeu a la versió més recent i torneu-ho a provar.</string>
<string name="startup_failed_service_error">El Briar no ha pogut iniciar un component necessari.\n\nActualitzeu l\'aplicació a la darrera versió i torna-ho a provar.</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 caducarà en %d dies i no es podrà renovar.</item>
</plurals>
<plurals name="old_android_expiry_warning">
<item quantity="one">L\'Android 4 ja no és compatible. El Briar deixarà de funcionar a %s (d\'aquí a %d dia). Instal·leu el Briar en un dispositiu més nou i creeu un compte nou.</item>
<item quantity="other">L\'Android 4 ja no és compatible. El Briar deixarà de funcionar a %s (d\'aquí a %d dies). Instal·leu el Briar en un dispositiu més nou i creeu un compte nou.</item>
</plurals>
<string name="expiry_date_reached">Aquesta versió de Briar ha caducat.\nGràcies per haver-lo provat!</string>
<string name="download_briar">Per continuar utilitzant Briar, baixeu la darrera versió.</string>
<string name="create_new_account">Haureu de crear un compte nou, però podeu utilitzar el mateix sobrenom.</string>
<string name="download_briar_button">Descarrega la darrera versió</string>
<string name="old_android_expiry_date_reached">El Briar ja no funciona amb l\'Android 4.\nInstal·leu el Briar en un dispositiu més nou.</string>
<string name="old_android_delete_account">Podeu tocar el botó següent per suprimir el vostre compte d\'aquest dispositiu.</string>
<string name="delete_account_button">Esborreu el compte</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>
@@ -145,6 +155,7 @@
<string name="error_start_activity">No està disponible en el vostre sistema</string>
<string name="status_heading">Estat</string>
<string name="error">Error</string>
<string name="info">Informació</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">No hi ha cap contacte per mostrar</string>
<string name="no_contacts_action">Toqueu la icona + per afegir un contacte</string>
@@ -216,6 +227,7 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<string name="menu_contact">Contacte</string>
<!--Adding Contacts-->
<string name="add_contact_title">Afegeix un contacte proper</string>
<string name="add_contact_error_two_way">Tots dos heu escanejat el codi QR de l\'altre?</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="try_again_button">Torna-ho a provar</string>
@@ -224,13 +236,16 @@ Així que l\'actualitzi li veureu una icona diferent .</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="qr_code_invalid">El codi QR és invàlid</string>
<string name="qr_code_too_old">El codi QR que heu escanejat prové d\'una versió antiga de %s.\n\nDemaneu al vostre contacte que l\'actualitzi i torneu a provar-ho.</string>
<string name="qr_code_too_new">El codi QR que heu escanejat prové d\'una versió de %s més nova que la vostra.\n\nActualitzeu la vostra aplicació i torneu a provar-ho.</string>
<string name="qr_code_too_old_1">El codi QR que heu escanejat prové d\'una versió anterior del Briar.\n\nDemaneu al vostre contacte que actualitzi a la versió més recent i, a continuació, torneu-ho a provar.</string>
<string name="qr_code_too_new_1">El codi QR que heu escanejat prové d\'una versió més recent del Briar.\n\nActualitzeu a la darrera versió i torneu-ho a provar.</string>
<string name="mailbox_qr_code_for_contact">El codi QR que heu escanejat prové del Briar Mailbox.\n\nSi voleu enllaçar una bústia de correu, seleccioneu Configuració &gt; Bústia del menú Briar.</string>
<string name="qr_code_format_unknown">El codi QR que heu escanejat no serveix per afegir un contacte del Briar.\n\nEscanejeu el codi QR que es mostra a la pantalla del vostre contacte.</string>
<string name="camera_error">Error de la càmera</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_error_title">No ha pogut connectar-se al vostre contacte</string>
<string name="connection_error_feedback">Si aquest problema persisteix, <a href="feedback">envieu-nos un comentari</a> per ajudar-nos a millorar l\'aplicació.</string>
<string name="info_both_must_scan">Tots dos heu d\'escanejar el codi QR de l\'altre</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Afegeix un contacte llunyà</string>
<string name="add_contact_nearby_title">Afegeix un contacte proper</string>
@@ -277,6 +292,7 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<string name="duplicate_link_dialog_text_1">Teniu una sol·licitud de contacte pendent amb l\'enllaç %s</string>
<string name="duplicate_link_dialog_text_1_contact">Ja teniu un contacte amb aquest enllaç: %s</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">%1$s i %2$s són la mateixa persona?</string>
<!--This is a button for answering that two nicknames do indeed refer to the same person. This
string will be used in a dialog button, so if the translation of this string is longer than 20
characters, please use "Yes" instead, and use "No" for the "Different Person" button-->
@@ -285,10 +301,17 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
will be used in a dialog button, so if the translation of this string longer than 20 characters,
please use "No" instead, and use "Yes" for the "Same Person" button-->
<string name="different_person_button">Persones diferents</string>
<string name="duplicate_link_dialog_text_3">%1$s i %2$s us han enviat el mateix enllaç.\n\nÉs possible que un d\'ells estigui intentant descobrir qui són els teus contactes.\n\nNo els diguis que has rebut el mateix enllaç d\'una altra persona.</string>
<string name="pending_contact_updated_toast">S\'ha actualitzat la sol·licitud de contacte pendent</string>
<string name="info_both_must_enter_links">Tots dos heu d\'afegir l\'enllaç de l\'altre</string>
<!--Peer trust levels-->
<string name="peer_trust_level_unverified">Contacte no verificat</string>
<string name="peer_trust_level_verified">Contacte verificat</string>
<string name="peer_trust_level_ourselves">Jo</string>
<string name="peer_trust_level_stranger">Estrany</string>
<!--Introductions-->
<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_onboarding_text">Presenteu els vostres contactes entre ells perquè es puguin connectar al Briar.</string>
<string name="introduction_menu_item">Presenta-li un altre contacte</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>
@@ -311,9 +334,14 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<!--Connect via Bluetooth-->
<string name="menu_item_connect_via_bluetooth">Connecta via bluetooth</string>
<string name="connect_via_bluetooth_title">Connecta via bluetooth</string>
<string name="connect_via_bluetooth_intro">En cas que les connexions Bluetooth no funcionin automàticament, podeu utilitzar aquesta pantalla per connectar-vos manualment.\n\nEl vostre contacte ha d\'estar a prop perquè això funcioni.\n\nVós i el vostre contacte hauríeu de prémer «Inicia» alhora.</string>
<string name="connect_via_bluetooth_already_discovering">Ja s\'està provant de connectar-se mitjançant Bluetooth. Torneu-ho a provar més tard.</string>
<string name="connect_via_bluetooth_no_location_permission">No es pot continuar sense permís per obtenir la posició.</string>
<string name="connect_via_bluetooth_no_bluetooth_permission">No es pot continuar sense el permís de dispositius propers</string>
<string name="connect_via_bluetooth_start">Connectant-se via Bluetooth...</string>
<string name="connect_via_bluetooth_success">S\'ha connectat via Bluetooth.</string>
<string name="connect_via_bluetooth_error">No s\'ha pogut connectar mitjançant Bluetooth.</string>
<string name="connect_via_bluetooth_error_not_supported">El Bluetooth no és compatible amb el dispositiu.</string>
<!--Private Groups-->
<string name="groups_list_empty">No hi ha cap grup per mostrar</string>
<string name="groups_list_empty_action">Toqueu la icona + per crear un grup o demaneu als vostres contactes que us afegeixin als seus grups</string>
@@ -406,6 +434,10 @@ Així que l\'actualitzi li veureu una icona diferent .</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_already_invited">La invitació ja s\'ha enviat</string>
<string name="forum_invitation_invite_received">La invitació ja s\'ha rebut</string>
<string name="forum_invitation_not_supported">Aquest contacte no és compatible</string>
<string name="forum_invitation_error">Això és un error i no és culpa vostre</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_declined_auto">La invitació al blog de %s s\'ha declinat automàticament.</string>
@@ -560,11 +592,104 @@ Així que l\'actualitzi li veureu una icona diferent .</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>
<!--Mailbox-->
<string name="mailbox_setup_connecting">S\'està connectant...</string>
<string name="mailbox_settings_title">Bústia</string>
<string name="mailbox_setup_title">Configuració de la bústia</string>
<string name="mailbox_setup_intro">Una bústia permet als vostres contactes enviar-vos missatges mentre esteu fora de línia. La bústia rebrà els vostres missatges i els emmagatzemarà fins que us connecteu.\n
\nPodeu instal·lar l\'aplicació Briar Mailbox en un dispositiu de recanvi. Mantingueu-lo connectat a l\'alimentació i a la Wi-Fi perquè estigui sempre en línia.</string>
<string name="mailbox_setup_download">Primer, instal·leu l\'aplicació Mailbox en un altre dispositiu cercant «Briar Mailbox» a Google Play o allà on hageu baixat el Briar.\n
\nA continuació, enllaceu la vostra bústia amb Briar escanejant el codi QR que mostra l\'aplicació Mailbox.</string>
<string name="mailbox_setup_download_link">Comparteix l\'enllaç de baixada</string>
<string name="mailbox_setup_button_scan">Escaneja el codi QR de la bústia</string>
<string name="permission_camera_qr_denied_body">Heu denegat l\'accés a la càmera, però per escanejar un codi QR cal utilitzar-la.\n\nConsidereu concedir l\'accés.</string>
<string name="mailbox_setup_connecting">S\'està connectant a la bústia...</string>
<!--This string is shown when connecting to a Mailbox for the first time. The placeholder will be replaced with a duration, e.g. "2 minutes".-->
<string name="mailbox_setup_connecting_info">Això pot trigar fins a %1s</string>
<string name="mailbox_qr_code_too_old">El codi QR que heu escanejat prové d\'una versió anterior del Briar Mailbox.\n\nActualitzeu el Briar Mailbox a la darrera versió i torneu-ho a provar.</string>
<string name="mailbox_qr_code_too_new">El codi QR que heu escanejat prové d\'una versió més recent de Briar Mailbox.\n\nActualitzeu el Briar a la darrera versió i torneu-ho a provar.</string>
<string name="contact_qr_code_for_mailbox">El codi QR que heu escanejat serveix per afegir un contacte del Briar.\n\nSi voleu afegir un contacte, aneu a la llista de contactes i toqueu la icona +.</string>
<string name="mailbox_setup_qr_code_wrong_description">El codi QR que heu escanejat no prové del Briar Mailbox.\n\nObriu l\'aplicació Briar Mailbox al dispositiu Mailbox i escanegeu el codi QR que hi apareix.</string>
<string name="mailbox_setup_already_paired_title">La bústia ja està enllaçada</string>
<string name="mailbox_setup_already_paired_description">Desenllaceu la bústia de l\'altre dispositiu i torneu-ho a provar.</string>
<string name="mailbox_setup_io_error_title">No s\'ha pogut connectar</string>
<string name="mailbox_setup_io_error_description">Assegureu-vos que tots dos dispositius estiguin connectats a Internet i torneu-ho a provar.</string>
<string name="mailbox_setup_assertion_error_title">Error de bústia</string>
<string name="mailbox_setup_assertion_error_description">Envieu comentaris (amb dades anònimes) mitjançant l\'aplicació Briar si el problema persisteix.</string>
<string name="mailbox_setup_camera_error_description">No s\'ha pogut accedir a la càmera. Torneu-ho a provar després de reiniciar el dispositiu.</string>
<string name="mailbox_setup_paired_title">Connectat</string>
<string name="mailbox_setup_paired_description">La vostra bústia s\'ha enllaçat correctament amb el Briar.\n
\nMantingueu la vostra bústia connectada a l\'alimentació i a la Wi-Fi perquè estigui sempre en línia.</string>
<string name="tor_offline_title">Fora de línia</string>
<string name="tor_offline_description">Assegureu-vos que aquest dispositiu estigui en línia i que es permeten les connexions a Internet.\n
\nDesprés, espereu que la icona del globus terraqüi a la pantalla de configuració de connexió es torni verda.</string>
<string name="tor_offline_button_check">Comproveu la configuració de la connexió</string>
<string name="mailbox_status_title">Estat de la bústia</string>
<string name="mailbox_status_connected_title">La bústia s\'està executant</string>
<string name="mailbox_status_problem_title">El Briar té problemes per connectar-se a la bústia</string>
<string name="mailbox_status_failure_title">La bústia no està disponible</string>
<string name="mailbox_status_app_too_old_title">En Briar és massa antic</string>
<string name="mailbox_status_app_too_old_message">Actualitzeu el Briar a la darrera versió i torneu-ho a provar.</string>
<string name="mailbox_status_mailbox_too_old_title">La bústia és massa antiga</string>
<string name="mailbox_status_mailbox_too_old_message">Actualitza la vostra bústia a la darrera versió i torneu-ho a provar.</string>
<string name="mailbox_status_check_button">Comprova la connexió</string>
<!--Example for string substitution: Last connection: 3min ago-->
<string name="mailbox_status_connected_info">Última connexió: %s</string>
<!--Indicates that there never was a connection to the mailbox. Last connection: Never-->
<string name="mailbox_status_connected_never">Mai</string>
<string name="mailbox_status_unlink_button">Desenllaça</string>
<string name="mailbox_status_unlink_dialog_title">Voleu desenllaçar la bústia?</string>
<string name="mailbox_status_unlink_dialog_question">Esteu segur que voleu desenllaçar la vostra bústia?</string>
<string name="mailbox_status_unlink_dialog_warning">Si desenllaçeu la vostra bústia no podreu rebre missatges mentre el Briar estigui fora de línia.</string>
<string name="mailbox_status_unlink_no_wipe_title">La vostra bústia s\'ha desenllaçat</string>
<string name="mailbox_status_unlink_no_wipe_message">La propera vegada que tingueu accés al vostre dispositiu Mailbox, obriu l\'aplicació Mailbox i toqueu el botó «Desenllaça» per completar el procés.\n\nSi ja no teniu accés al dispositiu Mailbox, no us preocupeu. Les vostres dades estan xifrades, de manera que es mantindran segures encara que no completeu el procés.</string>
<string name="mailbox_status_unlink_success">La vostra bústia s\'ha desenllaçat</string>
<string name="mailbox_error_notification_channel_title">Problema amb la bústia Briar</string>
<string name="mailbox_error_notification_title">La bústia Briar no està disponible</string>
<string name="mailbox_error_notification_text">Toqueu per solucionar el problema.</string>
<string name="mailbox_error_wizard_button">Soluciona el problema</string>
<string name="mailbox_error_wizard_title">Assistent per a la resolució de problemes de la bústia</string>
<string name="mailbox_error_wizard_question1">Teniu accés al vostre dispositiu Mailbox?</string>
<string name="mailbox_error_wizard_answer1">Sí, hi tinc accés ara mateix.</string>
<string name="mailbox_error_wizard_answer2">Ara mateix no, però hi puc accedir més tard.</string>
<string name="mailbox_error_wizard_answer3">No, ja no hi tinc accés.</string>
<string name="mailbox_error_wizard_info1_1">Comproveu que el dispositiu Mailbox estigui encès i connectat a Internet.</string>
<string name="mailbox_error_wizard_question1_1">Obriu l\'aplicació Mailbox. Què veus?</string>
<string name="mailbox_error_wizard_answer1_1">Veig instruccions per configurar la bústia</string>
<string name="mailbox_error_wizard_answer1_2">Veig un codi QR</string>
<string name="mailbox_error_wizard_answer1_3">Veig «El Mailbox s\'està executant»</string>
<string name="mailbox_error_wizard_answer1_4">Veig «Dispositiu fora de línia»</string>
<string name="mailbox_error_wizard_info1_1_1">Desenllaceu la vostra bústia mitjançant el botó següent i, a continuació, seguiu les instruccions del dispositiu Mailbox per enllaçar-la de nou.</string>
<string name="mailbox_error_wizard_info_1_1_2">Desenllaceu la bústia amb el botó següent i escanegeu el codi QR per tornar-la a enllaçar.</string>
<string name="mailbox_error_wizard_info1_1_3">Feu servir el botó següent per comprovar la connexió entre Briar i el Mailbox.\n\n
Si la connexió torna a fallar:\n
\u2022 Comproveu que les aplicacions Mailbox i Briar estiguin actualitzades a la darrera versió.\n
\u2022 Reinicieu els vostres dispositius Mailbox i Briar i torneu-ho a provar.</string>
<string name="mailbox_error_wizard_info1_1_4">Comproveu que el dispositiu Mailbox estigui connectat correctament a Internet.\n\nComproveu que l\'hora, data i zona horària del dispositiu Mailbox siguin correctes.\n\nComproveu que les aplicacions Mailbox i Briar estiguin actualitzades a la darrera versió.\ n\nReinicieu els vostres dispositius Mailbox i Briar i torneu-ho a provar.</string>
<string name="mailbox_error_wizard_info2">Torneu a aquesta pantalla quan tingueu accés al dispositiu.</string>
<string name="mailbox_error_wizard_info3">Desenllaceu la vostra bústia amb el botó següent.\n\nDesprés de desenllaçar la vostra bústia antiga, podeu configurar una bústia nova en qualsevol moment.</string>
<!--About-->
<string name="about_title">Quant a</string>
<string name="briar_version">Versió del Briar: %s</string>
<string name="tor_version">Versió del Tor: %s</string>
<string name="links">Enllaços</string>
<string name="briar_website">\u2022 <a href="">Lloc web</a></string>
<string name="briar_source_code">\u2022 <a href="">Codi font</a></string>
<string name="briar_changelog">\u2022 <a href="">Registre de canvis</a></string>
<string name="briar_privacy_policy">\u2022 <a href="">Política de privadesa</a></string>
<!--Here translators can add their names or Transifex usernames(eg "Thanks to all the contributors at the Localization Lab, especially Tom, Matthew and Jerry")-->
<string name="translator_thanks">Gràcies a tots els col·laboradors del Localization Lab, especialment a Jaime Muñoz</string>
<!--Conversation Settings-->
<string name="disappearing_messages_title">Missatges efímers</string>
<string name="disappearing_messages_explanation_long">Si activeu aquesta configuració, es crearà un/a nou/va
els missatges d\'aquesta conversa desapareixen automàticament al cap de 7\u00A0dies.
\n\nEl compte enrere per a la còpia del missatge del remitent comença després d\'haver estat lliurat.
El compte enrere comença per al destinatari després que hagi llegit el missatge.
\n\nEls missatges que desapareixeran estan marcats amb una icona de bomba.
\n\nTingues en compte que els destinataris encara poden fer còpies dels missatges que envieu.
\n\nSi canvieu aquesta configuració, s\'aplicarà als vostres missatges nous immediatament i als vostres
missatges del contacte un cop rebin el vostre següent missatge.
El vostre contacte també pot canviar aquesta configuració per a tots dos.</string>
<string name="learn_more">Més informació</string>
<string name="disappearing_messages_summary">Feu que els missatges futurs d\'aquesta conversa desapareguin automàticament al cap de 7\u00A0dies.</string>
<!--Settings Actions-->
<string name="pref_category_actions">Accions</string>
<string name="send_feedback">Envieu comentaris</string>
@@ -575,6 +700,7 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<string name="link_warning_open_link">Obre l\'enllaç</string>
<!--Crash Reporter-->
<string name="crash_report_title">Informe de fallida de Briar</string>
<string name="briar_crashed">El Briar ha fallat</string>
<string name="not_your_fault">Això no és culpa vostra.</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>
@@ -582,19 +708,35 @@ Així que l\'actualitzi li veureu una icona diferent .</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="privacy_policy">En enviar-nos dades, accepteu la nostra <a href="">política de privadesa</a></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="dev_report_user_info">Informació de l\'usuari</string>
<string name="dev_report_basic_info">Informació bàsica</string>
<string name="dev_report_device_info">Informació del dispositiu</string>
<string name="dev_report_stacktrace">Traça de la pila</string>
<string name="dev_report_time_info">Informació del temps</string>
<string name="dev_report_memory">Memòria</string>
<string name="dev_report_storage">Emmagatzematge</string>
<string name="dev_report_connectivity">Connectivitat</string>
<string name="dev_report_network_usage">Ús de la xarxa</string>
<string name="dev_report_build_config">Configuració de la compilació</string>
<string name="dev_report_logcat">Registre de l\'aplicació</string>
<string name="dev_report_device_features">Característiques del dispositiu</string>
<string name="send_report">Envia l\'informe</string>
<string name="close">Tanca</string>
<string name="dev_report_sending">S\'està enviant els comentaris...</string>
<string name="dev_report_sent">Comentaris enviats</string>
<string name="dev_report_saved">S\'ha desat l\'informe. Se us enviarà la propera vegada que inicieu sessió a Briar.</string>
<string name="dev_report_error">Error: no s\'ha pogut enviar l\'informe</string>
<!--Sign Out-->
<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à 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_body_api_30">Una altra aplicació està dibuixant a sobre del Briar. Per protegir la vostra seguretat, el Briar no respondrà als tocs quan hi hagi una altra aplicació dibuixant a sobre.\n\nReviseu les aplicacions a continuació per trobar l\'aplicació responsable.</string>
<string name="screen_filter_allow">Permet que aquestes aplicacions se sobreposin a Briar</string>
<string name="screen_filter_review_apps">Revisa les aplicacions</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 accedir a la càmera.</string>
@@ -602,7 +744,17 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<string name="permission_location_request_body">Per a descobrir dispositius Bluetooth, Briar necessita accedir a la vostra posició.\n\nBriar no guarda la vostra posició ni la comparteix amb ningú.</string>
<string name="permission_camera_location_title">Permís d\'accés a la càmera i a la posició</string>
<string name="permission_camera_location_request_body">Per escanejar el codi QR, Briar necessita accedir a la càmera.\n\nPer trobar dispositius Bluetooth, Briar necessita accedir a la vostra posició.\n\nBriar no guarda la vostra posició ni la comparteix amb ningú.</string>
<string name="permission_camera_bluetooth_title">Càmera i dispositius propers</string>
<string name="permission_camera_bluetooth_request_body">Per escanejar el codi QR, el Briar necessita accedir a la càmera.\n\nPer descobrir dispositius Bluetooth, el Briar necessita permís per trobar i connectar-se a dispositius propers.</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_location_denied_body">Heu denegat l\'accés a la vostra ubicació, però el Briar necessita aquest permís per descobrir dispositius Bluetooth.\n\nPenseu en concedir l\'accés.</string>
<string name="permission_location_setting_title">Configuració d\'ubicació</string>
<string name="permission_location_setting_body">La configuració d\'ubicació del dispositiu ha d\'estar activada per trobar altres dispositius mitjançant Bluetooth. Activeu la ubicació per continuar. Podeu tornar a desactivar-lo després.</string>
<string name="permission_location_setting_hotspot_body">La configuració d\'ubicació del dispositiu s\'ha d\'activar per crear un punt d\'accés Wi-Fi. Activeu la ubicació per continuar. Podeu tornar a desactivar-lo després.</string>
<string name="permission_location_setting_button">Activa la ubicació</string>
<string name="permission_bluetooth_title">Permís de dispositius propers</string>
<string name="permission_bluetooth_body">Per utilitzar la comunicació Bluetooth, el Briar necessita permís per trobar i connectar-se a dispositius propers.</string>
<string name="permission_bluetooth_denied_body">Heu denegat l\'accés als dispositius propers, però el Briar necessita aquest permís per utilitzar Bluetooth.\n\nPenseu en concedir l\'accés.</string>
<string name="qr_code">Codi QR</string>
<string name="show_qr_code_fullscreen">Mostra el codi QR a pantalla completa</string>
<!--App Locking-->
@@ -615,17 +767,92 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<!--Connections Screen-->
<string name="transports_help_text">Briar pot contactar els vostres contactes via Internet, WiFi o Bluetooth.\n\nTotes les connexions d\'Internet van via la xarxa Tor per privacitat.\n\nSi es pot arribar a un contacte per diversos mètodes, Briar els usa tots simultàniament.</string>
<!--Share app offline-->
<string name="hotspot_title">Comparteix aquesta aplicació fora de línia</string>
<string name="hotspot_intro">Compartiu aquesta aplicació amb algú proper sense connexió a Internet mitjançant la Wi-Fi del telèfon.
\n\nEl vostre telèfon iniciarà un punt d\'accés Wi-Fi. Les persones properes poden connectar-se al punt d\'accés i baixar l\'aplicació Briar des del telèfon.</string>
<string name="hotspot_button_start_sharing">Inicia el punt d\'accés</string>
<string name="hotspot_button_stop_sharing">Atura el punt d\'accés</string>
<string name="hotspot_progress_text_start">S\'està configurant el punt d\'accés...</string>
<string name="hotspot_notification_channel_title">Punt d\'accés Wi-Fi</string>
<string name="hotspot_notification_title">Compartint el Briar fora de línia</string>
<string name="hotspot_button_connected">Endavant</string>
<string name="permission_hotspot_location_request_body">Per crear un punt d\'accés Wi-Fi, el Briar necessita permís per accedir a la vostra ubicació.\n\nel Briar no emmagatzema la vostra ubicació ni la comparteix amb ningú.</string>
<string name="permission_hotspot_location_request_precise_body">Per crear un punt d\'accés Wi-Fi, el Briar necessita permís per accedir a la vostra ubicació precisa.\n\nel Briar no emmagatzema la vostra ubicació ni la comparteix amb ningú.</string>
<string name="permission_hotspot_location_denied_body">Heu denegat l\'accés a la vostra ubicació, però el Briar necessita aquest permís per crear un punt d\'accés Wi-Fi.\n\nPenseu en concedir l\'accés.</string>
<string name="permission_hotspot_location_denied_precise_body">Heu denegat l\'accés a la vostra ubicació precisa, però el Briar necessita aquest permís per crear un punt d\'accés Wi-Fi.\n\nPenseu en concedir l\'accés.</string>
<string name="wifi_settings_title">Configuració Wi-Fi</string>
<string name="wifi_settings_request_enable_body">Per crear un punt d\'accés Wi-Fi, el Briar ha d\'utilitzar Wi-Fi. Activeu-lo.</string>
<string name="hotspot_tab_manual">Manual</string>
<!--The placeholder to be inserted into the string 'hotspot_manual_wifi': People can connect by %s-->
<string name="hotspot_scanning_a_qr_code">escanejant un codi QR</string>
<!--Wi-Fi setup-->
<!--The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code'-->
<string name="hotspot_manual_wifi">El vostre telèfon proporciona un punt d\'accés Wi-Fi. Les persones que vulguin baixar el Briar es poden connectar al punt d\'accés afegint-lo a la configuració Wi-Fi del seu dispositiu mitjançant els detalls següents o mitjançant %s. Quan s\'hagin connectat al punt d\'accés, premeu «Següent».</string>
<string name="hotspot_manual_wifi_ssid">Nom de la xarxa</string>
<string name="hotspot_qr_wifi">El vostre telèfon proporciona un punt d\'accés Wi-Fi. Les persones que vulguin baixar el Briar poden connectar-se al punt d\'accés escanejant aquest codi QR. Quan s\'hagin connectat al punt d\'accés, premeu «Següent».</string>
<string name="hotspot_no_peers_connected">No hi ha cap dispositiu connectat</string>
<plurals name="hotspot_peers_connected">
<item quantity="one">%s dispositiu connectat</item>
<item quantity="other">%s dispositius connectats</item>
</plurals>
<!--Download link-->
<!--The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code'-->
<string name="hotspot_manual_site">El vostre telèfon proporciona un punt d\'accés Wi-Fi. Les persones connectades al punt d\'accés poden baixar el Briar escrivint l\'enllaç següent en un navegador web o %s.</string>
<string name="hotspot_manual_site_address">Adreça (URL)</string>
<string name="hotspot_qr_site">El vostre telèfon proporciona una punt d\'accés Wi-Fi. Les persones connectades al punt d\'accés poden baixar el Briar escanejant aquest codi QR.</string>
<!--e.g. Download Briar 1.2.20-->
<string name="website_download_title_1">Baixa el Briar %s</string>
<string name="website_download_intro_1">Algú a prop ha compartit el Briar amb vós.</string>
<string name="website_download_button">Baixa el Briar</string>
<string name="website_download_outro">Un cop finalitzada la baixada, obriu el fitxer baixat i instal·leu-lo.</string>
<string name="website_troubleshooting_title">Detecció d\'errors</string>
<string name="website_troubleshooting_1">Si no podeu baixar l\'aplicació, proveu amb un altre navegador web.</string>
<string name="website_troubleshooting_2_old">Per instal·lar l\'aplicació baixada, és possible que hàgiu de permetre la instal·lació d\'aplicacions des de «Fonts desconegudes» a la configuració del sistema. Després, potser haureu de tornar a baixar l\'aplicació. Us recomanem que desactiveu la configuració «Fonts desconegudes» després d\'instal·lar l\'aplicació.</string>
<string name="website_troubleshooting_2_new">Per instal·lar l\'aplicació baixada, potser haureu de permetre que el navegador instal·li aplicacions desconegudes. Després d\'instal·lar l\'aplicació, us recomanem que elimineu el permís del navegador per instal·lar aplicacions desconegudes.</string>
<string name="hotspot_help_wifi_title">Problemes amb la connexió Wi-Fi:</string>
<string name="hotspot_help_wifi_1">Proveu a desactivar i reactivar la Wi-Fi als dos telèfons i torneu-ho a provar.</string>
<string name="hotspot_help_wifi_2">Si el vostre telèfon es queixa que la Wi-Fi no té Internet, digueu-li que voleu mantenir-vos connectat de totes maneres.</string>
<string name="hotspot_help_wifi_3">Reinicieu el telèfon que executa el punt d\'accés Wi-Fi, inicieu el Briar i torneu a provar de compartir.</string>
<string name="hotspot_help_site_title">Problemes per visitar el lloc web local:</string>
<string name="hotspot_help_site_1">Torneu a comprovar que heu introduït l\'adreça exactament com es mostra. Un petit error pot fer que falli.</string>
<string name="hotspot_help_site_2">Assegureu-vos que el vostre telèfon encara estigui connectat a la Wi-Fi correcta (vegeu més amunt) quan intenteu accedir al lloc.</string>
<string name="hotspot_help_site_3">Si teniu un tallafoc, comproveu que no bloquegi l\'accés.</string>
<string name="hotspot_help_site_4">Si podeu visitar el lloc, però no baixar l\'aplicació Briar, proveu-ho amb un navegador web diferent.</string>
<string name="hotspot_help_fallback_title">No funciona res?</string>
<string name="hotspot_help_fallback_intro">Podeu provar de desar l\'aplicació com a fitxer .apk per compartir-la d\'una altra manera. Un cop el fitxer s\'ha transferit a l\'altre dispositiu, es pot utilitzar per instal·lar el Briar.
\n\nConsell: per compartir mitjançant Bluetooth, és possible que hàgiu de canviar el nom del fitxer perquè acabi amb .zip.</string>
<string name="hotspot_help_fallback_button">Desa l\'aplicació</string>
<!--error handling-->
<string name="hotspot_error_intro">S\'ha produït un error en intentar compartir l\'aplicació mitjançant Wi-Fi:</string>
<string name="hotspot_error_no_wifi_direct">El dispositiu no és compatible amb Wi-Fi Direct</string>
<string name="hotspot_error_start_callback_failed">El punt d\'accés no s\'ha pogut iniciar: error %s</string>
<string name="hotspot_error_start_callback_failed_unknown">El punt d\'accés no s\'ha pogut iniciar amb un error desconegut, motiu %d</string>
<string name="hotspot_error_start_callback_no_group_info">El punt d\'accés no s\'ha pogut iniciar: no hi ha informació del grup</string>
<string name="hotspot_error_web_server_start">S\'ha produït un error en iniciar el servidor web</string>
<string name="hotspot_error_web_server_serve">S\'ha produït un error en presentar el lloc web.\n\nSi el problema persisteix, envieu comentaris (amb dades anònimes) mitjançant l\'aplicació Briar.</string>
<string name="hotspot_flag_test">Avís: aquesta aplicació s\'ha instal·lat amb l\'Android Studio i NO es pot instal·lar en un altre dispositiu.</string>
<string name="hotspot_error_framework_busy">No es pot iniciar el punt d\'accés.\n\nSi teniu un altre punt d\'accés en execució o compartiu la vostra connexió a Internet mitjançant Wi-Fi, proveu d\'aturar-lo i torneu-ho a provar més tard.</string>
<!--Transfer Data via Removable Drives-->
<string name="removable_drive_menu_title">Connecta mitjançant una unitat extraïble</string>
<string name="removable_drive_intro">Si no us podeu connectar al vostre contacte a través d\'Internet, Wi-Fi o Bluetooth, el Briar també pot transferir missatges en una unitat extraïble, com ara un llapis USB o una targeta SD.</string>
<string name="removable_drive_explanation">Si no podeu connectar-vos al vostre contacte a través d\'Internet, Wi-Fi o Bluetooth, el Briar també pot transferir missatges en una unitat extraïble, com ara un llapis USB o una targeta SD.\n\nQuan feu servir el botó «Envia dades», qualsevol dada que estigui esperant per ser enviada al contacte s\'escriurà a la unitat extraïble. Això inclou missatges privats, fitxers adjunts, blogs, fòrums i grups privats.\n\nTot es xifrarà abans que s\'escrigui a la unitat extraïble.\n\nQuan el vostre contacte rebi la unitat extraïble, pot utilitzar el botó «Rebre dades» per importar els missatges al Briar.</string>
<string name="removable_drive_title_send">Envia dades</string>
<string name="removable_drive_title_receive">Rebre dades</string>
<string name="removable_drive_send_intro">Toqueu el botó següent per crear un fitxer nou que contingui els missatges xifrats. Podeu triar on es desarà el fitxer.\n\nSi voleu desar el fitxer en una unitat extraïble, inseriu la unitat ara.</string>
<string name="removable_drive_send_no_data">Actualment no hi ha cap missatge esperant per ser enviat a aquest contacte.</string>
<string name="removable_drive_send_not_supported">Aquest contacte utilitza una versió antiga del Briar o un dispositiu antic que no admet aquesta funció.</string>
<string name="removable_drive_send_button">Trieu el fitxer per exportar</string>
<string name="removable_drive_ongoing">Espereu que es completi la tasca en curs</string>
<string name="removable_drive_receive_intro">Toqueu el botó següent per triar el fitxer que t\'ha enviat el vostre contacte.\n\nSi el fitxer es troba en una unitat extraïble, inseriu-la ara.</string>
<string name="removable_drive_receive_button">Trieu el fitxer per importar</string>
<string name="removable_drive_success_send_title">Exportació correcta</string>
<string name="removable_drive_success_send_text">Les dades s\'han exportat correctament. Ara teniu 28 dies per transportar el fitxer al vostre contacte.\n\nSi el fitxer es troba en una unitat extraïble, feu servir la notificació de la barra d\'estat per expulsar la unitat abans de desconnectar-la.</string>
<string name="removable_drive_success_receive_title">Importació correcta</string>
<string name="removable_drive_success_receive_text">S\'han rebut tots els missatges xifrats continguts en aquest fitxer.</string>
<string name="removable_drive_error_send_title">S\'ha produït un error en exportar les dades</string>
<string name="removable_drive_error_send_text">S\'ha produït un error escrivint dades al fitxer.\n\nSi utilitzeu una unitat extraïble, assegureu-vos que estigui inserida correctament i torneu-ho a provar.\n\nSi l\'error persisteix, envieu comentaris per informar l\'equip del Briar sobre el assumpte.</string>
<string name="removable_drive_error_receive_title">S\'ha produït un error en importar les dades</string>
<string name="removable_drive_error_receive_text">El fitxer seleccionat no contenia res que el Briar pogués reconèixer.\n\nComproveu que heu triat el fitxer correcte.\n\nSi el vostre contacte va crear el fitxer fa més de 28 dies, el Briar no el podrà reconèixer.</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Alba</string>

View File

@@ -237,6 +237,8 @@
<string name="qr_code_invalid">Der QR-Code ist ungültig</string>
<string name="qr_code_too_old_1">Der QR-Code, den du gescannt hast, kommt von einer älteren Version von Briar.\n\nBitte deinen Kontakt, zur neuesten Version upzudaten, und versuche es erneut.</string>
<string name="qr_code_too_new_1">Der QR-Code, den du gescannt hast, kommt von einer neueren Version.\n\nBitte aktualisiere Briar auf die neueste Version und versuche es erneut.</string>
<string name="mailbox_qr_code_for_contact">Der QR-Code, den du gescannt hast, stammt von einer Briar-Mailbox.\n\nWenn du dich mit einer Mailbox verbinden willst, dann wähle in den Einstellungen &gt; Mailbox im Briar-Menü.</string>
<string name="qr_code_format_unknown">Der QR-Code, den du gescannt hast, ist nicht dazu gedacht, einen Kontakt hinzuzufügen.\n\nBitte scanne den QR-Code, der auf dem Display deines Kontaktes angezeigt wird.</string>
<string name="camera_error">Kamerafehler</string>
<string name="connecting_to_device">Verbinde mit Gerät\u2026</string>
<string name="authenticating_with_device">Authentifiziere Gerät\u2026</string>
@@ -431,6 +433,10 @@
<string name="forum_declined_toast">Einladung abgelehnt</string>
<string name="shared_by_format">Geteilt durch %s</string>
<string name="forum_invitation_already_sharing">Bereits geteilt.</string>
<string name="forum_invitation_already_invited">Die Einladung wurde bereits versendet.</string>
<string name="forum_invitation_invite_received">Die Einladung wurde bereits empfangen.</string>
<string name="forum_invitation_not_supported">Dies wird von diesem Kontakt nicht unterstützt.</string>
<string name="forum_invitation_error">Fehler. Dies ist ein Bug und ist nicht dein Fehler.</string>
<string name="forum_invitation_response_accepted_sent">Du hast die Forumeinladung von %s angenommen.</string>
<string name="forum_invitation_response_declined_sent">Du hast die Forumeinladung von %s abgelehnt.</string>
<string name="forum_invitation_response_declined_auto">Die Forumeinladung von %s wurde automatisch abgelehnt.</string>
@@ -594,7 +600,13 @@
<string name="mailbox_setup_download_link">Download-Link teilen</string>
<string name="mailbox_setup_button_scan">Mailbox QR-Code scannen</string>
<string name="permission_camera_qr_denied_body">Du hast den Zugriff auf die Kamera verweigert, aber das Scannen eines QR-Codes erfordert die Verwendung der Kamera.\n\nBitte gewähre den Zugriff.</string>
<string name="mailbox_setup_connecting">Verbinde mit der Mailbox…</string>
<!--This string is shown when connecting to a Mailbox for the first time. The placeholder will be replaced with a duration, e.g. "2 minutes".-->
<string name="mailbox_setup_connecting_info">Dies könnte %1s erfordern.</string>
<string name="mailbox_qr_code_too_old">Der QR-Code, den du gescannt hast, stammt von einer älteren Briar-Mailbox Version.\n\nBitte aktualisiere die Briar-Mailbox auf die neueste Version und probiere es noch einmal.</string>
<string name="mailbox_qr_code_too_new">Der QR-Code, den du gescannt hast, stammt von einer neueren Briar-Mailbox Version.\n\nBitte aktualisiere Briar auf die neueste Version und probiere es noch einmal.</string>
<string name="contact_qr_code_for_mailbox">Der QR-Code, den du gescannt hast, ist dafür gedacht, einen Kontakt hinzuzufügen.\n\nWenn du einen Kontakt hinzufügen möchtest, gehe bitte zur Kontaktliste und klicke auf das + Symbol.</string>
<string name="mailbox_setup_qr_code_wrong_description">Der QR-Code, den du gescannt hast, stammt nicht von einer Briar-Mailbox.\n\nBitte öffne die Briar-Mailbox auf deinem Mailbox-Gerät und scanne den davon angezeigten QR-Code.</string>
<string name="mailbox_setup_already_paired_title">Mailbox bereits verknüpft</string>
<string name="mailbox_setup_already_paired_description">Verknüpfung der Mailbox auf deinem anderen Gerät aufheben und erneut versuchen.</string>
<string name="mailbox_setup_io_error_title">Keine Verknüpfung möglich</string>

View File

@@ -446,6 +446,10 @@
<string name="forum_declined_toast">Invito declinato</string>
<string name="shared_by_format">Condiviso da %s</string>
<string name="forum_invitation_already_sharing">Già in condivisione</string>
<string name="forum_invitation_already_invited">Invito già spedito</string>
<string name="forum_invitation_invite_received">Invito già ricevuto</string>
<string name="forum_invitation_not_supported">Non supportato da questo contatto</string>
<string name="forum_invitation_error">Errore. Si tratta di un bug, non è colpa tua</string>
<string name="forum_invitation_response_accepted_sent">Hai accettato l\'invito al forum da %s</string>
<string name="forum_invitation_response_declined_sent">Hai declinato l\'invito al forum da %s</string>
<string name="forum_invitation_response_declined_auto">L\'invito di %s al forum è stato rifiutato automaticamente.</string>

View File

@@ -214,6 +214,7 @@
<string name="menu_contact">連絡先</string>
<!--Adding Contacts-->
<string name="add_contact_title">近くの人を連絡先に追加する</string>
<string name="add_contact_error_two_way">お互いのQRコードを読み取りましたか</string>
<string name="face_to_face">連絡先として追加したい人と会う必要があります。\n\nこれにより、だれかがあなたになりすましたり、メッセージを読んだりするのを防ぐことができます。</string>
<string name="continue_button">続ける</string>
<string name="try_again_button">もう一度やり直してください</string>
@@ -222,13 +223,16 @@
<string name="contact_added_toast">追加された連絡先:%s</string>
<string name="contact_already_exists">連絡先%sは既に存在しています </string>
<string name="qr_code_invalid">QRコードが無効です</string>
<string name="qr_code_too_old_1">スキャンしたQRコードはBriarの古いバージョンから取得されました。\n\n最新版へアップグレードしてもらって、もう一度お試しください。</string>
<string name="qr_code_too_new_1">スキャンしたQRコードは、新しいバージョンのBriarから取得されました。\n\n最新版にアップグレードしてから、もう一度お試しください。</string>
<string name="qr_code_too_old_1">読み取ったQRコードはBriarの古いバージョンから生じました。\n\n最新版へアップグレードしてもらって、もう一度お試しください。</string>
<string name="qr_code_too_new_1">読み取ったQRコードは、新しいバージョンのBriarから生じました。\n\n最新版にアップグレードしてから、もう一度お試しください。</string>
<string name="mailbox_qr_code_for_contact">読み取ったQRコードは、Briarメールボックスから生じるものです。\n\nメールボックスに関連付けたいならば、設定を選択してください&gt; Briarメニューからメールボックス</string>
<string name="qr_code_format_unknown">読み取ったQRコードは、Briarの連絡先を追加するために示されたものではありません。\n\nあなたの連絡先の画面上に表示されたQRコードを読み取ってください。</string>
<string name="camera_error">カメラエラー</string>
<string name="connecting_to_device">端末に接続中\u2026</string>
<string name="authenticating_with_device">端末同士での認証中\u2026</string>
<string name="connection_error_title">連絡先に接続できませんでした</string>
<string name="connection_error_feedback">この問題が解決しない場合、アプリを改善するために<a href="feedback">フィードバック</a>を送信してください。</string>
<string name="info_both_must_scan">お互いのQRコードを読み取る必要があります</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">離れた場所にいる相手を連絡先に追加</string>
<string name="add_contact_nearby_title">近くにいる相手を連絡先に追加</string>
@@ -285,6 +289,7 @@
<string name="different_person_button">別の人</string>
<string name="duplicate_link_dialog_text_3">%1$sと%2$sから同じリンクを受信しました。\n\nどちらかがあなたの連絡先の内容を知ろうとしている可能性があります。\n\n他の人から同じリンクを受け取ったことを伝えないでください。</string>
<string name="pending_contact_updated_toast">保留中の連絡先が更新されました</string>
<string name="info_both_must_enter_links">お互いのリンクを追加する必要があります</string>
<!--Peer trust levels-->
<string name="peer_trust_level_unverified">検証されてない連絡先</string>
<string name="peer_trust_level_verified">検証された連絡先</string>
@@ -569,11 +574,17 @@
<string name="mailbox_setup_intro">メールボックスはあなたがオフラインの間、連絡先があなたにメッセージを送信することを有効にします。メールボックスはあなたがオンラインになるまで、メッセージを受信し保管します。\n
\n予備端末上にBriarのメールボックスアプリをインストールできます。それを電源とWi-Fiに接続し、常時オンラインにしてください。</string>
<string name="mailbox_setup_download">最初に、Google PlayまたはBriarをダウンロードしたどこかで\"Briar Mailbox\"を検索して、他の端末上にメールボックスアプリをインストールします。\n
\nそして、メールボックスアプリによって表示されるQRコードをスキャンして、Briarとあなたのメールボックス結びつけます。</string>
\nそして、メールボックスアプリによって表示されるQRコードを読み取って、Briarとあなたのメールボックス結びつけます。</string>
<string name="mailbox_setup_download_link">ダウンロードリンクを共有</string>
<string name="mailbox_setup_button_scan">メールボックスのQRコードをスキャン</string>
<string name="permission_camera_qr_denied_body">カメラへのアクセスをが拒否されましたが、QRコードをスキャンするにはカメラを使用する必要があります。\n\nカメラへのアクセスの許可を考えてください</string>
<string name="mailbox_setup_button_scan">メールボックスのQRコードを読み取る</string>
<string name="permission_camera_qr_denied_body">あなたはカメラアクセスすることを拒否しましたが、QRコードを読み取るにはカメラを使用する必要があります。\n\nアクセス権を付与することを考慮願います</string>
<string name="mailbox_setup_connecting">メールボックスへ接続中…</string>
<!--This string is shown when connecting to a Mailbox for the first time. The placeholder will be replaced with a duration, e.g. "2 minutes".-->
<string name="mailbox_setup_connecting_info">%1sまでかかる場合があります</string>
<string name="mailbox_qr_code_too_old">読み取ったQRコードは、古いバージョンのBriarメールボックスから生じました。\n\nBriarメールボックスを最新版にアップグレードしてから、もう一度お試しください。</string>
<string name="mailbox_qr_code_too_new">読み取ったQRコードは、新しいバージョンのBriarメールボックスから生じました。\n\nBriarを最新版にアップグレードしてから、もう一度お試しください。</string>
<string name="contact_qr_code_for_mailbox">読み取ったQRコードは、Briarの連絡先を追加するためのものです。\n\n連絡先を追加したいならば、連絡先一覧に行き、+アイコンをタップしてください。</string>
<string name="mailbox_setup_qr_code_wrong_description">読み取ったQRコードは、Briarメールボックスから生じたものではありません。メールボックス端末上のBriarメールボックスアプリを開き、提示されたQRコードを読み取ってください。</string>
<string name="mailbox_setup_already_paired_title">メールボックスは既に結びつけられています</string>
<string name="mailbox_setup_already_paired_description">あなたの端末上のメールボックスの結びつけを解き、再試行してください。</string>
<string name="mailbox_setup_io_error_title">接続できません</string>
@@ -705,16 +716,22 @@
<string name="screen_filter_review_apps">アプリを確認</string>
<!--Permission Requests-->
<string name="permission_camera_title">カメラへのアクセス許可</string>
<string name="permission_camera_request_body">QRコードをスキャンするには、Briarはカメラにアクセスする必要があります。</string>
<string name="permission_camera_request_body">QRコードを読み取るには、Briarはカメラにアクセスする必要があります。</string>
<string name="permission_location_title">位置情報へのアクセスの許可</string>
<string name="permission_location_request_body">Bluetooth端末を検出するには、Briarがあなたの位置情報へのアクセスを必要とします。\n\nBriarはあなたの場所を保存したり、誰とも共有したりしません。</string>
<string name="permission_camera_location_title">カメラと位置情報</string>
<string name="permission_camera_location_request_body">QRコードをスキャンするには、Briarはカメラにアクセスする必要があります。\n\nBluetooth端末を検出するには、Briarは現在地情報にアクセスする許可が必要です。\n\nBriarは現在地を保存したり、誰とも共有したりしません。</string>
<string name="permission_camera_denied_body">カメラへのアクセスをが拒否されましたが、連絡先を追加するにはカメラを使用する必要があります。\n\nカメラへのアクセスの許可を考えてください。</string>
<string name="permission_location_denied_body">あなたの位置情報にアクセスすることを拒否しましたが、BriarはBluetooth端末を発見するのに、この権限が必要です。\n\nアクセス権を付与することを考慮願います。</string>
<string name="permission_camera_location_request_body">QRコードを読み取るには、Briarはカメラにアクセスする必要があります。\n\nBluetooth端末を検出するには、Briarは現在地情報にアクセスする許可が必要です。\n\nBriarは現在地を保存したり、誰とも共有したりしません。</string>
<string name="permission_camera_bluetooth_title">カメラと付近の端末</string>
<string name="permission_camera_bluetooth_request_body">QRコードを読み取るには、Briarはカメラにアクセスする必要があります。\n\nBluetooth端末を検出するには、Briarは付近の端末を探し接続する許可が必要です。</string>
<string name="permission_camera_denied_body">あなたはカメラにアクセスすることを拒否しましたが、連絡先を追加するには、カメラを使用する必要があります。\n\nアクセス権を付与することを考慮願います。</string>
<string name="permission_location_denied_body">あなたは位置情報にアクセスすることを拒否しましたが、BriarはBluetooth端末を発見するのに、この権限が必要です。\n\nアクセス権を付与することを考慮願います。</string>
<string name="permission_location_setting_title">位置情報設定</string>
<string name="permission_location_setting_body">端末の位置情報設定は、Bluetoothを介して他の端末を見つけるために、オンにする必要があります。続けるには位置情報を有効にしてください。その後、位置情報を無効にできます。</string>
<string name="permission_location_setting_hotspot_body">端末の位置情報設定は、Wi-Fiホットスポットを作成するために、オンにする必要があります。続けるには位置情報を有効にしてください。その後、位置情報を無効にできます。</string>
<string name="permission_location_setting_button">位置情報を有効化</string>
<string name="permission_bluetooth_title">付近の端末の権限</string>
<string name="permission_bluetooth_body">Bluetooth通信を使用するには、Briarは付近の端末を探し接続する権限が必要です。</string>
<string name="permission_bluetooth_denied_body">あなたは付近の端末にアクセスすることを拒否しましたが、BriarはBluetoothを使用するのに、この権限が必要です。\n\nアクセス権を付与することを考慮願います。</string>
<string name="qr_code">QRコード</string>
<string name="show_qr_code_fullscreen">QRコードを全画面表示する</string>
<!--App Locking-->
@@ -737,17 +754,19 @@
<string name="hotspot_notification_title">Briarをオフラインで共有</string>
<string name="hotspot_button_connected">次へ</string>
<string name="permission_hotspot_location_request_body">Wi-Fiホットスポットを作るには、Briarはあなたの位置情報にアクセスする権限が必要です。\n\nBriarはあなたの位置情報を保存せず、誰かに共有することもありません。</string>
<string name="permission_hotspot_location_denied_body">あなたの位置情報にアクセスすることを拒否しましたが、BriarはWi-Fiホットスポットを作るのに、この権限が必要です。\n\nアクセス権を付与することを考慮願います</string>
<string name="permission_hotspot_location_request_precise_body">Wi-Fiホットスポットを作るには、Briarはあなたの正確な位置情報にアクセスする権限が必要です。\n\nBriarはあなたの位置情報を保存せず、誰かに共有することもありません</string>
<string name="permission_hotspot_location_denied_body">あなたは位置情報にアクセスすることを拒否しましたが、BriarはWi-Fiホットスポットを作るのに、この権限が必要です。\n\nアクセス権を付与することを考慮願います。</string>
<string name="permission_hotspot_location_denied_precise_body">あなたは正確な位置情報にアクセスすることを拒否しましたが、BriarはWi-Fiホットスポットを作るのに、この権限が必要です。\n\nアクセス権を付与することを考慮願います。</string>
<string name="wifi_settings_title">Wi-Fi設定</string>
<string name="wifi_settings_request_enable_body">Wi-Fiホットスポットを作るには、BriarWi-Fiの使用が必要です。有効にしてください。</string>
<string name="hotspot_tab_manual">手動</string>
<!--The placeholder to be inserted into the string 'hotspot_manual_wifi': People can connect by %s-->
<string name="hotspot_scanning_a_qr_code">QRコードをスキャン</string>
<string name="hotspot_scanning_a_qr_code">QRコードを読み取る</string>
<!--Wi-Fi setup-->
<!--The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code'-->
<string name="hotspot_manual_wifi">あなたの電話機はWi-Fiホットスポットを提供しています。Briarのダウンロードを希望する人は、以下の方法で端末のWi-Fi設定に追加するか、または %s によるホットスポットに接続してください。そのホットスポットに接続されたら、「次へ」を押してください。</string>
<string name="hotspot_manual_wifi_ssid">ネットワーク名</string>
<string name="hotspot_qr_wifi">あなたの電話機はWi-Fiホットスポットを提供しています。Briarのダウンロードを希望する人は、このQRコードをスキャンしてホットスポットに接続してください。そのホットスポットに接続されたら、「次へ」を押してください。</string>
<string name="hotspot_qr_wifi">あなたの電話機はWi-Fiホットスポットを提供しています。Briarのダウンロードを希望する人は、このQRコードを読み取ってホットスポットに接続してください。そのホットスポットに接続されたら、「次へ」を押してください。</string>
<string name="hotspot_no_peers_connected">接続された端末なし</string>
<plurals name="hotspot_peers_connected">
<item quantity="other">%s機の接続された端末</item>
@@ -756,7 +775,7 @@
<!--The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code'-->
<string name="hotspot_manual_site">あなたの電話機はWi-Fiホットスポットを提供しています。ホットスポットに接続された人は、ウェブブラウザまたは %s 内で以下のリンクを入力して、Briarをダウンロードできます。</string>
<string name="hotspot_manual_site_address">アドレスURL</string>
<string name="hotspot_qr_site">あなたの電話機はWi-Fiホットスポットを提供しています。ホットスポットに接続された人は、このQRコードをスキャンして、Briarをダウンロードできます。</string>
<string name="hotspot_qr_site">あなたの電話機はWi-Fiホットスポットを提供しています。ホットスポットに接続された人は、このQRコードを読み取って、Briarをダウンロードできます。</string>
<!--e.g. Download Briar 1.2.20-->
<string name="website_download_title_1">Briar %sをダウンロード</string>
<string name="website_download_intro_1">近くの誰かが、あなたとBriarを共有しました。</string>

View File

@@ -283,7 +283,11 @@
<string name="duplicate_link_dialog_text_3">%1$sနှင့်%2$s တူညီသောလင့်ခ်ကို သင့်အား ပေးပို့ခဲ့သည်။ \n\n သူတို့ထဲမှ တစ်ဦးက သင့်အဆက်အသွယ်များသည် မည်သူဖြစ်သည်ကို ရှာဖွေရန် ကြိုးစားနေပေမည်။ \n\nသင်သည် အခြားသူတစ်ဦးထံမှ တူညီသောလင့်ခ်ကို ရရှိထားကြောင်း ၎င်းတို့အား မပြောပါနှင့်။</string>
<string name="pending_contact_updated_toast">ဆိုင်းငံ့ထားသော အဆက်အသွယ်ကို အပ်ဒိတ်လုပ်ပြီး</string>
<!--Peer trust levels-->
<string name="peer_trust_level_unverified">မစိစစ်ရသေးသော အဆက်အသွယ်</string>
<string name="peer_trust_level_verified">စိစစ်ထားပြီးသော အဆက်အသွယ်
</string>
<string name="peer_trust_level_ourselves">ကျွနု်ပ်ကို</string>
<string name="peer_trust_level_stranger">သူစိမ်း</string>
<!--Introductions-->
<string name="introduction_onboarding_title">သင့်ရဲ့ အဆက်အသွယ်လိပ်စာများကို မိတ်ဆက်ပါ</string>
<string name="introduction_menu_item">မိတ်ဆက်ခြင်း ပြုလုပ်ပါ</string>
@@ -404,6 +408,8 @@
<string name="forum_declined_toast">ဖိတ်ကြားမှု ငြင်းဆိုခဲ့သည်</string>
<string name="shared_by_format">%s မှ မျှဝေခဲ့သည်</string>
<string name="forum_invitation_already_sharing">မျှဝေထားခြင်းဖြစ်သည်</string>
<string name="forum_invitation_already_invited">ဖိတ်ကြားစာ ပို့ပြီးပြီ</string>
<string name="forum_invitation_invite_received">ဖိတ်ကြားစာ လက်ခံရရှိသည်</string>
<string name="forum_invitation_response_accepted_sent">%s မှ ဖိတ်ကြားမှုကို သင်သည် လက်ခံခဲ့ပါသည်။</string>
<string name="forum_invitation_response_declined_sent">%s မှ ဖိတ်ကြားမှုကို သင်သည် ငြင်းဆိုခဲ့ပါသည်။</string>
<string name="forum_invitation_response_declined_auto">%s မှ ပို့ထားသော ဖိုရမ်သို့ ဖိတ်ခေါ်ချက်ကို အလိုအလျောက် ငြင်းပယ်ခဲ့ပါသည်။</string>

View File

@@ -459,6 +459,10 @@
<string name="forum_declined_toast">Приглашение отклонено</string>
<string name="shared_by_format">Совместно %s</string>
<string name="forum_invitation_already_sharing">Уже поделился(-лась)</string>
<string name="forum_invitation_already_invited">Приглашение уже отправлено</string>
<string name="forum_invitation_invite_received">Приглашение уже получено</string>
<string name="forum_invitation_not_supported">Не поддерживается этим контактом</string>
<string name="forum_invitation_error">Ошибка. Это программная ошибка, а не ваша вина</string>
<string name="forum_invitation_response_accepted_sent">Вы приняли приглашение на форум от %s.</string>
<string name="forum_invitation_response_declined_sent">Вы отклонили приглашение на форум от %s.</string>
<string name="forum_invitation_response_declined_auto">Приглашение на форум от %s было отклонено автоматически.</string>

View File

@@ -238,6 +238,8 @@ dhe smund të hapet me këtë version.\n\nJu lutemi, përmirësojeni me versi
<string name="qr_code_invalid">Kodi QR është i pavlefshëm</string>
<string name="qr_code_too_old_1">Kodi QR që keni skanuar vjen prej një versioni të vjetër të Briar-it.\n\nJu lutemi, kërkojini kontaktit tuaj ta përmirësojë me versionin më të ri dhe mandej riprovoni.</string>
<string name="qr_code_too_new_1">Kodi QR që keni skanuar vjen prej një versioni më të ri të Briar-it.\n\nJu lutemi, përmirësojeni me versionin më të ri dhe mandej riprovoni.</string>
<string name="mailbox_qr_code_for_contact">Kodi QR që keni skanuar vjen prej Kutisë Postare të Briar-it.\n\nNëse doni një lidhje për te Kutia Postare, ju lutemi, zgjidhni Rregullime &gt; Kuti Postare, që nga menuja e Briar-it.</string>
<string name="qr_code_format_unknown">Kodi QR që keni skanuar sështë menduar për shtim të një kontakti Briar.\n\nJu lutemi, skanoni kodin QR të shfaqur te ekrani i kontaktit tuaj.</string>
<string name="camera_error">Gabim kamere</string>
<string name="connecting_to_device">Po lidhet me pajisjen\u2026</string>
<string name="authenticating_with_device">Po bëhet mirëfilltësimi me pajisjen\u2026</string>
@@ -432,6 +434,10 @@ dhe smund të hapet me këtë version.\n\nJu lutemi, përmirësojeni me versi
<string name="forum_declined_toast">Ftesa u hodh poshtë</string>
<string name="shared_by_format">Ndarë nga %s</string>
<string name="forum_invitation_already_sharing">Ndarë tashmë</string>
<string name="forum_invitation_already_invited">Ftesë e dërguar tashmë</string>
<string name="forum_invitation_invite_received">Ftesë e marrë tashmë</string>
<string name="forum_invitation_not_supported">Nuk mbulohet prej këtij kontakti</string>
<string name="forum_invitation_error">Gabim. Kjo është një e metë dhe jo për fajin tuaj</string>
<string name="forum_invitation_response_accepted_sent">Pranuat ftesën e forumit nga %s.</string>
<string name="forum_invitation_response_declined_sent">Hodhët poshtë ftesën e forumit nga %s.</string>
<string name="forum_invitation_response_declined_auto">Ftesa për në forum nga %s u hodh poshtë automatikisht.</string>
@@ -595,7 +601,13 @@ dhe smund të hapet me këtë version.\n\nJu lutemi, përmirësojeni me versi
<string name="mailbox_setup_download_link">Ndani Lidhje Shkarkimi</string>
<string name="mailbox_setup_button_scan">Skanoni kod QR Kutie postare</string>
<string name="permission_camera_qr_denied_body">Keni mohuar hyrjen te kamera, por skanimi i një kodi QR lyp përdorimin e kamerës.\n\nJu lutemi, shihni mundësinë e dhënies së hyrjes.</string>
<string name="mailbox_setup_connecting">Po bëhet lidhja me Kutinë postare…</string>
<!--This string is shown when connecting to a Mailbox for the first time. The placeholder will be replaced with a duration, e.g. "2 minutes".-->
<string name="mailbox_setup_connecting_info">Kjo mund të dojë deri në %1s</string>
<string name="mailbox_qr_code_too_old">Kodi QR që keni skanuar vjen prej një versioni të vjetër të Kutisë Postare të Briar-it.\n\nJu lutemi, përditësojeni Kutinë Postare të Briar-it me versionin më të ri dhe mandej riprovoni.</string>
<string name="mailbox_qr_code_too_new">Kodi QR që keni skanuar vjen prej një versioni më të ri të Kutisë Postare të Briar-it.\n\nJu lutemi, përmirësojeni Briar-in me versionin më të ri dhe mandej riprovoni.</string>
<string name="contact_qr_code_for_mailbox">Kodi QR që keni skanuar është për shtim të një kontakti Briar.\n\nNëse doni të shtoni një kontakt, ju lutemi, kaloni te lista e kontakteve dhe prekni ikonën +.</string>
<string name="mailbox_setup_qr_code_wrong_description">Kodi QR që keni skanuar svjen prej Kutisë Postare të Briar-it.\n\nJu lutemi, hapni aplikacionin Kuti Postare Briar në pajisjen tuaj me Kutinë Postare dhe skanoni kodin QR që paraqitet.</string>
<string name="mailbox_setup_already_paired_title">Kuti postare tashmë e lidhur</string>
<string name="mailbox_setup_already_paired_description">Shkëputeni Kutinë postare në pajisjen tuaj tjetër dhe riprovoni.</string>
<string name="mailbox_setup_io_error_title">Su bë dot lidhje</string>

View File

@@ -313,6 +313,10 @@
<string name="duplicate_link_dialog_text_3">%1$s та %2$s надіслали вам однакові посилання.\n\nХтось із них, можливо, намагається виявити ваші контакти.\n\nНе кажіть їм, що вже отримували це посилання від когось іще.</string>
<string name="pending_contact_updated_toast">Оновлено нерозглянутий контакт</string>
<!--Peer trust levels-->
<string name="peer_trust_level_unverified">Неперевірений контакт</string>
<string name="peer_trust_level_verified">Перевірений контакт</string>
<string name="peer_trust_level_ourselves">Я</string>
<string name="peer_trust_level_stranger">Чужинець</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Поділитися своїми контактами</string>
<string name="introduction_onboarding_text">Представте свої контакти навзаєм, щоб вони могли спілкуватись у Briar.</string>
@@ -341,6 +345,7 @@
<string name="connect_via_bluetooth_intro">Якщо Bluetooth-з\'єднання не спрацьовують автоматично, можете з\'єднатися вручну на цьому екрані.\n\nВаш контакт має бути поруч, щоб це спрацювало.\n\nВам обом слід одночасно натиснути «Почати».</string>
<string name="connect_via_bluetooth_already_discovering">Спроба Bluetooth-з\'єднання вже триває. Повторіть трохи пізніше.</string>
<string name="connect_via_bluetooth_no_location_permission">Не вдається продовжити без дозволу на отримання місцезнаходження</string>
<string name="connect_via_bluetooth_no_bluetooth_permission">Неможливо продовжите без дозволу пристроїв поблизу</string>
<string name="connect_via_bluetooth_start">Триває Bluetooth-з\'єднання...</string>
<string name="connect_via_bluetooth_success">Bluetooth-з\'єднання успішне</string>
<string name="connect_via_bluetooth_error">Не вдалося з\'єднатися через Bluetooth.</string>
@@ -443,6 +448,10 @@
<string name="forum_declined_toast">Запрошення було відхилено</string>
<string name="shared_by_format">Поширено %s</string>
<string name="forum_invitation_already_sharing">Вже поширюється</string>
<string name="forum_invitation_already_invited">Запрошення вже відправлене</string>
<string name="forum_invitation_invite_received">Запрошення вже отримано</string>
<string name="forum_invitation_not_supported">Не підтримується цим контактом</string>
<string name="forum_invitation_error">Помилка. Це збій програми, а не ваша провина</string>
<string name="forum_invitation_response_accepted_sent">Ви прийняли запрошення до форуму від %s.</string>
<string name="forum_invitation_response_declined_sent">Ви відхилили запрошення до форуму від %s.</string>
<string name="forum_invitation_response_declined_auto">Запрошення до форуму від %s відхилено автоматично.</string>
@@ -609,6 +618,7 @@
<string name="mailbox_setup_button_scan">Сканувати QR-код Mailbox</string>
<string name="permission_camera_qr_denied_body">Ви заборонили доступ до камери, але камера потрібна для сканування QR-коду.\n\nБудь ласка, надайте доступ.</string>
<!--This string is shown when connecting to a Mailbox for the first time. The placeholder will be replaced with a duration, e.g. "2 minutes".-->
<string name="mailbox_setup_connecting_info">Це може зайняти до %1s</string>
<string name="mailbox_setup_already_paired_title">Mailbox уже під\'єднано</string>
<string name="mailbox_setup_already_paired_description">Від\'єднайте Mailbox від свого іншого пристрою й повторіть спробу.</string>
<string name="mailbox_setup_io_error_title">Не вдалося з\'єднатися</string>
@@ -627,19 +637,39 @@
<string name="mailbox_status_connected_title">Mailbox працює</string>
<string name="mailbox_status_problem_title">Не вдається з\'єднати Briar із Mailbox</string>
<string name="mailbox_status_failure_title">Mailbox недоступна</string>
<string name="mailbox_status_app_too_old_title">Версія Briar занадто стара</string>
<string name="mailbox_status_app_too_old_message">Оновіть Briar до найновішої версії та спробуйте ще.</string>
<string name="mailbox_status_mailbox_too_old_title">Версія потової скриньки занадто стара</string>
<string name="mailbox_status_check_button">Перевірити з\'єднання</string>
<!--Example for string substitution: Last connection: 3min ago-->
<string name="mailbox_status_connected_info">З\'єднання було: %s</string>
<!--Indicates that there never was a connection to the mailbox. Last connection: Never-->
<string name="mailbox_status_connected_never">Ніколи</string>
<string name="mailbox_status_unlink_button">Від\'єднати</string>
<string name="mailbox_status_unlink_dialog_title">Від\'єднати потову скриньку?</string>
<string name="mailbox_status_unlink_dialog_question">Ви впевнені що хочете від\'єднати поштову скриньку?</string>
<string name="mailbox_status_unlink_dialog_warning">Якщо ви від\'єднаєте вашу поштову скриньку, ви не зможете отримувати повідомлення поки Briar не на зв\'язку</string>
<string name="mailbox_status_unlink_no_wipe_title">Ваша поштова скринька від\'єднана</string>
<string name="mailbox_status_unlink_success">Ваша поштова скринька від\'єднана</string>
<string name="mailbox_error_notification_channel_title">Проблема з поштовою скринькою Briar</string>
<string name="mailbox_error_notification_title">Поштова скринька Briar не доступна</string>
<string name="mailbox_error_notification_text">Натисніть щоб виправити проблему</string>
<string name="mailbox_error_wizard_button">Виправити проблему</string>
<string name="mailbox_error_wizard_question1">Ви маєте доступ до вашого пристрою Mailbox?</string>
<string name="mailbox_error_wizard_answer1_1">Я бачу вказівки щодо налаштування Mailbox</string>
<string name="mailbox_error_wizard_answer1_2">Я бачу QR-код</string>
<string name="mailbox_error_wizard_info2">Будь ласка повертайтесь до цього екрану коли у вас буде доступ до пристрою</string>
<!--About-->
<string name="about_title">Про</string>
<string name="briar_version">Версія Briar %s</string>
<string name="tor_version">Версія Tor %s</string>
<string name="links">Посилання</string>
<string name="briar_website">\u2022 <a href="">Вебсайт</a></string>
<string name="briar_source_code">\u2022 <a href="">Вихідний код</a></string>
<string name="briar_changelog">\u2022 <a href="">Список змін та доповнень</a></string>
<string name="briar_privacy_policy">\u2022 <a href="">Політика конфіденційности</a></string>
<!--Here translators can add their names or Transifex usernames(eg "Thanks to all the contributors at the Localization Lab, especially Tom, Matthew and Jerry")-->
<string name="translator_thanks">Дякуємо всім хто вніс свій внесок у Localization Lab</string>
<!--Conversation Settings-->
<string name="disappearing_messages_title">Самознищувані повідомлення</string>
<string name="disappearing_messages_explanation_long">Коли цей параметр увімкнено, нові
@@ -671,6 +701,7 @@
<string name="describe_crash">Опишіть, що сталося (за бажанням)</string>
<string name="enter_feedback">Введіть свій відгук</string>
<string name="optional_contact_email">Ваша електронна адреса (необов\'язково)</string>
<string name="privacy_policy">Відправляючи нам дані ви погоджуєтесь з нашою <a href="">політикою конфіденційности</a></string>
<string name="include_debug_report_crash">Додати анонімну інформацію про аварійний збій</string>
<string name="include_debug_report_feedback">Додати анонімну інформацію про цей пристрій</string>
<string name="dev_report_user_info">Користувацькі дані</string>
@@ -706,11 +737,13 @@
<string name="permission_location_request_body">Щоб знайти Bluetooth-пристрої, Briar потребує доступу до інформації про ваше місцезнаходження.\n\nBriar не зберігає дані про ваше місцезнаходження та не ділиться ні з ким цією інформацією.</string>
<string name="permission_camera_location_title">Камера та місцезнаходження</string>
<string name="permission_camera_location_request_body">Щоб відсканувати QR-код, Briar потребує доступу до камери.\n\nЩоб знайти Bluetooth-пристрої, Briar потребує доступу до інформації про ваше місцезнаходження.\n\nBriar не зберігає дані про ваше місцезнаходження та не ділиться ні з ким цією інформацією.</string>
<string name="permission_camera_bluetooth_title">Камера та пристрої поблизу</string>
<string name="permission_camera_denied_body">Ви відмовилися надати доступ до камери, але додавання контактів потребує використання камери.\n\nБудь ласка, подумайте про можливість надання доступу.</string>
<string name="permission_location_denied_body">Ви заборонили доступ до місцеперебування, але Briar потребує цього доступу, щоб знаходити Bluetooth-пристрої.\n\nБудь ласка, надайте доступ.</string>
<string name="permission_location_setting_title">Визначення місцеперебування</string>
<string name="permission_location_setting_body">Ваш пристрій не дозволяє визначати місцеперебування, а це потрібно для знаходження інших пристроїв через Bluetooth. Будь ласка, увімкніть визначення місцеперебування, щоб продовжити. Згодом ви зможете знову його вимкнути.</string>
<string name="permission_location_setting_button">Увімкнути визначення місцеперебування</string>
<string name="permission_bluetooth_title">Дозволи на пристроях поблизу</string>
<string name="qr_code">QR-код</string>
<string name="show_qr_code_fullscreen">Вивести QR-код на весь екран</string>
<!--App Locking-->
@@ -757,6 +790,9 @@
<string name="hotspot_manual_site_address">Адреса (URL)</string>
<string name="hotspot_qr_site">Ваш телефон надає точку доступу Wi-Fi. З\'єднані з нею можуть завантажити Briar, просканувавши цей QR-код.</string>
<!--e.g. Download Briar 1.2.20-->
<string name="website_download_title_1">Завантаження Briar %s</string>
<string name="website_download_intro_1">Хтось поблизу поділився з вами Briar.</string>
<string name="website_download_button">Завантажити Briar</string>
<string name="website_download_outro">Після завантаження, відкрийте завантажений файл і встановіть його.</string>
<string name="website_troubleshooting_title">Виправлення неполадок</string>
<string name="website_troubleshooting_1">Якщо не вдається завантажити застосунок, повторіть спробу іншим вебпереглядачем.</string>

View File

@@ -420,6 +420,10 @@
<string name="forum_declined_toast">邀请已谢绝</string>
<string name="shared_by_format">由 %s 分享</string>
<string name="forum_invitation_already_sharing">已在分享</string>
<string name="forum_invitation_already_invited">邀请已发送</string>
<string name="forum_invitation_invite_received">已收到邀请</string>
<string name="forum_invitation_not_supported">不被此联系人所支持</string>
<string name="forum_invitation_error">错误。这是一个故障,不是你的问题</string>
<string name="forum_invitation_response_accepted_sent">您接受了来自 %s的论坛邀请</string>
<string name="forum_invitation_response_declined_sent">您谢绝了来自 %s的论坛邀请</string>
<string name="forum_invitation_response_declined_auto">来自 %s 的论坛邀请被自动拒绝了</string>

View File

@@ -1,47 +1,66 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<!--Setup-->
<string name="setup_title">歡迎來到 Briar</string>
<string name="setup_name_explanation">您的暱稱將顯示在您發佈的任何內容旁。暱稱在創建帳戶後將無法更改。</string>
<string name="setup_next">下一步</string>
<string name="setup_password_intro">設置密碼</string>
<string name="setup_password_explanation">您的 Briar 帳戶被加密儲存在您的裝置上,而非在雲端。卸載 Briar 或忘記密碼將導致帳戶無法恢復。請設置不易被猜出的長密碼,比如四個隨機英文單詞,或是由隨機字母、數字和符號組成的十位字符。</string>
<string name="dnkm_doze_title">背景網絡連接</string>
<string name="dnkm_doze_intro">爲了接收消息Briar 需要在後臺保持連接</string>
<string name="dnkm_doze_explanation">Briar 需要保持背景網絡連接去接收消息。爲此,請停用電量優化選項。</string>
<string name="dnkm_doze_button">允許連接</string>
<string name="choose_nickname">設置暱稱</string>
<string name="choose_password">設置密碼</string>
<string name="confirm_password">確認密碼</string>
<string name="name_too_long">暱稱過長</string>
<string name="password_too_weak">密碼不夠強</string>
<string name="passwords_do_not_match">兩個密碼並不相同</string>
<string name="create_account_button">創建帳戶</string>
<string name="more_info">更多信息</string>
<string name="don_t_ask_again">不再詢問</string>
<string name="dnkm_huawei_protected_text">輕按下方的按鈕將 Briar 加入受保護程式列表。</string>
<string name="dnkm_huawei_protected_button">保護 Briar</string>
<string name="dnkm_huawei_protected_help">如果 Briar 未被加入受保護程式列表,它將無法在背景運行。</string>
<string name="dnkm_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">您的 Briar 帳戶被加密儲存在您的裝置上,而非在雲端。因此我們無法重置您的密碼。您是否希望刪除帳戶,重新開始?\n\n注意刪除帳戶將永久失去您以前的身份、聯絡人和訊息</string>
<string name="startup_failed_notification_title">Briar 無法啓動</string>
<string name="startup_failed_notification_text">輕按查看更多信息。</string>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Setup-->
<string name="setup_title">歡迎來到 Briar</string>
<string name="setup_name_explanation">您的暱稱將顯示在您發佈的任何內容旁。暱稱在創建帳戶後將無法更改。</string>
<string name="setup_next">下一步</string>
<string name="setup_password_intro">設置密碼</string>
<string name="setup_password_explanation">您的 Briar 帳戶被加密儲存在您的裝置上,而非在雲端。卸載 Briar 或忘記密碼將導致帳戶無法恢復。請設置不易被猜出的長密碼,比如四個隨機英文單詞,或是由隨機字母、數字和符號組成的十位字符。</string>
<string name="dnkm_doze_intro">爲了接收消息Briar 需要在後臺保持連接</string>
<string name="dnkm_doze_explanation">Briar 需要保持背景網絡連接去接收消息。爲此,請停用電量優化選項</string>
<string name="choose_nickname">設置暱稱</string>
<string name="choose_password">設置密碼</string>
<string name="confirm_password">確認密碼</string>
<string name="name_too_long">暱稱過長</string>
<string name="password_too_weak">密碼不夠強</string>
<string name="passwords_do_not_match">兩個密碼並不相同</string>
<string name="create_account_button">創建帳戶</string>
<string name="more_info">更多信息</string>
<string name="don_t_ask_again">不再詢問</string>
<string name="dnkm_huawei_protected_text">輕按下方的按鈕將 Briar 加入“受保護的程式”列表。</string>
<string name="dnkm_huawei_protected_button">保護 Briar</string>
<string name="dnkm_huawei_protected_help">如果 Briar 未被加入受保護程式列表,它將無法在背景運行</string>
<string name="dnkm_huawei_app_launch_text">請輕觸下方按鍵以打開\"啟動軟體\"畫面,確認 Briar 調至\"手動管理\"模式</string>
<string name="dnkm_huawei_app_launch_help"> Briar 未調至\"手動管理\"模式,它將無法在背景中執行</string>
<string name="dnkm_xiaomi_text">為能在背景中執行Briar 須被鎖定在最近使用應用軟體清單上</string>
<string name="dnkm_xiaomi_button">保護 Briar</string>
<string name="dnkm_xiaomi_help">若 Briar 未鎖到最近使用的程式清單裏,它將無法背景中執行。</string>
<string name="dnkm_xiaomi_dialog_body_old">1. 打開最近使用的程式清單(亦稱軟體快速切換)n\n2滑動到軟體圖像後會出現掛鎖圖標 \n\n3若掛鎖為打開狀態請輕觸它改變成關鎖。</string>
<string name="dnkm_xiaomi_dialog_body_new">1. 打開最近使用的程式清單(亦稱軟體快速切換)n\n2。若 Briar 旁邊出現掛鎖圖標則無須其它更動 \n\n3若無掛鎖則輕觸軟體圖標直到現出掛鎖按鍵點觸掛鎖進行更動。</string>
<string name="dnkm_xiaomi_lock_apps_text">請輕觸下方按鍵以開啟安全設定。觸點\"加速\"後再觸\"鎖定應用軟體\",將其設置為鎖定狀態。</string>
<string name="dnkm_xiaomi_lock_apps_help">若 Briar 在\"鎖定應用\"的畫面中未被設置為\"鎖定\",則無法在背景中執行。</string>
<string name="dnkm_warning_dozed_1">Briar 無法在背景運行</string>
<!--Login-->
<string name="enter_password">密碼</string>
<string name="try_again">密碼錯誤,請重試</string>
<string name="dialog_title_cannot_check_password">無法檢查密碼</string>
<string name="dialog_message_cannot_check_password">Briar 無法代您檢查密碼,請重新啟動設備來解決此問題。
</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">您的 Briar 帳戶被加密儲存在您的裝置上,而非在雲端。因此我們無法重置您的密碼。您是否希望刪除帳戶,重新開始?\n\n注意刪除帳戶將永久失去您以前的身份、聯絡人和訊息。</string>
<string name="startup_failed_activity_title">Briar 啓動失敗</string>
<string name="startup_failed_db_error">由於某些原因Briar 數據庫已損壞並且無法修復。您的帳戶、數據和所有聯絡人已經丟失。很不幸,您需要重新安裝 Briar 或者在提示輸入密碼時選擇“我忘記了密碼”來重新創建一個 Briar 帳戶。</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">Briar 無法開啓一個必需的插件。於通常情況下,重新安裝 Briar 可以解決該問題。但是由於 Briar 不使用中央服務器來儲存您的數據,這項操作將導致您的帳戶及相關的一切數據丟失</string>
<string name="startup_failed_clock_error">由於您設備上的時間有誤Briar 無法啟動
請重新設置好正確的時間後再試試。</string>
<string name="startup_failed_db_error">Briar 無法打開存有您帳號、聯絡人資料及聊天訊息的資料庫。\n\n請將 Briar 更新至最新版後再試試。再不然就得新建帳號,其方式是在彈出的密碼視窗中選取\"遺忘密碼\"來建立新帳號</string>
<string name="startup_failed_data_too_old_error">您的帳號在是之前的舊版本應用創建,但在本版本中無法啟用。\n\n您必須重新安裝舊的版本或是另設新帳號請在彈出的密碼視窗中選取\"遺忘密碼\"來設新帳號。</string>
<string name="startup_failed_data_too_new_error">您的帳號在是新版本的應用中建立,但在舊版中無法開啟。請更新到最新版後再試試看</string>
<string name="startup_failed_service_error">Briar 無法啟動所需的元件\n\n請昇級至最新版後再重試</string>
<plurals name="expiry_warning">
<item quantity="other">這是 Briar 的測試版本。您的帳戶將在 %d 天後到期,且無法延期。</item>
</plurals>
<plurals name="old_android_expiry_warning">
<item quantity="other">Briar 已不再支援 Android 4它將在%s停止運作(%d天時間)請在較新的設備上安裝Briar 並建立新帳號。</item>
</plurals>
<string name="expiry_date_reached">本軟件已過期。\n感謝您的測試</string>
<string name="download_briar">爲了繼續使用 Briar請下載最新版。</string>
<string name="create_new_account">您要創建一個新帳戶,但可以續用現時的暱稱。</string>
<string name="download_briar_button">下載最新版</string>
<string name="old_android_expiry_date_reached">Briar 不再支援 Android 4 \n 請在較新設備上安裝 Briar </string>
<string name="old_android_delete_account">您可以輕觸下方按鍵好從設備上刪除帳號</string>
<string name="delete_account_button">刪除帳戶</string>
<string name="startup_open_database">正在解密數據庫……</string>
<string name="startup_migrate_database">正在更新數據庫……</string>
<string name="startup_compact_database">正在壓縮數據庫……</string>
@@ -56,12 +75,36 @@
<string name="lock_button">鎖定程式</string>
<string name="settings_button">設置</string>
<string name="sign_out_button">登出</string>
<string name="transports_onboarding_text">觸點此處可控制 Briar 連接聯絡人的方式</string>
<!--Transports: Tor-->
<string name="transport_tor">互聯網</string>
<string name="tor_device_status_online_wifi">您的手機是透過 Wi-Fi 連上網際網路</string>
<string name="tor_device_status_online_mobile">您的手機是透過電信數據連上網際網路</string>
<string name="tor_device_status_offline">您的手機無法連上網際網路</string>
<string name="tor_plugin_status_enabling">Briar 正在連接網際網路</string>
<string name="tor_plugin_status_active">Briar 正在連接網際網路</string>
<string name="tor_plugin_status_inactive">Briar 無法連上網際網路</string>
<string name="tor_plugin_status_disabled">Briar 被設置為無法使用網際網路</string>
<string name="tor_plugin_status_disabled_mobile_data">Briar 被設置為無法使用電信數據</string>
<string name="tor_plugin_status_disabled_battery">當使用電池時Briar 被設置為無法使用網際網路</string>
<string name="tor_plugin_status_disabled_country_blocked">Briar 被設置為無法在這個國家使用網際網路</string>
<!--Transports: Wi-Fi-->
<string name="transport_lan">Wi-Fi</string>
<string name="transport_lan_long">同一個 Wi-Fi 網路</string>
<string name="lan_device_status_on">您的手機已連上 Wi-Fi</string>
<string name="lan_device_status_off">您的手機未連上 Wi-Fi</string>
<string name="lan_plugin_status_enabling">Briar 正在連接 Wi-Fi 網路</string>
<string name="lan_plugin_status_active">Briar 已連接 Wi-Fi 網路</string>
<string name="lan_plugin_status_inactive">Briar 無法連上 Wi-Fi 網路</string>
<string name="lan_plugin_status_disabled">Briar 被設為無法連接 Wi-Fi 網路</string>
<!--Transports: Bluetooth-->
<string name="transport_bt">藍牙</string>
<string name="bt_device_status_on">您的手機已開啟藍牙</string>
<string name="bt_device_status_off">您的手機關閉了藍牙</string>
<string name="bt_plugin_status_enabling">Briar 正在連接藍牙</string>
<string name="bt_plugin_status_active">Briar 已連上藍牙</string>
<string name="bt_plugin_status_inactive">Briar 無法連上藍牙</string>
<string name="bt_plugin_status_disabled">Briar 被設置成無法使用藍牙</string>
<!--Notifications-->
<string name="reminder_notification_title">已登出 Briar</string>
<string name="reminder_notification_text">輕按以重新登錄。</string>
@@ -97,6 +140,8 @@
<string name="allow">允許</string>
<string name="open">打開</string>
<string name="change">變動</string>
<string name="start">開始</string>
<string name="finish">結束</string>
<string name="no_data">沒有數據</string>
<string name="ellipsis">……</string>
<string name="text_too_long">輸入的文本過長</string>
@@ -104,13 +149,18 @@
<string name="fix">修復</string>
<string name="help">幫助</string>
<string name="sorry">抱歉</string>
<string name="error_start_activity">您的系統無法使用</string>
<string name="status_heading">狀態:</string>
<string name="error">錯誤</string>
<string name="info">資訊</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">尚無聯絡人可供顯示</string>
<string name="no_contacts_action">輕按 + 號即可添加聯絡人</string>
<string name="date_no_private_messages">沒有消息。</string>
<string name="no_private_messages">尚無消息可供顯示</string>
<string name="message_hint">輸入信</string>
<string name="message_hint">新訊</string>
<string name="message_hint_auto_delete">新消失的訊息</string>
<string name="message_error">發送訊息時出錯</string>
<string name="image_caption_hint">添加圖片描述(可選)</string>
<string name="image_attach">附加圖片</string>
<string name="image_attach_error">無法附加圖片</string>
@@ -118,6 +168,39 @@
<string name="image_attach_error_invalid_mime_type">圖片格式不支援: %s</string>
<string name="set_contact_alias">更改聯絡人姓名</string>
<string name="set_contact_alias_hint">聯絡人姓名</string>
<string name="menu_item_disappearing_messages">已消失的訊息</string>
<!--The first placeholder will show a duration like "7 days". The second placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_you_enabled">您的訊息將在%1$s%2$s 消失</string>
<!--The placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_you_disabled">您的訊息不會消失%1$s</string>
<!--The first placeholder will show a contact's name. The second placeholder will show a duration like "7 days". The third placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_contact_enabled">%1$s的訊息將在 %2$s. %3$s消失</string>
<plurals name="duration_minutes">
<item quantity="other">%d 分鐘</item>
</plurals>
<plurals name="duration_hours">
<item quantity="other">%d 小時</item>
</plurals>
<plurals name="duration_days">
<item quantity="other">%d 天</item>
</plurals>
<!--The first placeholder will show a contact's name. The second placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_contact_disabled">%1$s的訊息將不會消失%2$s</string>
<string name="tap_to_learn_more">輕觸了解詳情</string>
<string name="auto_delete_changed_warning_title">改變消失的訊息</string>
<string name="auto_delete_changed_warning_message_enabled">撰寫訊息之前已啟用了消失功能</string>
<string name="auto_delete_changed_warning_message_disabled">撰寫訊息之前已關閉了消失功能</string>
<string name="auto_delete_changed_warning_send">無論怎樣都要發送</string>
<string name="delete_all_messages">刪除全部訊息</string>
<string name="dialog_title_delete_all_messages">確認刪除訊息</string>
<string name="dialog_message_delete_all_messages">確定要刪除全部訊息嗎?</string>
<string name="dialog_title_not_all_messages_deleted">無法刪掉全部訊息</string>
<string name="dialog_message_not_deleted_ongoing_both">進行中的邀請與介紹訊息必須待完成同意後才能刪除</string>
<string name="dialog_message_not_deleted_ongoing_introductions">進行中介紹訊息必須待完成同意後方可刪除</string>
<string name="dialog_message_not_deleted_ongoing_invitations">進行中的邀請訊息必須待完成同意後才能刪除</string>
<string name="dialog_message_not_deleted_not_all_selected_both">要刪除邀請或介紹訊息,須先選取請求與回應 </string>
<string name="dialog_message_not_deleted_not_all_selected_introductions">要刪除介紹訊息,須先選取請求與回應 </string>
<string name="dialog_message_not_deleted_not_all_selected_invitations">要刪除邀請,須先選取請求與回應 </string>
<string name="delete_contact">刪除聯絡人</string>
<string name="dialog_title_delete_contact">確認刪除聯絡人</string>
<string name="dialog_message_delete_contact">確認要刪除該聯絡人和與其交流訊息嗎?</string>
@@ -133,8 +216,11 @@
<string name="dialog_message_no_image_support">您的聯絡人的 Briar 版本尚未支持圖片附件。一旦他們升級後您就會看到一個不同的圖標。</string>
<string name="dialog_title_image_support">您現在可以向此聯絡人發送圖片</string>
<string name="dialog_message_image_support">輕按此圖標即可附加圖片。</string>
<string name="messaging_too_many_attachments_toast">只會送出第一個%d圖像</string>
<string name="menu_contact">聯繫</string>
<!--Adding Contacts-->
<string name="add_contact_title">添加附近的聯絡人</string>
<string name="add_contact_error_two_way">你們互相掃瞄彼此的二維碼了嗎? </string>
<string name="face_to_face">您必須面對面添加聯絡人。\n\n這樣將防止他人冒充您的身份並查看您的信息。</string>
<string name="continue_button">繼續</string>
<string name="try_again_button">重試</string>
@@ -143,13 +229,16 @@
<string name="contact_added_toast">已添加聯絡人:%s</string>
<string name="contact_already_exists">聯絡人 %s 已存在</string>
<string name="qr_code_invalid">二維碼無效</string>
<string name="qr_code_too_old">您所掃的二維碼來自舊版本 %s 。\n\n請讓您的聯絡人升級到最新版本並重試。</string>
<string name="qr_code_too_new">您所掃的二維碼來自新版本 %s 。\n\n請升級到最新版本並重試。</string>
<string name="qr_code_too_old_1">您所掃的二維碼已是舊版本\n\n 請通知您的聯絡人更新至最新版後再試試</string>
<string name="qr_code_too_new_1">您所掃的二維碼為較新的版本\n\n 請更新至最新版後再試試</string>
<string name="mailbox_qr_code_for_contact">您所掃二維碼來自 Briar Mailbox\n\n 若要連接 Mailbox請到 Briar 主選單選取Mailbox設定。</string>
<string name="qr_code_format_unknown">您所掃二維碼並非用於新增聯絡人\n\n 請確認這是您的聯絡人畫面上的二維碼</string>
<string name="camera_error">相機出錯</string>
<string name="connecting_to_device">正在連接至裝置\u2026</string>
<string name="authenticating_with_device">正在驗證裝置\u2026</string>
<string name="connection_error_title">無法連接到您的聯絡人</string>
<string name="connection_error_feedback">如果該問題仍存在,請 <a href="feedback">發送反饋</a> 幫助我們改善程式。</string>
<string name="info_both_must_scan">你們必須互相掃瞄彼此的二維碼</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">添加遠處的聯絡人</string>
<string name="add_contact_nearby_title">添加附近的聯絡人</string>
@@ -193,8 +282,9 @@
<string name="offline_state">無互聯網連接</string>
<string name="duplicate_link_dialog_title">重覆的鏈接</string>
<string name="duplicate_link_dialog_text_1">您已經有此鏈接的聯絡人,尚待處理:%s</string>
<string name="duplicate_link_dialog_text_1_contact">您已有一名聯絡人是這個鏈結: %s</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">%s與%s是同一個人嗎</string>
<string name="duplicate_link_dialog_text_2"> %1$s 和 %2$s 是同一個人嗎?</string>
<!--This is a button for answering that two nicknames do indeed refer to the same person. This
string will be used in a dialog button, so if the translation of this string is longer than 20
characters, please use "Yes" instead, and use "No" for the "Different Person" button-->
@@ -203,11 +293,17 @@
will be used in a dialog button, so if the translation of this string longer than 20 characters,
please use "No" instead, and use "Yes" for the "Same Person" button-->
<string name="different_person_button">不同的人</string>
<string name="duplicate_link_dialog_text_3">%s和%s向您發送了相同的鏈接。\n\n 其中一個可能正試圖調查您的聯絡人是誰。\n\n請不要告訴他們您收到了來自其他人的相同鏈接。</string>
<string name="duplicate_link_dialog_text_3">%1$s和 %2$s向您發送了相同的鏈接。\n\n 其中一個可能正試圖調查您的聯絡人是誰。\n\n請不要告訴他們您收到了他人的相同鏈接。</string>
<string name="pending_contact_updated_toast">已更新尚待處理的聯絡人</string>
<string name="info_both_must_enter_links">你們必須互相加入對方的鏈接 </string>
<!--Peer trust levels-->
<string name="peer_trust_level_unverified">未驗證的聯絡人</string>
<string name="peer_trust_level_verified">已驗證的聯絡人</string>
<string name="peer_trust_level_ourselves"></string>
<string name="peer_trust_level_stranger">陌生人</string>
<!--Introductions-->
<string name="introduction_onboarding_title">介紹您的聯絡人</string>
<string name="introduction_onboarding_text">您可以互相介紹您的聯絡人,這樣他們可以直接在 Briar 上建立聯繫而不必親自見面。</string>
<string name="introduction_onboarding_text">介紹您的聯絡人互相認識,這樣他們就可透過 Briar 聯繫</string>
<string name="introduction_menu_item">做介紹</string>
<string name="introduction_activity_title">選擇聯絡人</string>
<string name="introduction_not_possible">您已將這兩位聯絡人互相介紹過。請先等待該任務完成。如果您或您的聯絡人很少在線,這可能需要一些時間。</string>
@@ -223,9 +319,21 @@
<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_declined_auto">對%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>
<string name="introduction_response_declined_received_by_introducee">%1$s 表示 %2$s 謝絕了介紹。</string>
<!--Connect via Bluetooth-->
<string name="menu_item_connect_via_bluetooth">通過藍牙連接</string>
<string name="connect_via_bluetooth_title">通過藍牙連接</string>
<string name="connect_via_bluetooth_intro">若無法自動連接藍牙,可在本畫面中進行手動連接\n\n 您的聯絡人必須就在附近才行 \n\n 你們雙方要同時按下\"啟動\"鍵。</string>
<string name="connect_via_bluetooth_already_discovering">已嘗試透過藍牙連接,請稍候再試</string>
<string name="connect_via_bluetooth_no_location_permission">須同意定位授權才能繼續</string>
<string name="connect_via_bluetooth_no_bluetooth_permission">無鄰近設備授權無法繼續</string>
<string name="connect_via_bluetooth_start">透過藍牙連接...</string>
<string name="connect_via_bluetooth_success">成功地以藍牙連接</string>
<string name="connect_via_bluetooth_error">無法透過藍牙連接</string>
<string name="connect_via_bluetooth_error_not_supported">本設備不支援藍牙</string>
<!--Private Groups-->
<string name="groups_list_empty">尚無群組可供展示</string>
<string name="groups_list_empty_action">輕按 + 號創建群組,或讓您的聯絡人分享群組給您</string>
@@ -267,6 +375,7 @@
</plurals>
<string name="groups_invitations_response_accepted_sent">您接受了來自 %s 的群組邀請。</string>
<string name="groups_invitations_response_declined_sent">您謝絕了來自 %s 的群組邀請。</string>
<string name="groups_invitations_response_declined_auto">來自%s的小組邀請已自動回絕</string>
<string name="groups_invitations_response_accepted_received">%s 接受了群組邀請。</string>
<string name="groups_invitations_response_declined_received">%s 謝絕了群組邀請。</string>
<string name="sharing_status_groups">只有創建者可以邀請新成員加入群組。下面是目前的群組成員。</string>
@@ -316,6 +425,7 @@
<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_declined_auto">來自%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>
@@ -349,6 +459,7 @@
<string name="blogs_sharing_snackbar">博客已分享給選中的聯絡人</string>
<string name="blogs_sharing_response_accepted_sent">您接受了來自 %s 的博客邀請。</string>
<string name="blogs_sharing_response_declined_sent">您謝絕了來自 %s 的博客邀請。</string>
<string name="blogs_sharing_response_declined_auto">來自%s的部落格邀請已自動回絕</string>
<string name="blogs_sharing_response_accepted_received">%s 接受了博客邀請。</string>
<string name="blogs_sharing_response_declined_received">%s 謝絕了博客邀請。</string>
<string name="blogs_sharing_invitation_received">%1$s 向您分享博客 “%2$s”。</string>
@@ -362,6 +473,8 @@
<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_import_exists">此來源已滙入</string>
<string name="blogs_rss_feeds">RSS 消息源</string>
<string name="blogs_rss_feeds_manage_imported">已導入:</string>
<string name="blogs_rss_feeds_manage_author">作者:</string>
<string name="blogs_rss_feeds_manage_updated">最後更新於:</string>
@@ -371,6 +484,10 @@
<string name="blogs_rss_feeds_manage_empty_state">尚無訂閱源可供顯示\n\n輕按 + 號導入訂閱源</string>
<string name="blogs_rss_feeds_manage_error">加載訂閱源時出錯。請稍候再試。</string>
<!--Settings Profile Picture-->
<string name="change_profile_picture">輕觸更改使用者圖像</string>
<string name="dialog_confirm_profile_picture_title">更改使用者圖像</string>
<string name="dialog_confirm_profile_picture_remark">只有您的聯絡人可乆看到這張照片</string>
<string name="change_profile_picture_failed_message">抱歉,更新個人圖像時出了一點問題</string>
<!--Settings Display-->
<string name="pref_language_title">語言 &amp; 區域</string>
<string name="pref_language_changed">該設置將會在 Briar 重啓後生效。請登出並重啓 Briar。</string>
@@ -382,10 +499,20 @@
<string name="pref_theme_auto">自動(白天)</string>
<string name="pref_theme_system">系統默認</string>
<!--Settings Connections-->
<string name="network_settings_title">連線</string>
<string name="bluetooth_setting">透過藍牙連接聯絡人</string>
<string name="wifi_setting">透過同一個 Wi-Fi 連接聯絡人</string>
<string name="tor_enable_title">透過網際網路連接聯絡人</string>
<string name="tor_enable_summary">為保護隱私,全部的連接都透過 Tor 網路</string>
<string name="tor_network_setting">連接 Tor 網路的方式</string>
<string name="tor_network_setting_automatic">基於位置自動選擇</string>
<string name="tor_network_setting_without_bridges">使用無橋接的 Tor 網路</string>
<string name="tor_network_setting_with_bridges">使用橋接的 Tor 網路</string>
<string name="tor_network_setting_never">不要連接到網際網路</string>
<!--How and when Briar will connect to Tor: E.g. "Don't connect to the Internet (in China)" or "Use Tor network with bridges (in Belarus)"-->
<string name="tor_network_setting_summary">自動選擇:%1$s在 %2$s</string>
<string name="tor_mobile_data_title">使用移動數據</string>
<string name="tor_only_when_charging_title">在充電時才連接到網際網路</string>
<string name="tor_only_when_charging_summary">當裝置使用電池電量時關閉網絡連接</string>
<!--Settings Security and Panic-->
<string name="security_settings_title">安全</string>
@@ -448,8 +575,108 @@
<string name="notify_sound_setting_disabled"></string>
<string name="choose_ringtone_title">選擇鈴聲</string>
<string name="cannot_load_ringtone">無法加載鈴聲</string>
<!--Settings Feedback-->
<string name="feedback_settings_title">反饋</string>
<!--Mailbox-->
<string name="mailbox_settings_title">Mailbox</string>
<string name="mailbox_setup_title">Mailbox 設置</string>
<string name="mailbox_setup_intro">Mailbox 讓聯絡人在您離線時仍然可向您發送訊息。在您上線前Mailbox 會代為接收並儲存給訊息。\n
\n 您可在備用設備上安裝 Briar Mailbox讓它接上電源與 Wi-Fi 以維持在線狀態</string>
<string name="mailbox_setup_download">首先,在另一台設備上透過 Google Play 或其它下載 Briar 的地方搜尋“Briar Mailbox”以安裝 Mailbox 。\n
\n再掃瞄 Mailbox 的二維掃瞄將 它與 Briar 鏈接起來。</string>
<string name="mailbox_setup_download_link">分享下載鏈接</string>
<string name="mailbox_setup_button_scan">掃描 Mailbox 二維碼</string>
<string name="permission_camera_qr_denied_body">您限制了相機使用權限,但掃瞄二維碼必須用到相關 \n\n 請同意使用照片</string>
<string name="mailbox_setup_connecting">Mailbox 連接中...</string>
<!--This string is shown when connecting to a Mailbox for the first time. The placeholder will be replaced with a duration, e.g. "2 minutes".-->
<string name="mailbox_setup_connecting_info">它要花上 %1s的時間</string>
<string name="mailbox_qr_code_too_old">您所掃的二維碼為舊版的 Briar Mailbox \n\n 請更新至最新版後再試試</string>
<string name="mailbox_qr_code_too_new">您所掃的二維碼為較新版的 Briar Mailbox \n\n 請更新至最新版後再試試</string>
<string name="contact_qr_code_for_mailbox">您所掃瞄的二維碼是為了添增 Briar 聯絡人 .\n\n 要新增聯絡人,請到聯絡人清單下輕觸 + 圖示</string>
<string name="mailbox_setup_qr_code_wrong_description">您所掃的二維碼並非來自 Briar Mailbox \n\n 請在 Mailbox 設備上打開 Briar Mailbox 應用再掃它所提供的二維碼。 </string>
<string name="mailbox_setup_already_paired_title">Mailbox 已連接</string>
<string name="mailbox_setup_already_paired_description">解除其它設備的 Mailbox 鏈接後再試試</string>
<string name="mailbox_setup_io_error_title">無法連接</string>
<string name="mailbox_setup_io_error_description">確認二台設備都已連上網後再重試</string>
<string name="mailbox_setup_assertion_error_title">Mailbox 出錯</string>
<string name="mailbox_setup_assertion_error_description">如果問題一直持續,請利用 Briar 發送反饋(其為匿名資料)</string>
<string name="mailbox_setup_camera_error_description">無法使用相機,不妨重開機後再試試</string>
<string name="mailbox_setup_paired_title">已連接</string>
<string name="mailbox_setup_paired_description">您已順利地完成 Briar Mailbox 的連接 \n
將 Mailbox 維持通電與Wi-Fi以保持在線狀態 </string>
<string name="tor_offline_title">離線</string>
<string name="tor_offline_description">確認這台設備可以順利地使用際網網路上線 \n
\n 接下來請等候連線畫面的球狀圖標變成綠色</string>
<string name="tor_offline_button_check">檢查連線設置</string>
<string name="mailbox_status_title">Mailbox 狀態</string>
<string name="mailbox_status_connected_title">郵箱正在運行</string>
<string name="mailbox_status_problem_title">Briar 無法順利連接 Mailbox</string>
<string name="mailbox_status_failure_title">Mailbox 無法使用</string>
<string name="mailbox_status_app_too_old_title">Briar 版本過舊</string>
<string name="mailbox_status_app_too_old_message">更新 Briar 至最新版後再試試看</string>
<string name="mailbox_status_mailbox_too_old_title">Mailbox 版本過舊</string>
<string name="mailbox_status_mailbox_too_old_message">更新 Mailbox 至最新版後再試試看</string>
<string name="mailbox_status_check_button">檢查連線</string>
<!--Example for string substitution: Last connection: 3min ago-->
<string name="mailbox_status_connected_info">最近一次連接%s</string>
<!--Indicates that there never was a connection to the mailbox. Last connection: Never-->
<string name="mailbox_status_connected_never">永不</string>
<string name="mailbox_status_unlink_button">解除鏈接</string>
<string name="mailbox_status_unlink_dialog_title">解除 Mailbox 鏈接? </string>
<string name="mailbox_status_unlink_dialog_question">確定要解除 Mailbox 鏈接? </string>
<string name="mailbox_status_unlink_dialog_warning">若移除 Mailbox 鏈接,您將無法在 Briar 離線時收取訊息</string>
<string name="mailbox_status_unlink_no_wipe_title">您已移除 Mailbox 鏈接</string>
<string name="mailbox_status_unlink_no_wipe_message">下回使用 Mailbox 設備時,請打開 Mailbox 輕觸\"移除鏈接\"鍵以完成手續。\n\n 若無法使用 Mailbox 設備也別擔心,因為您的資料都經過加密保護即便未能完成最後一道程序但依然安全。</string>
<string name="mailbox_status_unlink_success">您已移除 Mailbox 鏈接</string>
<string name="mailbox_error_notification_channel_title">Briar Mailbox 問題</string>
<string name="mailbox_error_notification_title">Briar Mailbox 無法使用</string>
<string name="mailbox_error_notification_text">輕觸以修復問題</string>
<string name="mailbox_error_wizard_button">解決問題</string>
<string name="mailbox_error_wizard_title">Mailbox 疑難處理精靈</string>
<string name="mailbox_error_wizard_question1">您可以取用 Mailbox 裝置嗎?</string>
<string name="mailbox_error_wizard_answer1">是的,我手上正拿著它</string>
<string name="mailbox_error_wizard_answer2">目前沒有,但我稍晚可以拿到</string>
<string name="mailbox_error_wizard_answer3">我無法再取用那台設備</string>
<string name="mailbox_error_wizard_info1_1">檢查 Mailbox 裝置是否開啟並已連上網際網路
</string>
<string name="mailbox_error_wizard_question1_1">打開 Mailbox 應用,您看到什麼東西</string>
<string name="mailbox_error_wizard_answer1_1">如何設置 Mailbox 的指示</string>
<string name="mailbox_error_wizard_answer1_2">出現二維碼</string>
<string name="mailbox_error_wizard_answer1_3">看到\" Mailbox 正在執行\"</string>
<string name="mailbox_error_wizard_answer1_4">顯示\"設備離線\"</string>
<string name="mailbox_error_wizard_info1_1_1">請按下方按鈕以移除 Mailbox 鏈接,之後再按設備指示重新鏈接</string>
<string name="mailbox_error_wizard_info_1_1_2">請按下方按鈕以移除 Mailbox 鏈接,之後再掃一次二維碼以重新鏈接</string>
<string name="mailbox_error_wizard_info1_1_3">請利用下方按鈕來檢查 Briar 與 Mailbox 之間的連接\n\n
若連接再次失敗:\n
\u2022 分別檢查 Mailbox Briar 是否為最新版本\n
\u2022 重開機 Mailbox and Briar 設備後再試試.</string>
<string name="mailbox_error_wizard_info1_1_4">檢查 Mailbox 設備是否妥當地連上網路 \n\n 確認設備上的時間、日期、時區正確 \n\n 檢查 Mailbox 和 Briar 皆為最新版本 \n\n 重啟 Mailbox 設備後再試試</string>
<string name="mailbox_error_wizard_info2">當您能使用該設備後,請再回到本畫面操作</string>
<string name="mailbox_error_wizard_info3">請按下方按鈕以移除 Mailbox 鏈接,移除後您隨時可以重新設置新的 Mailbox </string>
<!--About-->
<string name="about_title">關於</string>
<string name="briar_version">Briar 版本: %s</string>
<string name="tor_version">Tor 版本: %s</string>
<string name="links">鏈接</string>
<string name="briar_website">\u2022 <a href="">網站</a></string>
<string name="briar_source_code">\u2022 <a href="">源代碼</a></string>
<string name="briar_changelog">\u2022 <a href="">更新記錄</a></string>
<string name="briar_privacy_policy">\u2022 <a href="">隱私政策</a></string>
<!--Here translators can add their names or Transifex usernames(eg "Thanks to all the contributors at the Localization Lab, especially Tom, Matthew and Jerry")-->
<string name="translator_thanks">感謝所有 Localization Lab 的貢獻者</string>
<!--Conversation Settings-->
<string name="disappearing_messages_title">已消失的訊息</string>
<string name="disappearing_messages_explanation_long">打開這個設置將會
對話中的訊息將自動在 7\u00A0天後消失.
\n\n發送的訊息送達後開始倒數將消失的時間
在對方讀過訊息後即開始倒數時間
\n\n畫有炸彈圖示的訊息將會消失
\n\n請記住對方仍然可以保留您所送出的訊息
\n\n若您改變設定即會立即影響之後的新訊息
聯絡人接到下一則訊息後舊訊息即消失.
您的聯絡人也可為雙方更改設定.</string>
<string name="learn_more">了解更多</string>
<string name="disappearing_messages_summary">讓這個對話的訊息會在 7\u00A0 天後自動消失</string>
<!--Settings Actions-->
<string name="pref_category_actions">行為</string>
<string name="send_feedback">提交反饋</string>
<!--Link Warning-->
<string name="link_warning_title">鏈接警告</string>
@@ -458,7 +685,7 @@
<string name="link_warning_open_link">打開鏈接</string>
<!--Crash Reporter-->
<string name="crash_report_title">Briar 崩潰報告</string>
<string name="briar_crashed">抱歉Briar 崩潰</string>
<string name="briar_crashed">抱歉 Briar 崩潰</string>
<string name="not_your_fault">這並非您的錯誤所致。</string>
<string name="please_send_report">請發送崩潰報告,以幫助我們優化 Briar 。</string>
<string name="report_is_encrypted">我們保證會加密並安全地發送報告。</string>
@@ -466,17 +693,35 @@
<string name="describe_crash">描述發生了什麼(選填)</string>
<string name="enter_feedback">輸入您的反饋</string>
<string name="optional_contact_email">您的郵箱地址(選填)</string>
<string name="privacy_policy">向我們發送資料即表示您同意我們的 <a href="">隱私權政策</a></string>
<string name="include_debug_report_crash">包含關於本次崩潰的匿名數據</string>
<string name="include_debug_report_feedback">包含關於本裝置的匿名數據</string>
<string name="dev_report_user_info">用戶資訊 </string>
<string name="dev_report_basic_info">基本資訊</string>
<string name="dev_report_device_info">設備資訊</string>
<string name="dev_report_stacktrace">堆疊追踪</string>
<string name="dev_report_time_info">時間資訊</string>
<string name="dev_report_memory">記憶體</string>
<string name="dev_report_storage">容量</string>
<string name="dev_report_connectivity">連接能力</string>
<string name="dev_report_network_usage">網路用量</string>
<string name="dev_report_build_config">版本設定</string>
<string name="dev_report_logcat">應用軟體日誌</string>
<string name="dev_report_device_features">設備特色</string>
<string name="send_report">發送報告</string>
<string name="close">關閉</string>
<string name="dev_report_sending">發送反饋...</string>
<string name="dev_report_sent">送出反饋</string>
<string name="dev_report_saved">報告已保存,將於您下次登錄時發送。</string>
<string name="dev_report_error">錯誤: 發送報告失敗</string>
<!--Sign Out-->
<string name="progress_title_logout">正在登出 Briar……</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">檢測到屏幕覆蓋</string>
<string name="screen_filter_body">另一個程式覆蓋在 Briar 上。爲了保護您的安全Briar 在有其他程式覆蓋的情況下不會處理對觸控應。\n\n以下程式可能覆蓋在上方\n\n%1$s</string>
<string name="screen_filter_body_api_30">另一個程式覆蓋在 Briar 之上。爲了保護您的安全,在其他程式覆蓋的情況下 Briar 不會對觸控感應。\n\n檢查以下程式可能覆蓋的應用軟體</string>
<string name="screen_filter_allow">允許這些程式覆蓋在上方</string>
<string name="screen_filter_review_apps">評價應用軟體</string>
<!--Permission Requests-->
<string name="permission_camera_title">相機權限</string>
<string name="permission_camera_request_body">Briar 需要獲得相機權限以掃描二維碼。</string>
@@ -484,7 +729,17 @@
<string name="permission_location_request_body">Briar 需要位置信息權限以發現藍牙裝置。\n\nBriar 不會存儲您的位置或將它分享給任何人。</string>
<string name="permission_camera_location_title">相機和位置</string>
<string name="permission_camera_location_request_body"> Briar 需要相機權限以掃描二維碼。\n\nBriar 需要位置信息權限以發現藍牙裝置。\n\nBriar 不會存儲您的位置或將它分享給任何人。</string>
<string name="permission_camera_bluetooth_title">照機與鄰近設備</string>
<string name="permission_camera_bluetooth_request_body"> Briar 需要相機權限才能掃描二維碼。\n\n 需要位置信息權限以發現和連接鄰近的藍牙裝置。</string>
<string name="permission_camera_denied_body">您已拒絕相機權限,而添加聯絡人需要使用相機。\n\n請考慮授予相機權限。</string>
<string name="permission_location_denied_body">您拒絕取得位置定位之授權,但 Briar 必須要有此授權方能利用找到其它藍牙設備\n\n 請同意此權限</string>
<string name="permission_location_setting_title">位置設定</string>
<string name="permission_location_setting_body">您必須開啟設備的位置以借由藍牙找到其它設備, 當然您稍後可關閉位置功能</string>
<string name="permission_location_setting_hotspot_body">您必須開啟設備的位置功能才能建立 Wi-Fi 熱點,當然您稍後可關閉位置功能</string>
<string name="permission_location_setting_button">啟動定位</string>
<string name="permission_bluetooth_title">精近設備授權</string>
<string name="permission_bluetooth_body">使用藍牙通訊時Briar 必須取得授權去搜尋與連接鄰近的設備</string>
<string name="permission_bluetooth_denied_body">您拒絕鄰近設備的取用,但 Briar 必須要有此授權方能使用藍牙功能\n\n 請同意此權限</string>
<string name="qr_code">二維碼</string>
<string name="show_qr_code_fullscreen">全熒幕顯示二維碼</string>
<!--App Locking-->
@@ -495,6 +750,93 @@
<string name="lock_is_locked">Briar 已鎖定</string>
<string name="lock_tap_to_unlock">輕按以解鎖</string>
<!--Connections Screen-->
<string name="transports_help_text"> Briar 可透過網際網路、Wi-Fi 或藍牙來建立聯繫 \n\n 為隱私保護Briar 在網際網路上都是透過 Tor 網路 \n\n 若聯絡人能使用多種連線方式,則 Briar 會併行利用</string>
<!--Share app offline-->
<string name="hotspot_title">離線分享此應用</string>
<string name="hotspot_intro">透過手機 Wi-Fi 在即便沒有網際網路的情況下,仍可以向鄰近者分享 Briar 應用
\n\n 您的手機將開啟 Wi-Fi 熱點,連上此熱點後附近的人就可以從您的手機下載 Brair </string>
<string name="hotspot_button_start_sharing">啟動熱點</string>
<string name="hotspot_button_stop_sharing">關閉熱點</string>
<string name="hotspot_progress_text_start">設置熱點</string>
<string name="hotspot_notification_channel_title">Wi-Fi 熱點</string>
<string name="hotspot_notification_title">離線分享 Briar</string>
<string name="hotspot_button_connected">下一步</string>
<string name="permission_hotspot_location_request_body">建立 Wi-Fi 熱點Briar 須得到設備位置權限 \n\n Briar 不會儲存也不會與他人分享您的位置資料</string>
<string name="permission_hotspot_location_request_precise_body">建立 Wi-Fi 熱點Briar 必須了解設備的準確定位 \n\n Briar 不會儲存也不會與他人分享您的位置資料</string>
<string name="permission_hotspot_location_denied_body">您拒絕 Briar 使用設備位置權限但Briar 須有此授權才能為您建立 Wi-Fi 熱點\n\n 請考慮同意此項授權</string>
<string name="permission_hotspot_location_denied_precise_body">您拒絕 Briar 取得設備精確位置的資料但Briar 須有此授權才能為您建立 Wi-Fi 熱點\n\n 請考慮同意此項授權</string>
<string name="wifi_settings_title">Wi-Fi 設定</string>
<string name="wifi_settings_request_enable_body">要建立 Wi-Fi 熱點Briar 必項使用 Wi-Fi , 請打開它</string>
<string name="hotspot_tab_manual">手動</string>
<!--The placeholder to be inserted into the string 'hotspot_manual_wifi': People can connect by %s-->
<string name="hotspot_scanning_a_qr_code">掃瞄二維碼</string>
<!--Wi-Fi setup-->
<!--The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code'-->
<string name="hotspot_manual_wifi">您的手機正提供 Wi-Fi 熱點。想下载 Briar 的人先連上此熱點,其操作是在自己設備的 Wi-Fi 設置使用進階或借由 %s。順利連接熱點請按 “下一步”。</string>
<string name="hotspot_manual_wifi_ssid">網路名稱</string>
<string name="hotspot_qr_wifi">您的手機正提供 Wi-Fi 熱點。想下载 Briar 的人可掃瞄二維碼來連上此熱點。順利連接熱點,請按 “下一步”。</string>
<string name="hotspot_no_peers_connected">未有設備連接</string>
<plurals name="hotspot_peers_connected">
<item quantity="other">%s 個設備連接</item>
</plurals>
<!--Download link-->
<!--The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code'-->
<string name="hotspot_manual_site">您的手機正提供 Wi-Fi 熱點,連上此熱點的設備可打開瀏覽器鏈接或 %s以下載 Briar </string>
<string name="hotspot_manual_site_address">網址 (URL)</string>
<string name="hotspot_qr_site">您的手機提供了 Wi-Fi 熱點,連上此熱點的設備可借由掃這個二維碼來下載 Briar </string>
<!--e.g. Download Briar 1.2.20-->
<string name="website_download_title_1">下載 Briar %s</string>
<string name="website_download_intro_1">附近有人要與您分享 Briar</string>
<string name="website_download_button">下載 Briar</string>
<string name="website_download_outro">下載完畢後,請打開下載檔案進行安裝</string>
<string name="website_troubleshooting_title">故障排除</string>
<string name="website_troubleshooting_1">如無法下載本應用,請利用其它不同的網頁瀏覽器再試試</string>
<string name="website_troubleshooting_2_old">安裝時,您可能要打開系統設定中同意安裝不明來源應用的選項。勾選後再重新下載。建議在安裝完成後,取消安裝不明來源應用的授權。</string>
<string name="website_troubleshooting_2_new">安裝時,瀏覽器可能要調整為同意安裝不明來源應用。建議在安裝完成後,即取消瀏覽器可安裝不明來源應用的授權。</string>
<string name="hotspot_help_wifi_title">連線 Wi-Fi 出問題</string>
<string name="hotspot_help_wifi_1">二支手機都試著關掉重開 Wi-Fi Direct 再試看看</string>
<string name="hotspot_help_wifi_2">如您的手機顯示所連的 Wi-Fi 並無連接網際網路,請表示仍要繼續連接</string>
<string name="hotspot_help_wifi_3">請將執行 Wi-Fi 熱點的手機關掉重啟後再次打開 Briar 試試</string>
<string name="hotspot_help_site_title">無法訪問本地網站: </string>
<string name="hotspot_help_site_1">仔細檢查輸入的網址是否正確,小小錯誤也不行</string>
<string name="hotspot_help_site_2">當您嘗試訪問這個網站時,請先確認手機已連接正確的 Wi-Fi (見上)</string>
<string name="hotspot_help_site_3">若有安裝防護軟體,請確認它不會封鎖 Briar 存取</string>
<string name="hotspot_help_site_4">若您能訪問該網站但無法下載 Briar 應用,請先試試其它不同的瀏覽器</string>
<string name="hotspot_help_fallback_title">沒有用</string>
<string name="hotspot_help_fallback_intro">您可以試著把這個應用存成 .apk 檔案來作分享。該檔案被傳輸到其它設備上,就可用來安裝 Briar
\n\n 提示: 若透過藍牙分享則可能需先將副檔名改為 .zip </string>
<string name="hotspot_help_fallback_button">儲存應用軟體</string>
<!--error handling-->
<string name="hotspot_error_intro">無法透過 Wi-Fi 來分享 Briar 應用</string>
<string name="hotspot_error_no_wifi_direct">設備不能用 Wi-Fi Direct</string>
<string name="hotspot_error_start_callback_failed">熱點啟動失敗: 錯誤訊息 %s</string>
<string name="hotspot_error_start_callback_failed_unknown">熱點因不名錯誤無法啟動,原因為 %d</string>
<string name="hotspot_error_start_callback_no_group_info">熱點無法啟動: 無群組資訊</string>
<string name="hotspot_error_web_server_start">啟動網站伺服器時出錯</string>
<string name="hotspot_error_web_server_serve">網站出問題 \n\n 如果問題一直持續,請利用 Briar 發送反饋(其為匿名資料)</string>
<string name="hotspot_flag_test">警告:此應用安裝於 Android Studio 因此不能在其它設備上安裝</string>
<string name="hotspot_error_framework_busy">無法打開熱點 \n\n 或您正在執行其它熱點或是利用 Wi-Fi 分享網路連接,請先暫停它們然後再重試開啟熱點</string>
<!--Transfer Data via Removable Drives-->
<string name="removable_drive_menu_title">透過移動硬碟連線</string>
<string name="removable_drive_intro">若無法連上網際網路、也不能使用 Wi-Fi 或藍牙Briar 還可以透過移動裝置如 USB 或 SD 卡來傳輸訊息。</string>
<string name="removable_drive_explanation">若無法透過網際網路、藍牙或Wi-Fi 聯繫聯絡人Briar 還可以利用 隨身碟 或 SD 記憶卡來傳輸訊息 \n\n 若按下\"發送資料\" 鍵,原本待發送給聯絡人的資料將會寫入到隨身碟中,其包括私人訊息、附件、部落格、論壇與私人小組等 \n\n 這些資料都會予以加密再寫入移動式儲存器 \n\n 若您的聯絡人拿到這個隨身碟,他們可利用\"接收資料\"按鍵把資料滙入 Briar . </string>
<string name="removable_drive_title_send">發送資料</string>
<string name="removable_drive_title_receive">接收資料</string>
<string name="removable_drive_send_intro">輕敲下方按鍵即可建立一個涵蓋加密訊息的新檔案,並決定它要存放的位置\n\n 若要將新檔案存到隨身碟,現在即請接入。</string>
<string name="removable_drive_send_no_data">目前沒有待送出的訊息給這名聯絡人</string>
<string name="removable_drive_send_not_supported">這名聯絡人使用舊版本 Briar 或過舊的設備故無法使用此功能</string>
<string name="removable_drive_send_button">選取滙出之檔案</string>
<string name="removable_drive_ongoing">請靜候進行中的任務完成</string>
<string name="removable_drive_receive_intro">輕點下方來選擇聯絡人發送的檔案 \n\n 若檔案是在移動式硬碟,請接入</string>
<string name="removable_drive_receive_button">選取滙出之檔案</string>
<string name="removable_drive_success_send_title">成功滙出</string>
<string name="removable_drive_success_send_text">成功滙出資料,您有 28 天的時間可將此份檔案傳給您的聯絡人\n\n 若檔案存在可移式設備,利用狀態列的通知來退出它</string>
<string name="removable_drive_success_receive_title">成功滙入</string>
<string name="removable_drive_success_receive_text">已收到檔案中全部加密的訊息</string>
<string name="removable_drive_error_send_title">滙出資料出錯</string>
<string name="removable_drive_error_send_text">檔案無法寫入資料 \n\n 如是使用隨身碟,請檢查是否完整接入。若此問題持續無法解決,請向 Briar 團隊反應此問題</string>
<string name="removable_drive_error_receive_title">滙入資料出錯</string>
<string name="removable_drive_error_receive_text">Briar 無法辨識所選取的檔案 \n\n 請確認是否為正確檔案 \n\n 若選取的檔案是28 天之前所創建的,則 Briar 就無法辨識了。</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">愛麗詩</string>

View File

@@ -67,6 +67,7 @@
<!-- text colors -->
<color name="briar_text_link">@color/briar_blue_400</color>
<color name="briar_text_link_inverse">@android:color/white</color>
<color name="briar_text_primary">#df000000</color>
<color name="briar_text_primary_inverse">@android:color/white</color>
<color name="briar_text_secondary_inverse">#b4ffffff</color>

View File

@@ -461,6 +461,10 @@
<string name="forum_declined_toast">Invitation declined</string>
<string name="shared_by_format">Shared by %s</string>
<string name="forum_invitation_already_sharing">Already sharing</string>
<string name="forum_invitation_already_invited">Invitation already sent</string>
<string name="forum_invitation_invite_received">Invitation already received</string>
<string name="forum_invitation_not_supported">Not supported by this contact</string>
<string name="forum_invitation_error">Error. This is a bug and not your fault</string>
<string name="forum_invitation_response_accepted_sent">You accepted the forum invitation from %s.</string>
<string name="forum_invitation_response_declined_sent">You declined the forum invitation from %s.</string>
<string name="forum_invitation_response_declined_auto">The forum invitation from %s was automatically declined.</string>
@@ -516,7 +520,6 @@
<string name="blogs_rss_feeds_import_button">Import</string>
<string name="blogs_rss_feeds_import_hint">Enter the URL of the RSS feed</string>
<string name="blogs_rss_feeds_import_error">We are sorry! There was an error importing your feed.</string>
<string name="blogs_rss_feeds_import_exists">That feed is already imported.</string>
<string name="blogs_rss_feeds">RSS Feeds</string>
<string name="blogs_rss_feeds_manage_imported">Imported:</string>
<string name="blogs_rss_feeds_manage_author">Author:</string>

View File

@@ -5,47 +5,27 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class Feed implements Comparable<Feed> {
public class Feed {
private final String url;
private final Blog blog;
private final LocalAuthor localAuthor;
@Nullable
private final String description, rssAuthor;
private final RssProperties properties;
private final long added, updated, lastEntryTime;
public Feed(String url, Blog blog, LocalAuthor localAuthor,
@Nullable String description, @Nullable String rssAuthor,
public Feed(Blog blog, LocalAuthor localAuthor, RssProperties properties,
long added, long updated, long lastEntryTime) {
this.url = url;
this.blog = blog;
this.localAuthor = localAuthor;
this.description = description;
this.rssAuthor = rssAuthor;
this.properties = properties;
this.added = added;
this.updated = updated;
this.lastEntryTime = lastEntryTime;
}
public Feed(String url, Blog blog, LocalAuthor localAuthor,
@Nullable String description, @Nullable String rssAuthor,
long added) {
this(url, blog, localAuthor, description, rssAuthor, added, 0L, 0L);
}
public Feed(String url, Blog blog, LocalAuthor localAuthor, long added) {
this(url, blog, localAuthor, null, null, added, 0L, 0L);
}
public String getUrl() {
return url;
}
public GroupId getBlogId() {
return blog.getId();
}
@@ -62,14 +42,8 @@ public class Feed implements Comparable<Feed> {
return blog.getName();
}
@Nullable
public String getDescription() {
return description;
}
@Nullable
public String getRssAuthor() {
return rssAuthor;
public RssProperties getProperties() {
return properties;
}
public long getAdded() {
@@ -95,12 +69,7 @@ public class Feed implements Comparable<Feed> {
}
@Override
public int compareTo(Feed o) {
if (this == o) return 0;
long aTime = getAdded(), bTime = o.getAdded();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
return 0;
public int hashCode() {
return blog.hashCode();
}
}

View File

@@ -22,6 +22,9 @@ public interface FeedConstants {
String KEY_FEED_PRIVATE_KEY = "feedPrivateKey";
String KEY_FEED_DESC = "feedDesc";
String KEY_FEED_RSS_AUTHOR = "feedRssAuthor";
String KEY_FEED_RSS_TITLE = "feedRssTitle";
String KEY_FEED_RSS_LINK = "feedRssLink";
String KEY_FEED_RSS_URI = "feedRssUri";
String KEY_FEED_ADDED = "feedAdded";
String KEY_FEED_UPDATED = "feedUpdated";
String KEY_FEED_LAST_ENTRY = "feedLastEntryTime";

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.nullsafety.NotNullByDefault;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
@NotNullByDefault
@@ -22,10 +23,17 @@ public interface FeedManager {
int MAJOR_VERSION = 0;
/**
* Adds an RSS feed as a new dedicated blog.
* Adds an RSS feed as a new dedicated blog, or updates the existing blog
* if a blog for the feed already exists.
*/
Feed addFeed(String url) throws DbException, IOException;
/**
* Adds an RSS feed as a new dedicated blog, or updates the existing blog
* if a blog for the feed already exists.
*/
Feed addFeed(InputStream in) throws DbException, IOException;
/**
* Removes an RSS feed.
*/

View File

@@ -0,0 +1,86 @@
package org.briarproject.briar.api.feed;
import org.briarproject.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* The properties of an RSS feed, which may have been imported from a URL
* or a file.
*/
@Immutable
@NotNullByDefault
public class RssProperties {
@Nullable
private final String url, title, description, author, link, uri;
public RssProperties(@Nullable String url, @Nullable String title,
@Nullable String description, @Nullable String author,
@Nullable String link, @Nullable String uri) {
this.url = url;
this.title = title;
this.description = description;
this.author = author;
this.link = link;
this.uri = uri;
}
/**
* Returns the URL from which the RSS feed was imported, or null if the
* feed was imported from a file.
*/
@Nullable
public String getUrl() {
return url;
}
/**
* Returns the title property of the RSS feed, or null if no title was
* specified.
*/
@Nullable
public String getTitle() {
return title;
}
/**
* Returns the description property of the RSS feed, or null if no
* description was specified.
*/
@Nullable
public String getDescription() {
return description;
}
/**
* Returns the author property of the RSS feed, or null if no author was
* specified.
*/
@Nullable
public String getAuthor() {
return author;
}
/**
* Returns the link property of the RSS feed, or null if no link was
* specified. This is usually the URL of a webpage where the equivalent
* content can be viewed in a browser.
*/
@Nullable
public String getLink() {
return link;
}
/**
* Returns the URI property of the RSS feed, or null if no URI was
* specified. This may be a URL from which the feed can be downloaded,
* or it may be an opaque identifier such as a number that serves to
* distinguish this feed from other feeds produced by the same creator.
*/
@Nullable
public String getUri() {
return uri;
}
}

View File

@@ -9,16 +9,6 @@ public interface MessagingConstants {
*/
int MAX_PRIVATE_MESSAGE_TEXT_LENGTH = MAX_MESSAGE_BODY_LENGTH - 2048;
/**
* The maximum length of an incoming private message's text in UTF-8 bytes.
* This is higher than MAX_PRIVATE_MESSAGE_TEXT_LENGTH for compatibility
* with older peers.
* <p>
* TODO: Remove after a reasonable migration period (added 2021-03-12).
*/
int MAX_PRIVATE_MESSAGE_INCOMING_TEXT_LENGTH =
MAX_MESSAGE_BODY_LENGTH - 1024;
/**
* The maximum number of attachments per private message.
*/

View File

@@ -9,6 +9,7 @@ import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId;
import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.sharing.SharingManager.SharingStatus;
import org.briarproject.nullsafety.NotNullByDefault;
import java.util.Collection;
@@ -79,8 +80,13 @@ public interface GroupInvitationManager extends ConversationClient {
Collection<GroupInvitationItem> getInvitations() throws DbException;
/**
* Returns true if the given contact can be invited to the given private
* group.
* Returns the current {@link SharingStatus} for the given {@link Contact}
* and {@link PrivateGroup} identified by the given {@link GroupId}.
* This indicates whether the {@link PrivateGroup} can be shared
* with the contact.
*
* @throws ProtocolStateException if {@link PrivateGroup}
* was already dissolved.
*/
boolean isInvitationAllowed(Contact c, GroupId g) throws DbException;
SharingStatus getSharingStatus(Contact c, GroupId g) throws DbException;
}

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId;
import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient;
import org.briarproject.nullsafety.NotNullByDefault;
@@ -17,6 +18,38 @@ import javax.annotation.Nullable;
public interface SharingManager<S extends Shareable>
extends ConversationClient {
enum SharingStatus {
/**
* The {@link Shareable} can be shared with the requested contact.
*/
SHAREABLE,
/**
* The {@link Shareable} can not be shared with the requested contact,
* because the contact was already invited.
*/
INVITE_SENT,
/**
* The {@link Shareable} can not be shared with the requested contact,
* because the contact has already invited us.
*/
INVITE_RECEIVED,
/**
* The {@link Shareable} can not be shared with the requested contact,
* because it is already being shared.
*/
SHARING,
/**
* The {@link Shareable} can not be shared with the requested contact,
* because it is not supported by that contact.
* This could be a missing or outdated client.
*/
NOT_SUPPORTED,
/**
* The sharing session has encountered an error.
*/
ERROR
}
/**
* Sends an invitation to share the given group with the given contact,
* including optional text.
@@ -78,14 +111,24 @@ public interface SharingManager<S extends Shareable>
throws DbException;
/**
* Returns true if the group not already shared and no invitation is open
* Returns the current {@link SharingStatus} for the given {@link Contact}
* and {@link Shareable} identified by the given {@link GroupId}.
* This indicates whether the {@link Shareable} can be shared
* with the contact.
*
* @throws ProtocolStateException if we already left the {@link Shareable}.
*/
boolean canBeShared(GroupId g, Contact c) throws DbException;
SharingStatus getSharingStatus(GroupId g, Contact c) throws DbException;
/**
* Returns true if the group not already shared and no invitation is open
* Returns the current {@link SharingStatus} for the given {@link Contact}
* and {@link Shareable} identified by the given {@link GroupId}.
* This indicates whether the {@link Shareable} can be shared
* with the contact.
*
* @throws ProtocolStateException if we already left the {@link Shareable}.
*/
boolean canBeShared(Transaction txn, GroupId g, Contact c)
SharingStatus getSharingStatus(Transaction txn, GroupId g, Contact c)
throws DbException;
}

View File

@@ -28,6 +28,7 @@ dependencies {
testImplementation "org.jmock:jmock:$jmock_version"
testImplementation "org.jmock:jmock-junit4:$jmock_version"
testImplementation "org.jmock:jmock-imposters:$jmock_version"
testImplementation "com.squareup.okhttp3:mockwebserver:$mockwebserver_version"
testAnnotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"

View File

@@ -6,20 +6,22 @@ import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.briar.api.feed.Feed;
import javax.annotation.Nullable;
interface FeedFactory {
/**
* Create a new feed based on the feed url
* and the metadata of an existing {@link SyndFeed}.
*/
Feed createFeed(String url, SyndFeed feed);
Feed createFeed(@Nullable String url, SyndFeed sf);
/**
* Creates a new updated feed, based on the given existing feed,
* new metadata from the given {@link SyndFeed}
* and the time of the last feed entry.
*/
Feed createFeed(Feed feed, SyndFeed f, long lastEntryTime);
Feed updateFeed(Feed feed, SyndFeed sf, long lastEntryTime);
/**
* De-serializes a {@link BdfDictionary} into a {@link Feed}.

View File

@@ -13,20 +13,25 @@ import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogFactory;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.RssProperties;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.truncateUtf8;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_ADDED;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_AUTHOR;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_DESC;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_LAST_ENTRY;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_PRIVATE_KEY;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_AUTHOR;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_LINK;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_TITLE;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_URI;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_UPDATED;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_URL;
@@ -47,29 +52,33 @@ class FeedFactoryImpl implements FeedFactory {
}
@Override
public Feed createFeed(String url, SyndFeed syndFeed) {
String title = syndFeed.getTitle();
public Feed createFeed(@Nullable String url, SyndFeed sf) {
String title = sf.getTitle();
if (title == null) title = "RSS";
else title = StringUtils.truncateUtf8(title, MAX_AUTHOR_NAME_LENGTH);
else title = truncateUtf8(title, MAX_AUTHOR_NAME_LENGTH);
LocalAuthor localAuthor = authorFactory.createLocalAuthor(title);
Blog blog = blogFactory.createFeedBlog(localAuthor);
long added = clock.currentTimeMillis();
return new Feed(url, blog, localAuthor, added);
RssProperties properties = new RssProperties(url, sf.getTitle(),
sf.getDescription(), sf.getAuthor(), sf.getLink(), sf.getUri());
return new Feed(blog, localAuthor, properties, added, 0, 0);
}
@Override
public Feed createFeed(Feed feed, SyndFeed f, long lastEntryTime) {
public Feed updateFeed(Feed feed, SyndFeed sf, long lastEntryTime) {
long updated = clock.currentTimeMillis();
return new Feed(feed.getUrl(), feed.getBlog(), feed.getLocalAuthor(),
f.getDescription(), f.getAuthor(), feed.getAdded(), updated,
lastEntryTime);
String url = feed.getProperties().getUrl();
// Update the RSS properties
RssProperties properties = new RssProperties(url, sf.getTitle(),
sf.getDescription(), sf.getAuthor(), sf.getLink(), sf.getUri());
return new Feed(feed.getBlog(), feed.getLocalAuthor(), properties,
feed.getAdded(), updated, lastEntryTime);
}
@Override
public Feed createFeed(BdfDictionary d) throws FormatException {
String url = d.getString(KEY_FEED_URL);
BdfList authorList = d.getList(KEY_FEED_AUTHOR);
PrivateKey privateKey =
@@ -80,14 +89,21 @@ class FeedFactoryImpl implements FeedFactory {
author.getPublicKey(), privateKey);
Blog blog = blogFactory.createFeedBlog(localAuthor);
String desc = d.getOptionalString(KEY_FEED_DESC);
String url = d.getOptionalString(KEY_FEED_URL);
String description = d.getOptionalString(KEY_FEED_DESC);
String rssAuthor = d.getOptionalString(KEY_FEED_RSS_AUTHOR);
String title = d.getOptionalString(KEY_FEED_RSS_TITLE);
String link = d.getOptionalString(KEY_FEED_RSS_LINK);
String uri = d.getOptionalString(KEY_FEED_RSS_URI);
RssProperties properties = new RssProperties(url, title, description,
rssAuthor, link, uri);
long added = d.getLong(KEY_FEED_ADDED, 0L);
long updated = d.getLong(KEY_FEED_UPDATED, 0L);
long lastEntryTime = d.getLong(KEY_FEED_LAST_ENTRY, 0L);
return new Feed(url, blog, localAuthor, desc, rssAuthor, added,
updated, lastEntryTime);
return new Feed(blog, localAuthor, properties, added, updated,
lastEntryTime);
}
@Override
@@ -95,17 +111,25 @@ class FeedFactoryImpl implements FeedFactory {
LocalAuthor localAuthor = feed.getLocalAuthor();
BdfList authorList = clientHelper.toList(localAuthor);
BdfDictionary d = BdfDictionary.of(
new BdfEntry(KEY_FEED_URL, feed.getUrl()),
new BdfEntry(KEY_FEED_AUTHOR, authorList),
new BdfEntry(KEY_FEED_PRIVATE_KEY, localAuthor.getPrivateKey()),
new BdfEntry(KEY_FEED_ADDED, feed.getAdded()),
new BdfEntry(KEY_FEED_UPDATED, feed.getUpdated()),
new BdfEntry(KEY_FEED_LAST_ENTRY, feed.getLastEntryTime())
);
if (feed.getDescription() != null)
d.put(KEY_FEED_DESC, feed.getDescription());
if (feed.getRssAuthor() != null)
d.put(KEY_FEED_RSS_AUTHOR, feed.getRssAuthor());
RssProperties properties = feed.getProperties();
if (properties.getUrl() != null)
d.put(KEY_FEED_URL, properties.getUrl());
if (properties.getTitle() != null)
d.put(KEY_FEED_RSS_TITLE, properties.getTitle());
if (properties.getDescription() != null)
d.put(KEY_FEED_DESC, properties.getDescription());
if (properties.getAuthor() != null)
d.put(KEY_FEED_RSS_AUTHOR, properties.getAuthor());
if (properties.getLink() != null)
d.put(KEY_FEED_RSS_LINK, properties.getLink());
if (properties.getUri() != null)
d.put(KEY_FEED_RSS_URI, properties.getUri());
return d;
}

View File

@@ -31,7 +31,6 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.TaskScheduler;
import org.briarproject.bramble.api.system.Wakeful;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogManager.RemoveBlogHook;
@@ -39,6 +38,7 @@ import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.FeedManager;
import org.briarproject.briar.api.feed.RssProperties;
import org.briarproject.nullsafety.NotNullByDefault;
import java.io.IOException;
@@ -47,7 +47,11 @@ import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
@@ -61,17 +65,21 @@ import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import static java.util.Collections.singletonList;
import static java.util.Collections.sort;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.bramble.util.StringUtils.truncateUtf8;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
import static org.briarproject.briar.api.feed.FeedConstants.FETCH_DELAY_INITIAL;
import static org.briarproject.briar.api.feed.FeedConstants.FETCH_INTERVAL;
import static org.briarproject.briar.api.feed.FeedConstants.FETCH_UNIT;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEEDS;
import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
import static org.briarproject.briar.util.HtmlUtils.STRIP_ALL;
import static org.briarproject.briar.util.HtmlUtils.clean;
import static org.briarproject.briar.util.HtmlUtils.cleanAll;
import static org.briarproject.briar.util.HtmlUtils.cleanArticle;
@ThreadSafe
@NotNullByDefault
@@ -79,7 +87,7 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
RemoveBlogHook {
private static final Logger LOG =
Logger.getLogger(FeedManagerImpl.class.getName());
getLogger(FeedManagerImpl.class.getName());
private final TaskScheduler scheduler;
private final Executor ioExecutor;
@@ -89,6 +97,7 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
private final BlogManager blogManager;
private final BlogPostFactory blogPostFactory;
private final FeedFactory feedFactory;
private final FeedMatcher feedMatcher;
private final Clock clock;
private final WeakSingletonProvider<OkHttpClient> httpClientProvider;
private final AtomicBoolean fetcherStarted = new AtomicBoolean(false);
@@ -104,6 +113,7 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
BlogManager blogManager,
BlogPostFactory blogPostFactory,
FeedFactory feedFactory,
FeedMatcher feedMatcher,
WeakSingletonProvider<OkHttpClient> httpClientProvider,
Clock clock) {
this.scheduler = scheduler;
@@ -114,6 +124,7 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
this.blogManager = blogManager;
this.blogPostFactory = blogPostFactory;
this.feedFactory = feedFactory;
this.feedMatcher = feedMatcher;
this.httpClientProvider = httpClientProvider;
this.clock = clock;
}
@@ -160,37 +171,49 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
@Override
public Feed addFeed(String url) throws DbException, IOException {
// fetch syndication feed to get its metadata
SyndFeed f = fetchSyndFeed(url);
// fetch feed to get posts and metadata
SyndFeed sf = fetchAndCleanFeed(url);
return addFeed(url, sf);
}
Feed feed = feedFactory.createFeed(url, f);
@Override
public Feed addFeed(InputStream in) throws DbException, IOException {
// fetch feed to get posts and metadata
SyndFeed sf = fetchAndCleanFeed(in);
return addFeed(null, sf);
}
// store feed and new blog
Transaction txn = db.startTransaction(false);
try {
blogManager.addBlog(txn, feed.getBlog());
List<Feed> feeds = getFeeds(txn);
feeds.add(feed);
storeFeeds(txn, feeds);
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
private Feed addFeed(@Nullable String url, SyndFeed sf) throws DbException {
// extract properties from the feed
RssProperties properties = new RssProperties(url, sf.getTitle(),
sf.getDescription(), sf.getAuthor(), sf.getLink(), sf.getUri());
// check whether the properties match an existing feed
List<Feed> candidates = db.transactionWithResult(true, this::getFeeds);
Feed matched = feedMatcher.findMatchingFeed(properties, candidates);
Feed feed;
if (matched == null) {
LOG.info("Adding new feed");
feed = feedFactory.createFeed(url, sf);
// store feed metadata and new blog
db.transaction(false, txn -> {
blogManager.addBlog(txn, feed.getBlog());
List<Feed> feeds = getFeeds(txn);
feeds.add(feed);
storeFeeds(txn, feeds);
});
} else {
LOG.info("New feed matches an existing feed");
feed = matched;
}
// fetch feed again and post entries
Feed updatedFeed = fetchFeed(feed);
// post entries
long lastEntryTime = postFeedEntries(feed, sf.getEntries());
Feed updatedFeed = feedFactory.updateFeed(feed, sf, lastEntryTime);
// store feed again to also store last added entry
txn = db.startTransaction(false);
try {
List<Feed> feeds = getFeeds(txn);
feeds.remove(feed);
feeds.add(updatedFeed);
storeFeeds(txn, feeds);
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
}
// store feed metadata again to also store last entry time
updateFeeds(singletonList(updatedFeed));
return updatedFeed;
}
@@ -198,14 +221,9 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
@Override
public void removeFeed(Feed feed) throws DbException {
LOG.info("Removing RSS feed...");
Transaction txn = db.startTransaction(false);
try {
// this will call removingBlog() where the feed itself gets removed
blogManager.removeBlog(txn, feed.getBlog());
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
}
// this will call removingBlog() where the feed itself gets removed
db.transaction(false, txn ->
blogManager.removeBlog(txn, feed.getBlog()));
}
@Override
@@ -215,10 +233,12 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
// delete blog's RSS feed if we have it
boolean found = false;
List<Feed> feeds = getFeeds(txn);
for (Feed f : feeds) {
Iterator<Feed> it = feeds.iterator();
while (it.hasNext()) {
Feed f = it.next();
if (f.getBlogId().equals(b.getId())) {
it.remove();
found = true;
feeds.remove(f);
break;
}
}
@@ -248,7 +268,7 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
return feeds;
}
private void storeFeeds(@Nullable Transaction txn, List<Feed> feeds)
private void storeFeeds(Transaction txn, List<Feed> feeds)
throws DbException {
BdfList feedList = new BdfList();
@@ -257,19 +277,29 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
}
BdfDictionary gm = BdfDictionary.of(new BdfEntry(KEY_FEEDS, feedList));
try {
if (txn == null) {
clientHelper.mergeGroupMetadata(getLocalGroup().getId(), gm);
} else {
clientHelper.mergeGroupMetadata(txn, getLocalGroup().getId(),
gm);
}
clientHelper.mergeGroupMetadata(txn, getLocalGroup().getId(), gm);
} catch (FormatException e) {
throw new DbException(e);
}
}
private void storeFeeds(List<Feed> feeds) throws DbException {
storeFeeds(null, feeds);
/**
* Updates the given feeds in the stored list of feeds, without affecting
* any other feeds in the list or re-adding any of the given feeds that
* have been removed from the list.
*/
private void updateFeeds(List<Feed> updatedFeeds) throws DbException {
Map<GroupId, Feed> updatedMap = new HashMap<>();
for (Feed feed : updatedFeeds) updatedMap.put(feed.getBlogId(), feed);
db.transaction(false, txn -> {
List<Feed> feeds = getFeeds(txn);
ListIterator<Feed> it = feeds.listIterator();
while (it.hasNext()) {
Feed updated = updatedMap.get(it.next().getBlogId());
if (updated != null) it.set(updated);
}
storeFeeds(txn, feeds);
});
}
/**
@@ -295,65 +325,69 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
return;
}
if (feeds.isEmpty()) {
LOG.info("No RSS feeds to update");
return;
}
// Fetch and update all feeds
List<Feed> newFeeds = new ArrayList<>(feeds.size());
List<Feed> updatedFeeds = new ArrayList<>(feeds.size());
for (Feed feed : feeds) {
try {
newFeeds.add(fetchFeed(feed));
String url = feed.getProperties().getUrl();
if (url == null) continue;
// fetch and clean feed
SyndFeed sf = fetchAndCleanFeed(url);
// sort and add new entries
long lastEntryTime = postFeedEntries(feed, sf.getEntries());
updatedFeeds.add(
feedFactory.updateFeed(feed, sf, lastEntryTime));
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
newFeeds.add(feed);
}
}
// Store updated feeds
try {
storeFeeds(newFeeds);
updateFeeds(updatedFeeds);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
LOG.info("Done updating RSS feeds");
}
private SyndFeed fetchSyndFeed(String url) throws IOException {
// fetch feed
InputStream stream = getFeedInputStream(url);
SyndFeed f = getSyndFeed(stream);
stream.close();
if (f.getEntries().size() == 0)
throw new IOException("Feed has no entries");
// clean title
String title =
StringUtils.isNullOrEmpty(f.getTitle()) ? null : f.getTitle();
if (title != null) title = clean(title, STRIP_ALL);
f.setTitle(title);
// clean description
String description =
StringUtils.isNullOrEmpty(f.getDescription()) ? null :
f.getDescription();
if (description != null) description = clean(description, STRIP_ALL);
f.setDescription(description);
// clean author
String author =
StringUtils.isNullOrEmpty(f.getAuthor()) ? null : f.getAuthor();
if (author != null) author = clean(author, STRIP_ALL);
f.setAuthor(author);
return f;
private SyndFeed fetchAndCleanFeed(String url) throws IOException {
return fetchAndCleanFeed(getFeedInputStream(url));
}
private Feed fetchFeed(Feed feed) throws IOException, DbException {
// fetch and clean feed
SyndFeed f = fetchSyndFeed(feed.getUrl());
private SyndFeed fetchAndCleanFeed(InputStream in) throws IOException {
SyndFeed sf;
try {
sf = getSyndFeed(in);
} finally {
tryToClose(in, LOG, WARNING);
}
// sort and add new entries
long lastEntryTime = postFeedEntries(feed, f.getEntries());
// clean title
String title = sf.getTitle();
if (title != null) title = cleanAll(title);
sf.setTitle(isNullOrEmpty(title) ? "RSS" : title);
return feedFactory.createFeed(feed, f, lastEntryTime);
// clean description
String description = sf.getDescription();
if (description != null) description = cleanAll(description);
sf.setDescription(isNullOrEmpty(description) ? null : description);
// clean author
String author = sf.getAuthor();
if (author != null) author = cleanAll(author);
sf.setAuthor(isNullOrEmpty(author) ? null : author);
// set other relevant fields to null if empty
if ("".equals(sf.getLink())) sf.setLink(null);
if ("".equals(sf.getUri())) sf.setUri(null);
return sf;
}
private InputStream getFeedInputStream(String url) throws IOException {
@@ -380,12 +414,11 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
}
}
long postFeedEntries(Feed feed, List<SyndEntry> entries)
private long postFeedEntries(Feed feed, List<SyndEntry> entries)
throws DbException {
long lastEntryTime = feed.getLastEntryTime();
Transaction txn = db.startTransaction(false);
try {
return db.transactionWithResult(false, txn -> {
long lastEntryTime = feed.getLastEntryTime();
//noinspection Java8ListSort
sort(entries, getEntryComparator());
for (SyndEntry entry : entries) {
@@ -404,11 +437,8 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
if (entryTime > lastEntryTime) lastEntryTime = entryTime;
}
}
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
}
return lastEntryTime;
return lastEntryTime;
});
}
private void postEntry(Transaction txn, Feed feed, SyndEntry entry) {
@@ -417,7 +447,7 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
// build post text
StringBuilder b = new StringBuilder();
if (!StringUtils.isNullOrEmpty(entry.getTitle())) {
if (!isNullOrEmpty(entry.getTitle())) {
b.append("<h1>").append(entry.getTitle()).append("</h1>");
}
for (SyndContent content : entry.getContents()) {
@@ -430,7 +460,7 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
b.append(entry.getDescription().getValue());
}
b.append("<p>");
if (!StringUtils.isNullOrEmpty(entry.getAuthor())) {
if (!isNullOrEmpty(entry.getAuthor())) {
b.append("-- ").append(entry.getAuthor());
}
if (entry.getPublishedDate() != null) {
@@ -442,7 +472,7 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
}
b.append("</p>");
String link = entry.getLink();
if (!StringUtils.isNullOrEmpty(link)) {
if (!isNullOrEmpty(link)) {
b.append("<a href=\"").append(link).append("\">").append(link)
.append("</a>");
}
@@ -472,8 +502,8 @@ class FeedManagerImpl implements FeedManager, EventListener, OpenDatabaseHook,
}
private String getPostText(String text) {
text = clean(text, ARTICLE);
return StringUtils.truncateUtf8(text, MAX_BLOG_POST_TEXT_LENGTH);
text = cleanArticle(text);
return truncateUtf8(text, MAX_BLOG_POST_TEXT_LENGTH);
}
private Comparator<SyndEntry> getEntryComparator() {

View File

@@ -0,0 +1,22 @@
package org.briarproject.briar.feed;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.RssProperties;
import org.briarproject.nullsafety.NotNullByDefault;
import java.util.List;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@NotNullByDefault
interface FeedMatcher {
/**
* Returns the best match for the given candidate from the given list of
* feeds, or null if there are no matches.
*/
@Nullable
Feed findMatchingFeed(RssProperties candidate, List<Feed> feeds);
}

View File

@@ -0,0 +1,69 @@
package org.briarproject.briar.feed;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.RssProperties;
import org.briarproject.nullsafety.NotNullByDefault;
import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Inject;
@NotNullByDefault
class FeedMatcherImpl implements FeedMatcher {
private static final int MIN_MATCHING_FIELDS = 2;
@Inject
FeedMatcherImpl() {
}
@Nullable
@Override
public Feed findMatchingFeed(RssProperties candidate, List<Feed> feeds) {
// First pass: if the candidate was imported from a URL and we have
// a feed that was imported from the same URL then it's a match
String url = candidate.getUrl();
if (url != null) {
for (Feed f : feeds) {
if (url.equals(f.getProperties().getUrl())) return f;
}
}
// Second pass: if the candidate matches at least MIN_MATCHING_FIELDS
// out of the title, description, author, link and URI, then return the
// feed with the highest number of matching fields
int bestScore = 0;
Feed bestFeed = null;
String title = candidate.getTitle();
String description = candidate.getDescription();
String author = candidate.getAuthor();
String link = candidate.getLink();
String uri = candidate.getUri();
for (Feed f : feeds) {
int score = 0;
RssProperties p = f.getProperties();
if (title != null && title.equals(p.getTitle())) {
score++;
}
if (description != null && description.equals(p.getDescription())) {
score++;
}
if (author != null && author.equals(p.getAuthor())) {
score++;
}
if (link != null && link.equals(p.getLink())) {
score++;
}
if (uri != null && uri.equals(p.getUri())) {
score++;
}
if (score > bestScore) {
bestScore = score;
bestFeed = f;
}
}
if (bestScore >= MIN_MATCHING_FIELDS) return bestFeed;
// No match
return null;
}
}

View File

@@ -38,4 +38,9 @@ public class FeedModule {
FeedFactory provideFeedFactory(FeedFactoryImpl feedFactory) {
return feedFactory;
}
@Provides
FeedMatcher provideFeedMatcher(FeedMatcherImpl feedMatcher) {
return feedMatcher;
}
}

View File

@@ -33,7 +33,7 @@ import static org.briarproject.briar.api.attachment.MediaConstants.MSG_KEY_CONTE
import static org.briarproject.briar.api.attachment.MediaConstants.MSG_KEY_DESCRIPTOR_LENGTH;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_INCOMING_TEXT_LENGTH;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
import static org.briarproject.briar.messaging.MessageTypes.ATTACHMENT;
import static org.briarproject.briar.messaging.MessageTypes.PRIVATE_MESSAGE;
@@ -106,7 +106,7 @@ class PrivateMessageValidator implements MessageValidator {
// Client version 0.0: Private message text
checkSize(body, 1);
String text = body.getString(0);
checkLength(text, 0, MAX_PRIVATE_MESSAGE_INCOMING_TEXT_LENGTH);
checkLength(text, 0, MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
// Return the metadata
BdfDictionary meta = new BdfDictionary();
meta.put(MSG_KEY_TIMESTAMP, m.getTimestamp());
@@ -123,7 +123,7 @@ class PrivateMessageValidator implements MessageValidator {
// attachment headers, optional auto-delete timer.
checkSize(body, 3, 4);
String text = body.getOptionalString(1);
checkLength(text, 0, MAX_PRIVATE_MESSAGE_INCOMING_TEXT_LENGTH);
checkLength(text, 0, MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
BdfList headers = body.getList(2);
if (text == null) checkSize(headers, 1, MAX_ATTACHMENTS_PER_MESSAGE);
else checkSize(headers, 0, MAX_ATTACHMENTS_PER_MESSAGE);

View File

@@ -2,6 +2,12 @@ package org.briarproject.briar.privategroup.invitation;
interface GroupInvitationConstants {
/**
* Metadata key for the client's local group, indicating that contact
* groups have been created for all contacts.
*/
String GROUP_KEY_SETUP = "setup";
// Message metadata keys
String MSG_KEY_MESSAGE_TYPE = "messageType";
String MSG_KEY_PRIVATE_GROUP_ID = "privateGroupId";

View File

@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.MetadataParser;
import org.briarproject.bramble.api.db.DatabaseComponent;
@@ -26,6 +27,7 @@ import org.briarproject.bramble.api.versioning.ClientVersioningManager;
import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId;
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
import org.briarproject.briar.api.conversation.DeletionResult;
@@ -37,6 +39,7 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationItem;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
import org.briarproject.briar.api.sharing.SharingManager.SharingStatus;
import org.briarproject.briar.client.ConversationClientImpl;
import org.briarproject.nullsafety.NotNullByDefault;
@@ -56,7 +59,12 @@ import javax.inject.Inject;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
import static org.briarproject.briar.privategroup.invitation.CreatorState.DISSOLVED;
import static org.briarproject.briar.privategroup.invitation.CreatorState.ERROR;
import static org.briarproject.briar.privategroup.invitation.CreatorState.INVITED;
import static org.briarproject.briar.privategroup.invitation.CreatorState.JOINED;
import static org.briarproject.briar.privategroup.invitation.CreatorState.START;
import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_SETUP;
import static org.briarproject.briar.privategroup.invitation.MessageType.ABORT;
import static org.briarproject.briar.privategroup.invitation.MessageType.INVITE;
import static org.briarproject.briar.privategroup.invitation.MessageType.JOIN;
@@ -108,13 +116,35 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
@Override
public void onDatabaseOpened(Transaction txn) throws DbException {
// Create a local group to indicate that we've set this client up
// If the feature flag for this client was set and then cleared, we may
// have set things up for some pre-existing contacts and not others.
// If the local group exists and has GROUP_KEY_SETUP == true in its
// metadata then we've set things up for all contacts. Otherwise we
// need to check whether a contact group exists for each contact.
Group localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID,
MAJOR_VERSION);
if (db.containsGroup(txn, localGroup.getId())) return;
db.addGroup(txn, localGroup);
// Set things up for any pre-existing contacts
for (Contact c : db.getContacts(txn)) addingContact(txn, c);
try {
if (db.containsGroup(txn, localGroup.getId())) {
// If GROUP_KEY_SETUP == true then we're done
BdfDictionary meta = clientHelper
.getGroupMetadataAsDictionary(txn, localGroup.getId());
if (meta.getBoolean(GROUP_KEY_SETUP, false)) return;
} else {
db.addGroup(txn, localGroup);
}
BdfDictionary meta =
BdfDictionary.of(new BdfEntry(GROUP_KEY_SETUP, true));
clientHelper.mergeGroupMetadata(txn, localGroup.getId(), meta);
} catch (FormatException e) {
throw new DbException(e);
}
// Set things up for any contacts that haven't already been set up
for (Contact c : db.getContacts(txn)) {
Group contactGroup = getContactGroup(c);
if (!db.containsGroup(txn, contactGroup.getId())) {
addingContact(txn, c);
}
}
}
@Override
@@ -511,7 +541,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
}
@Override
public boolean isInvitationAllowed(Contact c, GroupId privateGroupId)
public SharingStatus getSharingStatus(Contact c, GroupId privateGroupId)
throws DbException {
GroupId contactGroupId = getContactGroup(c).getId();
SessionId sessionId = getSessionId(privateGroupId);
@@ -523,13 +553,23 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
StoredSession ss = getSession(txn, contactGroupId, sessionId);
db.commitTransaction(txn);
// The group can't be shared unless the contact supports the client
if (client != SHARED) return false;
if (client != SHARED) return SharingStatus.NOT_SUPPORTED;
// If there's no session, the contact can be invited
if (ss == null) return true;
if (ss == null) return SharingStatus.SHAREABLE;
// If the session's in the start state, the contact can be invited
CreatorSession session = sessionParser
.parseCreatorSession(contactGroupId, ss.bdfSession);
return session.getState() == START;
CreatorState state = session.getState();
if (state == START) return SharingStatus.SHAREABLE;
if (state == INVITED) return SharingStatus.INVITE_RECEIVED;
if (state == JOINED) return SharingStatus.SHARING;
// Apart from the common case that the contact LEFT the group,
// the creator can also be a LEFT state, after re-adding a contact
// and re-creating the session with #recreateSession()
if (state == CreatorState.LEFT) return SharingStatus.SHARING;
if (state == DISSOLVED) throw new ProtocolStateException();
if (state == ERROR) return SharingStatus.ERROR;
throw new AssertionError("Unhandled state: " + state.name());
} catch (FormatException e) {
throw new DbException(e);
} finally {

View File

@@ -4,6 +4,12 @@ import org.briarproject.briar.client.MessageTrackerConstants;
interface SharingConstants {
/**
* Metadata key for the client's local group, indicating that contact
* groups have been created for all contacts.
*/
String GROUP_KEY_SETUP = "setup";
// Message metadata keys
String MSG_KEY_MESSAGE_TYPE = "messageType";
String MSG_KEY_SHAREABLE_ID = "shareableId";

View File

@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.MetadataParser;
import org.briarproject.bramble.api.db.DatabaseComponent;
@@ -26,6 +27,7 @@ import org.briarproject.bramble.api.versioning.ClientVersioningManager;
import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId;
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
import org.briarproject.briar.api.conversation.ConversationRequest;
@@ -51,13 +53,19 @@ import javax.annotation.Nullable;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE;
import static org.briarproject.briar.sharing.MessageType.ABORT;
import static org.briarproject.briar.sharing.MessageType.ACCEPT;
import static org.briarproject.briar.sharing.MessageType.DECLINE;
import static org.briarproject.briar.sharing.MessageType.INVITE;
import static org.briarproject.briar.sharing.MessageType.LEAVE;
import static org.briarproject.briar.sharing.SharingConstants.GROUP_KEY_SETUP;
import static org.briarproject.briar.sharing.State.LOCAL_INVITED;
import static org.briarproject.briar.sharing.State.LOCAL_LEFT;
import static org.briarproject.briar.sharing.State.REMOTE_HANGING;
import static org.briarproject.briar.sharing.State.REMOTE_INVITED;
import static org.briarproject.briar.sharing.State.SHARING;
import static org.briarproject.briar.sharing.State.START;
@NotNullByDefault
abstract class SharingManagerImpl<S extends Shareable>
@@ -100,13 +108,35 @@ abstract class SharingManagerImpl<S extends Shareable>
@Override
public void onDatabaseOpened(Transaction txn) throws DbException {
// Create a local group to indicate that we've set this client up
// If the feature flag for this client was set and then cleared, we may
// have set things up for some pre-existing contacts and not others.
// If the local group exists and has GROUP_KEY_SETUP == true in its
// metadata then we've set things up for all contacts. Otherwise we
// need to check whether a contact group exists for each contact.
Group localGroup = contactGroupFactory.createLocalGroup(getClientId(),
getMajorVersion());
if (db.containsGroup(txn, localGroup.getId())) return;
db.addGroup(txn, localGroup);
// Set things up for any pre-existing contacts
for (Contact c : db.getContacts(txn)) addingContact(txn, c);
try {
if (db.containsGroup(txn, localGroup.getId())) {
// If GROUP_KEY_SETUP == true then we're done
BdfDictionary meta = clientHelper
.getGroupMetadataAsDictionary(txn, localGroup.getId());
if (meta.getBoolean(GROUP_KEY_SETUP, false)) return;
} else {
db.addGroup(txn, localGroup);
}
BdfDictionary meta =
BdfDictionary.of(new BdfEntry(GROUP_KEY_SETUP, true));
clientHelper.mergeGroupMetadata(txn, localGroup.getId(), meta);
} catch (FormatException e) {
throw new DbException(e);
}
// Set things up for any contacts that haven't already been set up
for (Contact c : db.getContacts(txn)) {
Group contactGroup = getContactGroup(c);
if (!db.containsGroup(txn, contactGroup.getId())) {
addingContact(txn, c);
}
}
}
@Override
@@ -269,7 +299,7 @@ abstract class SharingManagerImpl<S extends Shareable>
SessionId sessionId = getSessionId(shareableId);
try {
Contact contact = db.getContact(txn, contactId);
if (!canBeShared(txn, shareableId, contact))
if (getSharingStatus(txn, shareableId, contact) != SHAREABLE)
// we might have received an invitation in the meantime
return;
// Look up the session, if there is one
@@ -467,34 +497,42 @@ abstract class SharingManagerImpl<S extends Shareable>
}
@Override
public boolean canBeShared(GroupId g, Contact c) throws DbException {
public SharingStatus getSharingStatus(GroupId g, Contact c)
throws DbException {
Transaction txn = db.startTransaction(true);
try {
boolean canBeShared = canBeShared(txn, g, c);
SharingStatus sharingStatus = getSharingStatus(txn, g, c);
db.commitTransaction(txn);
return canBeShared;
return sharingStatus;
} finally {
db.endTransaction(txn);
}
}
@Override
public boolean canBeShared(Transaction txn, GroupId g, Contact c)
public SharingStatus getSharingStatus(Transaction txn, GroupId g, Contact c)
throws DbException {
// The group can't be shared unless the contact supports the client
Visibility client = clientVersioningManager.getClientVisibility(txn,
c.getId(), getShareableClientId(), getShareableMajorVersion());
if (client != SHARED) return false;
if (client != SHARED) return SharingStatus.NOT_SUPPORTED;
GroupId contactGroupId = getContactGroup(c).getId();
SessionId sessionId = getSessionId(g);
try {
StoredSession ss = getSession(txn, contactGroupId, sessionId);
// If there's no session, we can share the group with the contact
if (ss == null) return true;
if (ss == null) return SharingStatus.SHAREABLE;
// If the session's in the right state, the contact can be invited
Session session =
sessionParser.parseSession(contactGroupId, ss.bdfSession);
return session.getState().canInvite();
State state = session.getState();
if (state == START) return SharingStatus.SHAREABLE;
if (state == LOCAL_INVITED) return SharingStatus.INVITE_RECEIVED;
if (state == REMOTE_INVITED) return SharingStatus.INVITE_SENT;
if (state == SHARING) return SharingStatus.SHARING;
if (state == LOCAL_LEFT || state == REMOTE_HANGING)
throw new ProtocolStateException();
throw new AssertionError("Unhandled state: " + state.name());
} catch (FormatException e) {
throw new DbException(e);
}

View File

@@ -46,10 +46,6 @@ enum State {
return visibility;
}
public boolean canInvite() {
return this == START;
}
public boolean isAwaitingResponse() {
return this == LOCAL_INVITED || this == REMOTE_INVITED;
}

View File

@@ -7,12 +7,15 @@ import org.jsoup.safety.Safelist;
@NotNullByDefault
public class HtmlUtils {
public static Safelist STRIP_ALL = Safelist.none();
public static Safelist ARTICLE = Safelist.basic()
private static final Safelist STRIP_ALL = Safelist.none();
private static final Safelist ARTICLE = Safelist.basic()
.addTags("h1", "h2", "h3", "h4", "h5", "h6");
public static String clean(String s, Safelist list) {
return Jsoup.clean(s, list);
public static String cleanAll(String s) {
return Jsoup.clean(s, STRIP_ALL);
}
public static String cleanArticle(String s) {
return Jsoup.clean(s, ARTICLE);
}
}

View File

@@ -0,0 +1,159 @@
package org.briarproject.briar.feed;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogFactory;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.RssProperties;
import org.jmock.Expectations;
import org.junit.Test;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
import static org.briarproject.briar.api.blog.BlogManager.MAJOR_VERSION;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_ADDED;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_AUTHOR;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_DESC;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_LAST_ENTRY;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_PRIVATE_KEY;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_AUTHOR;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_LINK;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_TITLE;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_URI;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_UPDATED;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_URL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class FeedFactoryImplTest extends BrambleMockTestCase {
private final AuthorFactory authorFactory =
context.mock(AuthorFactory.class);
private final BlogFactory blogFactory = context.mock(BlogFactory.class);
private final ClientHelper clientHelper = context.mock(ClientHelper.class);
private final Clock clock = context.mock(Clock.class);
private final LocalAuthor localAuthor = getLocalAuthor();
private final Group blogGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
private final Blog blog = new Blog(blogGroup, localAuthor, true);
private final BdfList authorList = BdfList.of("foo");
private final long added = 123, updated = 234, lastEntryTime = 345;
private final String url = getRandomString(123);
private final String description = getRandomString(123);
private final String rssAuthor = getRandomString(123);
private final String title = getRandomString(123);
private final String link = getRandomString(123);
private final String uri = getRandomString(123);
private final FeedFactoryImpl feedFactory = new FeedFactoryImpl(
authorFactory, blogFactory, clientHelper, clock);
@Test
public void testSerialiseAndDeserialiseWithoutOptionalFields()
throws Exception {
RssProperties propertiesBefore = new RssProperties(null, null, null,
null, null, null);
Feed before = new Feed(blog, localAuthor, propertiesBefore, added,
updated, lastEntryTime);
context.checking(new Expectations() {{
oneOf(clientHelper).toList(localAuthor);
will(returnValue(authorList));
}});
BdfDictionary dict = feedFactory.feedToBdfDictionary(before);
BdfDictionary expectedDict = BdfDictionary.of(
new BdfEntry(KEY_FEED_AUTHOR, authorList),
new BdfEntry(KEY_FEED_PRIVATE_KEY, localAuthor.getPrivateKey()),
new BdfEntry(KEY_FEED_ADDED, added),
new BdfEntry(KEY_FEED_UPDATED, updated),
new BdfEntry(KEY_FEED_LAST_ENTRY, lastEntryTime)
);
assertEquals(expectedDict, dict);
context.checking(new Expectations() {{
oneOf(clientHelper).parseAndValidateAuthor(authorList);
will(returnValue(localAuthor));
oneOf(blogFactory).createFeedBlog(localAuthor);
will(returnValue(blog));
}});
Feed after = feedFactory.createFeed(dict);
RssProperties afterProperties = after.getProperties();
assertNull(afterProperties.getUrl());
assertNull(afterProperties.getTitle());
assertNull(afterProperties.getDescription());
assertNull(afterProperties.getAuthor());
assertNull(afterProperties.getLink());
assertNull(afterProperties.getUri());
assertEquals(added, after.getAdded());
assertEquals(updated, after.getUpdated());
assertEquals(lastEntryTime, after.getLastEntryTime());
}
@Test
public void testSerialiseAndDeserialiseWithOptionalFields()
throws Exception {
RssProperties propertiesBefore = new RssProperties(url, title,
description, rssAuthor, link, uri);
Feed before = new Feed(blog, localAuthor, propertiesBefore, added,
updated, lastEntryTime);
context.checking(new Expectations() {{
oneOf(clientHelper).toList(localAuthor);
will(returnValue(authorList));
}});
BdfDictionary dict = feedFactory.feedToBdfDictionary(before);
BdfDictionary expectedDict = BdfDictionary.of(
new BdfEntry(KEY_FEED_AUTHOR, authorList),
new BdfEntry(KEY_FEED_PRIVATE_KEY, localAuthor.getPrivateKey()),
new BdfEntry(KEY_FEED_ADDED, added),
new BdfEntry(KEY_FEED_UPDATED, updated),
new BdfEntry(KEY_FEED_LAST_ENTRY, lastEntryTime),
new BdfEntry(KEY_FEED_URL, url),
new BdfEntry(KEY_FEED_RSS_TITLE, title),
new BdfEntry(KEY_FEED_DESC, description),
new BdfEntry(KEY_FEED_RSS_AUTHOR, rssAuthor),
new BdfEntry(KEY_FEED_RSS_LINK, link),
new BdfEntry(KEY_FEED_RSS_URI, uri)
);
assertEquals(expectedDict, dict);
context.checking(new Expectations() {{
oneOf(clientHelper).parseAndValidateAuthor(authorList);
will(returnValue(localAuthor));
oneOf(blogFactory).createFeedBlog(localAuthor);
will(returnValue(blog));
}});
Feed after = feedFactory.createFeed(dict);
RssProperties afterProperties = after.getProperties();
assertEquals(url, afterProperties.getUrl());
assertEquals(title, afterProperties.getTitle());
assertEquals(description, afterProperties.getDescription());
assertEquals(rssAuthor, afterProperties.getAuthor());
assertEquals(link, afterProperties.getLink());
assertEquals(uri, afterProperties.getUri());
assertEquals(added, after.getAdded());
assertEquals(updated, after.getUpdated());
assertEquals(lastEntryTime, after.getLastEntryTime());
}
}

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.feed;
import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndEntryImpl;
import com.rometools.rome.feed.synd.SyndFeed;
import org.briarproject.bramble.api.WeakSingletonProvider;
import org.briarproject.bramble.api.client.ClientHelper;
@@ -25,28 +24,31 @@ import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.RssProperties;
import org.jmock.Expectations;
import org.junit.Test;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.io.ByteArrayInputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executor;
import javax.annotation.Nonnull;
import javax.net.SocketFactory;
import okhttp3.Dns;
import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.Collections.singletonList;
import static okhttp3.mockwebserver.SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.util.StringUtils.UTF_8;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEEDS;
import static org.briarproject.briar.api.feed.FeedManager.CLIENT_ID;
import static org.briarproject.briar.api.feed.FeedManager.MAJOR_VERSION;
import static org.hamcrest.Matchers.nullValue;
public class FeedManagerImplTest extends BrambleMockTestCase {
@@ -60,14 +62,10 @@ public class FeedManagerImplTest extends BrambleMockTestCase {
private final BlogPostFactory blogPostFactory =
context.mock(BlogPostFactory.class);
private final FeedFactory feedFactory = context.mock(FeedFactory.class);
private final FeedMatcher feedMatcher = context.mock(FeedMatcher.class);
private final Clock clock = context.mock(Clock.class);
private final Dns noDnsLookups = context.mock(Dns.class);
private final OkHttpClient client = new OkHttpClient.Builder()
.socketFactory(SocketFactory.getDefault())
.dns(noDnsLookups)
.connectTimeout(60_000, MILLISECONDS)
.build();
private final OkHttpClient client = new OkHttpClient.Builder().build();
private final WeakSingletonProvider<OkHttpClient> httpClientProvider =
new WeakSingletonProvider<OkHttpClient>() {
@Override
@@ -83,14 +81,20 @@ public class FeedManagerImplTest extends BrambleMockTestCase {
private final GroupId blogGroupId = blogGroup.getId();
private final LocalAuthor localAuthor = getLocalAuthor();
private final Blog blog = new Blog(blogGroup, localAuthor, true);
private final Feed feed =
new Feed("http://example.org", blog, localAuthor, 0);
private final BdfDictionary feedDict = new BdfDictionary();
private final Message message = getMessage(blogGroupId);
private final BlogPost blogPost = new BlogPost(message, null, localAuthor);
private final long now = System.currentTimeMillis();
// Round the publication date to a whole second to avoid rounding errors
private final long pubDate = now / 1000 * 1000 - 1000;
private final SimpleDateFormat sdf =
new SimpleDateFormat("EEE, dd MMM yy HH:mm:ss Z");
private final String pubDateString = sdf.format(new Date(pubDate));
private final FeedManagerImpl feedManager =
new FeedManagerImpl(scheduler, ioExecutor, db, contactGroupFactory,
clientHelper, blogManager, blogPostFactory, feedFactory,
httpClientProvider, clock);
feedMatcher, httpClientProvider, clock);
@Test
public void testFetchFeedsReturnsEarlyIfTorIsNotActive() {
@@ -99,55 +103,223 @@ public class FeedManagerImplTest extends BrambleMockTestCase {
}
@Test
public void testEmptyFetchFeeds() throws Exception {
BdfList feedList = new BdfList();
expectGetFeeds(feedList);
expectStoreFeed(feedList);
public void testFetchFeedsEmptyList() throws Exception {
// The list of feeds is empty
expectGetFeeds();
feedManager.setTorActive(true);
feedManager.fetchFeeds();
}
@Test
public void testFetchFeedsIoException() throws Exception {
BdfDictionary feedDict = new BdfDictionary();
BdfList feedList = BdfList.of(feedDict);
// Fetching the feed will fail
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse()
.setBody(" ")
.setSocketPolicy(DISCONNECT_DURING_RESPONSE_BODY));
expectGetFeeds(feedList);
context.checking(new Expectations() {{
oneOf(noDnsLookups).lookup("example.org");
will(throwException(new UnknownHostException()));
}});
expectStoreFeed(feedList);
Feed feed = createFeed(url, blog);
expectGetFeeds(feed);
expectGetAndStoreFeeds(feed);
feedManager.setTorActive(true);
feedManager.fetchFeeds();
}
@Test
public void testPostFeedEntriesEmptyDate() throws Exception {
Transaction txn = new Transaction(null, false);
List<SyndEntry> entries = new ArrayList<>();
entries.add(new SyndEntryImpl());
SyndEntry entry = new SyndEntryImpl();
entry.setUpdatedDate(new Date());
entries.add(entry);
String text = "<p>(" + entry.getUpdatedDate().toString() + ")</p>";
Message msg = getMessage(blogGroupId);
BlogPost post = new BlogPost(msg, null, localAuthor);
public void testFetchFeedsEmptyResponseBody() throws Exception {
// Fetching the feed will succeed, but parsing the empty body will fail
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse());
context.checking(new Expectations() {{
oneOf(db).startTransaction(false);
will(returnValue(txn));
oneOf(clock).currentTimeMillis();
will(returnValue(42L));
oneOf(blogPostFactory).createBlogPost(feed.getBlogId(), 42L, null,
localAuthor, text);
will(returnValue(post));
oneOf(blogManager).addLocalPost(txn, post);
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
Feed feed = createFeed(url, blog);
expectGetFeeds(feed);
expectGetAndStoreFeeds(feed);
feedManager.setTorActive(true);
feedManager.fetchFeeds();
}
@Test
public void testFetchFeedsNoEntries() throws Exception {
// Fetching and parsing the feed will succeed; there are no entries
String feedXml = createRssFeedXml();
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse().setBody(feedXml));
Feed feed = createFeed(url, blog);
expectGetFeeds(feed);
expectUpdateFeedNoEntries(feed);
expectGetAndStoreFeeds(feed);
feedManager.setTorActive(true);
feedManager.fetchFeeds();
}
@Test
public void testFetchFeedsOneEntry() throws Exception {
// Fetching and parsing the feed will succeed; there is one entry
String entryXml =
"<item><pubDate>" + pubDateString + "</pubDate></item>";
String feedXml = createRssFeedXml(entryXml);
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse().setBody(feedXml));
Feed feed = createFeed(url, blog);
expectGetFeeds(feed);
expectUpdateFeedOneEntry(feed);
expectGetAndStoreFeeds(feed);
feedManager.setTorActive(true);
feedManager.fetchFeeds();
}
@Test
public void testAddNewFeedFromUrl() throws Exception {
// Fetching and parsing the feed will succeed; there are no entries
String feedXml = createRssFeedXml();
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse().setBody(feedXml));
Feed newFeed = createFeed(url, blog);
Group existingBlogGroup = getGroup(BlogManager.CLIENT_ID,
BlogManager.MAJOR_VERSION);
Blog existingBlog = new Blog(existingBlogGroup, localAuthor, true);
Feed existingFeed = createFeed("http://example.com", existingBlog);
expectGetFeeds(existingFeed);
context.checking(new DbExpectations() {{
// The added feed doesn't match any existing feed
oneOf(feedMatcher).findMatchingFeed(with(any(RssProperties.class)),
with(singletonList(existingFeed)));
will(returnValue(null));
// Create the new feed
oneOf(feedFactory).createFeed(with(url), with(any(SyndFeed.class)));
will(returnValue(newFeed));
// Add the new feed to the list of feeds
Transaction txn = new Transaction(null, false);
oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(blogManager).addBlog(txn, blog);
expectGetFeeds(txn, existingFeed);
expectStoreFeeds(txn, existingFeed, newFeed);
}});
feedManager.postFeedEntries(feed, entries);
expectUpdateFeedNoEntries(newFeed);
expectGetAndStoreFeeds(existingFeed, newFeed);
feedManager.addFeed(url);
}
@Test
public void testAddExistingFeedFromUrl() throws Exception {
// Fetching and parsing the feed will succeed; there are no entries
String feedXml = createRssFeedXml();
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse().setBody(feedXml));
Feed newFeed = createFeed(url, blog);
expectGetFeeds(newFeed);
context.checking(new DbExpectations() {{
// The added feed matches an existing feed
oneOf(feedMatcher).findMatchingFeed(with(any(RssProperties.class)),
with(singletonList(newFeed)));
will(returnValue(newFeed));
}});
expectUpdateFeedNoEntries(newFeed);
expectGetAndStoreFeeds(newFeed);
feedManager.addFeed(url);
}
@Test
public void testAddNewFeedFromInputStream() throws Exception {
// Reading and parsing the feed will succeed; there are no entries
String feedXml = createRssFeedXml();
Feed newFeed = createFeed(null, blog);
Group existingBlogGroup = getGroup(BlogManager.CLIENT_ID,
BlogManager.MAJOR_VERSION);
Blog existingBlog = new Blog(existingBlogGroup, localAuthor, true);
Feed existingFeed = createFeed(null, existingBlog);
expectGetFeeds(existingFeed);
context.checking(new DbExpectations() {{
// The added feed doesn't match any existing feed
oneOf(feedMatcher).findMatchingFeed(with(any(RssProperties.class)),
with(singletonList(existingFeed)));
will(returnValue(null));
// Create the new feed
oneOf(feedFactory).createFeed(with(nullValue(String.class)),
with(any(SyndFeed.class)));
will(returnValue(newFeed));
// Add the new feed to the list of feeds
Transaction txn = new Transaction(null, false);
oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(blogManager).addBlog(txn, blog);
expectGetFeeds(txn, existingFeed);
expectStoreFeeds(txn, existingFeed, newFeed);
}});
expectUpdateFeedNoEntries(newFeed);
expectGetAndStoreFeeds(existingFeed, newFeed);
feedManager.addFeed(new ByteArrayInputStream(feedXml.getBytes(UTF_8)));
}
@Test
public void testAddExistingFeedFromInputStream() throws Exception {
// Reading and parsing the feed will succeed; there are no entries
String feedXml = createRssFeedXml();
Feed newFeed = createFeed(null, blog);
expectGetFeeds(newFeed);
context.checking(new DbExpectations() {{
// The added feed matches an existing feed
oneOf(feedMatcher).findMatchingFeed(with(any(RssProperties.class)),
with(singletonList(newFeed)));
will(returnValue(newFeed));
}});
expectUpdateFeedNoEntries(newFeed);
expectGetAndStoreFeeds(newFeed);
feedManager.addFeed(new ByteArrayInputStream(feedXml.getBytes(UTF_8)));
}
private Feed createFeed(String url, Blog blog) {
RssProperties properties = new RssProperties(url,
null, null, null, null, null);
return new Feed(blog, localAuthor, properties, 0, 0, 0);
}
private String createRssFeedXml(String... entries) {
StringBuilder sb = new StringBuilder();
sb.append("<rss version='2.0'><channel>");
for (String entry : entries) sb.append(entry);
sb.append("</channel></rss>");
return sb.toString();
}
private void expectGetLocalGroup() {
@@ -158,33 +330,88 @@ public class FeedManagerImplTest extends BrambleMockTestCase {
}});
}
private void expectGetFeeds(BdfList feedList) throws Exception {
private void expectGetFeeds(Feed... feeds) throws Exception {
Transaction txn = new Transaction(null, true);
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
}});
expectGetFeeds(txn, feeds);
}
private void expectGetFeeds(Transaction txn, Feed... feeds)
throws Exception {
BdfList feedList = new BdfList();
for (int i = 0; i < feeds.length; i++) {
feedList.add(new BdfDictionary());
}
BdfDictionary feedsDict =
BdfDictionary.of(new BdfEntry(KEY_FEEDS, feedList));
expectGetLocalGroup();
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
context.checking(new Expectations() {{
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, localGroupId);
will(returnValue(feedsDict));
if (feedList.size() == 1) {
oneOf(feedFactory).createFeed(feedDict);
will(returnValue(feed));
for (int i = 0; i < feeds.length; i++) {
oneOf(feedFactory).createFeed(feedList.getDictionary(i));
will(returnValue(feeds[i]));
}
}});
}
private void expectStoreFeed(BdfList feedList) throws Exception {
private void expectStoreFeeds(Transaction txn, Feed... feeds)
throws Exception {
BdfList feedList = new BdfList();
for (int i = 0; i < feeds.length; i++) {
feedList.add(new BdfDictionary());
}
BdfDictionary feedDict =
BdfDictionary.of(new BdfEntry(KEY_FEEDS, feedList));
expectGetLocalGroup();
context.checking(new Expectations() {{
oneOf(clientHelper).mergeGroupMetadata(localGroupId, feedDict);
if (feedList.size() == 1) {
oneOf(feedFactory).feedToBdfDictionary(feed);
will(returnValue(feedList.getDictionary(0)));
oneOf(clientHelper).mergeGroupMetadata(txn, localGroupId, feedDict);
for (int i = 0; i < feeds.length; i++) {
oneOf(feedFactory).feedToBdfDictionary(feeds[i]);
will(returnValue(feedList.getDictionary(i)));
}
}});
}
private void expectGetAndStoreFeeds(Feed... feeds) throws Exception {
context.checking(new DbExpectations() {{
Transaction txn = new Transaction(null, false);
oneOf(db).transaction(with(false), withDbRunnable(txn));
expectGetFeeds(txn, feeds);
expectStoreFeeds(txn, feeds);
}});
}
private void expectUpdateFeedNoEntries(Feed feed) throws Exception {
Transaction txn = new Transaction(null, false);
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(false), withDbCallable(txn));
oneOf(feedFactory).updateFeed(with(feed), with(any(SyndFeed.class)),
with(0L));
will(returnValue(feed));
}});
}
private void expectUpdateFeedOneEntry(Feed feed) throws Exception {
Transaction txn = new Transaction(null, false);
String body = "<p>(" + new Date(pubDate) + ")</p>";
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(false), withDbCallable(txn));
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(blogPostFactory).createBlogPost(blogGroupId, pubDate, null,
localAuthor, body);
will(returnValue(blogPost));
oneOf(blogManager).addLocalPost(txn, blogPost);
oneOf(feedFactory).updateFeed(with(feed), with(any(SyndFeed.class)),
with(pubDate));
will(returnValue(feed));
}});
}
}

View File

@@ -5,30 +5,43 @@ import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestDatabaseConfigModule;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.FeedManager;
import org.briarproject.nullsafety.NullSafety;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.util.Collection;
import javax.annotation.Nullable;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
import static org.briarproject.nullsafety.NullSafety.requireNonNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class FeedManagerIntegrationTest extends BrambleTestCase {
private static final String FEED_PATH =
"src/test/resources/briarproject.org_news_index.xml";
private static final String FEED_URL =
"https://briarproject.org/news/index.xml";
private static final String FEED_TITLE = "News - Briar";
private LifecycleManager lifecycleManager;
private FeedManager feedManager;
private BlogManager blogManager;
private final File testDir = TestUtils.getTestDirectory();
private final File testDir = getTestDirectory();
private final File testFile = new File(testDir, "feedTest");
@Before
@@ -38,7 +51,8 @@ public class FeedManagerIntegrationTest extends BrambleTestCase {
DaggerFeedManagerIntegrationTestComponent.builder()
.testDatabaseConfigModule(
new TestDatabaseConfigModule(testFile)).build();
FeedManagerIntegrationTestComponent.Helper.injectEagerSingletons(component);
FeedManagerIntegrationTestComponent.Helper
.injectEagerSingletons(component);
component.inject(this);
IdentityManager identityManager = component.getIdentityManager();
@@ -54,17 +68,30 @@ public class FeedManagerIntegrationTest extends BrambleTestCase {
}
@Test
public void testFeedImportAndRemoval() throws Exception {
public void testFeedImportAndRemovalFromUrl() throws Exception {
testFeedImportAndRemoval(FEED_URL, null);
}
@Test
public void testFeedImportAndRemovalFromFile() throws Exception {
testFeedImportAndRemoval(null, FEED_PATH);
}
private void testFeedImportAndRemoval(@Nullable String url,
@Nullable String path) throws Exception {
// initially, there's only the one personal blog
Collection<Blog> blogs = blogManager.getBlogs();
assertEquals(1, blogs.size());
Blog personalBlog = blogs.iterator().next();
// add feed into a dedicated blog
String url = "https://www.schneier.com/blog/atom.xml";
feedManager.addFeed(url);
if (url == null) {
feedManager.addFeed(new FileInputStream(requireNonNull(path)));
} else {
feedManager.addFeed(url);
}
// then there's the feed's blog now
// now there's the feed's blog too
blogs = blogManager.getBlogs();
assertEquals(2, blogs.size());
Blog feedBlog = null;
@@ -80,15 +107,16 @@ public class FeedManagerIntegrationTest extends BrambleTestCase {
assertTrue(feed.getLastEntryTime() > 0);
assertTrue(feed.getAdded() > 0);
assertTrue(feed.getUpdated() > 0);
assertEquals(url, feed.getUrl());
assertTrue(NullSafety.equals(url, feed.getProperties().getUrl()));
assertEquals(feedBlog, feed.getBlog());
assertEquals("Schneier on Security", feed.getTitle());
assertEquals(FEED_TITLE, feed.getTitle());
assertEquals(feed.getTitle(), feed.getBlog().getName());
assertEquals(feed.getTitle(), feed.getLocalAuthor().getName());
// check the feed entries have been added to the blog as expected
Collection<BlogPostHeader> headers =
blogManager.getPostHeaders(feedBlog.getId());
assertFalse(headers.isEmpty());
for (BlogPostHeader header : headers) {
assertTrue(header.isRssFeed());
}
@@ -105,6 +133,6 @@ public class FeedManagerIntegrationTest extends BrambleTestCase {
public void tearDown() throws Exception {
lifecycleManager.stopServices();
lifecycleManager.waitForShutdown();
TestUtils.deleteTestDirectory(testDir);
deleteTestDirectory(testDir);
}
}

View File

@@ -0,0 +1,179 @@
package org.briarproject.briar.feed;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.RssProperties;
import org.junit.Test;
import java.util.Random;
import static java.util.Arrays.asList;
import static org.briarproject.bramble.test.TestUtils.getClientId;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
public class FeedMatcherImplTest extends BrambleTestCase {
private static final String URL = "url";
private static final String TITLE = "title";
private static final String DESCRIPTION = "description";
private static final String AUTHOR = "author";
private static final String LINK = "link";
private static final String URI = "uri";
private final Random random = new Random();
private final ClientId clientId = getClientId();
private final LocalAuthor localAuthor = getLocalAuthor();
private final FeedMatcher matcher = new FeedMatcherImpl();
@Test
public void testFeedWithMatchingUrlIsChosen() {
RssProperties candidate = new RssProperties(URL,
TITLE, DESCRIPTION, AUTHOR, LINK, URI);
// The first feed has a different/null URL but matching RSS fields
Feed feed1 = createFeed(new RssProperties(nope(),
TITLE, DESCRIPTION, AUTHOR, LINK, URI));
// The second feed has a matching URL but different/null RSS fields
Feed feed2 = createFeed(new RssProperties(URL,
nope(), nope(), nope(), nope(), nope()));
Feed match = matcher.findMatchingFeed(candidate, asList(feed1, feed2));
// The matcher should choose the feed with the matching URL
assertNotNull(match);
assertSame(feed2, match);
}
@Test
public void testNullUrlIsNotMatched() {
// The candidate has a null URL
RssProperties candidate = new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, LINK, URI);
// The first feed has a non-null URL and matching RSS fields
Feed feed1 = createFeed(new RssProperties(URL,
TITLE, DESCRIPTION, AUTHOR, LINK, URI));
// The second feed has a null URL and different/null RSS fields
Feed feed2 = createFeed(new RssProperties(null,
nope(), nope(), nope(), nope(), nope()));
Feed match = matcher.findMatchingFeed(candidate, asList(feed1, feed2));
// The matcher should choose the feed with the matching RSS fields
assertNotNull(match);
assertSame(feed1, match);
}
@Test
public void testDoesNotMatchOneRssField() {
testDoesNotMatchRssFields(TITLE, nope(), nope(), nope(), nope());
testDoesNotMatchRssFields(nope(), DESCRIPTION, nope(), nope(), nope());
testDoesNotMatchRssFields(nope(), nope(), AUTHOR, nope(), nope());
testDoesNotMatchRssFields(nope(), nope(), nope(), LINK, nope());
testDoesNotMatchRssFields(nope(), nope(), nope(), nope(), URL);
}
private void testDoesNotMatchRssFields(String title, String description,
String author, String link, String uri) {
RssProperties candidate = new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, LINK, URL);
// The first feed has no matching RSS fields
Feed feed1 = createFeed(new RssProperties(null,
nope(), nope(), nope(), nope(), nope()));
// The second feed has the given RSS fields
Feed feed2 = createFeed(new RssProperties(null,
title, description, author, link, uri));
Feed match = matcher.findMatchingFeed(candidate, asList(feed1, feed2));
// The matcher should not choose either of the feeds
assertNull(match);
}
@Test
public void testMatchesTwoRssFields() {
testMatchesRssFields(TITLE, DESCRIPTION, nope(), nope(), nope());
testMatchesRssFields(nope(), DESCRIPTION, AUTHOR, nope(), nope());
testMatchesRssFields(nope(), nope(), AUTHOR, LINK, nope());
testMatchesRssFields(nope(), nope(), nope(), LINK, URI);
}
private void testMatchesRssFields(String title, String description,
String author, String link, String uri) {
RssProperties candidate = new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, LINK, URI);
// The first feed has no matching RSS fields
Feed feed1 = createFeed(new RssProperties(null,
nope(), nope(), nope(), nope(), nope()));
// The second feed has the given RSS fields
Feed feed2 = createFeed(new RssProperties(null,
title, description, author, link, uri));
// The third feed has one matching RSS field
Feed feed3 = createFeed(new RssProperties(null,
TITLE, nope(), nope(), nope(), nope()));
FeedMatcher matcher = new FeedMatcherImpl();
Feed match = matcher.findMatchingFeed(candidate,
asList(feed1, feed2, feed3));
// The matcher should choose the second feed
assertSame(feed2, match);
}
@Test
public void testFeedWithMostMatchingRssFieldsIsChosen() {
RssProperties candidate = new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, LINK, URI);
// The first feed has no matching RSS fields
Feed feed1 = createFeed(new RssProperties(null,
nope(), nope(), nope(), nope(), nope()));
// The second feed has three matching RSS fields
Feed feed2 = createFeed(new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, nope(), nope()));
// The third feed has two matching RSS fields
Feed feed3 = createFeed(new RssProperties(null,
TITLE, DESCRIPTION, nope(), nope(), nope()));
Feed match = matcher.findMatchingFeed(candidate,
asList(feed1, feed2, feed3));
// The matcher should choose the second feed
assertSame(feed2, match);
}
@Test
public void testNullRssFieldsAreNotMatched() {
// The candidate has a null URL and null RSS fields
RssProperties candidate = new RssProperties(null,
null, null, null, null, null);
// The first feed has a null URL and non-null RSS fields
Feed feed1 = createFeed(new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, LINK, URI));
// The second feed has a non-null URL and null RSS fields
Feed feed2 = createFeed(new RssProperties(URL,
null, null, null, null, null));
Feed match = matcher.findMatchingFeed(candidate, asList(feed1, feed2));
// The matcher should not choose either of the feeds
assertNull(match);
}
/**
* Returns an RSS field that doesn't match the default, either because it's
* null or because it's a different non-null value.
*/
private String nope() {
return random.nextBoolean() ? null : "x";
}
private Feed createFeed(RssProperties properties) {
Blog blog = new Blog(getGroup(clientId, 123), localAuthor, true);
return new Feed(blog, localAuthor, properties, 0, 0, 0);
}
}

View File

@@ -33,7 +33,7 @@ import static org.briarproject.briar.api.attachment.MediaConstants.MSG_KEY_DESCR
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MAX_AUTO_DELETE_TIMER_MS;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_INCOMING_TEXT_LENGTH;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
import static org.briarproject.briar.messaging.MessageTypes.ATTACHMENT;
import static org.briarproject.briar.messaging.MessageTypes.PRIVATE_MESSAGE;
@@ -58,7 +58,7 @@ public class PrivateMessageValidatorTest extends BrambleMockTestCase {
private final Message message = getMessage(group.getId());
private final long now = message.getTimestamp() + 1000;
private final String text =
getRandomString(MAX_PRIVATE_MESSAGE_INCOMING_TEXT_LENGTH);
getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
private final BdfList attachmentHeader = getAttachmentHeader();
private final MessageId attachmentId = new MessageId(getRandomId());
private final String contentType = getRandomString(MAX_CONTENT_TYPE_BYTES);
@@ -135,7 +135,7 @@ public class PrivateMessageValidatorTest extends BrambleMockTestCase {
@Test(expected = InvalidMessageException.class)
public void testRejectsTooLongTextForLegacyMessage() throws Exception {
String invalidText =
getRandomString(MAX_PRIVATE_MESSAGE_INCOMING_TEXT_LENGTH + 1);
getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH + 1);
testRejectsLegacyMessage(BdfList.of(invalidText));
}
@@ -193,7 +193,7 @@ public class PrivateMessageValidatorTest extends BrambleMockTestCase {
@Test(expected = InvalidMessageException.class)
public void testRejectsTooLongTextForPrivateMessage() throws Exception {
String invalidText =
getRandomString(MAX_PRIVATE_MESSAGE_INCOMING_TEXT_LENGTH + 1);
getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH + 1);
testRejectsPrivateMessage(
BdfList.of(PRIVATE_MESSAGE, invalidText, new BdfList()));

View File

@@ -23,6 +23,8 @@ import javax.annotation.Nullable;
import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHARING;
import static org.briarproject.briar.test.TestEventListener.assertEvent;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -157,8 +159,8 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest {
waitForEvents(c1);
// 0 can invite 1 again
assertTrue(groupInvitationManager0
.isInvitationAllowed(contact1From0, privateGroup.getId()));
assertEquals(SHAREABLE, groupInvitationManager0
.getSharingStatus(contact1From0, privateGroup.getId()));
// Before 1's timer elapses, 1 should still see the auto-decline message
c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
@@ -194,8 +196,8 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest {
assertEquals(0, getMessageHeaders(c0, contactId1From0).size());
// 0 can invite 1 again and really does invite
assertTrue(groupInvitationManager0
.isInvitationAllowed(contact1From0, privateGroup.getId()));
assertEquals(SHAREABLE, groupInvitationManager0
.getSharingStatus(contact1From0, privateGroup.getId()));
sendInvitation(privateGroup, contact1From0.getId(),
"Join this faster please!");
sync0To1(1, true);
@@ -395,8 +397,8 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest {
// 1 joined the PrivateGroup
assertEquals(pg,
c1.getPrivateGroupManager().getPrivateGroup(pg.getId()));
assertFalse(groupInvitationManager0
.isInvitationAllowed(contact1From0, pg.getId()));
assertEquals(SHARING, groupInvitationManager0
.getSharingStatus(contact1From0, pg.getId()));
}
@Test

View File

@@ -29,6 +29,9 @@ import javax.annotation.Nullable;
import static java.util.Collections.emptySet;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.INVITE_RECEIVED;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHARING;
import static org.briarproject.briar.test.BriarTestUtils.assertGroupCount;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -319,8 +322,8 @@ public class GroupInvitationIntegrationTest
sendInvitation(c0.getClock().currentTimeMillis(), null);
// invitation is not allowed before the first hasn't been answered
assertFalse(groupInvitationManager0
.isInvitationAllowed(contact1From0, privateGroup.getId()));
assertEquals(INVITE_RECEIVED, groupInvitationManager0
.getSharingStatus(contact1From0, privateGroup.getId()));
// deliver invitation and response
sync0To1(1, true);
@@ -329,8 +332,8 @@ public class GroupInvitationIntegrationTest
sync1To0(1, true);
// after invitation was declined, inviting again is possible
assertTrue(groupInvitationManager0
.isInvitationAllowed(contact1From0, privateGroup.getId()));
assertEquals(SHAREABLE, groupInvitationManager0
.getSharingStatus(contact1From0, privateGroup.getId()));
// send and accept the second invitation
sendInvitation(c0.getClock().currentTimeMillis(), "Second Invitation");
@@ -340,8 +343,8 @@ public class GroupInvitationIntegrationTest
sync1To0(1, true);
// invitation is not allowed since the member joined the group now
assertFalse(groupInvitationManager0
.isInvitationAllowed(contact1From0, privateGroup.getId()));
assertEquals(SHARING, groupInvitationManager0
.getSharingStatus(contact1From0, privateGroup.getId()));
// don't allow another invitation request
try {
@@ -723,8 +726,8 @@ public class GroupInvitationIntegrationTest
sync1To0(1, true);
// inviting again is not possible
assertFalse(groupInvitationManager0
.isInvitationAllowed(contact1From0, privateGroup.getId()));
assertEquals(SHARING, groupInvitationManager0
.getSharingStatus(contact1From0, privateGroup.getId()));
// contacts remove each other
removeAllContacts();
@@ -737,8 +740,8 @@ public class GroupInvitationIntegrationTest
addDefaultContacts();
// creator can still not invite again
assertFalse(groupInvitationManager0
.isInvitationAllowed(contact1From0, privateGroup.getId()));
assertEquals(SHARING, groupInvitationManager0
.getSharingStatus(contact1From0, privateGroup.getId()));
// finally invitee can remove group without issues
groupManager1.removePrivateGroup(privateGroup.getId());

View File

@@ -24,6 +24,7 @@ import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.DbExpectations;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId;
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
import org.briarproject.briar.api.privategroup.PrivateGroup;
@@ -38,7 +39,6 @@ import org.jmock.imposters.ByteBuddyClassImposteriser;
import org.junit.Test;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -46,7 +46,9 @@ import javax.annotation.Nullable;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static junit.framework.TestCase.fail;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE;
@@ -62,12 +64,16 @@ import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.GROU
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.MAJOR_VERSION;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.ERROR;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.INVITE_RECEIVED;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHARING;
import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_SETUP;
import static org.briarproject.briar.privategroup.invitation.MessageType.ABORT;
import static org.briarproject.briar.privategroup.invitation.MessageType.INVITE;
import static org.briarproject.briar.privategroup.invitation.MessageType.JOIN;
import static org.briarproject.briar.privategroup.invitation.MessageType.LEAVE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
@@ -120,9 +126,10 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
private final BdfDictionary bdfSession =
BdfDictionary.of(new BdfEntry("f", "o"));
private final Map<MessageId, BdfDictionary> oneResult =
Collections.singletonMap(storageMessage.getId(), bdfSession);
private final Map<MessageId, BdfDictionary> noResults =
Collections.emptyMap();
singletonMap(storageMessage.getId(), bdfSession);
private final Map<MessageId, BdfDictionary> noResults = emptyMap();
private final BdfDictionary localGroupMeta =
BdfDictionary.of(new BdfEntry(GROUP_KEY_SETUP, true));
public GroupInvitationManagerImplTest() {
@@ -163,21 +170,64 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
oneOf(db).containsGroup(txn, localGroup.getId());
will(returnValue(false));
oneOf(db).addGroup(txn, localGroup);
oneOf(clientHelper).mergeGroupMetadata(txn, localGroup.getId(),
localGroupMeta);
oneOf(db).getContacts(txn);
will(returnValue(singletonList(contact)));
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
MAJOR_VERSION, contact);
will(returnValue(contactGroup));
oneOf(db).containsGroup(txn, contactGroup.getId());
will(returnValue(false));
}});
expectAddingContact(contact, emptyList());
groupInvitationManager.onDatabaseOpened(txn);
}
@Test
public void testOpenDatabaseHookSubsequentTime() throws Exception {
public void testOpenDatabaseHookSubsequentTimeWithoutSetupFlag()
throws Exception {
context.checking(new Expectations() {{
// The local group exists but the metadata flag has not been set
oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
MAJOR_VERSION);
will(returnValue(localGroup));
oneOf(db).containsGroup(txn, localGroup.getId());
will(returnValue(true));
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
localGroup.getId());
will(returnValue(new BdfDictionary()));
// Store the metadata flag
oneOf(clientHelper).mergeGroupMetadata(txn, localGroup.getId(),
localGroupMeta);
// Check whether the contact group exists - it doesn't
oneOf(db).getContacts(txn);
will(returnValue(singletonList(contact)));
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
MAJOR_VERSION, contact);
will(returnValue(contactGroup));
oneOf(db).containsGroup(txn, contactGroup.getId());
will(returnValue(false));
}});
// Set things up for the contact
expectAddingContact(contact, emptyList());
groupInvitationManager.onDatabaseOpened(txn);
}
@Test
public void testOpenDatabaseHookSubsequentTimeWithSetupFlag()
throws Exception {
context.checking(new Expectations() {{
// The local group exists and the metadata flag has been set
oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
MAJOR_VERSION);
will(returnValue(localGroup));
oneOf(db).containsGroup(txn, localGroup.getId());
will(returnValue(true));
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
localGroup.getId());
will(returnValue(localGroupMeta));
}});
groupInvitationManager.onDatabaseOpened(txn);
}
@@ -871,31 +921,36 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
@Test
public void testIsInvitationAllowed() throws Exception {
expectIsInvitationAllowed(CreatorState.START);
assertTrue(groupInvitationManager
.isInvitationAllowed(contact, privateGroup.getId()));
assertEquals(SHAREABLE, groupInvitationManager
.getSharingStatus(contact, privateGroup.getId()));
}
@Test
public void testIsNotInvitationAllowed() throws Exception {
expectIsInvitationAllowed(CreatorState.DISSOLVED);
assertFalse(groupInvitationManager
.isInvitationAllowed(contact, privateGroup.getId()));
try {
groupInvitationManager
.getSharingStatus(contact, privateGroup.getId());
fail();
} catch (ProtocolStateException e) {
// expected
}
expectIsInvitationAllowed(CreatorState.ERROR);
assertFalse(groupInvitationManager
.isInvitationAllowed(contact, privateGroup.getId()));
assertEquals(ERROR, groupInvitationManager
.getSharingStatus(contact, privateGroup.getId()));
expectIsInvitationAllowed(CreatorState.INVITED);
assertFalse(groupInvitationManager
.isInvitationAllowed(contact, privateGroup.getId()));
assertEquals(INVITE_RECEIVED, groupInvitationManager
.getSharingStatus(contact, privateGroup.getId()));
expectIsInvitationAllowed(CreatorState.JOINED);
assertFalse(groupInvitationManager
.isInvitationAllowed(contact, privateGroup.getId()));
assertEquals(SHARING, groupInvitationManager
.getSharingStatus(contact, privateGroup.getId()));
expectIsInvitationAllowed(CreatorState.LEFT);
assertFalse(groupInvitationManager
.isInvitationAllowed(contact, privateGroup.getId()));
assertEquals(SHARING, groupInvitationManager
.getSharingStatus(contact, privateGroup.getId()));
}
private void expectIsInvitationAllowed(CreatorState state)
@@ -948,9 +1003,9 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
BdfDictionary.of(new BdfEntry("f3", "o"));
expectGetSession(oneResult, sessionId, contactGroup.getId());
expectGetSession(Collections.singletonMap(storageId2, bdfSession2),
expectGetSession(singletonMap(storageId2, bdfSession2),
sessionId, contactGroup2.getId());
expectGetSession(Collections.singletonMap(storageId3, bdfSession3),
expectGetSession(singletonMap(storageId3, bdfSession3),
sessionId, contactGroup3.getId());
context.checking(new Expectations() {{

View File

@@ -19,6 +19,7 @@ import java.util.Collection;
import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE;
import static org.briarproject.briar.test.TestEventListener.assertEvent;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -146,8 +147,8 @@ public abstract class AbstractAutoDeleteIntegrationTest
waitForEvents(c1);
// 0 can invite 1 again
assertTrue(getSharingManager0()
.canBeShared(getShareable().getId(), contact1From0));
assertEquals(SHAREABLE, getSharingManager0().getSharingStatus(
getShareable().getId(), contact1From0));
// Before 1's timer elapses, 1 should still see the auto-decline message
c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
@@ -183,8 +184,8 @@ public abstract class AbstractAutoDeleteIntegrationTest
assertEquals(0, getMessageHeaders(c0, contactId1From0).size());
// 0 can invite 1 again and really does invite
assertTrue(getSharingManager0()
.canBeShared(getShareable().getId(), contact1From0));
assertEquals(SHAREABLE, getSharingManager0()
.getSharingStatus(getShareable().getId(), contact1From0));
// Send invitation
getSharingManager0()
.sendInvitation(getShareable().getId(), contactId1From0,

View File

@@ -33,6 +33,8 @@ import java.util.Collection;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS;
import static org.briarproject.briar.api.blog.BlogSharingManager.CLIENT_ID;
import static org.briarproject.briar.api.blog.BlogSharingManager.MAJOR_VERSION;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHARING;
import static org.briarproject.briar.test.BriarTestUtils.assertGroupCount;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -106,14 +108,18 @@ public class BlogSharingIntegrationTest
public void testPersonalBlogCannotBeSharedWithOwner() throws Exception {
listenToEvents(true);
assertFalse(blogSharingManager0.canBeShared(blog1.getId(),
contact1From0));
assertFalse(blogSharingManager0.canBeShared(blog2.getId(),
contact2From0));
assertFalse(blogSharingManager1.canBeShared(blog0.getId(),
contact0From1));
assertFalse(blogSharingManager2.canBeShared(blog0.getId(),
contact0From2));
assertEquals(SHARING,
blogSharingManager0.getSharingStatus(blog1.getId(),
contact1From0));
assertEquals(SHARING,
blogSharingManager0.getSharingStatus(blog2.getId(),
contact2From0));
assertEquals(SHARING,
blogSharingManager1.getSharingStatus(blog0.getId(),
contact0From1));
assertEquals(SHARING,
blogSharingManager2.getSharingStatus(blog0.getId(),
contact0From2));
}
@Test
@@ -191,13 +197,13 @@ public class BlogSharingIntegrationTest
}
// sharer has own invitation message and response
assertEquals(2, db0.transactionWithResult(true, txn ->
blogSharingManager0.getMessageHeaders(txn, contactId1From0))
blogSharingManager0.getMessageHeaders(txn, contactId1From0))
.size());
// blog can not be shared again
assertFalse(blogSharingManager0.canBeShared(blog2.getId(),
contact1From0));
assertFalse(blogSharingManager1.canBeShared(blog2.getId(),
contact0From1));
assertEquals(SHARING, blogSharingManager0.getSharingStatus(
blog2.getId(), contact1From0));
assertEquals(SHARING, blogSharingManager1.getSharingStatus(
blog2.getId(), contact0From1));
// group message count is still correct
assertGroupCount(messageTracker0, g, 2, 1);
@@ -300,13 +306,13 @@ public class BlogSharingIntegrationTest
}
// sharer has own invitation message and response
assertEquals(2, db0.transactionWithResult(true, txn ->
blogSharingManager0.getMessageHeaders(txn, contactId1From0))
blogSharingManager0.getMessageHeaders(txn, contactId1From0))
.size());
// blog can not be shared again
assertFalse(blogSharingManager0.canBeShared(rssBlog.getId(),
contact1From0));
assertFalse(blogSharingManager1.canBeShared(rssBlog.getId(),
contact0From1));
assertEquals(SHARING, blogSharingManager0.getSharingStatus(
rssBlog.getId(), contact1From0));
assertEquals(SHARING, blogSharingManager1.getSharingStatus(
rssBlog.getId(), contact0From1));
// group message count is still correct
assertGroupCount(messageTracker0, g, 2, 1);
@@ -361,11 +367,11 @@ public class BlogSharingIntegrationTest
}
// sharer has own invitation message and response
assertEquals(2, db0.transactionWithResult(true, txn ->
blogSharingManager0.getMessageHeaders(txn, contactId1From0))
blogSharingManager0.getMessageHeaders(txn, contactId1From0))
.size());
// blog can be shared again
assertTrue(
blogSharingManager0.canBeShared(blog2.getId(), contact1From0));
assertEquals(SHAREABLE, blogSharingManager0.getSharingStatus(
blog2.getId(), contact1From0));
}
@Test
@@ -416,8 +422,8 @@ public class BlogSharingIntegrationTest
assertEquals(0,
blogSharingManager1.getSharedWith(blog2.getId()).size());
// blog can be shared again by sharer
assertTrue(
blogSharingManager0.canBeShared(blog2.getId(), contact1From0));
assertEquals(SHAREABLE, blogSharingManager0.getSharingStatus(
blog2.getId(), contact1From0));
}
@Test
@@ -534,8 +540,8 @@ public class BlogSharingIntegrationTest
.contains(contact0From1));
// 1 can again share blog 1 with 0
assertTrue(
blogSharingManager1.canBeShared(blog1.getId(), contact0From1));
assertEquals(SHAREABLE, blogSharingManager1.getSharingStatus(
blog1.getId(), contact0From1));
}
@Test
@@ -592,8 +598,8 @@ public class BlogSharingIntegrationTest
}
// 0 can share blog2 again with 1
assertTrue(
blogSharingManager0.canBeShared(blog2.getId(), contact1From0));
assertEquals(SHAREABLE, blogSharingManager0.getSharingStatus(
blog2.getId(), contact1From0));
}
@NotNullByDefault

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.client.ContactGroupFactory;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.MetadataParser;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
@@ -27,10 +28,11 @@ import org.jmock.Expectations;
import org.junit.Test;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getContact;
@@ -40,6 +42,7 @@ import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.briar.api.blog.BlogSharingManager.CLIENT_ID;
import static org.briarproject.briar.api.blog.BlogSharingManager.MAJOR_VERSION;
import static org.briarproject.briar.sharing.SharingConstants.GROUP_KEY_SETUP;
public class BlogSharingManagerImplTest extends BrambleMockTestCase {
@@ -57,14 +60,16 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
private final ContactGroupFactory contactGroupFactory =
context.mock(ContactGroupFactory.class);
private final BlogManager blogManager = context.mock(BlogManager.class);
@SuppressWarnings("unchecked")
private final ProtocolEngine<Blog> engine =
context.mock(ProtocolEngine.class);
private final LocalAuthor localAuthor = getLocalAuthor();
private final Author author = getAuthor();
private final Contact contact =
getContact(author, localAuthor.getId(), true);
private final ContactId contactId = contact.getId();
private final Collection<Contact> contacts =
Collections.singletonList(contact);
private final Collection<Contact> contacts = singletonList(contact);
private final Group localGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
private final Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
private final Group blogGroup =
@@ -73,9 +78,8 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
private final Group localBlogGroup =
getGroup(BlogManager.CLIENT_ID, BlogManager.MAJOR_VERSION);
private final Blog localBlog = new Blog(localBlogGroup, localAuthor, false);
@SuppressWarnings("unchecked")
private final ProtocolEngine<Blog> engine =
context.mock(ProtocolEngine.class);
private final BdfDictionary localGroupMeta =
BdfDictionary.of(new BdfEntry(GROUP_KEY_SETUP, true));
@SuppressWarnings("unchecked")
public BlogSharingManagerImplTest() {
@@ -104,9 +108,16 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
oneOf(db).containsGroup(txn, localGroup.getId());
will(returnValue(false));
oneOf(db).addGroup(txn, localGroup);
oneOf(clientHelper).mergeGroupMetadata(txn, localGroup.getId(),
localGroupMeta);
// Get contacts
oneOf(db).getContacts(txn);
will(returnValue(contacts));
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
MAJOR_VERSION, contact);
will(returnValue(contactGroup));
oneOf(db).containsGroup(txn, contactGroup.getId());
will(returnValue(false));
}});
// Set things up for the contact
expectAddingContact(txn);
@@ -115,7 +126,7 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
}
private void expectAddingContact(Transaction txn) throws Exception {
Map<MessageId, BdfDictionary> sessions = Collections.emptyMap();
Map<MessageId, BdfDictionary> sessions = emptyMap();
context.checking(new Expectations() {{
// Create the contact group and share it with the contact
@@ -145,16 +156,53 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
}
@Test
public void testOpenDatabaseHookSubsequentTime() throws Exception {
public void testOpenDatabaseHookSubsequentTimeWithoutSetupFlag()
throws Exception {
Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{
// The local group exists - everything has been set up
// The local group exists but the metadata flag has not been set
oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
MAJOR_VERSION);
will(returnValue(localGroup));
oneOf(db).containsGroup(txn, localGroup.getId());
will(returnValue(true));
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
localGroup.getId());
will(returnValue(new BdfDictionary()));
// Store the metadata flag
oneOf(clientHelper).mergeGroupMetadata(txn, localGroup.getId(),
localGroupMeta);
// Check whether the contact group exists - it doesn't
oneOf(db).getContacts(txn);
will(returnValue(contacts));
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
MAJOR_VERSION, contact);
will(returnValue(contactGroup));
oneOf(db).containsGroup(txn, contactGroup.getId());
will(returnValue(false));
}});
// Set things up for the contact
expectAddingContact(txn);
blogSharingManager.onDatabaseOpened(txn);
}
@Test
public void testOpenDatabaseHookSubsequentTimeWithSetupFlag()
throws Exception {
Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{
// The local group exists and the metadata flag has been set
oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
MAJOR_VERSION);
will(returnValue(localGroup));
oneOf(db).containsGroup(txn, localGroup.getId());
will(returnValue(true));
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
localGroup.getId());
will(returnValue(localGroupMeta));
}});
blogSharingManager.onDatabaseOpened(txn);

View File

@@ -13,6 +13,7 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.test.TestDatabaseConfigModule;
import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
import org.briarproject.briar.api.conversation.ConversationResponse;
import org.briarproject.briar.api.conversation.DeletionResult;
@@ -43,10 +44,13 @@ import javax.annotation.Nullable;
import static java.util.Collections.emptySet;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.TestCase.fail;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS;
import static org.briarproject.briar.api.forum.ForumSharingManager.CLIENT_ID;
import static org.briarproject.briar.api.forum.ForumSharingManager.MAJOR_VERSION;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE;
import static org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHARING;
import static org.briarproject.briar.test.BriarTestUtils.assertGroupCount;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -183,9 +187,11 @@ public class ForumSharingIntegrationTest
assertEquals(2, getMessages1From0().size());
// forum can not be shared again
Contact c1 = contactManager0.getContact(contactId1From0);
assertFalse(forumSharingManager0.canBeShared(forum.getId(), c1));
assertEquals(SHARING,
forumSharingManager0.getSharingStatus(forum.getId(), c1));
Contact c0 = contactManager1.getContact(contactId0From1);
assertFalse(forumSharingManager1.canBeShared(forum.getId(), c0));
assertEquals(SHARING,
forumSharingManager1.getSharingStatus(forum.getId(), c0));
}
@Test
@@ -271,7 +277,8 @@ public class ForumSharingIntegrationTest
assertEquals(2, getMessages1From0().size());
// forum can be shared again
Contact c1 = contactManager0.getContact(contactId1From0);
assertTrue(forumSharingManager0.canBeShared(forum.getId(), c1));
assertEquals(SHAREABLE,
forumSharingManager0.getSharingStatus(forum.getId(), c1));
// sharer un-subscribes from forum
forumManager0.removeForum(forum);
@@ -340,18 +347,22 @@ public class ForumSharingIntegrationTest
assertFalse(forumSharingManager1.getSharedWith(forum.getId())
.contains(contact0));
// forum can be shared again by sharer
assertTrue(forumSharingManager0
.canBeShared(forum.getId(), contact1From0));
assertEquals(SHAREABLE, forumSharingManager0
.getSharingStatus(forum.getId(), contact1From0));
// invitee that left can not yet share again
assertFalse(forumSharingManager1
.canBeShared(forum.getId(), contact0From1));
try {
forumSharingManager1.getSharingStatus(forum.getId(), contact0From1);
fail();
} catch (ProtocolStateException e) {
// expected
}
// sharer responds with leave message
sync0To1(1, true);
// invitee that left can now share again
assertTrue(forumSharingManager1
.canBeShared(forum.getId(), contact0From1));
assertEquals(SHAREABLE, forumSharingManager1
.getSharingStatus(forum.getId(), contact0From1));
}
@Test
@@ -405,13 +416,15 @@ public class ForumSharingIntegrationTest
assertFalse(forumSharingManager1.getSharedWith(forum.getId())
.contains(contact0));
// forum can be re-shared by invitee now
assertTrue(forumSharingManager1.canBeShared(forum.getId(), c0));
assertEquals(SHAREABLE,
forumSharingManager1.getSharingStatus(forum.getId(), c0));
// invitee responds with LEAVE message
sync1To0(1, true);
// sharer can share forum again as well now
assertTrue(forumSharingManager0.canBeShared(forum.getId(), c1));
assertEquals(SHAREABLE,
forumSharingManager0.getSharingStatus(forum.getId(), c1));
// invitee also un-subscribes forum without effect
forumManager1.removeForum(forum);
@@ -621,10 +634,10 @@ public class ForumSharingIntegrationTest
addContacts1And2();
// forum can be shared with contacts again
assertTrue(forumSharingManager0
.canBeShared(forum.getId(), contact1From0));
assertTrue(forumSharingManager0
.canBeShared(forum.getId(), contact2From0));
assertEquals(SHAREABLE, forumSharingManager0
.getSharingStatus(forum.getId(), contact1From0));
assertEquals(SHAREABLE, forumSharingManager0
.getSharingStatus(forum.getId(), contact2From0));
// send invitation
forumSharingManager0

View File

@@ -0,0 +1,95 @@
<feed xmlns="http://www.w3.org/2005/Atom">
<title>News - Briar</title>
<link href="https://briarproject.org/news/index.xml" rel="self"/>
<link href="https://briarproject.org/news/"/>
<updated>2022-12-05T12:00:00+00:00</updated>
<id>https://briarproject.org/news/</id>
<author>
<name>The Briar Team</name>
</author>
<generator>Hugo -- gohugo.io</generator>
<entry>
<title type="html"><![CDATA[Briar Desktop got another round of funding]]></title>
<link href="https://briarproject.org/news/2022-briar-desktop-nlnet-funding/"/>
<id>https://briarproject.org/news/2022-briar-desktop-nlnet-funding/</id>
<published>2022-12-05T12:00:00+00:00</published>
<updated>2022-12-05T12:00:00+00:00</updated>
<summary><![CDATA[Briar Desktop got another round of funding So far, Briar is only available as an Android app, which is preventing some organizations that work in repressive environments from using it as a secure communications tool and considering it as a more secure alternative to email. To remedy that, we have started working on a desktop app in September 2021 that is supposed to work on three major operating systems: Linux, macOS and Windows.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar is available on Google Play again]]></title>
<link href="https://briarproject.org/news/2022-briar-removed-from-google-play/"/>
<id>https://briarproject.org/news/2022-briar-removed-from-google-play/</id>
<published>2022-02-28T13:20:00+00:00</published>
<updated>2022-02-28T13:20:00+00:00</updated>
<summary><![CDATA[Status update Update (February 28, 13:20 UTC): Briar is available on Google Play again.
Briar was briefly removed from Google Play because we didn&rsquo;t provide Google&rsquo;s review team with a username and password for testing the app. We provided Google with a username and password for testing and the app is now available again.
About Briar Briar is a messaging app designed for activists, journalists, and anyone else who needs a safe, easy and robust way to communicate.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar 1.4 released - offline app sharing, message transfer via SD cards and USB sticks]]></title>
<link href="https://briarproject.org/news/2021-briar-1.4-released/"/>
<id>https://briarproject.org/news/2021-briar-1.4-released/</id>
<published>2021-11-15T00:00:00+00:00</published>
<updated>2021-11-15T00:00:00+00:00</updated>
<summary><![CDATA[Press release The Briar Project released version 1.4 of its Android app today. This release adds a couple of new features, highlighted below.
First of all, users can now share the app offline. Prior to this release, the only way to get the app was to to download it from the internet, which requires an internet connection. Now, it is possible to share the app offline to others who don&rsquo;t have it installed.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar 1.3 released - image attachments, profile images and disappearing messages]]></title>
<link href="https://briarproject.org/news/2021-briar-1.3-released/"/>
<id>https://briarproject.org/news/2021-briar-1.3-released/</id>
<published>2021-06-07T00:00:00+00:00</published>
<updated>2021-06-07T00:00:00+00:00</updated>
<summary><![CDATA[Press release The Briar Project released version 1.3 of its Android app today. Thanks to support from eQualit.ie, this release adds several new features that have been requested by many users over the years.
With today&rsquo;s release, users can upload profile pictures that will be visible only to their contacts.
Lots of people have asked for a way to send images via Briar. We listened! This release adds the ability to send images in private conversations.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar 1.2 released, contacts can now be added by exchanging links]]></title>
<link href="https://briarproject.org/news/2019-briar-1.2-released-remote-contacts/"/>
<id>https://briarproject.org/news/2019-briar-1.2-released-remote-contacts/</id>
<published>2019-11-06T00:00:00+00:00</published>
<updated>2019-11-06T00:00:00+00:00</updated>
<summary><![CDATA[Press Release The Briar Project released version 1.2 of its Android app today. This release allows users to add each other securely by exchanging links. Previously users needed to meet in person or ask a mutual contact to introduce them.
Most messenger apps find your contacts by uploading your phone&rsquo;s contact list to a server. Since Briar is designed to protect metadata and contact relationships, it instead uses the Tor network to connect directly and securely to the person you&rsquo;re adding, without revealing your contact list to anyone.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar 1.1 released with dark theme, new emoji and more]]></title>
<link href="https://briarproject.org/news/2018-briar-1.1-released/"/>
<id>https://briarproject.org/news/2018-briar-1.1-released/</id>
<published>2018-09-14T00:00:00+00:00</published>
<updated>2018-09-14T00:00:00+00:00</updated>
<summary><![CDATA[Press Release The Briar Project released version 1.1 of its Android app today. This release adds new features following the app&rsquo;s first public release in May.
Thanks to support from the Open Technology Fund, the new release has a dark theme designed by Ura Design. Users can switch between the light and dark themes, or use an automatic mode that activates the dark theme at night. The conversation screen has also been redesigned, with rounded message bubbles and a new color scheme.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar - Secure P2P Messenger Releases First Version, Receives New Funding]]></title>
<link href="https://briarproject.org/news/2018-1.0-released-new-funding/"/>
<id>https://briarproject.org/news/2018-1.0-released-new-funding/</id>
<published>2018-05-09T00:00:00+00:00</published>
<updated>2018-05-09T00:00:00+00:00</updated>
<summary><![CDATA[Press Release The peer-to-peer messenger Briar released its first stable version today. It is available for Android devices from Google Play or F-Droid. This release follows a security audit and a 10 month public beta period during which many bugs were fixed and lots of feedback was received. The Briar Project wishes to thank all beta testers for their contributions.
The development of Briar will continue with help from the Open Technology Fund, which has previously supported the project as part of its mission to promote internet freedom worldwide.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar - Darknet Messenger Releases Beta, Passes Security Audit]]></title>
<link href="https://briarproject.org/news/2017-beta-released-security-audit/"/>
<id>https://briarproject.org/news/2017-beta-released-security-audit/</id>
<published>2017-07-21T00:00:00+00:00</published>
<updated>2017-07-21T00:00:00+00:00</updated>
<summary><![CDATA[Press Release After extensive private beta tests, the first public beta of Briar was released today. Briar is a secure messaging app for Android.
Unlike other popular apps, Briar does not require servers to work. It connects users directly using a peer-to-peer network. This makes it resistant to censorship and allows it to work even without internet access.
The app encrypts all data end-to-end and also hides metadata about who is communicating.]]></summary>
</entry>
</feed>

View File

@@ -32,13 +32,14 @@ buildscript {
// okhttp 3.12.x is supported until end of 2021, newer versions need minSdk 21
okhttp_version = "3.12.13"
jackson_version = "2.13.4"
tor_version = "0.4.5.14"
tor_version = "0.4.7.13"
obfs4proxy_version = "0.0.14-tor1"
snowflake_version = "2.3.1"
jsoup_version = '1.15.3'
bouncy_castle_version = '1.71'
junit_version = "4.13.2"
jmock_version = '2.12.0'
mockwebserver_version = '4.9.3'
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'