Compare commits

...

21 Commits

Author SHA1 Message Date
akwizgran
6b022afa67 Bump version numbers for 1.1.8 release. 2019-06-28 14:48:00 +01:00
akwizgran
e8b454b25b Update translations. 2019-06-28 14:47:03 +01:00
Torsten Grote
54c05b5ffe Merge branch '1606-bump-client-minor-version' into 'master'
Bump client minor version to avoid triggering crash

Closes #1606

See merge request briar/briar!1150
2019-06-28 13:28:37 +00:00
akwizgran
d145a082f5 Bump client minor version to avoid triggering crash. 2019-06-28 14:07:28 +01:00
akwizgran
4fd012c31a Merge branch 'compress-images' into 'master'
Compress images

See merge request briar/briar!1147
2019-06-26 14:21:24 +00:00
akwizgran
95d06770bf Rename 'scale' to 'inSampleSize' for clarity. 2019-06-26 14:36:40 +01:00
akwizgran
428247b7b2 Initialise result LiveData before starting task. 2019-06-26 14:31:40 +01:00
akwizgran
a921361a56 Inject ImageSizeCalculator. 2019-06-26 12:40:28 +01:00
akwizgran
fe7dfa721e Compress image attachments. 2019-06-25 16:55:09 +01:00
akwizgran
92eb06a9e9 Refactor attachment creation to use injection. 2019-06-25 16:29:54 +01:00
Torsten Grote
5beed1a748 Merge branch '1594-preview-fails-to-load' into 'master'
Use a fresh LiveData for each attachment creation task

Closes #1594

See merge request briar/briar!1144
2019-06-20 14:05:43 +00:00
Torsten Grote
774047d856 Merge branch '1585-check-attachment-content-type' into 'master'
Improve handling of missing attachments in UI

See merge request briar/briar!1142
2019-06-20 14:04:02 +00:00
Torsten Grote
fc28e7aa88 Merge branch 'nickname-nitpicks' into 'master'
Nickname nitpicks

See merge request briar/briar!1143
2019-06-20 13:41:25 +00:00
Torsten Grote
78459499b2 Merge branch '1593-qr-code-assertion-error' into 'master'
Keep enum methods used by ZXing

Closes #1593

See merge request briar/briar!1146
2019-06-19 23:45:49 +00:00
akwizgran
c2973608d7 Keep enum methods used by ZXing. 2019-06-19 16:36:39 +01:00
akwizgran
be1c33cb42 Use a fresh LiveData for each attachment creation task. 2019-06-19 13:43:04 +01:00
akwizgran
c955466bda Load missing attachments when they arrive. 2019-06-19 12:47:18 +01:00
akwizgran
593a0c4632 Improve handling of missing and invalid attachments. 2019-06-19 11:23:57 +01:00
akwizgran
ed20b2d8d6 Use attachment header to retrieve attachment. 2019-06-19 10:57:13 +01:00
akwizgran
9ab9e02f8a Trim whitespace from nicknames (useful for auto-complete). 2019-06-18 17:24:08 +01:00
akwizgran
3f70ae3c8c Use same input type for nicknames everywhere. 2019-06-18 17:19:39 +01:00
39 changed files with 1246 additions and 643 deletions

View File

@@ -11,8 +11,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 26 targetSdkVersion 26
versionCode 10107 versionCode 10108
versionName "1.1.7" versionName "1.1.8"
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

View File

@@ -22,8 +22,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 26 targetSdkVersion 26
versionCode 10107 versionCode 10108
versionName "1.1.7" versionName "1.1.8"
applicationId "org.briarproject.briar.android" applicationId "org.briarproject.briar.android"
buildConfigField "String", "GitHash", buildConfigField "String", "GitHash",
"\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\"" "\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\""

View File

@@ -5,6 +5,10 @@
# QR codes # QR codes
-keep class com.google.zxing.Result -keep class com.google.zxing.Result
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# RSS libraries # RSS libraries
-keep,includedescriptorclasses class com.rometools.rome.feed.synd.impl.** { *; } -keep,includedescriptorclasses class com.rometools.rome.feed.synd.impl.** { *; }

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule; import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule; import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest; import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -13,6 +14,7 @@ import dagger.Component;
@Singleton @Singleton
@Component(modules = { @Component(modules = {
AppModule.class, AppModule.class,
AttachmentModule.class,
BriarCoreModule.class, BriarCoreModule.class,
BrambleAndroidModule.class, BrambleAndroidModule.class,
BriarAccountModule.class, BriarAccountModule.class,

View File

@@ -47,15 +47,17 @@ public class AttachmentRetrieverIntegrationTest {
); );
private final MessageId msgId = new MessageId(getRandomId()); private final MessageId msgId = new MessageId(getRandomId());
private final ImageHelper imageHelper = new ImageHelperImpl();
private final AttachmentRetriever retriever = private final AttachmentRetriever retriever =
new AttachmentRetriever(null, dimensions); new AttachmentRetrieverImpl(null, dimensions, imageHelper,
new ImageSizeCalculator(imageHelper));
@Test @Test
public void testSmallJpegImage() throws Exception { public void testSmallJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(smallKitten); InputStream is = getUrlInputStream(smallKitten);
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId()); assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth()); assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight()); assertEquals(240, item.getHeight());
@@ -70,8 +72,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testBigJpegImage() throws Exception { public void testBigJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(originalKitten); InputStream is = getUrlInputStream(originalKitten);
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId()); assertEquals(msgId, item.getMessageId());
assertEquals(1728, item.getWidth()); assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight()); assertEquals(2592, item.getHeight());
@@ -86,8 +88,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallPngImage() throws Exception { public void testSmallPngImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/png"); AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
InputStream is = getUrlInputStream(pngKitten); InputStream is = getUrlInputStream(pngKitten);
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId()); assertEquals(msgId, item.getMessageId());
assertEquals(737, item.getWidth()); assertEquals(737, item.getWidth());
assertEquals(510, item.getHeight()); assertEquals(510, item.getHeight());
@@ -102,8 +104,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testUberGif() throws Exception { public void testUberGif() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(uberGif); InputStream is = getUrlInputStream(uberGif);
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth()); assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight()); assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -117,8 +119,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testLottaPixels() throws Exception { public void testLottaPixels() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(lottaPixel); InputStream is = getUrlInputStream(lottaPixel);
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(64250, item.getWidth()); assertEquals(64250, item.getWidth());
assertEquals(64250, item.getHeight()); assertEquals(64250, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -132,8 +134,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testImageIoCrash() throws Exception { public void testImageIoCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(imageIoCrash); InputStream is = getUrlInputStream(imageIoCrash);
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1184, item.getWidth()); assertEquals(1184, item.getWidth());
assertEquals(448, item.getHeight()); assertEquals(448, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -147,8 +149,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testGimpCrash() throws Exception { public void testGimpCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(gimpCrash); InputStream is = getUrlInputStream(gimpCrash);
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth()); assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight()); assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -162,8 +164,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testOptiPngAfl() throws Exception { public void testOptiPngAfl() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(optiPngAfl); InputStream is = getUrlInputStream(optiPngAfl);
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(32, item.getWidth()); assertEquals(32, item.getWidth());
assertEquals(32, item.getHeight()); assertEquals(32, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -177,8 +179,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testLibrawError() throws Exception { public void testLibrawError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(librawError); InputStream is = getUrlInputStream(librawError);
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertTrue(item.hasError()); assertTrue(item.hasError());
} }
@@ -186,8 +188,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallAnimatedGifMaxDimensions() throws Exception { public void testSmallAnimatedGifMaxDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated.gif"); InputStream is = getAssetInputStream("animated.gif");
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(65535, item.getWidth()); assertEquals(65535, item.getWidth());
assertEquals(65535, item.getHeight()); assertEquals(65535, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -201,8 +203,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallAnimatedGifHugeDimensions() throws Exception { public void testSmallAnimatedGifHugeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated2.gif"); InputStream is = getAssetInputStream("animated2.gif");
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(10000, item.getWidth()); assertEquals(10000, item.getWidth());
assertEquals(10000, item.getHeight()); assertEquals(10000, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -216,8 +218,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallGifLargeDimensions() throws Exception { public void testSmallGifLargeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("error_large.gif"); InputStream is = getAssetInputStream("error_large.gif");
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(16384, item.getWidth()); assertEquals(16384, item.getWidth());
assertEquals(16384, item.getHeight()); assertEquals(16384, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -231,8 +233,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testHighError() throws Exception { public void testHighError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_high.jpg"); InputStream is = getAssetInputStream("error_high.jpg");
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth()); assertEquals(1, item.getWidth());
assertEquals(10000, item.getHeight()); assertEquals(10000, item.getHeight());
assertEquals(dimensions.minWidth, item.getThumbnailWidth()); assertEquals(dimensions.minWidth, item.getThumbnailWidth());
@@ -246,8 +248,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testWideError() throws Exception { public void testWideError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_wide.jpg"); InputStream is = getAssetInputStream("error_wide.jpg");
Attachment a = new Attachment(is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1920, item.getWidth()); assertEquals(1920, item.getWidth());
assertEquals(1, item.getHeight()); assertEquals(1, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());

View File

@@ -30,6 +30,7 @@ import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider; import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.briar.BriarCoreEagerSingletons; import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader; import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.login.SignInReminderReceiver; import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.reporting.BriarReportSender; import org.briarproject.briar.android.reporting.BriarReportSender;
@@ -68,7 +69,8 @@ import dagger.Component;
BriarCoreModule.class, BriarCoreModule.class,
BrambleAndroidModule.class, BrambleAndroidModule.class,
BriarAccountModule.class, BriarAccountModule.class,
AppModule.class AppModule.class,
AttachmentModule.class
}) })
public interface AndroidComponent public interface AndroidComponent
extends BrambleCoreEagerSingletons, BrambleAndroidEagerSingletons, extends BrambleCoreEagerSingletons, BrambleAndroidEagerSingletons,

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.account;
import android.os.Bundle; import android.os.Bundle;
import android.support.design.widget.TextInputEditText; import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout; import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -10,7 +11,6 @@ import android.widget.Button;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
@@ -20,6 +20,7 @@ import static android.view.inputmethod.EditorInfo.IME_ACTION_NEXT;
import static android.view.inputmethod.EditorInfo.IME_ACTION_NONE; import static android.view.inputmethod.EditorInfo.IME_ACTION_NONE;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.toUtf8;
import static org.briarproject.briar.android.util.UiUtils.setError; import static org.briarproject.briar.android.util.UiUtils.setError;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard; import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@@ -77,7 +78,7 @@ public class AuthorNameFragment extends SetupFragment {
@Override @Override
public void onTextChanged(CharSequence authorName, int i, int i1, int i2) { public void onTextChanged(CharSequence authorName, int i, int i1, int i2) {
int authorNameLength = StringUtils.toUtf8(authorName.toString()).length; int authorNameLength = toUtf8(authorName.toString().trim()).length;
boolean error = authorNameLength > MAX_AUTHOR_NAME_LENGTH; boolean error = authorNameLength > MAX_AUTHOR_NAME_LENGTH;
setError(authorNameWrapper, getString(R.string.name_too_long), error); setError(authorNameWrapper, getString(R.string.name_too_long), error);
boolean enabled = authorNameLength > 0 && !error; boolean enabled = authorNameLength > 0 && !error;
@@ -89,8 +90,11 @@ public class AuthorNameFragment extends SetupFragment {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
setupController.setAuthorName(authorNameInput.getText().toString()); Editable text = authorNameInput.getText();
setupController.showPasswordFragment(); if (text != null) {
setupController.setAuthorName(text.toString().trim());
setupController.showPasswordFragment();
}
} }
} }

View File

@@ -1,6 +1,8 @@
package org.briarproject.briar.android.attachment; package org.briarproject.briar.android.attachment;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory.Options;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@@ -12,11 +14,17 @@ import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException; import org.jsoup.UnsupportedMimeTypeException;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collection; import java.util.Collection;
import java.util.logging.Logger; import java.util.logging.Logger;
import static android.graphics.Bitmap.CompressFormat.JPEG;
import static android.graphics.BitmapFactory.decodeStream;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose; import static org.briarproject.bramble.util.IoUtils.tryToClose;
@@ -24,6 +32,7 @@ import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES; import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault @NotNullByDefault
class AttachmentCreationTask { class AttachmentCreationTask {
@@ -31,8 +40,11 @@ class AttachmentCreationTask {
private static Logger LOG = private static Logger LOG =
getLogger(AttachmentCreationTask.class.getName()); getLogger(AttachmentCreationTask.class.getName());
private static final int MAX_ATTACHMENT_DIMENSION = 1000;
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
private final ContentResolver contentResolver; private final ContentResolver contentResolver;
private final ImageSizeCalculator imageSizeCalculator;
private final GroupId groupId; private final GroupId groupId;
private final Collection<Uri> uris; private final Collection<Uri> uris;
private final boolean needsSize; private final boolean needsSize;
@@ -43,24 +55,26 @@ class AttachmentCreationTask {
AttachmentCreationTask(MessagingManager messagingManager, AttachmentCreationTask(MessagingManager messagingManager,
ContentResolver contentResolver, ContentResolver contentResolver,
AttachmentCreator attachmentCreator, GroupId groupId, AttachmentCreator attachmentCreator,
Collection<Uri> uris, boolean needsSize) { ImageSizeCalculator imageSizeCalculator,
GroupId groupId, Collection<Uri> uris, boolean needsSize) {
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
this.contentResolver = contentResolver; this.contentResolver = contentResolver;
this.imageSizeCalculator = imageSizeCalculator;
this.groupId = groupId; this.groupId = groupId;
this.uris = uris; this.uris = uris;
this.needsSize = needsSize; this.needsSize = needsSize;
this.attachmentCreator = attachmentCreator; this.attachmentCreator = attachmentCreator;
} }
public void cancel() { void cancel() {
canceled = true; canceled = true;
attachmentCreator = null; attachmentCreator = null;
} }
@IoExecutor @IoExecutor
public void storeAttachments() { void storeAttachments() {
for (Uri uri: uris) processUri(uri); for (Uri uri : uris) processUri(uri);
AttachmentCreator attachmentCreator = this.attachmentCreator; AttachmentCreator attachmentCreator = this.attachmentCreator;
if (!canceled && attachmentCreator != null) if (!canceled && attachmentCreator != null)
attachmentCreator.onAttachmentCreationFinished(); attachmentCreator.onAttachmentCreationFinished();
@@ -98,6 +112,8 @@ class AttachmentCreationTask {
} }
InputStream is = contentResolver.openInputStream(uri); InputStream is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException(); if (is == null) throw new IOException();
is = compressImage(is, contentType);
contentType = "image/jpeg";
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
AttachmentHeader h = messagingManager AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is); .addLocalAttachment(groupId, timestamp, contentType, is);
@@ -113,4 +129,48 @@ class AttachmentCreationTask {
return false; return false;
} }
private InputStream compressImage(InputStream is, String contentType)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
Bitmap bitmap = createBitmap(is, contentType);
for (int quality = 100; quality >= 0; quality -= 10) {
if (!bitmap.compress(JPEG, quality, out))
throw new IOException();
if (out.size() <= MAX_IMAGE_SIZE) {
if (LOG.isLoggable(INFO)) {
LOG.info("Compressed image to "
+ out.size() + " bytes, quality " + quality);
}
return new ByteArrayInputStream(out.toByteArray());
}
out.reset();
}
throw new IOException();
} finally {
tryToClose(is, LOG, WARNING);
}
}
private Bitmap createBitmap(InputStream is, String contentType)
throws IOException {
is = new BufferedInputStream(is);
Size size = imageSizeCalculator.getSize(is, contentType);
if (size.error) throw new IOException();
if (LOG.isLoggable(INFO))
LOG.info("Original image size: " + size.width + "x" + size.height);
int dimension = Math.max(size.width, size.height);
int inSampleSize = 1;
while (dimension > MAX_ATTACHMENT_DIMENSION) {
inSampleSize *= 2;
dimension /= 2;
}
if (LOG.isLoggable(INFO))
LOG.info("Scaling attachment by factor of " + inSampleSize);
Options options = new Options();
options.inSampleSize = inSampleSize;
Bitmap bitmap = decodeStream(is, null, options);
if (bitmap == null) throw new IOException();
return bitmap;
}
} }

View File

@@ -1,82 +1,24 @@
package org.briarproject.briar.android.attachment; package org.briarproject.briar.android.attachment;
import android.app.Application;
import android.arch.lifecycle.LiveData; import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.FileTooBigException;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault @NotNullByDefault
public class AttachmentCreator { public interface AttachmentCreator {
private static Logger LOG = getLogger(AttachmentCreator.class.getName());
private final Application app;
@IoExecutor
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
new CopyOnWriteArrayList<>();
private final MutableLiveData<AttachmentResult> result =
new MutableLiveData<>();
@Nullable
private AttachmentCreationTask task;
public AttachmentCreator(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager, AttachmentRetriever retriever) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.retriever = retriever;
}
@UiThread @UiThread
public LiveData<AttachmentResult> storeAttachments( LiveData<AttachmentResult> storeAttachments(LiveData<GroupId> groupId,
LiveData<GroupId> groupId, Collection<Uri> newUris) { Collection<Uri> newUris);
if (task != null || !uris.isEmpty())
throw new IllegalStateException();
uris.addAll(newUris);
observeForeverOnce(groupId, id -> {
if (id == null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(messagingManager,
app.getContentResolver(), this, id, uris, needsSize);
ioExecutor.execute(() -> task.storeAttachments());
});
return result;
}
/** /**
* This should be only called after configuration changes. * This should be only called after configuration changes.
@@ -84,68 +26,10 @@ public class AttachmentCreator {
* They are already being created and returned by the existing LiveData. * They are already being created and returned by the existing LiveData.
*/ */
@UiThread @UiThread
public LiveData<AttachmentResult> getLiveAttachments() { LiveData<AttachmentResult> getLiveAttachments();
if (task == null || uris.isEmpty())
throw new IllegalStateException();
// A task is already running. It will update the result LiveData.
// So nothing more to do here.
return result;
}
@IoExecutor
void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize) {
// get and cache AttachmentItem for ImagePreview
try {
Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.getAttachmentItem(h, a, needsSize);
if (item.hasError()) throw new IOException();
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, item);
itemResults.add(itemResult);
result.postValue(getResult(false));
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onAttachmentError(uri, e);
}
}
@IoExecutor
void onAttachmentError(Uri uri, Throwable t) {
// get error message
String errorMsg;
if (t instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
errorMsg = app.getString(
R.string.image_attach_error_invalid_mime_type, mimeType);
} else if (t instanceof FileTooBigException) {
int mb = MAX_IMAGE_SIZE / 1024 / 1024;
errorMsg = app.getString(R.string.image_attach_error_too_big, mb);
} else {
errorMsg = null; // generic error
}
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, errorMsg);
itemResults.add(itemResult);
result.postValue(getResult(false));
// expect to receive a cancel from the UI
}
@IoExecutor
void onAttachmentCreationFinished() {
result.postValue(getResult(true));
}
@UiThread @UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() { List<AttachmentHeader> getAttachmentHeadersForSending();
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
headers.add(itemResult.getItem().getHeader());
}
return headers;
}
/** /**
* Marks the attachments as sent and adds the items to the cache for display * Marks the attachments as sent and adds the items to the cache for display
@@ -153,66 +37,24 @@ public class AttachmentCreator {
* @param id The MessageId of the sent message. * @param id The MessageId of the sent message.
*/ */
@UiThread @UiThread
public void onAttachmentsSent(MessageId id) { void onAttachmentsSent(MessageId id);
List<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState();
}
/** /**
* Needs to be called when created attachments will not be sent anymore. * Needs to be called when created attachments will not be sent anymore.
*/ */
@UiThread @UiThread
public void cancel() { void cancel();
if (task == null) throw new AssertionError();
task.cancel();
deleteUnsentAttachments();
resetState();
}
@UiThread @UiThread
private void resetState() { void deleteUnsentAttachments();
task = null;
uris.clear();
itemResults.clear();
result.setValue(null);
}
@UiThread @IoExecutor
public void deleteUnsentAttachments() { void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
// Make a copy for the IoExecutor as we clear the itemResults soon boolean needsSize);
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() != null)
headers.add(itemResult.getItem().getHeader());
}
ioExecutor.execute(() -> {
for (AttachmentHeader header : headers) {
try {
messagingManager.removeAttachment(header);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
private AttachmentResult getResult(boolean finished) { @IoExecutor
// Make a copy of the list, void onAttachmentError(Uri uri, Throwable t);
// because our copy will continue to change in the background.
// (As it's a CopyOnWriteArrayList,
// the code that receives the result can safely do simple things
// like iterating over the list,
// but anything that involves calling more than one list method
// is still unsafe.)
Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
return new AttachmentResult(items, finished);
}
} @IoExecutor
void onAttachmentCreationFinished();
}

View File

@@ -0,0 +1,233 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.FileTooBigException;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
class AttachmentCreatorImpl implements AttachmentCreator {
private static Logger LOG =
getLogger(AttachmentCreatorImpl.class.getName());
private final Application app;
@IoExecutor
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
private final ImageSizeCalculator imageSizeCalculator;
private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
new CopyOnWriteArrayList<>();
@Nullable
private AttachmentCreationTask task;
@Nullable
private volatile MutableLiveData<AttachmentResult> result;
@Inject
AttachmentCreatorImpl(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager, AttachmentRetriever retriever,
ImageSizeCalculator imageSizeCalculator) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.retriever = retriever;
this.imageSizeCalculator = imageSizeCalculator;
}
@Override
@UiThread
public LiveData<AttachmentResult> storeAttachments(
LiveData<GroupId> groupId, Collection<Uri> newUris) {
if (task != null || result != null || !uris.isEmpty())
throw new IllegalStateException();
MutableLiveData<AttachmentResult> result = new MutableLiveData<>();
this.result = result;
uris.addAll(newUris);
observeForeverOnce(groupId, id -> {
if (id == null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(messagingManager,
app.getContentResolver(), this, imageSizeCalculator, id,
uris, needsSize);
ioExecutor.execute(() -> task.storeAttachments());
});
return result;
}
@Override
@UiThread
public LiveData<AttachmentResult> getLiveAttachments() {
MutableLiveData<AttachmentResult> result = this.result;
if (task == null || result == null || uris.isEmpty())
throw new IllegalStateException();
// A task is already running. It will update the result LiveData.
// So nothing more to do here.
return result;
}
@Override
@IoExecutor
public void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize) {
// get and cache AttachmentItem for ImagePreview
try {
Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.getAttachmentItem(a, needsSize);
if (item.hasError()) throw new IOException();
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, item);
itemResults.add(itemResult);
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(false));
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onAttachmentError(uri, e);
}
}
@Override
@IoExecutor
public void onAttachmentError(Uri uri, Throwable t) {
// get error message
String errorMsg;
if (t instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
errorMsg = app.getString(
R.string.image_attach_error_invalid_mime_type, mimeType);
} else if (t instanceof FileTooBigException) {
int mb = MAX_IMAGE_SIZE / 1024 / 1024;
errorMsg = app.getString(R.string.image_attach_error_too_big, mb);
} else {
errorMsg = null; // generic error
}
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, errorMsg);
itemResults.add(itemResult);
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(false));
// expect to receive a cancel from the UI
}
@Override
@IoExecutor
public void onAttachmentCreationFinished() {
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(true));
}
@Override
@UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() {
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
headers.add(itemResult.getItem().getHeader());
}
return headers;
}
@Override
@UiThread
public void onAttachmentsSent(MessageId id) {
List<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState();
}
@Override
@UiThread
public void cancel() {
if (task == null) throw new AssertionError();
task.cancel();
deleteUnsentAttachments();
resetState();
}
@UiThread
private void resetState() {
task = null;
uris.clear();
itemResults.clear();
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) {
result.setValue(null);
this.result = null;
}
}
@Override
@UiThread
public void deleteUnsentAttachments() {
// Make a copy for the IoExecutor as we clear the itemResults soon
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() != null)
headers.add(itemResult.getItem().getHeader());
}
ioExecutor.execute(() -> {
for (AttachmentHeader header : headers) {
try {
messagingManager.removeAttachment(header);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
private AttachmentResult getResult(boolean finished) {
// Make a copy of the list,
// because our copy will continue to change in the background.
// (As it's a CopyOnWriteArrayList,
// the code that receives the result can safely do simple things
// like iterating over the list,
// but anything that involves calling more than one list method
// is still unsafe.)
Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
return new AttachmentResult(items, finished);
}
}

View File

@@ -10,7 +10,7 @@ import javax.annotation.concurrent.Immutable;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public class AttachmentDimensions { class AttachmentDimensions {
final int defaultSize; final int defaultSize;
final int minWidth, maxWidth; final int minWidth, maxWidth;
@@ -26,7 +26,7 @@ public class AttachmentDimensions {
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
} }
public static AttachmentDimensions getAttachmentDimensions(Resources res) { static AttachmentDimensions getAttachmentDimensions(Resources res) {
int defaultSize = int defaultSize =
res.getDimensionPixelSize(R.dimen.message_bubble_image_default); res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
int minWidth = res.getDimensionPixelSize( int minWidth = res.getDimensionPixelSize(

View File

@@ -68,7 +68,7 @@ public class AttachmentItem implements Parcelable {
header = new AttachmentHeader(messageId, mimeType); header = new AttachmentHeader(messageId, mimeType);
} }
AttachmentHeader getHeader() { public AttachmentHeader getHeader() {
return header; return header;
} }

View File

@@ -4,12 +4,14 @@ import android.arch.lifecycle.LiveData;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@UiThread @UiThread
@NotNullByDefault
public interface AttachmentManager { public interface AttachmentManager {
LiveData<AttachmentResult> storeAttachments(Collection<Uri> uri, LiveData<AttachmentResult> storeAttachments(Collection<Uri> uri,

View File

@@ -0,0 +1,43 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions;
@Module
public class AttachmentModule {
@Provides
ImageHelper provideImageHelper(ImageHelperImpl imageHelper) {
return imageHelper;
}
@Provides
ImageSizeCalculator provideImageSizeCalculator(ImageHelper imageHelper) {
return new ImageSizeCalculator(imageHelper);
}
@Provides
AttachmentDimensions provideAttachmentDimensions(Application app) {
return getAttachmentDimensions(app.getResources());
}
@Provides
@Singleton
AttachmentRetriever provideAttachmentRetriever(
AttachmentRetrieverImpl attachmentRetriever) {
return attachmentRetriever;
}
@Provides
@Singleton
AttachmentCreator provideAttachmentCreator(
AttachmentCreatorImpl attachmentCreator) {
return attachmentCreator;
}
}

View File

@@ -1,276 +1,29 @@
package org.briarproject.briar.android.attachment; package org.briarproject.briar.android.attachment;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.media.ExifInterface;
import android.webkit.MimeTypeMap;
import com.bumptech.glide.util.MarkEnforcingInputStream;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90;
import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE;
import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE;
import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH;
import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH;
import static android.support.media.ExifInterface.TAG_ORIENTATION;
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.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@NotNullByDefault @NotNullByDefault
public class AttachmentRetriever { public interface AttachmentRetriever {
private static final Logger LOG = void cachePut(MessageId messageId, List<AttachmentItem> attachments);
getLogger(AttachmentRetriever.class.getName());
private static final int READ_LIMIT = 1024 * 8192;
private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final int defaultSize;
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
@VisibleForTesting
AttachmentRetriever(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper) {
this.messagingManager = messagingManager;
this.imageHelper = imageHelper;
defaultSize = dimensions.defaultSize;
minWidth = dimensions.minWidth;
maxWidth = dimensions.maxWidth;
minHeight = dimensions.minHeight;
maxHeight = dimensions.maxHeight;
}
public AttachmentRetriever(MessagingManager messagingManager,
AttachmentDimensions dimensions) {
this(messagingManager, dimensions, new ImageHelper() {
@Override
public DecodeResult decodeStream(InputStream is) {
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
String mimeType = options.outMimeType;
if (mimeType == null) mimeType = "";
return new DecodeResult(options.outWidth, options.outHeight,
mimeType);
}
@Nullable
@Override
public String getExtensionFromMimeType(String mimeType) {
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
return mimeTypeMap.getExtensionFromMimeType(mimeType);
}
});
}
public void cachePut(MessageId messageId, List<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
@Nullable @Nullable
public List<AttachmentItem> cacheGet(MessageId messageId) { List<AttachmentItem> cacheGet(MessageId messageId);
return attachmentCache.get(messageId);
}
@DatabaseExecutor Attachment getMessageAttachment(AttachmentHeader h) throws DbException;
public List<Pair<AttachmentHeader, Attachment>> getMessageAttachments(
List<AttachmentHeader> headers) throws DbException {
long start = now();
List<Pair<AttachmentHeader, Attachment>> attachments =
new ArrayList<>(headers.size());
for (AttachmentHeader h : headers) {
Attachment a = messagingManager.getAttachment(h.getMessageId());
attachments.add(new Pair<>(h, a));
}
logDuration(LOG, "Loading attachments", start);
return attachments;
}
@DatabaseExecutor
Attachment getMessageAttachment(AttachmentHeader h) throws DbException {
return messagingManager.getAttachment(h.getMessageId());
}
/**
* Creates {@link AttachmentItem}s from the passed headers and Attachments.
* <p>
* Note: This closes the {@link Attachment}'s {@link InputStream}.
*/
public List<AttachmentItem> getAttachmentItems(
List<Pair<AttachmentHeader, Attachment>> attachments) {
boolean needsSize = attachments.size() == 1;
List<AttachmentItem> items = new ArrayList<>(attachments.size());
for (Pair<AttachmentHeader, Attachment> a : attachments) {
AttachmentItem item =
getAttachmentItem(a.getFirst(), a.getSecond(), needsSize);
items.add(item);
}
return items;
}
/** /**
* Creates an {@link AttachmentItem} from the {@link Attachment}'s * Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream} which will be closed when this method returns. * {@link InputStream} which will be closed when this method returns.
*/ */
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a, AttachmentItem getAttachmentItem(Attachment a, boolean needsSize);
boolean needsSize) {
if (!needsSize) {
String extension =
imageHelper.getExtensionFromMimeType(h.getContentType());
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
}
return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
}
Size size = new Size();
InputStream is = new MarkEnforcingInputStream(
new BufferedInputStream(a.getStream()));
is.mark(READ_LIMIT);
try {
// use exif to get size
if (h.getContentType().equals("image/jpeg")) {
size = getSizeFromExif(is);
}
} catch (IOException e) {
logException(LOG, WARNING, e);
}
try {
// use BitmapFactory to get size
if (size.error) {
is.reset();
// need to mark again to re-add read limit
is.mark(READ_LIMIT);
size = getSizeFromBitmap(is);
}
} catch (IOException e) {
logException(LOG, WARNING, e);
} finally {
tryToClose(is, LOG, WARNING);
}
// calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) {
thumbnailSize =
getThumbnailSize(size.width, size.height, size.mimeType);
}
// get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error;
if (!h.getContentType().equals(size.mimeType)) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("Header has different mime type (" +
h.getContentType() + ") than image (" + size.mimeType +
").");
}
hasError = true;
}
if (extension == null) extension = "";
return new AttachmentItem(h, size.width, size.height, extension,
thumbnailSize.width, thumbnailSize.height, hasError);
}
/**
* Gets the size of a JPEG {@link InputStream} if EXIF info is available.
*/
private Size getSizeFromExif(InputStream is) throws IOException {
ExifInterface exif = new ExifInterface(is);
// these can return 0 independent of default value
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0);
if (width == 0 || height == 0) return new Size();
int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0);
if (orientation == ORIENTATION_ROTATE_90 ||
orientation == ORIENTATION_ROTATE_270 ||
orientation == ORIENTATION_TRANSVERSE ||
orientation == ORIENTATION_TRANSPOSE) {
//noinspection SuspiciousNameCombination
return new Size(height, width, "image/jpeg");
}
return new Size(width, height, "image/jpeg");
}
/**
* Gets the size of any image {@link InputStream}.
*/
private Size getSizeFromBitmap(InputStream is) {
DecodeResult result = imageHelper.decodeStream(is);
if (result.width < 1 || result.height < 1) return new Size();
return new Size(result.width, result.height, result.mimeType);
}
private Size getThumbnailSize(int width, int height, String mimeType) {
float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
if (scaleFactor > 1) scaleFactor = 1f;
int thumbnailWidth = (int) (width * scaleFactor);
int thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) {
widthPercentage = minWidth / (float) width;
heightPercentage = minHeight / (float) height;
scaleFactor = Math.max(widthPercentage, heightPercentage);
thumbnailWidth = (int) (width * scaleFactor);
thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
}
return new Size(thumbnailWidth, thumbnailHeight, mimeType);
}
private static class Size {
private final int width;
private final int height;
private final String mimeType;
private final boolean error;
private Size(int width, int height, String mimeType) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.error = false;
}
private Size() {
this.width = 0;
this.height = 0;
this.mimeType = "";
this.error = true;
}
}
} }

View File

@@ -0,0 +1,129 @@
package org.briarproject.briar.android.attachment;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
@NotNullByDefault
class AttachmentRetrieverImpl implements AttachmentRetriever {
private static final Logger LOG =
getLogger(AttachmentRetrieverImpl.class.getName());
private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final ImageSizeCalculator imageSizeCalculator;
private final int defaultSize;
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
@Inject
AttachmentRetrieverImpl(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper,
ImageSizeCalculator imageSizeCalculator) {
this.messagingManager = messagingManager;
this.imageHelper = imageHelper;
this.imageSizeCalculator = imageSizeCalculator;
defaultSize = dimensions.defaultSize;
minWidth = dimensions.minWidth;
maxWidth = dimensions.maxWidth;
minHeight = dimensions.minHeight;
maxHeight = dimensions.maxHeight;
}
@Override
public void cachePut(MessageId messageId,
List<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
@Override
@Nullable
public List<AttachmentItem> cacheGet(MessageId messageId) {
return attachmentCache.get(messageId);
}
@Override
public Attachment getMessageAttachment(AttachmentHeader h)
throws DbException {
return messagingManager.getAttachment(h);
}
@Override
public AttachmentItem getAttachmentItem(Attachment a, boolean needsSize) {
AttachmentHeader h = a.getHeader();
if (!needsSize) {
String extension =
imageHelper.getExtensionFromMimeType(h.getContentType());
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
}
return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
}
InputStream is = new BufferedInputStream(a.getStream());
Size size = imageSizeCalculator.getSize(is, h.getContentType());
// calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) {
thumbnailSize =
getThumbnailSize(size.width, size.height, size.mimeType);
}
// get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error;
if (!h.getContentType().equals(size.mimeType)) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("Header has different mime type (" +
h.getContentType() + ") than image (" + size.mimeType +
").");
}
hasError = true;
}
if (extension == null) extension = "";
return new AttachmentItem(h, size.width, size.height, extension,
thumbnailSize.width, thumbnailSize.height, hasError);
}
private Size getThumbnailSize(int width, int height, String mimeType) {
float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
if (scaleFactor > 1) scaleFactor = 1f;
int thumbnailWidth = (int) (width * scaleFactor);
int thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) {
widthPercentage = minWidth / (float) width;
heightPercentage = minHeight / (float) height;
scaleFactor = Math.max(widthPercentage, heightPercentage);
thumbnailWidth = (int) (width * scaleFactor);
thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
}
return new Size(thumbnailWidth, thumbnailHeight, mimeType);
}
}

View File

@@ -7,7 +7,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream; import java.io.InputStream;
@NotNullByDefault @NotNullByDefault
interface ImageHelper { public interface ImageHelper {
DecodeResult decodeStream(InputStream is); DecodeResult decodeStream(InputStream is);

View File

@@ -0,0 +1,39 @@
package org.briarproject.briar.android.attachment;
import android.graphics.BitmapFactory;
import android.support.annotation.Nullable;
import android.webkit.MimeTypeMap;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@NotNullByDefault
class ImageHelperImpl implements ImageHelper {
@Inject
ImageHelperImpl() {
}
@Override
public DecodeResult decodeStream(InputStream is) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
String mimeType = options.outMimeType;
if (mimeType == null) mimeType = "";
return new DecodeResult(options.outWidth, options.outHeight,
mimeType);
}
@Nullable
@Override
public String getExtensionFromMimeType(String mimeType) {
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
return mimeTypeMap.getExtensionFromMimeType(mimeType);
}
}

View File

@@ -0,0 +1,94 @@
package org.briarproject.briar.android.attachment;
import android.support.media.ExifInterface;
import com.bumptech.glide.util.MarkEnforcingInputStream;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90;
import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE;
import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE;
import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH;
import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH;
import static android.support.media.ExifInterface.TAG_ORIENTATION;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class ImageSizeCalculator {
private static final Logger LOG =
getLogger(ImageSizeCalculator.class.getName());
private static final int READ_LIMIT = 1024 * 8192;
private final ImageHelper imageHelper;
ImageSizeCalculator(ImageHelper imageHelper) {
this.imageHelper = imageHelper;
}
Size getSize(InputStream is, String contentType) {
Size size = new Size();
is = new MarkEnforcingInputStream(is);
is.mark(READ_LIMIT);
if (contentType.equals("image/jpeg")) {
try {
// use exif to get size
size = getSizeFromExif(is);
is.reset();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
if (size.error) {
// need to mark again to re-add read limit
is.mark(READ_LIMIT);
try {
// use BitmapFactory to get size
size = getSizeFromBitmap(is);
is.reset();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
return size;
}
/**
* Gets the size of a JPEG {@link InputStream} if EXIF info is available.
*/
private Size getSizeFromExif(InputStream is) throws IOException {
ExifInterface exif = new ExifInterface(is);
// these can return 0 independent of default value
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0);
if (width == 0 || height == 0) return new Size();
int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0);
if (orientation == ORIENTATION_ROTATE_90 ||
orientation == ORIENTATION_ROTATE_270 ||
orientation == ORIENTATION_TRANSVERSE ||
orientation == ORIENTATION_TRANSPOSE) {
//noinspection SuspiciousNameCombination
return new Size(height, width, "image/jpeg");
}
return new Size(width, height, "image/jpeg");
}
/**
* Gets the size of any image {@link InputStream}.
*/
private Size getSizeFromBitmap(InputStream is) {
DecodeResult result = imageHelper.decodeStream(is);
if (result.width < 1 || result.height < 1) return new Size();
return new Size(result.width, result.height, result.mimeType);
}
}

View File

@@ -0,0 +1,23 @@
package org.briarproject.briar.android.attachment;
class Size {
final int width;
final int height;
final String mimeType;
final boolean error;
Size(int width, int height, String mimeType) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.error = false;
}
Size() {
this.width = 0;
this.height = 0;
this.mimeType = "";
this.error = true;
}
}

View File

@@ -82,19 +82,20 @@ public class NicknameFragment extends BaseFragment {
@Nullable @Nullable
private String getNicknameOrNull() { private String getNicknameOrNull() {
Editable name = contactNameInput.getText(); Editable text = contactNameInput.getText();
if (name == null || name.toString().trim().length() == 0) { if (text == null || text.toString().trim().length() == 0) {
contactNameLayout.setError(getString(R.string.nickname_missing)); contactNameLayout.setError(getString(R.string.nickname_missing));
contactNameInput.requestFocus(); contactNameInput.requestFocus();
return null; return null;
} }
if (utf8IsTooLong(name.toString(), MAX_AUTHOR_NAME_LENGTH)) { String name = text.toString().trim();
if (utf8IsTooLong(name, MAX_AUTHOR_NAME_LENGTH)) {
contactNameLayout.setError(getString(R.string.name_too_long)); contactNameLayout.setError(getString(R.string.name_too_long));
contactNameInput.requestFocus(); contactNameInput.requestFocus();
return null; return null;
} }
contactNameLayout.setError(null); contactNameLayout.setError(null);
return name.toString().trim(); return name;
} }
private void onAddButtonClicked() { private void onAddButtonClicked() {

View File

@@ -82,7 +82,7 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
} }
private void onSetButtonClicked() { private void onSetButtonClicked() {
String alias = aliasEditText.getText().toString(); String alias = aliasEditText.getText().toString().trim();
if (toUtf8(alias).length > MAX_AUTHOR_NAME_LENGTH) { if (toUtf8(alias).length > MAX_AUTHOR_NAME_LENGTH) {
aliasEditLayout.setError(getString(R.string.name_too_long)); aliasEditLayout.setError(getString(R.string.name_too_long));
} else { } else {

View File

@@ -37,6 +37,7 @@ import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.event.EventListener;
@@ -81,6 +82,7 @@ import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import java.util.ArrayList; import java.util.ArrayList;
@@ -107,10 +109,12 @@ import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.view.Gravity.RIGHT; import static android.view.Gravity.RIGHT;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.sort; import static java.util.Collections.sort;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
@@ -138,7 +142,7 @@ public class ConversationActivity extends BriarActivity
public static final String CONTACT_ID = "briar.CONTACT_ID"; public static final String CONTACT_ID = "briar.CONTACT_ID";
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName()); getLogger(ConversationActivity.class.getName());
private static final int TRANSITION_DURATION_MS = 500; private static final int TRANSITION_DURATION_MS = 500;
private static final int ONBOARDING_DELAY_MS = 250; private static final int ONBOARDING_DELAY_MS = 250;
@@ -171,6 +175,8 @@ public class ConversationActivity extends BriarActivity
volatile GroupInvitationManager groupInvitationManager; volatile GroupInvitationManager groupInvitationManager;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>(); private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, PrivateMessageHeader> missingAttachments =
new ConcurrentHashMap<>();
private final Observer<String> contactNameObserver = name -> { private final Observer<String> contactNameObserver = name -> {
requireNonNull(name); requireNonNull(name);
loadMessages(); loadMessages();
@@ -434,29 +440,40 @@ public class ConversationActivity extends BriarActivity
}); });
} }
private void eagerlyLoadMessageSize(PrivateMessageHeader h) private void eagerlyLoadMessageSize(PrivateMessageHeader h) {
throws DbException { try {
MessageId id = h.getId(); MessageId id = h.getId();
// If the message has text, load it // If the message has text, load it
if (h.hasText()) { if (h.hasText()) {
String text = textCache.get(id); String text = textCache.get(id);
if (text == null) { if (text == null) {
LOG.info("Eagerly loading text for latest message"); LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id); text = messagingManager.getMessageText(id);
textCache.put(id, requireNonNull(text)); textCache.put(id, requireNonNull(text));
}
} }
} // If the message has a single image, load its size - for multiple
// If the message has a single image, load its size - for multiple // images we use a grid so the size is fixed
// images we use a grid so the size is fixed List<AttachmentHeader> headers = h.getAttachmentHeaders();
if (h.getAttachmentHeaders().size() == 1) { if (headers.size() == 1) {
List<AttachmentItem> items = attachmentRetriever.cacheGet(id); List<AttachmentItem> items = attachmentRetriever.cacheGet(id);
if (items == null) { if (items == null) {
LOG.info("Eagerly loading image size for latest message"); LOG.info("Eagerly loading image size for latest message");
items = attachmentRetriever.getAttachmentItems( AttachmentHeader header = headers.get(0);
attachmentRetriever.getMessageAttachments( try {
h.getAttachmentHeaders())); Attachment a = attachmentRetriever
attachmentRetriever.cachePut(id, items); .getMessageAttachment(header);
AttachmentItem item =
attachmentRetriever.getAttachmentItem(a, true);
attachmentRetriever.cachePut(id, singletonList(item));
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
}
}
} }
} catch (DbException e) {
logException(LOG, WARNING, e);
} }
} }
@@ -534,16 +551,30 @@ public class ConversationActivity extends BriarActivity
&& adapter.isScrolledToBottom(layoutManager); && adapter.isScrolledToBottom(layoutManager);
} }
private void loadMessageAttachments(MessageId messageId, private void loadMessageAttachments(PrivateMessageHeader h) {
List<AttachmentHeader> headers) { // TODO: Use placeholders for missing/invalid attachments
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
List<Pair<AttachmentHeader, Attachment>> attachments =
attachmentRetriever.getMessageAttachments(headers);
// TODO move getting the items off to IoExecutor, if size == 1 // TODO move getting the items off to IoExecutor, if size == 1
List<AttachmentItem> items = List<AttachmentHeader> headers = h.getAttachmentHeaders();
attachmentRetriever.getAttachmentItems(attachments); boolean needsSize = headers.size() == 1;
displayMessageAttachments(messageId, items); List<AttachmentItem> items = new ArrayList<>(headers.size());
for (AttachmentHeader header : headers) {
try {
Attachment a = attachmentRetriever
.getMessageAttachment(header);
AttachmentItem item = attachmentRetriever
.getAttachmentItem(a, needsSize);
items.add(item);
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
return;
}
}
// Don't cache items unless all are present and valid
attachmentRetriever.cachePut(h.getId(), items);
displayMessageAttachments(h.getId(), items);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }
@@ -553,7 +584,6 @@ public class ConversationActivity extends BriarActivity
private void displayMessageAttachments(MessageId m, private void displayMessageAttachments(MessageId m,
List<AttachmentItem> items) { List<AttachmentItem> items) {
runOnUiThreadUnlessDestroyed(() -> { runOnUiThreadUnlessDestroyed(() -> {
attachmentRetriever.cachePut(m, items);
Pair<Integer, ConversationMessageItem> pair = Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m); adapter.getMessageItem(m);
if (pair != null) { if (pair != null) {
@@ -567,6 +597,13 @@ public class ConversationActivity extends BriarActivity
@Override @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof AttachmentReceivedEvent) {
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
if (a.getContactId().equals(contactId)) {
LOG.info("Attachment received");
onAttachmentReceived(a.getMessageId());
}
}
if (e instanceof ContactRemovedEvent) { if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e; ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) { if (c.getContactId().equals(contactId)) {
@@ -617,6 +654,15 @@ public class ConversationActivity extends BriarActivity
scrollToBottom(); scrollToBottom();
} }
@UiThread
private void onAttachmentReceived(MessageId attachmentId) {
PrivateMessageHeader h = missingAttachments.remove(attachmentId);
if (h != null) {
LOG.info("Missing attachment received");
loadMessageAttachments(h);
}
}
@UiThread @UiThread
private void onNewConversationMessage(ConversationMessageHeader h) { private void onNewConversationMessage(ConversationMessageHeader h) {
if (h instanceof ConversationRequest || if (h instanceof ConversationRequest ||
@@ -903,11 +949,11 @@ public class ConversationActivity extends BriarActivity
} }
@Override @Override
public List<AttachmentItem> getAttachmentItems(MessageId m, public List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h) {
List<AttachmentHeader> headers) { List<AttachmentItem> attachments =
List<AttachmentItem> attachments = attachmentRetriever.cacheGet(m); attachmentRetriever.cacheGet(h.getId());
if (attachments == null) { if (attachments == null) {
loadMessageAttachments(m, headers); loadMessageAttachments(h);
return emptyList(); return emptyList();
} }
return attachments; return attachments;

View File

@@ -18,7 +18,6 @@ import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager; import org.briarproject.bramble.api.settings.SettingsManager;
@@ -51,7 +50,6 @@ import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce; import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@@ -101,10 +99,13 @@ public class ConversationViewModel extends AndroidViewModel
@Inject @Inject
ConversationViewModel(Application application, ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor, @DatabaseExecutor Executor dbExecutor,
@IoExecutor Executor ioExecutor, TransactionManager db, TransactionManager db,
MessagingManager messagingManager, ContactManager contactManager, MessagingManager messagingManager,
ContactManager contactManager,
SettingsManager settingsManager, SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory) { PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) {
super(application); super(application);
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.db = db; this.db = db;
@@ -112,10 +113,8 @@ public class ConversationViewModel extends AndroidViewModel
this.contactManager = contactManager; this.contactManager = contactManager;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.privateMessageFactory = privateMessageFactory; this.privateMessageFactory = privateMessageFactory;
this.attachmentRetriever = new AttachmentRetriever(messagingManager, this.attachmentRetriever = attachmentRetriever;
getAttachmentDimensions(application.getResources())); this.attachmentCreator = attachmentCreator;
this.attachmentCreator = new AttachmentCreator(getApplication(),
ioExecutor, messagingManager, attachmentRetriever);
messagingGroupId = Transformations messagingGroupId = Transformations
.map(contact, c -> messagingManager.getContactGroup(c).getId()); .map(contact, c -> messagingManager.getContactGroup(c).getId());
contactDeleted.setValue(false); contactDeleted.setValue(false);

View File

@@ -15,7 +15,6 @@ import org.briarproject.briar.api.forum.ForumInvitationRequest;
import org.briarproject.briar.api.forum.ForumInvitationResponse; import org.briarproject.briar.api.forum.ForumInvitationResponse;
import org.briarproject.briar.api.introduction.IntroductionRequest; import org.briarproject.briar.api.introduction.IntroductionRequest;
import org.briarproject.briar.api.introduction.IntroductionResponse; import org.briarproject.briar.api.introduction.IntroductionResponse;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
@@ -56,8 +55,7 @@ class ConversationVisitor implements
if (h.getAttachmentHeaders().isEmpty()) { if (h.getAttachmentHeaders().isEmpty()) {
attachments = emptyList(); attachments = emptyList();
} else { } else {
attachments = attachmentCache attachments = attachmentCache.getAttachmentItems(h);
.getAttachmentItems(h.getId(), h.getAttachmentHeaders());
} }
if (h.isLocal()) { if (h.isLocal()) {
item = new ConversationMessageItem( item = new ConversationMessageItem(
@@ -295,7 +293,6 @@ class ConversationVisitor implements
} }
interface AttachmentCache { interface AttachmentCache {
List<AttachmentItem> getAttachmentItems(MessageId m, List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h);
List<AttachmentHeader> headers);
} }
} }

View File

@@ -12,7 +12,6 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
@@ -135,10 +134,10 @@ public class ImageViewModel extends AndroidViewModel {
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp, private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
@Nullable Runnable afterCopy) { @Nullable Runnable afterCopy) {
MessageId messageId = attachment.getMessageId();
dbExecutor.execute(() -> { dbExecutor.execute(() -> {
try { try {
Attachment a = messagingManager.getAttachment(messageId); Attachment a =
messagingManager.getAttachment(attachment.getHeader());
copyImageFromDb(a, osp, afterCopy); copyImageFromDb(a, osp, afterCopy);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);

View File

@@ -9,8 +9,8 @@ import com.bumptech.glide.load.data.DataFetcher;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.InputStream; import java.io.InputStream;
@@ -50,11 +50,12 @@ class BriarDataFetcher implements DataFetcher<InputStream> {
@Override @Override
public void loadData(Priority priority, public void loadData(Priority priority,
DataCallback<? super InputStream> callback) { DataCallback<? super InputStream> callback) {
MessageId id = attachment.getMessageId();
dbExecutor.execute(() -> { dbExecutor.execute(() -> {
if (cancel) return; if (cancel) return;
try { try {
inputStream = messagingManager.getAttachment(id).getStream(); Attachment a =
messagingManager.getAttachment(attachment.getHeader());
inputStream = a.getStream();
callback.onDataReady(inputStream); callback.onDataReady(inputStream);
} catch (DbException e) { } catch (DbException e) {
callback.onLoadFailed(e); callback.onLoadFailed(e);

View File

@@ -31,7 +31,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:hint="@string/set_contact_alias_hint" android:hint="@string/set_contact_alias_hint"
android:inputType="textPersonName" android:inputType="text|textCapWords"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_medium"/> android:textSize="@dimen/text_size_medium"/>

View File

@@ -118,7 +118,9 @@
<string name="message_hint">Idatzi mezua</string> <string name="message_hint">Idatzi mezua</string>
<string name="image_caption_hint">Gehitu epigrafea (aukerakoa)</string> <string name="image_caption_hint">Gehitu epigrafea (aukerakoa)</string>
<string name="image_attach">Erantsi irudia</string> <string name="image_attach">Erantsi irudia</string>
<string name="image_attach_error">Ezin izan da irudia erantsi</string> <string name="image_attach_error">Ezin izan dira irudiak erantsi</string>
<string name="image_attach_error_too_big">Irudia handiegia da. Muga %d MB da.</string>
<string name="image_attach_error_invalid_mime_type">Onartu gabeko irudi formatua: %s</string>
<string name="set_contact_alias">Aldatu kontaktuaren izena</string> <string name="set_contact_alias">Aldatu kontaktuaren izena</string>
<string name="set_contact_alias_hint">Kontaktuaren izena</string> <string name="set_contact_alias_hint">Kontaktuaren izena</string>
<string name="set_alias_button">Aldatu</string> <string name="set_alias_button">Aldatu</string>
@@ -138,6 +140,7 @@
<string name="dialog_title_image_support">Orain irudiak bidali ahal dizkiozu eranskin gisa kontaktu honei</string> <string name="dialog_title_image_support">Orain irudiak bidali ahal dizkiozu eranskin gisa kontaktu honei</string>
<string name="dialog_message_image_support">Sakatu ikono hau irudiak eransteko.</string> <string name="dialog_message_image_support">Sakatu ikono hau irudiak eransteko.</string>
<!--Adding Contacts--> <!--Adding Contacts-->
<string name="add_contact_title">Gehitu inguruko kontaktua</string>
<string name="face_to_face">Kontaktu gisa gehitu nahi duzun pertsonarekin aurrez aurre egon behar duzu.\n\nHonek etorkizunean inork zure itxurak egitea edo zure mezuak irakurtzea eragotziko du.</string> <string name="face_to_face">Kontaktu gisa gehitu nahi duzun pertsonarekin aurrez aurre egon behar duzu.\n\nHonek etorkizunean inork zure itxurak egitea edo zure mezuak irakurtzea eragotziko du.</string>
<string name="continue_button">Jarraitu</string> <string name="continue_button">Jarraitu</string>
<string name="try_again_button">Saiatu berriro</string> <string name="try_again_button">Saiatu berriro</string>
@@ -155,14 +158,66 @@
<string name="connection_error_explanation">Egiaztatu biak Wi-Fi sare berera konektatuta zaudetela.</string> <string name="connection_error_explanation">Egiaztatu biak Wi-Fi sare berera konektatuta zaudetela.</string>
<string name="connection_error_feedback">Arazoa mantentzen bada, mesedez <a href="feedback">bidali iruzkin bat</a> aplikazioa hobetzen laguntzeko.</string> <string name="connection_error_feedback">Arazoa mantentzen bada, mesedez <a href="feedback">bidali iruzkin bat</a> aplikazioa hobetzen laguntzeko.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Gehitu urruneko kontaktua</string>
<string name="add_contact_nearby_title">Gehitu inguruko kontaktua</string>
<string name="add_contact_remotely_title">Gehitu urruneko kontaktua</string>
<string name="contact_name_hint">Eman ezizena kontaktuari</string>
<string name="contact_link_intro">Sartu zure kontaktuaren ezizena hemen</string>
<string name="contact_link_hint">Kontaktuaren esteka</string>
<string name="paste_button">Itsatsi</string> <string name="paste_button">Itsatsi</string>
<string name="add_contact_button">Gehitu kontaktua</string>
<string name="copy_button">Kopiatu</string> <string name="copy_button">Kopiatu</string>
<string name="share_button">Elkarbanatu</string> <string name="share_button">Elkarbanatu</string>
<string name="send_link_title">Partekatu estekak</string>
<string name="add_contact_choose_nickname">Aukeratu ezizena</string>
<string name="add_contact_choose_a_nickname">Sartu ezizen bat</string>
<string name="nickname_intro">Eman ezizena zure kontaktuari. Zuk ikusi dezakezu, beste inork ez</string>
<string name="your_link">Eman esteka hau gehitu nahi duzun kontaktuari</string>
<string name="link_clip_label">Briar esteka</string>
<string name="link_copied_toast">Esteka kopiatuta</string>
<string name="adding_contact_error">Errore bat gertatu da kontaktua gehitzean.</string>
<string name="pending_contact_requests_snackbar">Kontaktu eskaerak daude aztertzeke</string>
<string name="pending_contact_requests">Kontaktu eskaerak aztertzeke</string>
<string name="no_pending_contacts">Ez dago kontakturik aztertzeke</string>
<string name="add_contact_remote_connecting">Elkartzen</string> <string name="add_contact_remote_connecting">Elkartzen</string>
<string name="waiting_for_contact_to_come_online">Kontaktua konektatu bitartean zain...</string>
<string name="connecting">Elkartzen</string> <string name="connecting">Elkartzen</string>
<string name="adding_contact">Kontaktua gehitzen...</string>
<string name="adding_contact_failed">Kontaktua gehitzeak huts egin du</string>
<string name="dialog_title_remove_pending_contact">Berretsi kentzea</string>
<string name="dialog_message_remove_pending_contact">Kontaktu hau gehitzen ari da oraindik. orain kentzen baduzu, ez da gehituko.</string>
<string name="own_link_error">Sartu zure kontaktuaren esteka, ez zurea</string>
<string name="nickname_missing">Sartu ezizen bat</string>
<string name="invalid_link">Esteka baliogabea</string>
<string name="unsupported_link">Esteka hau Briar-en bertsio berriago batetik dator. Eguneratu azken bertsiora eta saiatu berriro.</string>
<string name="intent_own_link">Zure esteka ireki duzu. Erabili gehitu nahi duzun kontaktuarena!</string>
<string name="missing_link">Sartu esteka bat</string>
<!--This is a numeral indicating the first step in a series of screens--> <!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string> <string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens--> <!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string>
<plurals name="contact_added_notification_text">
<item quantity="one">Kontaktu berria gehitu da</item>
<item quantity="other">%d kontaktu berri gehitu dira.</item>
</plurals>
<string name="adding_contact_slow_warning">Kontaktu hau gehitzeak ohi baino denbora gehiago hartzen ari da</string>
<string name="adding_contact_slow_title">Ezin da kontaktuarekin kontaktatu</string>
<string name="adding_contact_slow_text">Kontaktu hau gehitzeak ohi baino denbora gehiago hartzen du.\n\nEgiaztatu kontaktua esteka jaso duela eta zu gehitu zaituela:</string>
<string name="offline_state">Internet konexiorik ez</string>
<string name="duplicate_link_dialog_title">Bikoiztutako esteka</string>
<string name="duplicate_link_dialog_text_1">Baduzu kontaktu bat aztertzeke esteka honekin: %s</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">Ziur %s eta %s pertsona bera direla?</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-->
<string name="same_person_button">Pertsona bera</string>
<!--This is a button for answering that two nicknames refer to different people. This 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">Pertsona desberdina</string>
<string name="duplicate_link_dialog_text_3">%s eta %s erabiltzaileek esteka bera bidali dizute.\n\nBietako batek agian zure kontaktuak zeintzuk diren asmatu nahi du.\n\nEz esan esteka bera jaso duzula beste norbaitengandik.</string>
<string name="pending_contact_updated_toast">Aztertzeke zegoen kontaktua eguneratu da</string>
<!--Introductions--> <!--Introductions-->
<string name="introduction_onboarding_title">Aurkeztu zure kontaktuak</string> <string name="introduction_onboarding_title">Aurkeztu zure kontaktuak</string>
<string name="introduction_onboarding_text">Zure kontaktuak elkarren artean aurkez ditzakezu, Briar bidez konektatzeko aurrez aurre egon behar izan ez dezaten.</string> <string name="introduction_onboarding_text">Zure kontaktuak elkarren artean aurkez ditzakezu, Briar bidez konektatzeko aurrez aurre egon behar izan ez dezaten.</string>

View File

@@ -2,21 +2,21 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!--Setup--> <!--Setup-->
<string name="setup_title">به Briar (برایر) خوش آمدید</string> <string name="setup_title">به Briar (برایر) خوش آمدید</string>
<string name="setup_name_explanation">نام مستعارتان کنار هر مطلب شما قرار خواهد گرفت و بعد از ایجاد حساب کاربری امکان تغییر آن وجود ندارد.</string> <string name="setup_name_explanation">نام مستعار شما کنار هر مطلبی که پست کنید قرار خواهد گرفت. بعد از ایجاد حساب کاربری امکان تغییر آن وجود ندارد.</string>
<string name="setup_next">بعدی</string> <string name="setup_next">بعدی</string>
<string name="setup_password_intro">یک رمز عبور انتخاب کنید</string> <string name="setup_password_intro">یک گذرواژه انتخاب کنید</string>
<string name="setup_password_explanation">حساب کاربری Briar (برایر) شما به صورت رمزگذاری شده روی دستگاه شما به جای حافظه ابری ذخیره شده است. اگر شما رمز عبور خود را فراموش کنید یا Briar (برایر) را پاک کنید، راهی برای بازیابی حساب کاربری شما وجود نخواهد داشت. <string name="setup_password_explanation">حساب کاربری Briar (برایر) شما به صورت رمزگذاری شده روی دستگاه شما به جای حافظه ابری ذخیره شده است. اگر گذرواژه خود را فراموش کنید یا Briar (برایر) را پاک کنید، راهی برای بازیابی حساب کاربری شما وجود نخواهد داشت.
یک رمز عبور طولانی انتخاب کنید که حدس آن سخت باشد، مثل چهار عبارت تصادفی یا ده لغت تصادفی با اعداد و نماد ها.</string> یک گذرواژه طولانی انتخاب کنید که حدس آن سخت باشد، مثل چهار عبارت تصادفی یا ده لغت تصادفی با اعداد و نماد ها.</string>
<string name="setup_doze_title">اتصال های پس زمینه</string> <string name="setup_doze_title">اتصال های پس زمینه</string>
<string name="setup_doze_intro">برای دریافت پیام، Briar (برایر) نیاز دارد تا در پس زمینه اتصال داشته باشد.</string> <string name="setup_doze_intro">برای دریافت پیام، Briar (برایر) نیاز دارد تا در پس زمینه اتصال داشته باشد.</string>
<string name="setup_doze_explanation">برای دریافت پیام، Briar (برایر) نیاز دارد تا در پس زمینه اتصال داشته باشد. لطفا بهینه سازی باتری را غیر فعال کنید تا Briar (برایر) بتواند به اتصال خود ادامه دهد.</string> <string name="setup_doze_explanation">برای دریافت پیام، Briar (برایر) نیاز دارد تا در پس زمینه اتصال داشته باشد. لطفا بهینه سازی باتری را غیر فعال کنید تا Briar (برایر) بتواند به اتصال خود ادامه دهد.</string>
<string name="setup_doze_button">دادن اجازه به اتصالات</string> <string name="setup_doze_button">دادن اجازه به ارتباطات</string>
<string name="choose_nickname">نام مستعار خود را انتخاب کنید</string> <string name="choose_nickname">نام مستعار خود را انتخاب کنید</string>
<string name="choose_password">رمز عبور خود را انتخاب کنید</string> <string name="choose_password">گذرواژه خود را انتخاب کنید</string>
<string name="confirm_password">رمز عبور خود را تایید کنید</string> <string name="confirm_password">گذرواژه خود را تایید کنید</string>
<string name="name_too_long">نام بیش از حد طولانی می باشد</string> <string name="name_too_long">نام بیش از حد طولانی می باشد</string>
<string name="password_too_weak">رمز عبور ضعیف می باشد</string> <string name="password_too_weak">گذرواژه ضعیف می باشد</string>
<string name="passwords_do_not_match">رمز های عبور مطابقت ندارند </string> <string name="passwords_do_not_match">رمز های عبور مطابقت ندارند </string>
<string name="create_account_button">ایجاد حساب کاربری</string> <string name="create_account_button">ایجاد حساب کاربری</string>
<string name="more_info">اطلاعات بیشتر</string> <string name="more_info">اطلاعات بیشتر</string>
@@ -26,21 +26,21 @@
<string name="setup_huawei_help">اگر Briar (برایر) به فهرست برنامه های محافظت شده اضافه نشده، نمی تواند در پس زمینه مشغول به کار باشد.</string> <string name="setup_huawei_help">اگر Briar (برایر) به فهرست برنامه های محافظت شده اضافه نشده، نمی تواند در پس زمینه مشغول به کار باشد.</string>
<string name="warning_dozed">ناتوانی %s برای اجراء در پس زمینه</string> <string name="warning_dozed">ناتوانی %s برای اجراء در پس زمینه</string>
<!--Login--> <!--Login-->
<string name="enter_password">رمز عبور</string> <string name="enter_password">گذرواژه</string>
<string name="try_again">رمز عبور اشتباه است، لطفا دوباره سعی کنید</string> <string name="try_again">گذرواژه اشتباه است، لطفا دوباره سعی کنید</string>
<string name="sign_in_button">ورود</string> <string name="sign_in_button">ورود</string>
<string name="forgotten_password">رمز عبور را فراموش کرده ام</string> <string name="forgotten_password">گذرواژه خود را فراموش کرده ام</string>
<string name="dialog_title_lost_password">رمز عبور گمشده</string> <string name="dialog_title_lost_password">گذرواژه گمشده</string>
<string name="dialog_message_lost_password">حساب کاربری Briar (برایر) شما به صورت رمزنگاری شده روی سیستم شما به جای حافظه ابری ذخیره شده است برای همین ما نمی توانیم رمز عبور شما را به صورت مجدد تنظیم کنیم. آیا مایل هستید تا حساب کاربری شما را پاک کنیم و دوباره از ابتدا شروع کنیم؟ <string name="dialog_message_lost_password">حساب کاربری Briar (برایر) شما به صورت رمزنگاری شده روی سیستم شما به جای حافظه ابری ذخیره شده است برای همین ما نمی توانیم گذرواژه شما را به صورت مجدد تنظیم کنیم. آیا مایل هستید تا حساب کاربری شما را پاک کنیم و دوباره از ابتدا شروع کنیم؟
اخطار: هویت های شما، مخاطبان شما و پیام های شما برای همیشه از بین خواهند رفت.</string> اخطار: هویت های شما، مخاطبان شما و پیام های شما برای همیشه از بین خواهند رفت.</string>
<string name="startup_failed_notification_title">Briar (برایر) نمی تواند شروع به کار کند.</string> <string name="startup_failed_notification_title">Briar (برایر) نمی تواند شروع به کار کند.</string>
<string name="startup_failed_notification_text">برای اطلاعات بیشتر کلیک کنید</string> <string name="startup_failed_notification_text">برای اطلاعات بیشتر کلیک کنید</string>
<string name="startup_failed_activity_title">خطا در شروع Briar (برایر)</string> <string name="startup_failed_activity_title">خطا در شروع Briar (برایر)</string>
<string name="startup_failed_db_error">به دلایلی، دیتابیس Briar (برایر) شما خراب شده و قابل اصلاح نیست. حساب کاربری شما، داده های شما و تمام مخاطبان شما از بین رفته اند. متاسفانه، شما یا باید Briar (برایر) را دوباره نصب کنید یا یک حساب کاربری جدید با انتخاب \'رمز عبور ام را فراموش کرده ام\' انتخاب کنید.</string> <string name="startup_failed_db_error">به دلایلی، پایگاه داده Briar (برایر) شما خراب شده و قابل اصلاح نیست. حساب کاربری شما، داده های شما و تمام مخاطبان شما از بین رفته اند. متاسفانه، شما یا باید Briar (برایر) را دوباره نصب کنید یا یک حساب کاربری جدید در هنگام خواست گذرواژه با انتخاب گذرواژه خود را فراموش کرده ام\' ایجاد کنید.</string>
<string name="startup_failed_data_too_old_error">حساب کاربری شما با یک نسخه قدیمی از این برنامه ایجاد شده است و به همین خاطربا این نسخه نمی تواند باز شود. شما باید نسخه قدیمی را دوباره نصب کنید یا یک حساب کاربری جدید با \"رمز عبور ام را فراموش کرده ام\" در صفحه ی رمز عبور ایجاد کنید.</string> <string name="startup_failed_data_too_old_error">حساب کاربری شما با یک نسخه قدیمی از این برنامه ایجاد شده است و به همین خاطر با این نسخه نمی تواند باز شود. شما باید نسخه قدیمی را دوباره نصب کنید یا یک حساب کاربری جدید با \"گذرواژه خود را فراموش کرده ام\" در صفحه ی گذرواژه ایجاد کنید.</string>
<string name="startup_failed_data_too_new_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_service_error">Briar (برایر) نمی تواند یک افزونه ضروری را اجراء کند. نصب دوباره Briar (برایر) معمولا این مشکل را حل میکند. هرچند، توجه داشته باشید که حساب کاربری و تمام داده های مرتبط با آن را از دست خواهید داد از آنجایی که Briar (برایر) از هیچ سرور مرکزی برای ذخیره داده های شما استفاده نمی کند.</string>
<plurals name="expiry_warning"> <plurals name="expiry_warning">
<item quantity="one">این یک نسخه آزمایشی از Briar (برایر) می باشد. حساب کاربری شما در %d روز آینده به پایان می رسد و امکان تمدید آن وجود نخواهد داشت.</item> <item quantity="one">این یک نسخه آزمایشی از Briar (برایر) می باشد. حساب کاربری شما در %d روز آینده به پایان می رسد و امکان تمدید آن وجود نخواهد داشت.</item>
<item quantity="other">این یک نسخه آزمایشی از Briar (برایر) می باشد. حساب کاربری شما در %d روز آینده به پایان می رسد و امکان تمدید آن وجود نخواهد داشت.</item> <item quantity="other">این یک نسخه آزمایشی از Briar (برایر) می باشد. حساب کاربری شما در %d روز آینده به پایان می رسد و امکان تمدید آن وجود نخواهد داشت.</item>
@@ -50,15 +50,15 @@
بابت تست از شما سپاسگزاریم.</string> بابت تست از شما سپاسگزاریم.</string>
<string name="download_briar">برای اینکه به استفاده خود از Briar (برایر) ادامه دهید، لطفا نسخه ۱.۰ را دانلود کنید.</string> <string name="download_briar">برای اینکه به استفاده خود از Briar (برایر) ادامه دهید، لطفا نسخه ۱.۰ را دانلود کنید.</string>
<string name="create_new_account">لازم است تا یک حساب کاربری جدید ایجاد کنید، می توانید از نام مستعار یکسان برای حساب های کاربری استفاده کنید.</string> <string name="create_new_account">لازم است تا یک حساب کاربری جدید ایجاد کنید، ولی می توانید از همان نام مستعار استفاده کنید.</string>
<string name="download_briar_button">دانلود Briar (برایر) 1.0</string> <string name="download_briar_button">دانلود Briar (برایر) 1.0</string>
<string name="startup_open_database">رمزگشایی سیستم ...</string> <string name="startup_open_database">در حال رمزگشایی سیستم ...</string>
<string name="startup_migrate_database">به روز رسانی سیستم ...</string> <string name="startup_migrate_database">در حال ارتقا سیستم ...</string>
<string name="startup_compact_database">درحال فشرده‌سازی پایگاه داده...</string> <string name="startup_compact_database">درحال فشرده‌ سازی پایگاه داده...</string>
<!--Navigation Drawer--> <!--Navigation Drawer-->
<string name="nav_drawer_open_description">باز کردن منوی برنامه</string> <string name="nav_drawer_open_description">باز کردن منوی برنامه</string>
<string name="nav_drawer_close_description">بستن منوی برنامه</string> <string name="nav_drawer_close_description">بستن منوی برنامه</string>
<string name="contact_list_button">مخاطبین</string> <string name="contact_list_button">مخاطبان</string>
<string name="groups_button">گروه های خصوصی</string> <string name="groups_button">گروه های خصوصی</string>
<string name="forums_button">تالار های گفتمان</string> <string name="forums_button">تالار های گفتمان</string>
<string name="blogs_button">بلاگ ها</string> <string name="blogs_button">بلاگ ها</string>
@@ -72,7 +72,7 @@
<string name="transport_lan">وای فای</string> <string name="transport_lan">وای فای</string>
<!--Notifications--> <!--Notifications-->
<string name="reminder_notification_title">از Briar (برایر) خارج شد</string> <string name="reminder_notification_title">از Briar (برایر) خارج شد</string>
<string name="reminder_notification_text">برای وارد شدن دوباره کلیک کنید.</string> <string name="reminder_notification_text">برای وارد شدن دوباره ضربه بزنید.</string>
<string name="reminder_notification_channel_title">یادآور ورود Briar (برایر)</string> <string name="reminder_notification_channel_title">یادآور ورود Briar (برایر)</string>
<string name="reminder_notification_dismiss">رد کردن</string> <string name="reminder_notification_dismiss">رد کردن</string>
<string name="ongoing_notification_title">وارد Briar (برایر) شد</string> <string name="ongoing_notification_title">وارد Briar (برایر) شد</string>
@@ -115,16 +115,18 @@
<string name="show_onboarding">نمایش پنجره راهنما</string> <string name="show_onboarding">نمایش پنجره راهنما</string>
<string name="fix">اصلاح</string> <string name="fix">اصلاح</string>
<string name="help">راهنما</string> <string name="help">راهنما</string>
<string name="sorry">متاسفم</string> <string name="sorry">پوزش</string>
<!--Contacts and Private Conversations--> <!--Contacts and Private Conversations-->
<string name="no_contacts">هیچ مخاطبی برای نمایش وجود ندارد</string> <string name="no_contacts">هیچ مخاطبی برای نمایش وجود ندارد</string>
<string name="no_contacts_action">برای افزودن مخاطب روی + کلیک کنید</string> <string name="no_contacts_action">برای افزودن مخاطب روی + کلیک کنید</string>
<string name="date_no_private_messages">هیچ پیامی موجود نیست</string> <string name="date_no_private_messages">هیچ پیامی موجود نیست</string>
<string name="no_private_messages">هیچ پیامی برای نشان دادن وجود ندارد</string> <string name="no_private_messages">هیچ پیامی برای نشان دادن وجود ندارد</string>
<string name="message_hint">نوشتن پیام</string> <string name="message_hint">نوشتن پیام</string>
<string name="image_caption_hint">یک عنوان اضافه کنید (اختیاری)</string> <string name="image_caption_hint">افزودن یک شرح (اختیاری)</string>
<string name="image_attach">تصویر پیوست</string> <string name="image_attach">پیوست کردن تصویر</string>
<string name="image_attach_error">تصویر پیوست نشد</string> <string name="image_attach_error">پیوست کردن تصویر(ها) ممکن نمی باشد</string>
<string name="image_attach_error_too_big">تصویر بیش از اندازه بزرگ است. محدودیت %d مگابایت می باشد.</string>
<string name="image_attach_error_invalid_mime_type">فرمت تصویر پشتیبانی نمی‌شود: %s</string>
<string name="set_contact_alias">تغییر نام مخاطب</string> <string name="set_contact_alias">تغییر نام مخاطب</string>
<string name="set_contact_alias_hint">نام مخاطب</string> <string name="set_contact_alias_hint">نام مخاطب</string>
<string name="set_alias_button">تغییر</string> <string name="set_alias_button">تغییر</string>
@@ -136,15 +138,19 @@
<string name="you">شما</string> <string name="you">شما</string>
<string name="save_image">ذخیره تصویر</string> <string name="save_image">ذخیره تصویر</string>
<string name="dialog_title_save_image">ذخیره تصویر؟</string> <string name="dialog_title_save_image">ذخیره تصویر؟</string>
<string name="dialog_message_save_image">ذخیره این تصویر به دیگر برنامه‌ها اجازه می‌دهد تا به آن دسترسی داشته باشند.\n\n آیا مطمئن هستید که می‌خواهید ذخیره کنید؟</string> <string name="dialog_message_save_image">ذخیره این تصویر به دیگر برنامه‌ها اجازه می‌دهد تا به آن دسترسی داشته باشند.
آیا از ذخیره کردن مطمئن می باشید؟</string>
<string name="save_image_success">تصویر ذخیره شد</string> <string name="save_image_success">تصویر ذخیره شد</string>
<string name="save_image_error">تصویر ذخیره نشد</string> <string name="save_image_error">تصویر ذخیره نشد</string>
<string name="dialog_title_no_image_support">تصاویر در دسترس نیست</string> <string name="dialog_title_no_image_support">تصاویر در دسترس نمی باشند</string>
<string name="dialog_message_no_image_support">مخاطب برایر شما هنوز از پیوست‌های تصویر پشتیبانی نمی‌کند. هنگامی که آنها را ارتقا دهید یک آیکون دیگر خواهید دید.</string> <string name="dialog_message_no_image_support">Briar (برایر) مخاطب شما هنوز از پیوست‌ های تصویری پشتیبانی نمی‌کند. هنگامی که آنها ارتقا دادند یک آیکون متفاوت خواهید دید.</string>
<string name="dialog_title_image_support">شما هم اکنون می‌توانید تصاویر را به این مخاطب ارسال کنید</string> <string name="dialog_title_image_support">شما هم اکنون می‌توانید به این مخاطب تصاویر ارسال کنید</string>
<string name="dialog_message_image_support">برای پیوست تصاویر به روی آیکون ضربه بزنید</string> <string name="dialog_message_image_support">برای پیوست کردن تصاویر به روی آیکون ضربه بزنید.</string>
<!--Adding Contacts--> <!--Adding Contacts-->
<string name="add_contact_title">افزودن مخاطب از نزدیک</string>
<string name="face_to_face">برای اضافه کردن فرد به عنوان مخاطب باید با او به صورت حضوری ملاقات کنید. <string name="face_to_face">برای اضافه کردن فرد به عنوان مخاطب باید با او به صورت حضوری ملاقات کنید.
این از جعل هویت شما و یا از خوانده شدن پیام هایتان در آینده جلوگیری خواهد کرد.</string> این از جعل هویت شما و یا از خوانده شدن پیام هایتان در آینده جلوگیری خواهد کرد.</string>
<string name="continue_button">ادامه</string> <string name="continue_button">ادامه</string>
<string name="try_again_button">دوباره سعی کنید</string> <string name="try_again_button">دوباره سعی کنید</string>
@@ -152,29 +158,86 @@
<string name="exchanging_contact_details">تبادیل جزییات مخاطبu2026\</string> <string name="exchanging_contact_details">تبادیل جزییات مخاطبu2026\</string>
<string name="contact_added_toast">مخاطب اضافه شد: %s</string> <string name="contact_added_toast">مخاطب اضافه شد: %s</string>
<string name="contact_already_exists">مخاطب %s از قبل وجود دارد</string> <string name="contact_already_exists">مخاطب %s از قبل وجود دارد</string>
<string name="qr_code_invalid">کد QR نامعتبر می باشد</string> <string name="qr_code_invalid">کد کیوآر نامعتبر می باشد</string>
<string name="qr_code_too_old">کد QR اسکن شده متعلق به یک نسخه قدیمی از %s است. <string name="qr_code_too_old">کد کیوآر اسکن شده متعلق به یک نسخه قدیمی از %s است.
لطفا از مخاطب‌تان بخواهید تا به آخرین نسخه ارتقا داده و سپس دوباره تلاش کنید.</string> لطفا از مخاطب‌ خود بخواهید تا به آخرین نسخه ارتقا داده و سپس دوباره تلاش کنید.</string>
<string name="qr_code_too_new">کد QR که شما اسکن کرده‌اید متعلق به یک نسخه جدیدتر از %s است. <string name="qr_code_too_new">کد کیوآر اسکن شده توسط شما متعلق به یک نسخه جدیدتر از %s است.
لطفا به آخرین نسخه ارتقا داده و دوباره تلاش کنید.</string> لطفا به آخرین نسخه ارتقا داده و دوباره تلاش کنید.</string>
<string name="camera_error">خطای دوربین</string> <string name="camera_error">خطای دوربین</string>
<string name="connecting_to_device">اتصال به دستگاهu2026\</string> <string name="connecting_to_device">اتصال به دستگاهu2026\</string>
<string name="authenticating_with_device">تصدیق سازی با دستگاه u2026\</string> <string name="authenticating_with_device">تصدیق سازی با دستگاه u2026\</string>
<string name="connection_error_title">اتصال به مخاطب شما برقرار نشد</string> <string name="connection_error_title">اتصال به مخاطب شما برقرار نشد</string>
<string name="connection_error_explanation">لطفا مطمئن شوید که هردو شما به شبکه وای فای یکسان متصل هستید.</string> <string name="connection_error_explanation">لطفا مطمئن شوید که هر دو شما به شبکه وای فای یکسان متصل هستید.</string>
<string name="connection_error_feedback">اگر این مشکل ادامه پیدا کرد، لطفا <a href="feedback">بازخورد ارسال کنید</a> تا به ما در ارتقاء برنامه کمک کنید.</string> <string name="connection_error_feedback">اگر این مشکل ادامه پیدا کرد، لطفا <a href="feedback">بازخورد ارسال کنید</a> تا به ما در بهبود برنامه کمک کنید.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">افزودن مخاطب از دور</string>
<string name="add_contact_nearby_title">افزودن مخاطب از نزدیک</string>
<string name="add_contact_remotely_title">افزودن مخاطب از راه دور</string>
<string name="contact_name_hint">دادن یک نام مستعار به مخاطب</string>
<string name="contact_link_intro">پیوند آمده از مخاطب خود را اینجا وارد کنید</string>
<string name="contact_link_hint">پیوند مخاطب</string>
<string name="paste_button">چسباندن</string> <string name="paste_button">چسباندن</string>
<string name="copy_button">تکثیر</string> <string name="add_contact_button">افزودن مخاطب</string>
<string name="share_button">هم‌رسانی</string> <string name="copy_button">کپی</string>
<string name="share_button">اشتراک گذاری</string>
<string name="send_link_title">تبادل پیوندها</string>
<string name="add_contact_choose_nickname">انتخاب نام مستعار</string>
<string name="add_contact_choose_a_nickname">یک نام مستعار وارد کنید</string>
<string name="nickname_intro">به مخاطب خود یک نام مستعار بدهید. فقط شما قادر به دیدن آن می باشید.</string>
<string name="your_link">برای افزودن مخاطب این پیوند را به او بدهید</string>
<string name="link_clip_label">پیوند Briar (برایر)</string>
<string name="link_copied_toast">پیوند کپی شد</string>
<string name="adding_contact_error">یک خطا هنگام افزودن مخاطب رخ داد.</string>
<string name="pending_contact_requests_snackbar">درخواست های مخاطب معلق وجود دارند</string>
<string name="pending_contact_requests">درخواست های مخاطب معلق</string>
<string name="no_pending_contacts">هیچ مخاطب معلقی وجود ندارد</string>
<string name="add_contact_remote_connecting">در حال اتصال…</string> <string name="add_contact_remote_connecting">در حال اتصال…</string>
<string name="waiting_for_contact_to_come_online">انتظار برای آنلاین شدن مخاطب...</string>
<string name="connecting">در حال اتصال…</string> <string name="connecting">در حال اتصال…</string>
<string name="adding_contact">در حال افزودن مخاطب...</string>
<string name="adding_contact_failed">افزودن مخاطب ناموفق بود</string>
<string name="dialog_title_remove_pending_contact">تایید حذف</string>
<string name="dialog_message_remove_pending_contact">این مخاطب هنوز در حال اضافه شدن می باشد. اگر اکنون آن را حذف کنید، دیگر افزوده نخواهد شد.</string>
<string name="own_link_error">به جای پیوند خود، پیوند مخاطب را وارد کنید</string>
<string name="nickname_missing">لطفا یک نام مستعار وارد کنید</string>
<string name="invalid_link">پیوند نامعتبر</string>
<string name="unsupported_link">این پیوند از یک نسخه جدیدتر Briar (برایر) آمده است. لطفا به آخرین نسخه ارتقا دهید و دوباره امتحان کنید.</string>
<string name="intent_own_link">شما پیوند خود را باز کردید. پیوند مخاطبی که میخواهید اضافه کنید را استفاده کنید!</string>
<string name="missing_link">لطفا یک پیوند وارد کنید</string>
<!--This is a numeral indicating the first step in a series of screens--> <!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">۱</string> <string name="step_1">۱</string>
<!--This is a numeral indicating the second step in a series of screens--> <!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">۲</string> <string name="step_2">۲</string>
<plurals name="contact_added_notification_text">
<item quantity="one">مخاطب جدید افزوده شد.</item>
<item quantity="other">%d مخاطب جدید افزوده شد.</item>
</plurals>
<string name="adding_contact_slow_warning">افزودن این مخاطب بیشتر از حد معمول وقت گرفته است</string>
<string name="adding_contact_slow_title">عدم توانایی در اتصال به مخاطب</string>
<string name="adding_contact_slow_text">افزودن این مخاطب بیش تر از معمول وقت گرفته است.
لطفا بررسی کنید که مخاطب شما پیوندتان را دریافت کرده و شما را افزوده است:</string>
<string name="offline_state">ارتباط با اینترنت برقرار نیست</string>
<string name="duplicate_link_dialog_title">پیوند تکراری</string>
<string name="duplicate_link_dialog_text_1">شما هم اکنون یک مخاطب معلق با این پیوند دارید: %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>
<!--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-->
<string name="same_person_button">همان شخص</string>
<!--This is a button for answering that two nicknames refer to different people. This 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">شخص متفاوت</string>
<string name="duplicate_link_dialog_text_3">%s و %s پیوند یکسانی را به شما ارسال کردند.
یکی از آن ها ممکن است سعی در شناسایی مخاطبان شما داشته باشد.
به آن ها نگویید که همان لینک را از یک شخص دیگر دریافت کرده اید.</string>
<string name="pending_contact_updated_toast">مخاطب معلق به روز رسانی شد</string>
<!--Introductions--> <!--Introductions-->
<string name="introduction_onboarding_title">معرفی مخاطبان</string> <string name="introduction_onboarding_title">معرفی مخاطبان</string>
<string name="introduction_onboarding_text">شما می توانید مخاطبان خود را به یکدیگر معرفی کنید، در این صورت دیگر نیازی به دیدار حضوری برای اتصال روی Briar (برایر) نمی باشد.</string> <string name="introduction_onboarding_text">شما می توانید مخاطبان خود را به یکدیگر معرفی کنید، در این صورت دیگر نیازی به دیدار حضوری برای اتصال روی Briar (برایر) نمی باشد.</string>
@@ -367,11 +430,11 @@
<string name="blogs_rss_feeds_manage_delete_error">خوراک نمی تواند پاک شود!</string> <string name="blogs_rss_feeds_manage_delete_error">خوراک نمی تواند پاک شود!</string>
<string name="blogs_rss_feeds_manage_empty_state">هیچ خوراک RSS برای نمایش وجود ندارد <string name="blogs_rss_feeds_manage_empty_state">هیچ خوراک RSS برای نمایش وجود ندارد
برای وارد کردن خوراک روی آیکون + کلیک کنید</string> برای وارد کردن خوراک روی آیکون + ضربه بزنید</string>
<string name="blogs_rss_feeds_manage_error">مشکلی با بارگذاری فیدهای شما وجود داشت. لطفا بعدا امتحان کنید.</string> <string name="blogs_rss_feeds_manage_error">مشکلی با بارگذاری فیدهای شما وجود داشت. لطفا بعدا امتحان کنید.</string>
<!--Settings Display--> <!--Settings Display-->
<string name="pref_language_title">زبان و منطقه</string> <string name="pref_language_title">زبان و منطقه</string>
<string name="pref_language_changed">این تنظیمات زمانی که Briar (برایر) را ری استارت کنید تاثیر خود را می گذارند. لطفا خارج شوید و Briar (برایر) را ری استارت کنید.</string> <string name="pref_language_changed">این تنظیمات زمانی که Briar (برایر) را ری استارت کنید تاثیر خود را می گذارند. لطفا خارج شوید و Briar (برایر) را دوباره راه اندازی کنید.</string>
<string name="pref_language_default">پیش فرض سیستم</string> <string name="pref_language_default">پیش فرض سیستم</string>
<string name="display_settings_title">نمایش</string> <string name="display_settings_title">نمایش</string>
<string name="pref_theme_title">قالب</string> <string name="pref_theme_title">قالب</string>
@@ -385,15 +448,15 @@
<string name="bluetooth_setting_enabled">هر زمانی که مخاطبان نزدیک هستند</string> <string name="bluetooth_setting_enabled">هر زمانی که مخاطبان نزدیک هستند</string>
<string name="bluetooth_setting_disabled">فقط در هنگام افزودن مخاطبان</string> <string name="bluetooth_setting_disabled">فقط در هنگام افزودن مخاطبان</string>
<string name="tor_network_setting">اتصال از طریق اینترنت (تور)</string> <string name="tor_network_setting">اتصال از طریق اینترنت (تور)</string>
<string name="tor_network_setting_automatic">اتوماتیک مبتنی بر مکان</string> <string name="tor_network_setting_automatic">خودکار مبتنی بر موقعیت</string>
<string name="tor_network_setting_without_bridges">استفاده از تور بدون بریج</string> <string name="tor_network_setting_without_bridges">استفاده از تور بدون پل ها</string>
<string name="tor_network_setting_with_bridges">استفاده از تور با بریج</string> <string name="tor_network_setting_with_bridges">استفاده از تور با پل ها</string>
<string name="tor_network_setting_never">وصل نشو</string> <string name="tor_network_setting_never">وصل نشو</string>
<!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)--> <!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)-->
<string name="tor_network_setting_summary">خودکار: %1$s(در %2$s)</string> <string name="tor_network_setting_summary">خودکار: %1$s(در %2$s)</string>
<string name="tor_mobile_data_title">استفاده از داده موبایل</string> <string name="tor_mobile_data_title">استفاده از داده موبایل</string>
<string name="tor_only_when_charging_title">اتصال از طریق اینترنت (تور) فقط در هنگام شارژ</string> <string name="tor_only_when_charging_title">اتصال از طریق اینترنت (تور) فقط در هنگام شارژ</string>
<string name="tor_only_when_charging_summary">وقتی دستگاه با باتری در حال اجرا است، ارتباط به اینترنت را غیرفعال میکند</string> <string name="tor_only_when_charging_summary">ارتباط اینترنت را هنگامی که دستگاه در حال استفاده از باتری خود می باشد را غیرفعال می کند</string>
<!--Settings Security and Panic--> <!--Settings Security and Panic-->
<string name="security_settings_title">امنیت</string> <string name="security_settings_title">امنیت</string>
<string name="pref_lock_title">قفل برنامه</string> <string name="pref_lock_title">قفل برنامه</string>
@@ -414,17 +477,17 @@
<string name="pref_lock_timeout_60">۱ ساعت</string> <string name="pref_lock_timeout_60">۱ ساعت</string>
<string name="pref_lock_timeout_never">هرگز</string> <string name="pref_lock_timeout_never">هرگز</string>
<string name="pref_lock_timeout_never_summary">هیچوقت Briar (برایر) را به صورت خودکار قفل نکن</string> <string name="pref_lock_timeout_never_summary">هیچوقت Briar (برایر) را به صورت خودکار قفل نکن</string>
<string name="change_password">تغییر رمز عبور</string> <string name="change_password">تغییر گذرواژه</string>
<string name="current_password">رمز عبور فعلی</string> <string name="current_password">گذرواژه فعلی</string>
<string name="choose_new_password">رمز عبور جدید</string> <string name="choose_new_password">گذرواژه جدید</string>
<string name="confirm_new_password">تائید رمز جدید</string> <string name="confirm_new_password">تائید گذرواژه جدید</string>
<string name="password_changed">رمز عبور تغییر کرده است</string> <string name="password_changed">گذرواژه تغییر کرده است</string>
<string name="panic_setting">برپایی دکمه هراس</string> <string name="panic_setting">نصب دکمه هراس</string>
<string name="panic_setting_title">دکمه هراس</string> <string name="panic_setting_title">دکمه هراس</string>
<string name="panic_setting_hint">تنظیم نحوه واکنش Briar (برایر) هنگام فعال سازی دکمه هراس</string> <string name="panic_setting_hint">تنظیم نحوه واکنش Briar (برایر) هنگام فعال سازی دکمه هراس</string>
<string name="panic_app_setting_title">برنامه دکمه هراس</string> <string name="panic_app_setting_title">برنامه دکمه هراس</string>
<string name="unknown_app">یک برنامه ناشناخته</string> <string name="unknown_app">یک برنامه ناشناخته</string>
<string name="panic_app_setting_summary">هیچ برنامه تنظیم نشده است</string> <string name="panic_app_setting_summary">هیچ برنامه ای تنظیم نشده است</string>
<string name="panic_app_setting_none">هیچکدام</string> <string name="panic_app_setting_none">هیچکدام</string>
<string name="dialog_title_connect_panic_app">تایید برنامه هراس</string> <string name="dialog_title_connect_panic_app">تایید برنامه هراس</string>
<string name="dialog_message_connect_panic_app">آیا مطمئن هستید که میخواهید به %1$s اجازه دهید تا باعث عملیات مخرب دکمه هراس بشود؟</string> <string name="dialog_message_connect_panic_app">آیا مطمئن هستید که میخواهید به %1$s اجازه دهید تا باعث عملیات مخرب دکمه هراس بشود؟</string>
@@ -432,7 +495,7 @@
<string name="panic_setting_signout_title">خروج</string> <string name="panic_setting_signout_title">خروج</string>
<string name="panic_setting_signout_summary">در صورت کلیک بر روی کلید هراس از Briar (برایر) خارج شو</string> <string name="panic_setting_signout_summary">در صورت کلیک بر روی کلید هراس از Briar (برایر) خارج شو</string>
<string name="purge_setting_title">حذف حساب کاربری</string> <string name="purge_setting_title">حذف حساب کاربری</string>
<string name="purge_setting_summary">پاک کردن حساب کاربری Briar (برایر) شما در صورتی که دکمه هراس فشار داده شود. اخطار: این باعث پاک شدن دائمی تمام هویت ها، مخاطبان و پیام های شما خواهد شد</string> <string name="purge_setting_summary">در صورت فشار دادن دکمه هراس حساب کاربری Briar (برایر) شما حذف خواهد شد. اخطار: این باعث حذف دائمی تمام هویت ها، مخاطبان و پیام های شما خواهد شد</string>
<string name="uninstall_setting_title">پاک کردن Briar (برایر)</string> <string name="uninstall_setting_title">پاک کردن Briar (برایر)</string>
<string name="uninstall_setting_summary">این نیازمند تایید دستی در موعد هراس میباشد</string> <string name="uninstall_setting_summary">این نیازمند تایید دستی در موعد هراس میباشد</string>
<!--Settings Notifications--> <!--Settings Notifications-->
@@ -461,10 +524,10 @@
<string name="feedback_settings_title">بازخورد</string> <string name="feedback_settings_title">بازخورد</string>
<string name="send_feedback">ارسال بازخورد</string> <string name="send_feedback">ارسال بازخورد</string>
<!--Link Warning--> <!--Link Warning-->
<string name="link_warning_title">هشدار لینک</string> <string name="link_warning_title">هشدار پیوند</string>
<string name="link_warning_intro">با باز کردن لینک زیر شما یک برنامه بیرونی را باز خواهید کرد.</string> <string name="link_warning_intro">با باز کردن لینک زیر شما یک برنامه بیرونی را باز خواهید کرد.</string>
<string name="link_warning_text">این می‌تواند برای شناسایی شما مورد استفاده قرار بگیرد. قبل از باز کردن آن به قابل اطمینان بودن شخصی که آن را برای شما ارسال کرده فکر کنید و بعد برای احتیاط آن را با مرورگر تور باز کنید.</string> <string name="link_warning_text">این می‌تواند برای شناسایی شما مورد استفاده قرار بگیرد. قبل از باز کردن این پیوند به قابل اطمینان بودن شخصی که آن را برای شما ارسال کرده فکر کنید و بعد برای احتیاط آن را با مرورگر تور باز کنید.</string>
<string name="link_warning_open_link">باز کردن لینک</string> <string name="link_warning_open_link">باز کردن پیوند</string>
<!--Crash Reporter--> <!--Crash Reporter-->
<string name="crash_report_title">گزارش خطای Briar (برایر)</string> <string name="crash_report_title">گزارش خطای Briar (برایر)</string>
<string name="briar_crashed">متاسفیم، Briar (برایر) از کار افتاده است.</string> <string name="briar_crashed">متاسفیم، Briar (برایر) از کار افتاده است.</string>
@@ -493,21 +556,27 @@
<string name="screen_filter_allow">به این برنامه ها اجازه بده تا روی Briar (برایر) قرار بگیرند</string> <string name="screen_filter_allow">به این برنامه ها اجازه بده تا روی Briar (برایر) قرار بگیرند</string>
<!--Permission Requests--> <!--Permission Requests-->
<string name="permission_camera_title">دسترسی به دوربین</string> <string name="permission_camera_title">دسترسی به دوربین</string>
<string name="permission_camera_request_body">برای اسکن کردن کد QR دسترسی به دوربین لازم است.</string> <string name="permission_camera_request_body">برای اسکن کردن کد کیوآر دسترسی به دوربین لازم است.</string>
<string name="permission_location_title">مجوز موقعیت محل</string> <string name="permission_location_title">اجازه موقعیت</string>
<string name="permission_location_request_body">برای پیدا کردن دستگاه‌های بلوتوث، برایر نیاز به اجازه دسترسی به موقعیت مکانی شما دارد.\n\n برایر موقعیت مکان شما را ذخیره نمی‌کند و آن را با کسی به اشتراک نمی‌گذارد.</string> <string name="permission_location_request_body">برای پیدا کردن دستگاه‌های بلوتوث، Briar (برایر) نیاز به اجازه دسترسی به موقعیت شما دارد.
Briar (برایر) موقعیت شما را ذخیره نمی‌کند و آن را با کسی به اشتراک نمی‌گذارد.</string>
<string name="permission_camera_location_title">دوربین و موقعیت</string> <string name="permission_camera_location_title">دوربین و موقعیت</string>
<string name="permission_camera_location_request_body">برای اسکن کد کیوآر، برایر نیاز به دسترسی به دوربین دارد.\n\n برای پیدا کردن دستگاه‌های بلوتوث، برایر نیاز به اجازه دسترسی به موقعیت مکانی شما دارد.\n\n برایر موقعیت مکان شما را ذخیره نمی‌کند و آن را با کسی به اشتراک نمی‌گذارد.</string> <string name="permission_camera_location_request_body">برای اسکن کد کیوآر، Briar (برایر) نیاز به دسترسی به دوربین دارد.
برای پیدا کردن دستگاه‌های بلوتوث، Briar (برایر) نیاز به اجازه دسترسی به موقعیت شما دارد.
Briar (برایر) موقعیت شما را ذخیره نمی‌کند و آن را با کسی به اشتراک نمی‌گذارد.</string>
<string name="permission_camera_denied_body">شما دسترسی به دوربین را رد کرده اید، اما افزودن مخاطب نیاز به دوربین دارد. <string name="permission_camera_denied_body">شما دسترسی به دوربین را رد کرده اید، اما افزودن مخاطب نیاز به دوربین دارد.
لطفا اجازه دسترسی را بدهید.</string> لطفا اجازه دسترسی را بدهید.</string>
<string name="qr_code">کد QR</string> <string name="qr_code">کد کیوآر</string>
<string name="show_qr_code_fullscreen">نمایش کد QR به صورت فول اسکرین</string> <string name="show_qr_code_fullscreen">نمایش کد کیوآر به صورت فول اسکرین</string>
<!--App Locking--> <!--App Locking-->
<string name="lock_unlock">آنلاک کردن Briar (برایر)</string> <string name="lock_unlock">باز کردن قفل Briar (برایر)</string>
<string name="lock_unlock_verbose">PIN، الگو یا رمز عبور دستگاه خود را برای آنلاک کردن Briar (برایر) وارد کنید</string> <string name="lock_unlock_verbose">پین، الگو یا گذرواژه دستگاه خود را برای باز کردن قفل Briar (برایر) وارد کنید</string>
<string name="lock_unlock_fingerprint_description">برای ادامه سنسور اثر انگشت را با انگشت ثبت شده تان لمس کنید</string> <string name="lock_unlock_fingerprint_description">برای ادامه سنسور اثر انگشت را با انگشت ثبت شده تان لمس کنید</string>
<string name="lock_unlock_password">از رمز عبور استفاده کنید</string> <string name="lock_unlock_password">استفاده از گذرواژه</string>
<string name="lock_is_locked">Briar (برایر) قفل می باشد</string> <string name="lock_is_locked">Briar (برایر) قفل می باشد</string>
<string name="lock_tap_to_unlock">برای آنلاک کردن کلیک کنید</string> <string name="lock_tap_to_unlock">برای آنلاک کردن کلیک کنید</string>
<!--Screenshots--> <!--Screenshots-->
@@ -520,7 +589,7 @@
<!--This is a message to be used in screenshots. Please use the same translation for Bob!--> <!--This is a message to be used in screenshots. Please use the same translation for Bob!-->
<string name="screenshot_message_1">هی باب!</string> <string name="screenshot_message_1">هی باب!</string>
<!--This is a message to be used in screenshots. Please use the same translation for Alice!--> <!--This is a message to be used in screenshots. Please use the same translation for Alice!-->
<string name="screenshot_message_2">هی آلیس! ممنون از اینکه درباره Briar به من گفتی!</string> <string name="screenshot_message_2">هی آلیس! ممنون از اینکه درباره Briar (برایر) به من گفتی!</string>
<!--This is a message to be used in screenshots.--> <!--This is a message to be used in screenshots.-->
<string name="screenshot_message_3">خواهش میکنم، امیدوارم ازش خوشت بیاد 😀</string> <string name="screenshot_message_3">خواهش میکنم، امیدوارم ازش خوشت بیاد 😀</string>
</resources> </resources>

View File

@@ -125,6 +125,11 @@
<string name="date_no_private_messages">Brak wiadomości.</string> <string name="date_no_private_messages">Brak wiadomości.</string>
<string name="no_private_messages">Brak wiadomości do pokazania</string> <string name="no_private_messages">Brak wiadomości do pokazania</string>
<string name="message_hint">Typ wiadomości</string> <string name="message_hint">Typ wiadomości</string>
<string name="image_caption_hint">Dodaj podpis (opcjonalne)</string>
<string name="image_attach">Załącz zdjęcie</string>
<string name="image_attach_error_too_big">Zdjęcie jest zbyt duże. Limit wynosi %d MB.</string>
<string name="image_attach_error_invalid_mime_type">Format zdjęcia jest nieobsługiwany: %s</string>
<string name="set_contact_alias">Zmień imię kontaktu</string>
<string name="set_contact_alias_hint">Nazwa kontaktu</string> <string name="set_contact_alias_hint">Nazwa kontaktu</string>
<string name="set_alias_button">Zmień</string> <string name="set_alias_button">Zmień</string>
<string name="delete_contact">Usuń kontakt</string> <string name="delete_contact">Usuń kontakt</string>
@@ -134,6 +139,13 @@
<!--This is shown in the action bar when opening an image in fullscreen that the user sent--> <!--This is shown in the action bar when opening an image in fullscreen that the user sent-->
<string name="you">You</string> <string name="you">You</string>
<string name="save_image">Zapisz obrazek</string> <string name="save_image">Zapisz obrazek</string>
<string name="dialog_title_save_image">Zapisać Zdjęcie?</string>
<string name="dialog_message_save_image">Zapisanie tego zdjęcia pozwoli innym aplikacjom na dostęp do niego.\n\nJesteś pewien, że chcesz go zapisać?</string>
<string name="save_image_success">Zdjęcie zostało zapisane</string>
<string name="save_image_error">Nie udało się zapisać zdjęcia</string>
<string name="dialog_title_no_image_support">Zdjęcie Niedostępne</string>
<string name="dialog_message_no_image_support">Briar twojego kontaktu nie obsługuje jeszcze załączników zdjęć. Jak wykonają aktualizację, zobaczysz inną ikonę.</string>
<string name="dialog_title_image_support">Możesz teraz wysyłać zdjęcia do tego kontaktu</string>
<!--Adding Contacts--> <!--Adding Contacts-->
<string name="face_to_face">Musisz spotkać się z osobą którą chcesz dodać jako kontakt.\n\nTo uniemożliwi komukolwiek podszyć się pod Ciebie lub czytać wysyłane wiadomości.</string> <string name="face_to_face">Musisz spotkać się z osobą którą chcesz dodać jako kontakt.\n\nTo uniemożliwi komukolwiek podszyć się pod Ciebie lub czytać wysyłane wiadomości.</string>
<string name="continue_button">Kontynuuj</string> <string name="continue_button">Kontynuuj</string>
@@ -153,11 +165,21 @@
<string name="paste_button">Wklej</string> <string name="paste_button">Wklej</string>
<string name="copy_button">Kopiuj</string> <string name="copy_button">Kopiuj</string>
<string name="share_button">Udostępnij</string> <string name="share_button">Udostępnij</string>
<string name="link_copied_toast">Link skopiowany</string>
<string name="add_contact_remote_connecting">Trwa łączenie...</string> <string name="add_contact_remote_connecting">Trwa łączenie...</string>
<string name="connecting">Trwa łączenie...</string> <string name="connecting">Trwa łączenie...</string>
<string name="missing_link">Proszę, wprowadź link</string>
<!--This is a numeral indicating the first step in a series of screens--> <!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string> <string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens--> <!--This is a numeral indicating the second step in a series of screens-->
<string name="offline_state">Brak połączenia z Internetem</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<!--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-->
<!--This is a button for answering that two nicknames refer to different people. This 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-->
<!--Introductions--> <!--Introductions-->
<string name="introduction_onboarding_title">Udostępnij swoje kontakty.</string> <string name="introduction_onboarding_title">Udostępnij swoje kontakty.</string>
<string name="introduction_onboarding_text">Możesz udostępniać kontakty, tak aby osoby nie musiały spotykać się osobiście.</string> <string name="introduction_onboarding_text">Możesz udostępniać kontakty, tak aby osoby nie musiały spotykać się osobiście.</string>
@@ -361,10 +383,14 @@
<!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)--> <!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)-->
<string name="tor_network_setting_summary">Automatycznie: %1$s (za %2$s)</string> <string name="tor_network_setting_summary">Automatycznie: %1$s (za %2$s)</string>
<string name="tor_mobile_data_title">Używaj danych komórkowych</string> <string name="tor_mobile_data_title">Używaj danych komórkowych</string>
<string name="tor_only_when_charging_title">Łącz przez Internet (Tor) tylko podczas ładowania</string>
<string name="tor_only_when_charging_summary">Wyłącza połączenie Internetowe, gdy urządzenie pracuje na baterii</string>
<!--Settings Security and Panic--> <!--Settings Security and Panic-->
<string name="security_settings_title">Bezpieczeństwo</string> <string name="security_settings_title">Bezpieczeństwo</string>
<string name="pref_lock_title">Blokada aplikacji</string>
<string name="pref_lock_summary">Użyj blokady ekranu urządzenia by chronić Briar gdy zalogowany</string> <string name="pref_lock_summary">Użyj blokady ekranu urządzenia by chronić Briar gdy zalogowany</string>
<string name="pref_lock_disabled_summary">Aby użyć tej funkcji skonfiguruj blokadę ekranu na swoim urządzeniu</string> <string name="pref_lock_disabled_summary">Aby użyć tej funkcji skonfiguruj blokadę ekranu na swoim urządzeniu</string>
<string name="pref_lock_timeout_title">Czas bezczynności aplikacji</string>
<!--The %s placeholder is replaced with the following time spans, e.g. 5 Minutes, 1 Hour--> <!--The %s placeholder is replaced with the following time spans, e.g. 5 Minutes, 1 Hour-->
<string name="pref_lock_timeout_summary">Gdy nie używam Briar, automatycznie zablokuj po %s</string> <string name="pref_lock_timeout_summary">Gdy nie używam Briar, automatycznie zablokuj po %s</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"--> <!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
@@ -393,6 +419,7 @@
<string name="panic_app_setting_none">Brak</string> <string name="panic_app_setting_none">Brak</string>
<string name="dialog_title_connect_panic_app">Potwierdź Awaryjną Aplikację</string> <string name="dialog_title_connect_panic_app">Potwierdź Awaryjną Aplikację</string>
<string name="dialog_message_connect_panic_app">Czy na pewno chcesz pozwolić %1$s aby działała jako przycisk wyjścia awaryjnego?</string> <string name="dialog_message_connect_panic_app">Czy na pewno chcesz pozwolić %1$s aby działała jako przycisk wyjścia awaryjnego?</string>
<string name="panic_setting_destructive_action">Działania Destrukcyjne</string>
<string name="panic_setting_signout_title">Wyloguj się</string> <string name="panic_setting_signout_title">Wyloguj się</string>
<string name="panic_setting_signout_summary">Wyloguj się z Briar jeśli przycisk zostanie wciśnięty</string> <string name="panic_setting_signout_summary">Wyloguj się z Briar jeśli przycisk zostanie wciśnięty</string>
<string name="purge_setting_title">Skasuj Konto</string> <string name="purge_setting_title">Skasuj Konto</string>
@@ -427,6 +454,7 @@
<!--Link Warning--> <!--Link Warning-->
<string name="link_warning_title">Uważaj na link</string> <string name="link_warning_title">Uważaj na link</string>
<string name="link_warning_intro">Otwierasz link w zewnętrznej aplikacji.</string> <string name="link_warning_intro">Otwierasz link w zewnętrznej aplikacji.</string>
<string name="link_warning_text">Może to posłużyć do twojej identyfikacji. Zastanów się czy ufasz osobie która wysłała Ci ten link i rozważ otwarcie go za pomocą Przeglądarki Tor.</string>
<string name="link_warning_open_link">Otwórz Link</string> <string name="link_warning_open_link">Otwórz Link</string>
<!--Crash Reporter--> <!--Crash Reporter-->
<string name="crash_report_title">Zgłoś błąd w Briar</string> <string name="crash_report_title">Zgłoś błąd w Briar</string>
@@ -454,6 +482,7 @@
<string name="permission_camera_title">Dostęp do aparatu</string> <string name="permission_camera_title">Dostęp do aparatu</string>
<string name="permission_camera_request_body">Aby zeskanować kod QR, Briar potrzebuje mieć dostęp do aparatu.</string> <string name="permission_camera_request_body">Aby zeskanować kod QR, Briar potrzebuje mieć dostęp do aparatu.</string>
<string name="permission_camera_location_title">Kamera i lokalizacja</string> <string name="permission_camera_location_title">Kamera i lokalizacja</string>
<string name="permission_camera_location_request_body">Aby zeskanować kod QR, Briar potrzebuje dostępu do kamery.\n\nAby odkryć urządzenia Bluetooth, Briar potrzebuje zezwolenia na dostęp do Twojej lokalizacji.\n\nBriar nie przechowuje Twojej lokalizacji ani nie udostępnia jej nikomu.</string>
<string name="permission_camera_denied_body">Odmówiłeś dostępu do kamery, lecz dodawanie kontaktów tego wymaga.\n\nProszę udzielić dostępu.</string> <string name="permission_camera_denied_body">Odmówiłeś dostępu do kamery, lecz dodawanie kontaktów tego wymaga.\n\nProszę udzielić dostępu.</string>
<string name="qr_code">Kod QR</string> <string name="qr_code">Kod QR</string>
<string name="show_qr_code_fullscreen">Pokaż QR na pełnym ekranie</string> <string name="show_qr_code_fullscreen">Pokaż QR na pełnym ekranie</string>
@@ -466,9 +495,15 @@
<string name="lock_tap_to_unlock">Dotknij by odblokować</string> <string name="lock_tap_to_unlock">Dotknij by odblokować</string>
<!--Screenshots--> <!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.--> <!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Alicja</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.--> <!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_bob">Bob</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.--> <!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_carol">Karol</string>
<!--This is a message to be used in screenshots. Please use the same translation for Bob!--> <!--This is a message to be used in screenshots. Please use the same translation for Bob!-->
<string name="screenshot_message_1">Hej Bob!</string>
<!--This is a message to be used in screenshots. Please use the same translation for Alice!--> <!--This is a message to be used in screenshots. Please use the same translation for Alice!-->
<string name="screenshot_message_2">Hej Alicja! Dzięki za opowiedzenie mi o Briarze!</string>
<!--This is a message to be used in screenshots.--> <!--This is a message to be used in screenshots.-->
<string name="screenshot_message_3">Nie ma problemu, mam nadzieję, że ci się podoba 😀</string>
</resources> </resources>

View File

@@ -116,6 +116,12 @@
<string name="date_no_private_messages">Sem Mensagens</string> <string name="date_no_private_messages">Sem Mensagens</string>
<string name="no_private_messages">Nenhuma mensagem para ser exibida</string> <string name="no_private_messages">Nenhuma mensagem para ser exibida</string>
<string name="message_hint">Digite a mensagem</string> <string name="message_hint">Digite a mensagem</string>
<string name="image_caption_hint">Adicione uma legenda (opcional)</string>
<string name="image_attach">Anexar imagem</string>
<string name="image_attach_error">Não foi possível anexar imagem(s)</string>
<string name="image_attach_error_too_big">Imagem muito grande. O limite é de %dMB. </string>
<string name="image_attach_error_invalid_mime_type">Formato da imagem não suportado: %s</string>
<string name="set_contact_alias">Alterar nome do contato</string>
<string name="set_contact_alias_hint">Nome de contato</string> <string name="set_contact_alias_hint">Nome de contato</string>
<string name="set_alias_button">Trocar</string> <string name="set_alias_button">Trocar</string>
<string name="delete_contact">Apagar contato</string> <string name="delete_contact">Apagar contato</string>
@@ -124,7 +130,17 @@
<string name="contact_deleted_toast">Contato apagado</string> <string name="contact_deleted_toast">Contato apagado</string>
<!--This is shown in the action bar when opening an image in fullscreen that the user sent--> <!--This is shown in the action bar when opening an image in fullscreen that the user sent-->
<string name="you">Você</string> <string name="you">Você</string>
<string name="save_image">Salvar imagem</string>
<string name="dialog_title_save_image">Salvar imagem?</string>
<string name="dialog_message_save_image">Salvando essa imagem irá permitir que outros aplicativos a acessem.\n\nVocê tem certeza que quer salvar? </string>
<string name="save_image_success">Imagem salva</string>
<string name="save_image_error">Não foi possível salvar imagem</string>
<string name="dialog_title_no_image_support">Imagens indisponíveis</string>
<string name="dialog_message_no_image_support">Seu contato do Briar ainda não suporta anexo de imagens. Quando ele atualizar um você verá ícone diferente.</string>
<string name="dialog_title_image_support">Você agora pode enviar imagens para esse contato</string>
<string name="dialog_message_image_support">Toque nesse ícone para anexar imagens.</string>
<!--Adding Contacts--> <!--Adding Contacts-->
<string name="add_contact_title">Adicionar contato que está próximo</string>
<string name="face_to_face">Você deve estar frente-a-frente com a pessoa que deseja adicionar como contato.\n\nIsso evita que alguém se passe por você ou leia suas mensagens no futuro.</string> <string name="face_to_face">Você deve estar frente-a-frente com a pessoa que deseja adicionar como contato.\n\nIsso evita que alguém se passe por você ou leia suas mensagens no futuro.</string>
<string name="continue_button">Continuar</string> <string name="continue_button">Continuar</string>
<string name="try_again_button">Tente novamente</string> <string name="try_again_button">Tente novamente</string>
@@ -142,15 +158,66 @@
<string name="connection_error_explanation">Por favor. certifique se ambos estão conectados na mesma rede Wi-Fi.</string> <string name="connection_error_explanation">Por favor. certifique se ambos estão conectados na mesma rede Wi-Fi.</string>
<string name="connection_error_feedback">Se o problema persistir, for favor <a href="feedback">enviar feedback</a> para nós ajudar a melhor o app.</string> <string name="connection_error_feedback">Se o problema persistir, for favor <a href="feedback">enviar feedback</a> para nós ajudar a melhor o app.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Adicionar contato à distância</string>
<string name="add_contact_nearby_title">Adicionar contato que está próximo</string>
<string name="add_contact_remotely_title">Adicionar contato à distância</string>
<string name="contact_name_hint">De um apelido ao contato</string>
<string name="contact_link_intro">Insira o link do seu contato aqui</string>
<string name="contact_link_hint">Link do contato</string>
<string name="paste_button">Colar</string> <string name="paste_button">Colar</string>
<string name="add_contact_button">Adicionar contato</string>
<string name="copy_button">Copiar</string> <string name="copy_button">Copiar</string>
<string name="share_button">Compartilhar</string> <string name="share_button">Compartilhar</string>
<string name="send_link_title">Passem os links</string>
<string name="add_contact_choose_nickname">Escolha um apelido</string>
<string name="add_contact_choose_a_nickname">Insira um apelido</string>
<string name="nickname_intro">De um apelido ao contato. Somente você pode vê-lo. </string>
<string name="your_link">Passe esse link para o contato que você quer adicionar</string>
<string name="link_clip_label">Link Briar</string>
<string name="link_copied_toast">Link copiado</string>
<string name="adding_contact_error">Ocorreu um erro ao adicionar o contato.</string>
<string name="pending_contact_requests_snackbar">Existem solicitações de contato pendentes</string>
<string name="pending_contact_requests">Solicitações de contatos pendentes</string>
<string name="no_pending_contacts">Sem contatos pendentes</string>
<string name="add_contact_remote_connecting">Conectando...</string> <string name="add_contact_remote_connecting">Conectando...</string>
<string name="waiting_for_contact_to_come_online">Esperando pelo usuário estar online ...</string>
<string name="connecting">Conectando...</string> <string name="connecting">Conectando...</string>
<string name="adding_contact">Adicionando contato ...</string>
<string name="adding_contact_failed">Adicionar o contato falhou</string>
<string name="dialog_title_remove_pending_contact">Confirme a remoção </string>
<string name="dialog_message_remove_pending_contact">Esse contato ainda está sendo adicionado. Se você removê-lo agora, o mesmo não será adicionado</string>
<string name="own_link_error">Insira o link do seu contato, não seu próprio </string>
<string name="nickname_missing">Por favor insira um apelido</string>
<string name="invalid_link">Link invalido</string>
<string name="unsupported_link">Esse link veio de uma versão mais recente do Briar. Por favor atualize para versão mais recente e tente novamente </string>
<string name="intent_own_link">Você abriu seu próprio link. Utilize o link do contato que quer adicionar!</string>
<string name="missing_link">Por favor insira um link</string>
<!--This is a numeral indicating the first step in a series of screens--> <!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string> <string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens--> <!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string> <string name="step_2">2</string>
<plurals name="contact_added_notification_text">
<item quantity="one">Novo contato adicionado.</item>
<item quantity="other">%dnovos contados adicionados.</item>
</plurals>
<string name="adding_contact_slow_warning">Adicionar este contato está demorando mais que o normal</string>
<string name="adding_contact_slow_title">Não foi possível conectar ao contato </string>
<string name="adding_contact_slow_text">Adicionar este contato está demorando mais que o normal.\n\nPor favor se certifique que o seu contato recebeu seu link e adicionou você: </string>
<string name="offline_state">Sem conexão à Internet</string>
<string name="duplicate_link_dialog_title">Link duplicado</string>
<string name="duplicate_link_dialog_text_1">Você já tem um contato pendente com esse link: %s </string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">%s e %s são a mesma pessoa?</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-->
<string name="same_person_button">Mesma pessoa</string>
<!--This is a button for answering that two nicknames refer to different people. This 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">Pessoa diferente</string>
<string name="duplicate_link_dialog_text_3">%s e %s te enviaram o mesmo link.\n\nUm deles talvez esteja tentando descobrir quem são seus contatos.\n\nNão conte pra eles que recebeu o mesmo link de outra pessoa.</string>
<string name="pending_contact_updated_toast">Contato pendente atualizado</string>
<!--Introductions--> <!--Introductions-->
<string name="introduction_onboarding_title">Apresente seus contatos</string> <string name="introduction_onboarding_title">Apresente seus contatos</string>
<string name="introduction_onboarding_text">Você pode apresentar seus contatos entre si, assim eles não precisam se encontrar pessoalmente para se comunicar no Briar.</string> <string name="introduction_onboarding_text">Você pode apresentar seus contatos entre si, assim eles não precisam se encontrar pessoalmente para se comunicar no Briar.</string>
@@ -346,6 +413,8 @@
<!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)--> <!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)-->
<string name="tor_network_setting_summary">Automático: %1$s (em %2$s) </string> <string name="tor_network_setting_summary">Automático: %1$s (em %2$s) </string>
<string name="tor_mobile_data_title">Usar dados moveis</string> <string name="tor_mobile_data_title">Usar dados moveis</string>
<string name="tor_only_when_charging_title">Conectar via Internet (Tor) apenas enquanto carregando</string>
<string name="tor_only_when_charging_summary">Desabilita conexão à Internet quando seu dispositivo estiver somente na bateria</string>
<!--Settings Security and Panic--> <!--Settings Security and Panic-->
<string name="security_settings_title">Segurança</string> <string name="security_settings_title">Segurança</string>
<string name="pref_lock_title">Bloqueio de app</string> <string name="pref_lock_title">Bloqueio de app</string>
@@ -415,6 +484,7 @@
<!--Link Warning--> <!--Link Warning-->
<string name="link_warning_title">Aviso sobre Link</string> <string name="link_warning_title">Aviso sobre Link</string>
<string name="link_warning_intro">Você está prestes a abrir esse link em um Aplicativo Externo.</string> <string name="link_warning_intro">Você está prestes a abrir esse link em um Aplicativo Externo.</string>
<string name="link_warning_text">Isso pode ser usado para identificar você. Pense sobre se quer ou não confiar na pessoa que te enviou esse link e considere abrindo o no Tor Browser</string>
<string name="link_warning_open_link">Abrir Link</string> <string name="link_warning_open_link">Abrir Link</string>
<!--Crash Reporter--> <!--Crash Reporter-->
<string name="crash_report_title">Relatório de falhas do Briar</string> <string name="crash_report_title">Relatório de falhas do Briar</string>

View File

@@ -114,6 +114,8 @@
<string name="image_caption_hint">添加一段描述(可选)</string> <string name="image_caption_hint">添加一段描述(可选)</string>
<string name="image_attach">附加图片</string> <string name="image_attach">附加图片</string>
<string name="image_attach_error">无法附加图片</string> <string name="image_attach_error">无法附加图片</string>
<string name="image_attach_error_too_big">图片过大。上限为 %d MB。</string>
<string name="image_attach_error_invalid_mime_type">不支持的图片格式:%s</string>
<string name="set_contact_alias">更改联系人姓名</string> <string name="set_contact_alias">更改联系人姓名</string>
<string name="set_contact_alias_hint">联系人姓名</string> <string name="set_contact_alias_hint">联系人姓名</string>
<string name="set_alias_button">更改</string> <string name="set_alias_button">更改</string>
@@ -133,6 +135,7 @@
<string name="dialog_title_image_support">你现在可以向此联系人发送图片</string> <string name="dialog_title_image_support">你现在可以向此联系人发送图片</string>
<string name="dialog_message_image_support">轻按此图标即可附加图片。</string> <string name="dialog_message_image_support">轻按此图标即可附加图片。</string>
<!--Adding Contacts--> <!--Adding Contacts-->
<string name="add_contact_title">添加附近的联系人</string>
<string name="face_to_face">您必须面对面添加联系人。\n\n这样将防止未来他人冒充您的身份并查看您的信息。</string> <string name="face_to_face">您必须面对面添加联系人。\n\n这样将防止未来他人冒充您的身份并查看您的信息。</string>
<string name="continue_button">继续</string> <string name="continue_button">继续</string>
<string name="try_again_button">重试</string> <string name="try_again_button">重试</string>
@@ -150,8 +153,65 @@
<string name="connection_error_explanation">请确保你们连接到了同一个无线局域网中。</string> <string name="connection_error_explanation">请确保你们连接到了同一个无线局域网中。</string>
<string name="connection_error_feedback">如果该问题仍存在,请 <a href="feedback">发送反馈</a> 帮助我们改善应用。</string> <string name="connection_error_feedback">如果该问题仍存在,请 <a href="feedback">发送反馈</a> 帮助我们改善应用。</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">添加远处的联系人</string>
<string name="add_contact_nearby_title">添加附近的联系人</string>
<string name="add_contact_remotely_title">添加远处的联系人</string>
<string name="contact_name_hint">添加联系人昵称</string>
<string name="contact_link_intro">在此输入您联系人的链接</string>
<string name="contact_link_hint">联系人的链接</string>
<string name="paste_button">粘贴</string>
<string name="add_contact_button">添加联系人</string>
<string name="copy_button">复制</string>
<string name="share_button">分享</string>
<string name="send_link_title">交换链接</string>
<string name="add_contact_choose_nickname">选择昵称</string>
<string name="add_contact_choose_a_nickname">输入昵称</string>
<string name="nickname_intro">为联系人添加一个昵称。仅您可见。</string>
<string name="your_link">将此链接发送给您希望添加的联系人</string>
<string name="link_clip_label">Briar 链接</string>
<string name="link_copied_toast">链接已复制</string>
<string name="adding_contact_error">添加联系人时发生错误。</string>
<string name="pending_contact_requests_snackbar">有待处理的联系人请求</string>
<string name="pending_contact_requests">待处理的联系人请求</string>
<string name="no_pending_contacts">没有待处理的联系人</string>
<string name="add_contact_remote_connecting">连接中……</string>
<string name="waiting_for_contact_to_come_online">正在等待联系人上线……</string>
<string name="connecting">连接中……</string>
<string name="adding_contact">正在添加联系人……</string>
<string name="adding_contact_failed">添加联系人失败</string>
<string name="dialog_title_remove_pending_contact">确认移除</string>
<string name="dialog_message_remove_pending_contact">这个联系人还在添加中。如果您现在移除,则此联系人不会被添加。</string>
<string name="own_link_error">输入您联系人的链接,不要输入您自己的</string>
<string name="nickname_missing">请输入一个昵称</string>
<string name="invalid_link">无效的链接</string>
<string name="unsupported_link">这条链接来自更新版本的 Briar。请升级到最新版本后并重试。</string>
<string name="intent_own_link">您打开了自己的链接。请使用您希望添加的联系人的链接!</string>
<string name="missing_link">请输入一个链接</string>
<!--This is a numeral indicating the first step in a series of screens--> <!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens--> <!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string>
<plurals name="contact_added_notification_text">
<item quantity="other">%d 新联系人已添加。</item>
</plurals>
<string name="adding_contact_slow_warning">添加此联系人比通常花费了更多时间</string>
<string name="adding_contact_slow_title">无法连接到联系人</string>
<string name="adding_contact_slow_text">添加此联系人比通常花费了更多时间。\n\n请检查您的联系人是否收到您的链接并已添加您</string>
<string name="offline_state">无网络连接</string>
<string name="duplicate_link_dialog_title">重复的链接</string>
<string name="duplicate_link_dialog_text_1">您已经有此链接的待处理联系人:</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">%s 和 %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-->
<string name="same_person_button">同一个人</string>
<!--This is a button for answering that two nicknames refer to different people. This 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">不同的人</string>
<string name="duplicate_link_dialog_text_3">%s 和 %s 给你发送了相同的链接。\n\n其中一个人可能企图找出谁是你的联系人。\n\n不要告诉他们你从其他人那里收到了相同的链接。</string>
<string name="pending_contact_updated_toast">待处理联系人已更新</string>
<!--Introductions--> <!--Introductions-->
<string name="introduction_onboarding_title">介绍您的联系人</string> <string name="introduction_onboarding_title">介绍您的联系人</string>
<string name="introduction_onboarding_text">您可以互相介绍您的联系人,这样他们可以直接在 Briar 上建立联系而不必亲自见面。</string> <string name="introduction_onboarding_text">您可以互相介绍您的联系人,这样他们可以直接在 Briar 上建立联系而不必亲自见面。</string>

View File

@@ -2,14 +2,13 @@ package org.briarproject.briar.android.attachment;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.Test; import org.junit.Test;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
@@ -25,32 +24,30 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
100, 50, 200, 75, 300 100, 50, 200, 75, 300
); );
private final MessageId msgId = new MessageId(getRandomId()); private final MessageId msgId = new MessageId(getRandomId());
private final Attachment attachment = new Attachment(
new BufferedInputStream(
new ByteArrayInputStream(getRandomBytes(42))));
private final MessagingManager messagingManager =
context.mock(MessagingManager.class);
private final ImageHelper imageHelper = context.mock(ImageHelper.class); private final ImageHelper imageHelper = context.mock(ImageHelper.class);
private final AttachmentRetriever controller = private final ImageSizeCalculator imageSizeCalculator;
new AttachmentRetriever( private final AttachmentRetriever retriever;
messagingManager,
dimensions, public AttachmentRetrieverTest() {
imageHelper context.setImposteriser(ClassImposteriser.INSTANCE);
); MessagingManager messagingManager =
context.mock(MessagingManager.class);
imageSizeCalculator = context.mock(ImageSizeCalculator.class);
retriever = new AttachmentRetrieverImpl(messagingManager, dimensions,
imageHelper, imageSizeCalculator);
}
@Test @Test
public void testNoSize() { public void testNoSize() {
String mimeType = "image/jpeg"; String mimeType = "image/jpeg";
AttachmentHeader h = getAttachmentHeader(mimeType); Attachment attachment = getAttachment(mimeType);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(imageHelper).getExtensionFromMimeType(mimeType); oneOf(imageHelper).getExtensionFromMimeType(mimeType);
will(returnValue("jpg")); will(returnValue("jpg"));
}}); }});
AttachmentItem item = AttachmentItem item = retriever.getAttachmentItem(attachment, false);
controller.getAttachmentItem(h, attachment, false);
assertEquals(mimeType, item.getMimeType()); assertEquals(mimeType, item.getMimeType());
assertEquals("jpg", item.getExtension()); assertEquals("jpg", item.getExtension());
assertFalse(item.hasError()); assertFalse(item.hasError());
@@ -59,31 +56,31 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
@Test @Test
public void testNoSizeWrongMimeTypeProducesError() { public void testNoSizeWrongMimeTypeProducesError() {
String mimeType = "application/octet-stream"; String mimeType = "application/octet-stream";
AttachmentHeader h = getAttachmentHeader(mimeType); Attachment attachment = getAttachment(mimeType);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(imageHelper).getExtensionFromMimeType(mimeType); oneOf(imageHelper).getExtensionFromMimeType(mimeType);
will(returnValue(null)); will(returnValue(null));
}}); }});
AttachmentItem item = AttachmentItem item = retriever.getAttachmentItem(attachment, false);
controller.getAttachmentItem(h, attachment, false);
assertTrue(item.hasError()); assertTrue(item.hasError());
} }
@Test @Test
public void testSmallJpegImage() { public void testSmallJpegImage() {
String mimeType = "image/jpeg"; String mimeType = "image/jpeg";
AttachmentHeader h = getAttachmentHeader(mimeType); Attachment attachment = getAttachment(mimeType);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(imageHelper).decodeStream(with(any(InputStream.class))); oneOf(imageSizeCalculator).getSize(with(any(InputStream.class)),
will(returnValue(new DecodeResult(160, 240, mimeType))); with(mimeType));
will(returnValue(new Size(160, 240, mimeType)));
oneOf(imageHelper).getExtensionFromMimeType(mimeType); oneOf(imageHelper).getExtensionFromMimeType(mimeType);
will(returnValue("jpg")); will(returnValue("jpg"));
}}); }});
AttachmentItem item = controller.getAttachmentItem(h, attachment, true); AttachmentItem item = retriever.getAttachmentItem(attachment, true);
assertEquals(msgId, item.getMessageId()); assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth()); assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight()); assertEquals(240, item.getHeight());
@@ -97,16 +94,17 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
@Test @Test
public void testBigJpegImage() { public void testBigJpegImage() {
String mimeType = "image/jpeg"; String mimeType = "image/jpeg";
AttachmentHeader h = getAttachmentHeader(mimeType); Attachment attachment = getAttachment(mimeType);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(imageHelper).decodeStream(with(any(InputStream.class))); oneOf(imageSizeCalculator).getSize(with(any(InputStream.class)),
will(returnValue(new DecodeResult(1728, 2592, mimeType))); with(mimeType));
will(returnValue(new Size(1728, 2592, mimeType)));
oneOf(imageHelper).getExtensionFromMimeType(mimeType); oneOf(imageHelper).getExtensionFromMimeType(mimeType);
will(returnValue("jpg")); will(returnValue("jpg"));
}}); }});
AttachmentItem item = controller.getAttachmentItem(h, attachment, true); AttachmentItem item = retriever.getAttachmentItem(attachment, true);
assertEquals(1728, item.getWidth()); assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight()); assertEquals(2592, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -116,21 +114,24 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
@Test @Test
public void testMalformedError() { public void testMalformedError() {
AttachmentHeader h = getAttachmentHeader("image/jpeg"); String mimeType = "image/jpeg";
Attachment attachment = getAttachment(mimeType);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(imageHelper).decodeStream(with(any(InputStream.class))); oneOf(imageSizeCalculator).getSize(with(any(InputStream.class)),
will(returnValue(new DecodeResult(0, 0, ""))); with(mimeType));
will(returnValue(new Size()));
oneOf(imageHelper).getExtensionFromMimeType(""); oneOf(imageHelper).getExtensionFromMimeType("");
will(returnValue(null)); will(returnValue(null));
}}); }});
AttachmentItem item = controller.getAttachmentItem(h, attachment, true); AttachmentItem item = retriever.getAttachmentItem(attachment, true);
assertTrue(item.hasError()); assertTrue(item.hasError());
} }
private AttachmentHeader getAttachmentHeader(String contentType) { private Attachment getAttachment(String contentType) {
return new AttachmentHeader(msgId, contentType); AttachmentHeader header = new AttachmentHeader(msgId, contentType);
InputStream in = new ByteArrayInputStream(getRandomBytes(42));
return new Attachment(header, in);
} }
} }

View File

@@ -1,15 +1,27 @@
package org.briarproject.briar.api.messaging; package org.briarproject.briar.api.messaging;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream; import java.io.InputStream;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class Attachment { public class Attachment {
private final AttachmentHeader header;
private final InputStream stream; private final InputStream stream;
public Attachment(InputStream stream) { public Attachment(AttachmentHeader header, InputStream stream) {
this.header = header;
this.stream = stream; this.stream = stream;
} }
public AttachmentHeader getHeader() {
return header;
}
public InputStream getStream() { public InputStream getStream() {
return stream; return stream;
} }

View File

@@ -0,0 +1,14 @@
package org.briarproject.briar.api.messaging;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
/**
* An exception that is thrown when an {@link AttachmentHeader} is used to
* load an {@link Attachment}, and the header refers to a message that is not
* an attachment, or to an attachment that does not have the expected content
* type.
*/
@NotNullByDefault
public class InvalidAttachmentException extends DbException {
}

View File

@@ -30,7 +30,7 @@ public interface MessagingManager extends ConversationClient {
/** /**
* The current minor version of the messaging client. * The current minor version of the messaging client.
*/ */
int MINOR_VERSION = 1; int MINOR_VERSION = 2;
/** /**
* Stores a local private message. * Stores a local private message.
@@ -40,7 +40,7 @@ public interface MessagingManager extends ConversationClient {
/** /**
* Stores a local attachment message. * Stores a local attachment message.
* *
* @throws FileTooBigException * @throws FileTooBigException If the attachment is too big
*/ */
AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp, AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp,
String contentType, InputStream is) throws DbException, IOException; String contentType, InputStream is) throws DbException, IOException;
@@ -68,9 +68,13 @@ public interface MessagingManager extends ConversationClient {
String getMessageText(MessageId m) throws DbException; String getMessageText(MessageId m) throws DbException;
/** /**
* Returns the attachment with the given ID. * Returns the attachment with the given message ID and content type.
*
* @throws InvalidAttachmentException If the header refers to a message
* that is not an attachment, or to an attachment that does not have the
* expected content type
*/ */
Attachment getAttachment(MessageId m) throws DbException; Attachment getAttachment(AttachmentHeader h) throws DbException;
/** /**
* Returns true if the contact with the given {@link ContactId} does support * Returns true if the contact with the given {@link ContactId} does support

View File

@@ -32,6 +32,7 @@ import org.briarproject.briar.api.conversation.ConversationMessageHeader;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.FileTooBigException; import org.briarproject.briar.api.messaging.FileTooBigException;
import org.briarproject.briar.api.messaging.InvalidAttachmentException;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
@@ -374,13 +375,20 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
} }
@Override @Override
public Attachment getAttachment(MessageId m) throws DbException { public Attachment getAttachment(AttachmentHeader h) throws DbException {
// TODO: Support large messages // TODO: Support large messages
MessageId m = h.getMessageId();
byte[] body = clientHelper.getMessage(m).getBody(); byte[] body = clientHelper.getMessage(m).getBody();
try { try {
BdfDictionary meta = clientHelper.getMessageMetadataAsDictionary(m); BdfDictionary meta = clientHelper.getMessageMetadataAsDictionary(m);
Long messageType = meta.getOptionalLong(MSG_KEY_MSG_TYPE);
if (messageType == null || messageType != ATTACHMENT)
throw new InvalidAttachmentException();
String contentType = meta.getString(MSG_KEY_CONTENT_TYPE);
if (!contentType.equals(h.getContentType()))
throw new InvalidAttachmentException();
int offset = meta.getLong(MSG_KEY_DESCRIPTOR_LENGTH).intValue(); int offset = meta.getLong(MSG_KEY_DESCRIPTOR_LENGTH).intValue();
return new Attachment(new ByteArrayInputStream(body, offset, return new Attachment(h, new ByteArrayInputStream(body, offset,
body.length - offset)); body.length - offset));
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);

View File

@@ -65,7 +65,7 @@ public class MessagingModule {
conversationManager.registerConversationClient(messagingManager); conversationManager.registerConversationClient(messagingManager);
// Advertise the current or previous minor version depending on the // Advertise the current or previous minor version depending on the
// feature flag // feature flag
int minorVersion = featureFlags.shouldEnableImageAttachments() ? 1 : 0; int minorVersion = featureFlags.shouldEnableImageAttachments() ? 2 : 0;
clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION, clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
minorVersion, messagingManager); minorVersion, messagingManager);
return messagingManager; return messagingManager;