From 41411b0e2ee55ac68738e817a22a2a03e591479c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 20 Jun 2019 18:24:23 -0300 Subject: [PATCH 001/582] Refactor attachment loading to support incremental display once loaded --- .../AttachmentRetrieverIntegrationTest.java | 59 ++++----- .../attachment/AttachmentCreatorImpl.java | 12 +- .../android/attachment/AttachmentItem.java | 61 ++++++--- .../attachment/AttachmentRetriever.java | 27 ++-- .../attachment/AttachmentRetrieverImpl.java | 118 ++++++++++++++---- .../android/attachment/UnavailableItem.java | 34 +++++ .../conversation/ConversationActivity.java | 102 ++++++--------- .../conversation/ConversationMessageItem.java | 13 +- .../android/conversation/ImageActivity.java | 8 +- .../android/conversation/ImageAdapter.java | 5 +- .../android/conversation/ImageFragment.java | 11 +- .../android/conversation/ImageViewHolder.java | 27 ++-- .../src/main/res/drawable/ic_image_broken.xml | 4 +- .../main/res/drawable/ic_image_missing.xml | 10 ++ .../attachment/AttachmentRetrieverTest.java | 24 ++-- .../briar/api/messaging/MessagingManager.java | 2 +- 16 files changed, 333 insertions(+), 184 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java create mode 100644 briar-android/src/main/res/drawable/ic_image_missing.xml diff --git a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java index 55e847ce0..9c4ffc8d5 100644 --- a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java +++ b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java @@ -13,8 +13,9 @@ import java.util.Random; import androidx.test.ext.junit.runners.AndroidJUnit4; import static androidx.test.InstrumentationRegistry.getContext; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @RunWith(AndroidJUnit4.class) @@ -35,7 +36,7 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getAssetInputStream("kitten_small.jpg"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(msgId, item.getMessageId()); assertEquals(160, item.getWidth()); assertEquals(240, item.getHeight()); @@ -43,7 +44,7 @@ public class AttachmentRetrieverIntegrationTest { assertEquals(240, item.getThumbnailHeight()); assertEquals("image/jpeg", item.getMimeType()); assertJpgOrJpeg(item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -51,7 +52,7 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getAssetInputStream("kitten_original.jpg"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(msgId, item.getMessageId()); assertEquals(1728, item.getWidth()); assertEquals(2592, item.getHeight()); @@ -59,7 +60,7 @@ public class AttachmentRetrieverIntegrationTest { assertEquals(dimensions.maxHeight, item.getThumbnailHeight()); assertEquals("image/jpeg", item.getMimeType()); assertJpgOrJpeg(item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -67,7 +68,7 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/png"); InputStream is = getAssetInputStream("kitten.png"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(msgId, item.getMessageId()); assertEquals(737, item.getWidth()); assertEquals(510, item.getHeight()); @@ -75,7 +76,7 @@ public class AttachmentRetrieverIntegrationTest { assertEquals(138, item.getThumbnailHeight()); assertEquals("image/png", item.getMimeType()); assertEquals("png", item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -83,14 +84,14 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); InputStream is = getAssetInputStream("uber.gif"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(1, item.getWidth()); assertEquals(1, item.getHeight()); assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailHeight()); assertEquals("image/gif", item.getMimeType()); assertEquals("gif", item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -98,14 +99,14 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getAssetInputStream("lottapixel.jpg"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(64250, item.getWidth()); assertEquals(64250, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailHeight()); assertEquals("image/jpeg", item.getMimeType()); assertJpgOrJpeg(item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -113,14 +114,14 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/png"); InputStream is = getAssetInputStream("image_io_crash.png"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(1184, item.getWidth()); assertEquals(448, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailHeight()); assertEquals("image/png", item.getMimeType()); assertEquals("png", item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -128,14 +129,14 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); InputStream is = getAssetInputStream("gimp_crash.gif"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(1, item.getWidth()); assertEquals(1, item.getHeight()); assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailHeight()); assertEquals("image/gif", item.getMimeType()); assertEquals("gif", item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -143,14 +144,14 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); InputStream is = getAssetInputStream("opti_png_afl.gif"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(32, item.getWidth()); assertEquals(32, item.getHeight()); assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailHeight()); assertEquals("image/gif", item.getMimeType()); assertEquals("gif", item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -158,8 +159,8 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getAssetInputStream("libraw_error.jpg"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); - assertTrue(item.hasError()); + AttachmentItem item = retriever.createAttachmentItem(a, true); + assertEquals(ERROR, item.getState()); } @Test @@ -167,14 +168,14 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); InputStream is = getAssetInputStream("animated.gif"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(65535, item.getWidth()); assertEquals(65535, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailHeight()); assertEquals("image/gif", item.getMimeType()); assertEquals("gif", item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -182,14 +183,14 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); InputStream is = getAssetInputStream("animated2.gif"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(10000, item.getWidth()); assertEquals(10000, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailHeight()); assertEquals("image/gif", item.getMimeType()); assertEquals("gif", item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -197,14 +198,14 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); InputStream is = getAssetInputStream("error_large.gif"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(16384, item.getWidth()); assertEquals(16384, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailHeight()); assertEquals("image/gif", item.getMimeType()); assertEquals("gif", item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -212,14 +213,14 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getAssetInputStream("error_high.jpg"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(1, item.getWidth()); assertEquals(10000, item.getHeight()); assertEquals(dimensions.minWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxHeight, item.getThumbnailHeight()); assertEquals("image/jpeg", item.getMimeType()); assertJpgOrJpeg(item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -227,14 +228,14 @@ public class AttachmentRetrieverIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getAssetInputStream("error_wide.jpg"); Attachment a = new Attachment(h, is); - AttachmentItem item = retriever.getAttachmentItem(a, true); + AttachmentItem item = retriever.createAttachmentItem(a, true); assertEquals(1920, item.getWidth()); assertEquals(1, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailHeight()); assertEquals("image/jpeg", item.getMimeType()); assertJpgOrJpeg(item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } private InputStream getAssetInputStream(String name) throws Exception { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java index 882ef569f..60f498fdd 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java @@ -34,6 +34,7 @@ import androidx.lifecycle.MutableLiveData; 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.attachment.AttachmentItem.State.ERROR; import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE; @@ -109,8 +110,8 @@ class AttachmentCreatorImpl implements AttachmentCreator { // get and cache AttachmentItem for ImagePreview try { Attachment a = retriever.getMessageAttachment(h); - AttachmentItem item = retriever.getAttachmentItem(a, needsSize); - if (item.hasError()) throw new IOException(); + AttachmentItem item = retriever.createAttachmentItem(a, needsSize); + if (item.getState() == ERROR) throw new IOException(); AttachmentItemResult itemResult = new AttachmentItemResult(uri, item); itemResults.add(itemResult); @@ -167,13 +168,6 @@ class AttachmentCreatorImpl implements AttachmentCreator { @Override @UiThread public void onAttachmentsSent(MessageId id) { - List 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(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java index a72c31948..a19827eab 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java @@ -7,24 +7,27 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.messaging.AttachmentHeader; -import java.util.concurrent.atomic.AtomicLong; - import javax.annotation.concurrent.Immutable; import androidx.annotation.Nullable; +import static java.lang.System.arraycopy; import static java.util.Objects.requireNonNull; +import static org.briarproject.bramble.util.StringUtils.toHexString; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.LOADING; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.MISSING; @Immutable @NotNullByDefault public class AttachmentItem implements Parcelable { + public enum State {LOADING, MISSING, AVAILABLE, ERROR} + private final AttachmentHeader header; private final int width, height; private final String extension; private final int thumbnailWidth, thumbnailHeight; - private final boolean hasError; - private final long instanceId; + private final State state; public static final Creator CREATOR = new Creator() { @@ -39,19 +42,33 @@ public class AttachmentItem implements Parcelable { } }; - private static final AtomicLong NEXT_INSTANCE_ID = new AtomicLong(0); - AttachmentItem(AttachmentHeader header, int width, int height, String extension, int thumbnailWidth, int thumbnailHeight, - boolean hasError) { + State state) { this.header = header; this.width = width; this.height = height; this.extension = extension; this.thumbnailWidth = thumbnailWidth; this.thumbnailHeight = thumbnailHeight; - this.hasError = hasError; - instanceId = NEXT_INSTANCE_ID.getAndIncrement(); + this.state = state; + } + + /** + * Use only for {@link MISSING} or {@link LOADING} items. + */ + AttachmentItem(AttachmentHeader header, int width, int height, + State state) { + this(header, width, height, "", width, height, state); + if (state != MISSING && state != LOADING) + throw new IllegalArgumentException(); + } + + /** + * Use when the item does not need a size. + */ + AttachmentItem(AttachmentHeader header, String extension, State state) { + this(header, 0, 0, extension, 0, 0, state); } protected AttachmentItem(Parcel in) { @@ -64,8 +81,7 @@ public class AttachmentItem implements Parcelable { extension = requireNonNull(in.readString()); thumbnailWidth = in.readInt(); thumbnailHeight = in.readInt(); - hasError = in.readByte() != 0; - instanceId = in.readLong(); + state = State.valueOf(requireNonNull(in.readString())); header = new AttachmentHeader(messageId, mimeType); } @@ -101,12 +117,20 @@ public class AttachmentItem implements Parcelable { return thumbnailHeight; } - public boolean hasError() { - return hasError; + public State getState() { + return state; } - public String getTransitionName() { - return String.valueOf(instanceId); + public String getTransitionName(MessageId conversationItemId) { + int len = MessageId.LENGTH; + byte[] instanceId = new byte[len * 2]; + arraycopy(header.getMessageId().getBytes(), 0, instanceId, 0, len); + arraycopy(conversationItemId.getBytes(), 0, instanceId, len, len); + return toHexString(instanceId); + } + + boolean hasSize() { + return width != 0 && height != 0; } @Override @@ -123,14 +147,15 @@ public class AttachmentItem implements Parcelable { dest.writeString(extension); dest.writeInt(thumbnailWidth); dest.writeInt(thumbnailHeight); - dest.writeByte((byte) (hasError ? 1 : 0)); - dest.writeLong(instanceId); + dest.writeString(state.name()); } @Override public boolean equals(@Nullable Object o) { return o instanceof AttachmentItem && - instanceId == ((AttachmentItem) o).instanceId; + header.getMessageId().equals( + ((AttachmentItem) o).header.getMessageId() + ); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java index 33d709ab7..f12b01ecb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java @@ -1,29 +1,40 @@ package org.briarproject.briar.android.attachment; +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.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.PrivateMessageHeader; import java.io.InputStream; import java.util.List; -import androidx.annotation.Nullable; - @NotNullByDefault public interface AttachmentRetriever { - void cachePut(MessageId messageId, List attachments); - - @Nullable - List cacheGet(MessageId messageId); - + @DatabaseExecutor Attachment getMessageAttachment(AttachmentHeader h) throws DbException; + List getAttachmentItems(PrivateMessageHeader messageHeader); + + /** + * Retrieves item size and adds the item to the cache, if available. + */ + @DatabaseExecutor + void cacheAttachmentItem(MessageId conversationMessageId, + AttachmentHeader h) throws DbException; + /** * Creates an {@link AttachmentItem} from the {@link Attachment}'s * {@link InputStream} which will be closed when this method returns. */ - AttachmentItem getAttachmentItem(Attachment a, boolean needsSize); + AttachmentItem createAttachmentItem(Attachment a, boolean needsSize); + + @DatabaseExecutor + Pair loadAttachmentItem(MessageId attachmentId) + throws DbException; + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java index 53a376aea..30ee170bb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java @@ -1,14 +1,20 @@ package org.briarproject.briar.android.attachment; +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.NoSuchMessageException; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.android.attachment.AttachmentItem.State; import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.MessagingManager; +import org.briarproject.briar.api.messaging.PrivateMessageHeader; import java.io.BufferedInputStream; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -16,10 +22,12 @@ import java.util.logging.Logger; import javax.inject.Inject; -import androidx.annotation.Nullable; - import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.LOADING; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.MISSING; @NotNullByDefault class AttachmentRetrieverImpl implements AttachmentRetriever { @@ -34,7 +42,11 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { private final int minWidth, maxWidth; private final int minHeight, maxHeight; - private final Map> attachmentCache = + // Info for AttachmentItems that are either still LOADING or MISSING + private final Map unavailableItems = + new ConcurrentHashMap<>(); + // We cache only items in their final state: AVAILABLE or ERROR + private final Map itemCache = new ConcurrentHashMap<>(); @Inject @@ -52,40 +64,95 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { } @Override - public void cachePut(MessageId messageId, - List attachments) { - attachmentCache.put(messageId, attachments); - } - - @Override - @Nullable - public List cacheGet(MessageId messageId) { - return attachmentCache.get(messageId); - } - - @Override + @DatabaseExecutor public Attachment getMessageAttachment(AttachmentHeader h) throws DbException { return messagingManager.getAttachment(h); } @Override - public AttachmentItem getAttachmentItem(Attachment a, boolean needsSize) { + public List getAttachmentItems( + PrivateMessageHeader messageHeader) { + List headers = messageHeader.getAttachmentHeaders(); + List items = new ArrayList<>(headers.size()); + boolean needsSize = headers.size() == 1; + for (AttachmentHeader h : headers) { + AttachmentItem item = itemCache.get(h.getMessageId()); + if (item == null || (needsSize && !item.hasSize())) { + item = new AttachmentItem(h, defaultSize, defaultSize, LOADING); + UnavailableItem unavailableItem = new UnavailableItem( + messageHeader.getId(), h, needsSize); + unavailableItems.put(h.getMessageId(), unavailableItem); + } + items.add(item); + } + return items; + } + + @Override + @DatabaseExecutor + public void cacheAttachmentItem(MessageId conversationMessageId, + AttachmentHeader h) throws DbException { + try { + Attachment a = messagingManager.getAttachment(h); + // this adds it to the cache automatically + createAttachmentItem(a, true); + } catch (NoSuchMessageException e) { + LOG.info("Attachment not received yet"); + } + } + + @Override + @DatabaseExecutor + public Pair loadAttachmentItem( + MessageId attachmentId) throws DbException { + UnavailableItem unavailableItem = unavailableItems.get(attachmentId); + if (unavailableItem == null) throw new AssertionError(); + + MessageId conversationMessageId = + unavailableItem.getConversationMessageId(); + AttachmentHeader h = unavailableItem.getHeader(); + boolean needsSize = unavailableItem.needsSize(); + + AttachmentItem item; + try { + Attachment a = messagingManager.getAttachment(h); + item = createAttachmentItem(a, needsSize); + unavailableItems.remove(attachmentId); + } catch (NoSuchMessageException e) { + LOG.info("Attachment not received yet"); + // unavailable item is still tracked, no need to add it again + item = new AttachmentItem(h, defaultSize, defaultSize, MISSING); + } + return new Pair<>(conversationMessageId, item); + } + + @Override + public AttachmentItem createAttachmentItem(Attachment a, + boolean needsSize) { AttachmentHeader h = a.getHeader(); - if (!needsSize) { + AttachmentItem item = itemCache.get(h.getMessageId()); + if (item != null && (needsSize && item.hasSize())) return item; + + if (needsSize) { + InputStream is = new BufferedInputStream(a.getStream()); + Size size = imageSizeCalculator.getSize(is, h.getContentType()); + item = createAttachmentItem(h, size); + } else { String extension = imageHelper.getExtensionFromMimeType(h.getContentType()); - boolean hasError = false; + State state = AVAILABLE; if (extension == null) { extension = ""; - hasError = true; + state = ERROR; } - return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError); + item = new AttachmentItem(h, extension, state); } + itemCache.put(h.getMessageId(), item); + return item; + } - InputStream is = new BufferedInputStream(a.getStream()); - Size size = imageSizeCalculator.getSize(is, h.getContentType()); - + private AttachmentItem createAttachmentItem(AttachmentHeader h, Size size) { // calculate thumbnail size Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType); if (!size.error) { @@ -104,8 +171,9 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { hasError = true; } if (extension == null) extension = ""; - return new AttachmentItem(h, size.width, size.height, extension, - thumbnailSize.width, thumbnailSize.height, hasError); + State state = hasError ? ERROR : AVAILABLE; + return new AttachmentItem(h, size.width, size.height, + extension, thumbnailSize.width, thumbnailSize.height, state); } private Size getThumbnailSize(int width, int height, String mimeType) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java new file mode 100644 index 000000000..9aa648020 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java @@ -0,0 +1,34 @@ +package org.briarproject.briar.android.attachment; + +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.messaging.AttachmentHeader; + +import javax.annotation.concurrent.Immutable; + +@Immutable +class UnavailableItem { + + private final MessageId conversationMessageId; + private final AttachmentHeader header; + private final boolean needsSize; + + UnavailableItem(MessageId conversationMessageId, + AttachmentHeader header, boolean needsSize) { + this.conversationMessageId = conversationMessageId; + this.header = header; + this.needsSize = needsSize; + } + + MessageId getConversationMessageId() { + return conversationMessageId; + } + + AttachmentHeader getHeader() { + return header; + } + + boolean needsSize() { + return needsSize; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 6aa6986c7..1aba980d7 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -27,7 +27,6 @@ import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; 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.EventBus; import org.briarproject.bramble.api.event.EventListener; @@ -65,13 +64,13 @@ import org.briarproject.briar.api.client.ProtocolStateException; import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.conversation.ConversationManager; import org.briarproject.briar.api.conversation.ConversationMessageHeader; +import org.briarproject.briar.api.conversation.ConversationMessageVisitor; import org.briarproject.briar.api.conversation.ConversationRequest; import org.briarproject.briar.api.conversation.ConversationResponse; import org.briarproject.briar.api.conversation.DeletionResult; import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent; import org.briarproject.briar.api.forum.ForumSharingManager; import org.briarproject.briar.api.introduction.IntroductionManager; -import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.PrivateMessageHeader; @@ -118,8 +117,6 @@ import static androidx.core.app.ActivityOptionsCompat.makeSceneTransitionAnimati import static androidx.core.view.ViewCompat.setTransitionName; import static androidx.lifecycle.Lifecycle.State.STARTED; import static androidx.recyclerview.widget.SortedList.INVALID_POSITION; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; import static java.util.Collections.sort; import static java.util.Objects.requireNonNull; import static java.util.logging.Level.INFO; @@ -133,9 +130,11 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.join; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.LOADING; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION; import static org.briarproject.briar.android.conversation.ImageActivity.DATE; +import static org.briarproject.briar.android.conversation.ImageActivity.ITEM_ID; import static org.briarproject.briar.android.conversation.ImageActivity.NAME; import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName; import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName; @@ -185,8 +184,6 @@ public class ConversationActivity extends BriarActivity volatile GroupInvitationManager groupInvitationManager; private final Map textCache = new ConcurrentHashMap<>(); - private final Map missingAttachments = - new ConcurrentHashMap<>(); private final Observer contactNameObserver = name -> { requireNonNull(name); loadMessages(); @@ -264,6 +261,7 @@ public class ConversationActivity extends BriarActivity adapter = new ConversationAdapter(this, this); list = findViewById(R.id.conversationView); layoutManager = new LinearLayoutManager(this); + layoutManager.setStackFromEnd(true); list.setLayoutManager(layoutManager); list.setAdapter(adapter); list.setEmptyText(getString(R.string.no_private_messages)); @@ -540,6 +538,7 @@ public class ConversationActivity extends BriarActivity }); } + @DatabaseExecutor private void eagerlyLoadMessageSize(PrivateMessageHeader h) { try { MessageId id = h.getId(); @@ -556,21 +555,10 @@ public class ConversationActivity extends BriarActivity // images we use a grid so the size is fixed List headers = h.getAttachmentHeaders(); if (headers.size() == 1) { - List items = attachmentRetriever.cacheGet(id); - if (items == null) { - LOG.info("Eagerly loading image size for latest message"); - AttachmentHeader header = headers.get(0); - try { - Attachment a = attachmentRetriever - .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); - } - } + LOG.info("Eagerly loading image size for latest message"); + AttachmentHeader header = headers.get(0); + // get the item to retrieve its size + attachmentRetriever.cacheAttachmentItem(h.getId(), header); } } catch (DbException e) { logException(LOG, WARNING, e); @@ -651,44 +639,34 @@ public class ConversationActivity extends BriarActivity && adapter.isScrolledToBottom(layoutManager); } - private void loadMessageAttachments(PrivateMessageHeader h) { - // TODO: Use placeholders for missing/invalid attachments + private void loadMessageAttachments(List items) { runOnDbThread(() -> { - try { - // TODO move getting the items off to IoExecutor, if size == 1 - List headers = h.getAttachmentHeaders(); - boolean needsSize = headers.size() == 1; - List 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) { - logException(LOG, WARNING, e); + for (AttachmentItem item : items) { + if (item.getState() == LOADING) + loadMessageAttachment(item.getMessageId()); } }); } - private void displayMessageAttachments(MessageId m, - List items) { + @DatabaseExecutor + private void loadMessageAttachment(MessageId attachmentId) { + try { + Pair pair = + attachmentRetriever.loadAttachmentItem(attachmentId); + MessageId conversationMessageId = pair.getFirst(); + AttachmentItem item = pair.getSecond(); + updateMessageAttachment(conversationMessageId, item); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + } + + private void updateMessageAttachment(MessageId m, AttachmentItem item) { runOnUiThreadUnlessDestroyed(() -> { Pair pair = adapter.getMessageItem(m); - if (pair != null) { + if (pair != null && pair.getSecond().updateAttachments(item)) { boolean scroll = shouldScrollWhenUpdatingMessage(); - pair.getSecond().setAttachments(items); adapter.notifyItemChanged(pair.getFirst()); if (scroll) scrollToBottom(); } @@ -765,11 +743,7 @@ public class ConversationActivity extends BriarActivity @UiThread private void onAttachmentReceived(MessageId attachmentId) { - PrivateMessageHeader h = missingAttachments.remove(attachmentId); - if (h != null) { - LOG.info("Missing attachment received"); - loadMessageAttachments(h); - } + runOnDbThread(() -> loadMessageAttachment(attachmentId)); } @UiThread @@ -780,7 +754,7 @@ public class ConversationActivity extends BriarActivity observeOnce(viewModel.getContactDisplayName(), this, name -> addConversationItem(h.accept(visitor))); } else { - // visitor also loads message text (if existing) + // visitor also loads message text and attachments (if existing) addConversationItem(h.accept(visitor)); } } @@ -1107,8 +1081,9 @@ public class ConversationActivity extends BriarActivity i.putExtra(ATTACHMENT_POSITION, attachments.indexOf(item)); i.putExtra(NAME, name); i.putExtra(DATE, messageItem.getTime()); + i.putExtra(ITEM_ID, messageItem.getId().getBytes()); // restoring list position should not trigger android bug #224270 - String transitionName = item.getTransitionName(); + String transitionName = item.getTransitionName(messageItem.getId()); ActivityOptionsCompat options = makeSceneTransitionAnimation(this, view, transitionName); ActivityCompat.startActivity(this, i, options.toBundle()); @@ -1147,15 +1122,14 @@ public class ConversationActivity extends BriarActivity return text; } + /** + * Called by {@link PrivateMessageHeader#accept(ConversationMessageVisitor)} + */ @Override public List getAttachmentItems(PrivateMessageHeader h) { - List attachments = - attachmentRetriever.cacheGet(h.getId()); - if (attachments == null) { - loadMessageAttachments(h); - return emptyList(); - } - return attachments; + List items = attachmentRetriever.getAttachmentItems(h); + loadMessageAttachments(items); + return items; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java index b3e20b3f7..8fc50d299 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java @@ -9,12 +9,13 @@ import java.util.List; import javax.annotation.concurrent.NotThreadSafe; import androidx.annotation.LayoutRes; +import androidx.annotation.UiThread; @NotThreadSafe @NotNullByDefault class ConversationMessageItem extends ConversationItem { - private List attachments; + private final List attachments; ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h, List attachments) { @@ -26,8 +27,14 @@ class ConversationMessageItem extends ConversationItem { return attachments; } - void setAttachments(List attachments) { - this.attachments = attachments; + @UiThread + boolean updateAttachments(AttachmentItem item) { + int pos = attachments.indexOf(item); + if (pos != -1 && attachments.get(pos).getState() != item.getState()) { + attachments.set(pos, item); + return true; + } + return false; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java index 749ef495c..874e4b95f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -17,6 +17,7 @@ import com.google.android.material.appbar.AppBarLayout; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; @@ -69,6 +70,7 @@ public class ImageActivity extends BriarActivity final static String ATTACHMENT_POSITION = "position"; final static String NAME = "name"; final static String DATE = "date"; + final static String ITEM_ID = "itemId"; @RequiresApi(api = 16) private final static int UI_FLAGS_DEFAULT = @@ -82,6 +84,7 @@ public class ImageActivity extends BriarActivity private AppBarLayout appBarLayout; private ViewPager viewPager; private List attachments; + private MessageId conversationMessageId; @Override public void injectActivity(ActivityComponent component) { @@ -136,6 +139,7 @@ public class ImageActivity extends BriarActivity String date = formatDateAbsolute(this, time); contactName.setText(name); dateView.setText(date); + conversationMessageId = new MessageId(i.getByteArrayExtra(ITEM_ID)); // Set up image ViewPager viewPager = findViewById(R.id.viewPager); @@ -325,8 +329,8 @@ public class ImageActivity extends BriarActivity @Override public Fragment getItem(int position) { - Fragment f = ImageFragment - .newInstance(attachments.get(position), isFirst); + Fragment f = ImageFragment.newInstance( + attachments.get(position), conversationMessageId, isFirst); isFirst = false; return f; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java index 9f456f71b..f0401b4cc 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java @@ -49,7 +49,8 @@ class ImageAdapter extends Adapter { public ImageViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) { View v = LayoutInflater.from(viewGroup.getContext()).inflate( R.layout.list_item_image, viewGroup, false); - return new ImageViewHolder(v, imageSize); + requireNonNull(conversationItem); + return new ImageViewHolder(v, imageSize, conversationItem.getId()); } @Override @@ -58,7 +59,7 @@ class ImageAdapter extends Adapter { // get item requireNonNull(conversationItem); AttachmentItem item = items.get(position); - // set onClick listener + // set onClick listener, if not missing or error imageViewHolder.itemView.setOnClickListener(v -> listener.onAttachmentClicked(v, conversationItem, item) ); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java index 9f8ee39d4..c2f368df6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java @@ -15,6 +15,7 @@ import com.bumptech.glide.request.target.Target; import com.github.chrisbanes.photoview.PhotoView; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.BaseActivity; import org.briarproject.briar.android.attachment.AttachmentItem; @@ -33,6 +34,7 @@ import static android.widget.ImageView.ScaleType.FIT_START; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION; +import static org.briarproject.briar.android.conversation.ImageActivity.ITEM_ID; @MethodsNotNullByDefault @ParametersAreNonnullByDefault @@ -45,14 +47,17 @@ public class ImageFragment extends Fragment { private AttachmentItem attachment; private boolean isFirst; + private MessageId conversationItemId; private ImageViewModel viewModel; private PhotoView photoView; - static ImageFragment newInstance(AttachmentItem a, boolean isFirst) { + static ImageFragment newInstance(AttachmentItem a, + MessageId conversationMessageId, boolean isFirst) { ImageFragment f = new ImageFragment(); Bundle args = new Bundle(); args.putParcelable(ATTACHMENT_POSITION, a); args.putBoolean(IS_FIRST, isFirst); + args.putByteArray(ITEM_ID, conversationMessageId.getBytes()); f.setArguments(args); return f; } @@ -70,6 +75,8 @@ public class ImageFragment extends Fragment { Bundle args = requireNonNull(getArguments()); attachment = requireNonNull(args.getParcelable(ATTACHMENT_POSITION)); isFirst = args.getBoolean(IS_FIRST); + conversationItemId = + new MessageId(requireNonNull(args.getByteArray(ITEM_ID))); } @Nullable @@ -107,7 +114,7 @@ public class ImageFragment extends Fragment { // set transition name only when not animatable, // because the animation won't start otherwise photoView.setTransitionName( - attachment.getTransitionName()); + attachment.getTransitionName(conversationItemId)); } // Move image to the top if overlapping toolbar if (viewModel.isOverlappingToolbar(photoView, resource)) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java index 76eaab5e3..344d7dde0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java @@ -7,6 +7,7 @@ import android.widget.ImageView; import com.bumptech.glide.load.Transformation; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.R; import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.conversation.glide.BriarImageTransformation; @@ -18,8 +19,12 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder; import androidx.recyclerview.widget.StaggeredGridLayoutManager.LayoutParams; import static android.os.Build.VERSION.SDK_INT; +import static android.widget.ImageView.ScaleType.CENTER_CROP; +import static android.widget.ImageView.ScaleType.FIT_CENTER; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR; @NotNullByDefault class ImageViewHolder extends ViewHolder { @@ -29,25 +34,33 @@ class ImageViewHolder extends ViewHolder { protected final ImageView imageView; private final int imageSize; + private final MessageId conversationItemId; - ImageViewHolder(View v, int imageSize) { + ImageViewHolder(View v, int imageSize, MessageId conversationItemId) { super(v); imageView = v.findViewById(R.id.imageView); this.imageSize = imageSize; + this.conversationItemId = conversationItemId; } void bind(AttachmentItem attachment, Radii r, boolean single, boolean needsStretch) { - if (attachment.hasError()) { - GlideApp.with(imageView) - .clear(imageView); - imageView.setImageResource(ERROR_RES); + setImageViewDimensions(attachment, single, needsStretch); + if (attachment.getState() != AVAILABLE) { + GlideApp.with(imageView).clear(imageView); + if (attachment.getState() == ERROR) { + imageView.setImageResource(ERROR_RES); + } else { + imageView.setImageResource(R.drawable.ic_image_missing); + } + imageView.setScaleType(FIT_CENTER); } else { - setImageViewDimensions(attachment, single, needsStretch); loadImage(attachment, r); if (SDK_INT >= 21) { - imageView.setTransitionName(attachment.getTransitionName()); + imageView.setTransitionName( + attachment.getTransitionName(conversationItemId)); } + imageView.setScaleType(CENTER_CROP); } } diff --git a/briar-android/src/main/res/drawable/ic_image_broken.xml b/briar-android/src/main/res/drawable/ic_image_broken.xml index 318d05b36..096a4b505 100644 --- a/briar-android/src/main/res/drawable/ic_image_broken.xml +++ b/briar-android/src/main/res/drawable/ic_image_broken.xml @@ -1,6 +1,6 @@ + + diff --git a/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java b/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java index 4558c2ff8..8fae2c224 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java @@ -14,9 +14,9 @@ import java.io.InputStream; import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getRandomId; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; public class AttachmentRetrieverTest extends BrambleMockTestCase { @@ -47,10 +47,10 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase { will(returnValue("jpg")); }}); - AttachmentItem item = retriever.getAttachmentItem(attachment, false); + AttachmentItem item = retriever.createAttachmentItem(attachment, false); assertEquals(mimeType, item.getMimeType()); assertEquals("jpg", item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -63,8 +63,8 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase { will(returnValue(null)); }}); - AttachmentItem item = retriever.getAttachmentItem(attachment, false); - assertTrue(item.hasError()); + AttachmentItem item = retriever.createAttachmentItem(attachment, false); + assertEquals(ERROR, item.getState()); } @Test @@ -80,7 +80,7 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase { will(returnValue("jpg")); }}); - AttachmentItem item = retriever.getAttachmentItem(attachment, true); + AttachmentItem item = retriever.createAttachmentItem(attachment, true); assertEquals(msgId, item.getMessageId()); assertEquals(160, item.getWidth()); assertEquals(240, item.getHeight()); @@ -88,7 +88,7 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase { assertEquals(240, item.getThumbnailHeight()); assertEquals(mimeType, item.getMimeType()); assertEquals("jpg", item.getExtension()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -104,12 +104,12 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase { will(returnValue("jpg")); }}); - AttachmentItem item = retriever.getAttachmentItem(attachment, true); + AttachmentItem item = retriever.createAttachmentItem(attachment, true); assertEquals(1728, item.getWidth()); assertEquals(2592, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxHeight, item.getThumbnailHeight()); - assertFalse(item.hasError()); + assertEquals(AVAILABLE, item.getState()); } @Test @@ -125,8 +125,8 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase { will(returnValue(null)); }}); - AttachmentItem item = retriever.getAttachmentItem(attachment, true); - assertTrue(item.hasError()); + AttachmentItem item = retriever.createAttachmentItem(attachment, true); + assertEquals(ERROR, item.getState()); } private Attachment getAttachment(String contentType) { diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java index 0dfecb8ab..ccb8f776f 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java @@ -68,7 +68,7 @@ public interface MessagingManager extends ConversationClient { String getMessageText(MessageId m) throws DbException; /** - * Returns the attachment with the given message ID and content type. + * Returns the attachment with the given attachment header. * * @throws InvalidAttachmentException If the header refers to a message * that is not an attachment, or to an attachment that does not have the From 4122e0852ad15ed3329e8ac776339375b80dc0a2 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 24 Sep 2019 13:45:28 -0300 Subject: [PATCH 002/582] Show placeholders for missing attachments in ImageActivity and display attachments as they arrive while ImageActivity is open. --- .../android/conversation/ImageFragment.java | 109 +++++++++++------- .../android/conversation/ImageViewHolder.java | 8 +- .../android/conversation/ImageViewModel.java | 33 +++++- 3 files changed, 101 insertions(+), 49 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java index c2f368df6..4a61b0dfb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java @@ -24,6 +24,7 @@ import org.briarproject.briar.android.conversation.glide.GlideApp; import javax.annotation.ParametersAreNonnullByDefault; import javax.inject.Inject; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; @@ -33,14 +34,19 @@ import static android.os.Build.VERSION.SDK_INT; import static android.widget.ImageView.ScaleType.FIT_START; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE; +import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION; import static org.briarproject.briar.android.conversation.ImageActivity.ITEM_ID; @MethodsNotNullByDefault @ParametersAreNonnullByDefault -public class ImageFragment extends Fragment { +public class ImageFragment extends Fragment + implements RequestListener { private final static String IS_FIRST = "isFirst"; + @DrawableRes + private static final int ERROR_RES = R.drawable.ic_image_broken; @Inject ViewModelProvider.Factory viewModelFactory; @@ -89,55 +95,72 @@ public class ImageFragment extends Fragment { viewModel = ViewModelProviders.of(requireNonNull(getActivity()), viewModelFactory).get(ImageViewModel.class); + viewModel.getOnAttachmentLoaded() + .observeEvent(this, this::onAttachmentLoaded); photoView = v.findViewById(R.id.photoView); photoView.setScaleLevels(1, 2, 4); photoView.setOnClickListener(view -> viewModel.clickImage()); - // Request Listener - RequestListener listener = new RequestListener() { - - @Override - public boolean onLoadFailed(@Nullable GlideException e, - Object model, Target target, - boolean isFirstResource) { - if (getActivity() != null && isFirst) - getActivity().supportStartPostponedEnterTransition(); - return false; - } - - @Override - public boolean onResourceReady(Drawable resource, Object model, - Target target, DataSource dataSource, - boolean isFirstResource) { - if (SDK_INT >= 21 && !(resource instanceof Animatable)) { - // set transition name only when not animatable, - // because the animation won't start otherwise - photoView.setTransitionName( - attachment.getTransitionName(conversationItemId)); - } - // Move image to the top if overlapping toolbar - if (viewModel.isOverlappingToolbar(photoView, resource)) { - photoView.setScaleType(FIT_START); - } - if (getActivity() != null && isFirst) { - getActivity().supportStartPostponedEnterTransition(); - } - return false; - } - }; - - // Load Image - GlideApp.with(this) - .load(attachment) - // TODO allow if size < maxTextureSize ? -// .override(SIZE_ORIGINAL) - .diskCacheStrategy(NONE) - .error(R.drawable.ic_image_broken) - .addListener(listener) - .into(photoView); + if (attachment.getState() == AVAILABLE) { + loadImage(); + // postponed transition will be started when Image was loaded + } else if (attachment.getState() == ERROR) { + photoView.setImageResource(ERROR_RES); + startPostponedTransition(); + } else { + photoView.setImageResource(R.drawable.ic_image_missing); + startPostponedTransition(); + } return v; } + private void loadImage() { + GlideApp.with(this) + .load(attachment) + // TODO allow if size < maxTextureSize ? +// .override(SIZE_ORIGINAL) + .diskCacheStrategy(NONE) + .error(ERROR_RES) + .addListener(this) + .into(photoView); + } + + private void onAttachmentLoaded(MessageId messageId) { + if (attachment.getMessageId().equals(messageId)) loadImage(); + } + + @Override + public boolean onLoadFailed(@Nullable GlideException e, + Object model, Target target, + boolean isFirstResource) { + startPostponedTransition(); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, + Target target, DataSource dataSource, + boolean isFirstResource) { + if (SDK_INT >= 21 && !(resource instanceof Animatable)) { + // set transition name only when not animatable, + // because the animation won't start otherwise + photoView.setTransitionName( + attachment.getTransitionName(conversationItemId)); + } + // Move image to the top if overlapping toolbar + if (viewModel.isOverlappingToolbar(photoView, resource)) { + photoView.setScaleType(FIT_START); + } + startPostponedTransition(); + return false; + } + + private void startPostponedTransition() { + if (getActivity() != null && isFirst) { + getActivity().supportStartPostponedEnterTransition(); + } + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java index 344d7dde0..659bcf29e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java @@ -56,12 +56,12 @@ class ImageViewHolder extends ViewHolder { imageView.setScaleType(FIT_CENTER); } else { loadImage(attachment, r); - if (SDK_INT >= 21) { - imageView.setTransitionName( - attachment.getTransitionName(conversationItemId)); - } imageView.setScaleType(CENTER_CROP); } + if (SDK_INT >= 21) { + imageView.setTransitionName( + attachment.getTransitionName(conversationItemId)); + } } private void setImageViewDimensions(AttachmentItem a, boolean single, diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java index 0c251cf60..58ef47dba 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java @@ -7,13 +7,18 @@ import android.view.View; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.lifecycle.IoExecutor; 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.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.MessagingManager; +import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent; import java.io.File; import java.io.FileOutputStream; @@ -41,16 +46,19 @@ import static org.briarproject.bramble.util.IoUtils.copyAndClose; import static org.briarproject.bramble.util.LogUtils.logException; @NotNullByDefault -public class ImageViewModel extends AndroidViewModel { +public class ImageViewModel extends AndroidViewModel implements EventListener { private static Logger LOG = getLogger(ImageViewModel.class.getName()); private final MessagingManager messagingManager; + private final EventBus eventBus; @DatabaseExecutor private final Executor dbExecutor; @IoExecutor private final Executor ioExecutor; + private final MutableLiveEvent attachmentLoaded = + new MutableLiveEvent<>(); /** * true means there was an error saving the image, false if image was saved. */ @@ -62,13 +70,34 @@ public class ImageViewModel extends AndroidViewModel { @Inject ImageViewModel(Application application, - MessagingManager messagingManager, + MessagingManager messagingManager, EventBus eventBus, @DatabaseExecutor Executor dbExecutor, @IoExecutor Executor ioExecutor) { super(application); this.messagingManager = messagingManager; + this.eventBus = eventBus; this.dbExecutor = dbExecutor; this.ioExecutor = ioExecutor; + + eventBus.addListener(this); + } + + @Override + protected void onCleared() { + super.onCleared(); + eventBus.removeListener(this); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof AttachmentReceivedEvent) { + attachmentLoaded + .postEvent(((AttachmentReceivedEvent) e).getMessageId()); + } + } + + LiveEvent getOnAttachmentLoaded() { + return attachmentLoaded; } void clickImage() { From b7d3cd7990400a1c710bf53c318843dfc7f9d2ae Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 17 Oct 2019 13:25:04 -0300 Subject: [PATCH 003/582] [android] support attachments arriving *before* the message containing them --- .../android/attachment/AttachmentRetriever.java | 11 +++++++++++ .../attachment/AttachmentRetrieverImpl.java | 5 ++++- .../conversation/ConversationActivity.java | 17 ++++++++++++----- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java index f12b01ecb..9b5d14812 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java @@ -12,6 +12,9 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader; import java.io.InputStream; import java.util.List; +import androidx.annotation.Nullable; + + @NotNullByDefault public interface AttachmentRetriever { @@ -33,6 +36,14 @@ public interface AttachmentRetriever { */ AttachmentItem createAttachmentItem(Attachment a, boolean needsSize); + /** + * Load an {@link AttachmentItem} from the database. + * + * @return a pair of the {@link MessageId} of the conversation message + * and the {@link AttachmentItem} + * or {@code null} when the conversation message did not yet arrive. + */ + @Nullable @DatabaseExecutor Pair loadAttachmentItem(MessageId attachmentId) throws DbException; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java index 30ee170bb..f13556b12 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java @@ -22,6 +22,8 @@ import java.util.logging.Logger; import javax.inject.Inject; +import androidx.annotation.Nullable; + import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE; @@ -103,11 +105,12 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { } @Override + @Nullable @DatabaseExecutor public Pair loadAttachmentItem( MessageId attachmentId) throws DbException { UnavailableItem unavailableItem = unavailableItems.get(attachmentId); - if (unavailableItem == null) throw new AssertionError(); + if (unavailableItem == null) return null; MessageId conversationMessageId = unavailableItem.getConversationMessageId(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 1aba980d7..fccf6ae59 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -643,16 +643,21 @@ public class ConversationActivity extends BriarActivity runOnDbThread(() -> { for (AttachmentItem item : items) { if (item.getState() == LOADING) - loadMessageAttachment(item.getMessageId()); + loadMessageAttachment(item.getMessageId(), false); } }); } @DatabaseExecutor - private void loadMessageAttachment(MessageId attachmentId) { + private void loadMessageAttachment(MessageId attachmentId, + boolean allowedToFail) { try { - Pair pair = - attachmentRetriever.loadAttachmentItem(attachmentId); + Pair pair = attachmentRetriever + .loadAttachmentItem(attachmentId); + if (pair == null && allowedToFail) { + LOG.warning("Attachment arrived before message"); + return; + } else if (pair == null) throw new AssertionError(); MessageId conversationMessageId = pair.getFirst(); AttachmentItem item = pair.getSecond(); updateMessageAttachment(conversationMessageId, item); @@ -743,7 +748,9 @@ public class ConversationActivity extends BriarActivity @UiThread private void onAttachmentReceived(MessageId attachmentId) { - runOnDbThread(() -> loadMessageAttachment(attachmentId)); + // This is allowed to fail, because the conversation message + // might arrive *after* the attachment. + runOnDbThread(() -> loadMessageAttachment(attachmentId, true)); } @UiThread From a1cf485ecc71d41e507a6b61d201bbd0feb532bb Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 7 Nov 2019 16:53:42 -0300 Subject: [PATCH 004/582] [android] address first round of code review for attachment placeholders --- .../briar/android/attachment/AttachmentItem.java | 2 +- .../briar/android/attachment/AttachmentRetriever.java | 4 ++-- .../briar/android/attachment/AttachmentRetrieverImpl.java | 6 ++++-- .../briar/android/attachment/UnavailableItem.java | 2 ++ .../briar/android/conversation/ConversationActivity.java | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java index a19827eab..e0c4b8694 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java @@ -55,7 +55,7 @@ public class AttachmentItem implements Parcelable { } /** - * Use only for {@link MISSING} or {@link LOADING} items. + * Use only for {@link State MISSING} or {@link State LOADING} items. */ AttachmentItem(AttachmentHeader header, int width, int height, State state) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java index 9b5d14812..298c8cbbb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java @@ -27,7 +27,7 @@ public interface AttachmentRetriever { * Retrieves item size and adds the item to the cache, if available. */ @DatabaseExecutor - void cacheAttachmentItem(MessageId conversationMessageId, + void cacheAttachmentItemWithSize(MessageId conversationMessageId, AttachmentHeader h) throws DbException; /** @@ -41,7 +41,7 @@ public interface AttachmentRetriever { * * @return a pair of the {@link MessageId} of the conversation message * and the {@link AttachmentItem} - * or {@code null} when the conversation message did not yet arrive. + * or {@code null} when the private message did not yet arrive. */ @Nullable @DatabaseExecutor diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java index f13556b12..8724ff8ee 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java @@ -26,6 +26,7 @@ import androidx.annotation.Nullable; 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.briar.android.attachment.AttachmentItem.State.AVAILABLE; import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR; import static org.briarproject.briar.android.attachment.AttachmentItem.State.LOADING; @@ -93,7 +94,7 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { @Override @DatabaseExecutor - public void cacheAttachmentItem(MessageId conversationMessageId, + public void cacheAttachmentItemWithSize(MessageId conversationMessageId, AttachmentHeader h) throws DbException { try { Attachment a = messagingManager.getAttachment(h); @@ -135,11 +136,12 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { boolean needsSize) { AttachmentHeader h = a.getHeader(); AttachmentItem item = itemCache.get(h.getMessageId()); - if (item != null && (needsSize && item.hasSize())) return item; + if (item != null && (needsSize == item.hasSize())) return item; if (needsSize) { InputStream is = new BufferedInputStream(a.getStream()); Size size = imageSizeCalculator.getSize(is, h.getContentType()); + tryToClose(is, LOG, WARNING); item = createAttachmentItem(h, size); } else { String extension = diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java index 9aa648020..72951da3b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java @@ -1,11 +1,13 @@ package org.briarproject.briar.android.attachment; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.messaging.AttachmentHeader; import javax.annotation.concurrent.Immutable; @Immutable +@NotNullByDefault class UnavailableItem { private final MessageId conversationMessageId; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index fccf6ae59..ac03699a6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -558,7 +558,7 @@ public class ConversationActivity extends BriarActivity LOG.info("Eagerly loading image size for latest message"); AttachmentHeader header = headers.get(0); // get the item to retrieve its size - attachmentRetriever.cacheAttachmentItem(h.getId(), header); + attachmentRetriever.cacheAttachmentItemWithSize(h.getId(), header); } } catch (DbException e) { logException(LOG, WARNING, e); From 31f42d44af47f383d8e6a293caee55e93276ba08 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 11 Nov 2019 16:39:35 -0300 Subject: [PATCH 005/582] [android] Refactor attachment loading to use LiveData --- .../AttachmentRetrieverIntegrationTest.java | 2 +- .../android/attachment/AttachmentItem.java | 8 +- .../attachment/AttachmentRetriever.java | 27 +++-- .../attachment/AttachmentRetrieverImpl.java | 112 ++++++++++++------ .../conversation/ConversationActivity.java | 64 +++++----- .../attachment/AttachmentRetrieverTest.java | 7 +- 6 files changed, 134 insertions(+), 86 deletions(-) diff --git a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java index 9c4ffc8d5..866aecec4 100644 --- a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java +++ b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java @@ -28,7 +28,7 @@ public class AttachmentRetrieverIntegrationTest { private final ImageHelper imageHelper = new ImageHelperImpl(); private final AttachmentRetriever retriever = - new AttachmentRetrieverImpl(null, dimensions, imageHelper, + new AttachmentRetrieverImpl(null, null, dimensions, imageHelper, new ImageSizeCalculator(imageHelper)); @Test diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java index e0c4b8694..58c5d9160 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java @@ -21,7 +21,13 @@ import static org.briarproject.briar.android.attachment.AttachmentItem.State.MIS @NotNullByDefault public class AttachmentItem implements Parcelable { - public enum State {LOADING, MISSING, AVAILABLE, ERROR} + public enum State { + LOADING, MISSING, AVAILABLE, ERROR; + + public boolean isFinal() { + return this == AVAILABLE || this == ERROR; + } + } private final AttachmentHeader header; private final int width, height; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java index 298c8cbbb..c45c36ab7 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.attachment; -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.nullsafety.NotNullByDefault; @@ -8,11 +7,12 @@ 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.PrivateMessageHeader; +import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent; import java.io.InputStream; import java.util.List; -import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; @NotNullByDefault @@ -21,10 +21,19 @@ public interface AttachmentRetriever { @DatabaseExecutor Attachment getMessageAttachment(AttachmentHeader h) throws DbException; - List getAttachmentItems(PrivateMessageHeader messageHeader); + /** + * Returns a list of observable {@link LiveData} + * that get updated as the state of their {@link AttachmentItem}s changes. + */ + List> getAttachmentItems( + PrivateMessageHeader messageHeader); /** * Retrieves item size and adds the item to the cache, if available. + *

+ * Use this to eagerly load the attachment size before it gets displayed. + * This is needed for messages containing a single attachment. + * Messages with more than one attachment use a standard size. */ @DatabaseExecutor void cacheAttachmentItemWithSize(MessageId conversationMessageId, @@ -37,15 +46,11 @@ public interface AttachmentRetriever { AttachmentItem createAttachmentItem(Attachment a, boolean needsSize); /** - * Load an {@link AttachmentItem} from the database. - * - * @return a pair of the {@link MessageId} of the conversation message - * and the {@link AttachmentItem} - * or {@code null} when the private message did not yet arrive. + * Loads an {@link AttachmentItem} + * that arrived via an {@link AttachmentReceivedEvent} + * and notifies the associated {@link LiveData}. */ - @Nullable @DatabaseExecutor - Pair loadAttachmentItem(MessageId attachmentId) - throws DbException; + void loadAttachmentItem(MessageId attachmentId); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java index 8724ff8ee..2fa8c5849 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.attachment; -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.NoSuchMessageException; @@ -18,15 +17,19 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; import java.util.logging.Logger; import javax.inject.Inject; -import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import static java.util.Objects.requireNonNull; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.IoUtils.tryToClose; +import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE; import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR; import static org.briarproject.briar.android.attachment.AttachmentItem.State.LOADING; @@ -38,6 +41,8 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { private static final Logger LOG = getLogger(AttachmentRetrieverImpl.class.getName()); + @DatabaseExecutor + private final Executor dbExecutor; private final MessagingManager messagingManager; private final ImageHelper imageHelper; private final ImageSizeCalculator imageSizeCalculator; @@ -45,17 +50,17 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { private final int minWidth, maxWidth; private final int minHeight, maxHeight; - // Info for AttachmentItems that are either still LOADING or MISSING - private final Map unavailableItems = - new ConcurrentHashMap<>(); - // We cache only items in their final state: AVAILABLE or ERROR - private final Map itemCache = - new ConcurrentHashMap<>(); + private final Map> + itemsWithSize = new ConcurrentHashMap<>(); + private final Map> + itemsWithoutSize = new ConcurrentHashMap<>(); @Inject - AttachmentRetrieverImpl(MessagingManager messagingManager, + AttachmentRetrieverImpl(@DatabaseExecutor Executor dbExecutor, + MessagingManager messagingManager, AttachmentDimensions dimensions, ImageHelper imageHelper, ImageSizeCalculator imageSizeCalculator) { + this.dbExecutor = dbExecutor; this.messagingManager = messagingManager; this.imageHelper = imageHelper; this.imageSizeCalculator = imageSizeCalculator; @@ -74,20 +79,37 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { } @Override - public List getAttachmentItems( + public List> getAttachmentItems( PrivateMessageHeader messageHeader) { List headers = messageHeader.getAttachmentHeaders(); - List items = new ArrayList<>(headers.size()); + List> items = new ArrayList<>(headers.size()); boolean needsSize = headers.size() == 1; for (AttachmentHeader h : headers) { - AttachmentItem item = itemCache.get(h.getMessageId()); - if (item == null || (needsSize && !item.hasSize())) { - item = new AttachmentItem(h, defaultSize, defaultSize, LOADING); - UnavailableItem unavailableItem = new UnavailableItem( - messageHeader.getId(), h, needsSize); - unavailableItems.put(h.getMessageId(), unavailableItem); + // try cache for existing item live data + MutableLiveData liveData; + if (needsSize) liveData = itemsWithSize.get(h.getMessageId()); + else { + // try items with size first, as they work as well + liveData = itemsWithSize.get(h.getMessageId()); + if (liveData == null) + liveData = itemsWithoutSize.get(h.getMessageId()); } - items.add(item); + + // create new live data with LOADING item if cache miss + if (liveData == null) { + AttachmentItem item = new AttachmentItem(h, + defaultSize, defaultSize, LOADING); + final MutableLiveData finalLiveData = + new MutableLiveData<>(item); + // kick-off loading of attachment, will post to live data + dbExecutor.execute( + () -> loadAttachmentItem(h, needsSize, finalLiveData)); + // add new LiveData to cache + liveData = finalLiveData; + if (needsSize) itemsWithSize.put(h.getMessageId(), liveData); + else itemsWithoutSize.put(h.getMessageId(), liveData); + } + items.add(liveData); } return items; } @@ -98,46 +120,63 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { AttachmentHeader h) throws DbException { try { Attachment a = messagingManager.getAttachment(h); - // this adds it to the cache automatically - createAttachmentItem(a, true); + AttachmentItem item = createAttachmentItem(a, true); + MutableLiveData liveData = + new MutableLiveData<>(item); + itemsWithSize.put(h.getMessageId(), liveData); } catch (NoSuchMessageException e) { LOG.info("Attachment not received yet"); } } @Override - @Nullable @DatabaseExecutor - public Pair loadAttachmentItem( - MessageId attachmentId) throws DbException { - UnavailableItem unavailableItem = unavailableItems.get(attachmentId); - if (unavailableItem == null) return null; + public void loadAttachmentItem(MessageId attachmentId) { + // try to find LiveData for attachment in both caches + MutableLiveData liveData; + boolean needsSize = true; + liveData = itemsWithSize.get(attachmentId); + if (liveData == null) { + needsSize = false; + liveData = itemsWithoutSize.get(attachmentId); + } - MessageId conversationMessageId = - unavailableItem.getConversationMessageId(); - AttachmentHeader h = unavailableItem.getHeader(); - boolean needsSize = unavailableItem.needsSize(); + // If no LiveData for the attachment exists, + // its message did not yet arrive and we can ignore it for now. + if (liveData == null) return; + // actually load the attachment item + AttachmentHeader h = requireNonNull(liveData.getValue()).getHeader(); + loadAttachmentItem(h, needsSize, liveData); + } + + /** + * Loads an {@link AttachmentItem} from the database + * and notifies the given {@link LiveData}. + */ + @DatabaseExecutor + private void loadAttachmentItem(AttachmentHeader h, boolean needsSize, + MutableLiveData liveData) { + Attachment a; AttachmentItem item; try { - Attachment a = messagingManager.getAttachment(h); + a = messagingManager.getAttachment(h); item = createAttachmentItem(a, needsSize); - unavailableItems.remove(attachmentId); } catch (NoSuchMessageException e) { LOG.info("Attachment not received yet"); - // unavailable item is still tracked, no need to add it again item = new AttachmentItem(h, defaultSize, defaultSize, MISSING); + } catch (DbException e) { + logException(LOG, WARNING, e); + item = new AttachmentItem(h, "", ERROR); } - return new Pair<>(conversationMessageId, item); + liveData.postValue(item); } @Override public AttachmentItem createAttachmentItem(Attachment a, boolean needsSize) { + AttachmentItem item; AttachmentHeader h = a.getHeader(); - AttachmentItem item = itemCache.get(h.getMessageId()); - if (item != null && (needsSize == item.hasSize())) return item; - if (needsSize) { InputStream is = new BufferedInputStream(a.getStream()); Size size = imageSizeCalculator.getSize(is, h.getContentType()); @@ -153,7 +192,6 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { } item = new AttachmentItem(h, extension, state); } - itemCache.put(h.getMessageId(), item); return item; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index ac03699a6..539d0e0aa 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -96,6 +96,7 @@ import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityOptionsCompat; import androidx.core.content.ContextCompat; +import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProviders; @@ -130,7 +131,6 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.join; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION; -import static org.briarproject.briar.android.attachment.AttachmentItem.State.LOADING; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION; import static org.briarproject.briar.android.conversation.ImageActivity.DATE; @@ -558,7 +558,8 @@ public class ConversationActivity extends BriarActivity LOG.info("Eagerly loading image size for latest message"); AttachmentHeader header = headers.get(0); // get the item to retrieve its size - attachmentRetriever.cacheAttachmentItemWithSize(h.getId(), header); + attachmentRetriever + .cacheAttachmentItemWithSize(h.getId(), header); } } catch (DbException e) { logException(LOG, WARNING, e); @@ -639,33 +640,6 @@ public class ConversationActivity extends BriarActivity && adapter.isScrolledToBottom(layoutManager); } - private void loadMessageAttachments(List items) { - runOnDbThread(() -> { - for (AttachmentItem item : items) { - if (item.getState() == LOADING) - loadMessageAttachment(item.getMessageId(), false); - } - }); - } - - @DatabaseExecutor - private void loadMessageAttachment(MessageId attachmentId, - boolean allowedToFail) { - try { - Pair pair = attachmentRetriever - .loadAttachmentItem(attachmentId); - if (pair == null && allowedToFail) { - LOG.warning("Attachment arrived before message"); - return; - } else if (pair == null) throw new AssertionError(); - MessageId conversationMessageId = pair.getFirst(); - AttachmentItem item = pair.getSecond(); - updateMessageAttachment(conversationMessageId, item); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - } - private void updateMessageAttachment(MessageId m, AttachmentItem item) { runOnUiThreadUnlessDestroyed(() -> { Pair pair = @@ -748,9 +722,8 @@ public class ConversationActivity extends BriarActivity @UiThread private void onAttachmentReceived(MessageId attachmentId) { - // This is allowed to fail, because the conversation message - // might arrive *after* the attachment. - runOnDbThread(() -> loadMessageAttachment(attachmentId, true)); + runOnDbThread( + () -> attachmentRetriever.loadAttachmentItem(attachmentId)); } @UiThread @@ -1134,9 +1107,32 @@ public class ConversationActivity extends BriarActivity */ @Override public List getAttachmentItems(PrivateMessageHeader h) { - List items = attachmentRetriever.getAttachmentItems(h); - loadMessageAttachments(items); + List> liveDataList = + attachmentRetriever.getAttachmentItems(h); + List items = new ArrayList<>(liveDataList.size()); + for (LiveData liveData : liveDataList) { + liveData.observe(this, new AttachmentObserver(h.getId(), liveData)); + items.add(requireNonNull(liveData.getValue())); + } return items; } + private class AttachmentObserver implements Observer { + private final MessageId conversationMessageId; + private final LiveData liveData; + + private AttachmentObserver(MessageId conversationMessageId, + LiveData liveData) { + this.conversationMessageId = conversationMessageId; + this.liveData = liveData; + } + + @Override + public void onChanged(AttachmentItem attachmentItem) { + updateMessageAttachment(conversationMessageId, attachmentItem); + if (attachmentItem.getState().isFinal()) + liveData.removeObserver(this); + } + } + } diff --git a/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java b/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java index 8fae2c224..5504da00b 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java @@ -2,6 +2,7 @@ package org.briarproject.briar.android.attachment; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.bramble.test.ImmediateExecutor; import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.MessagingManager; @@ -11,6 +12,7 @@ import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.util.concurrent.Executor; import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getRandomId; @@ -33,8 +35,9 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase { MessagingManager messagingManager = context.mock(MessagingManager.class); imageSizeCalculator = context.mock(ImageSizeCalculator.class); - retriever = new AttachmentRetrieverImpl(messagingManager, dimensions, - imageHelper, imageSizeCalculator); + Executor dbExecutor = new ImmediateExecutor(); + retriever = new AttachmentRetrieverImpl(dbExecutor, messagingManager, + dimensions, imageHelper, imageSizeCalculator); } @Test From 7c22016b811efd1ff0ade9ce8ae7200767956627 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 23 Jan 2020 10:22:02 -0300 Subject: [PATCH 006/582] [android] attach some smaller image attachment issues --- .../android/attachment/AttachmentItem.java | 12 ++++--- .../attachment/AttachmentRetriever.java | 7 ++++ .../android/attachment/UnavailableItem.java | 36 ------------------- .../conversation/ConversationActivity.java | 16 ++++----- .../android/conversation/ImageAdapter.java | 2 +- 5 files changed, 23 insertions(+), 50 deletions(-) delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java index 58c5d9160..9ce231c7d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java @@ -135,10 +135,6 @@ public class AttachmentItem implements Parcelable { return toHexString(instanceId); } - boolean hasSize() { - return width != 0 && height != 0; - } - @Override public int describeContents() { return 0; @@ -156,6 +152,10 @@ public class AttachmentItem implements Parcelable { dest.writeString(state.name()); } + /** + * This is used to identity if two items are the same, + * irrespective of their state or size. + */ @Override public boolean equals(@Nullable Object o) { return o instanceof AttachmentItem && @@ -164,4 +164,8 @@ public class AttachmentItem implements Parcelable { ); } + @Override + public int hashCode() { + return header.getMessageId().hashCode(); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java index c45c36ab7..2bf6b69ea 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java @@ -49,6 +49,13 @@ public interface AttachmentRetriever { * Loads an {@link AttachmentItem} * that arrived via an {@link AttachmentReceivedEvent} * and notifies the associated {@link LiveData}. + * + * Note that you need to call {@link #getAttachmentItems(PrivateMessageHeader)} + * first to get the LiveData. + * + * It is possible that no LiveData is available, + * because the message of the AttachmentItem did not arrive, yet. + * In this case, the load wil be deferred until the message arrives. */ @DatabaseExecutor void loadAttachmentItem(MessageId attachmentId); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java deleted file mode 100644 index 72951da3b..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/UnavailableItem.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.briarproject.briar.android.attachment; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.briar.api.messaging.AttachmentHeader; - -import javax.annotation.concurrent.Immutable; - -@Immutable -@NotNullByDefault -class UnavailableItem { - - private final MessageId conversationMessageId; - private final AttachmentHeader header; - private final boolean needsSize; - - UnavailableItem(MessageId conversationMessageId, - AttachmentHeader header, boolean needsSize) { - this.conversationMessageId = conversationMessageId; - this.header = header; - this.needsSize = needsSize; - } - - MessageId getConversationMessageId() { - return conversationMessageId; - } - - AttachmentHeader getHeader() { - return header; - } - - boolean needsSize() { - return needsSize; - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 539d0e0aa..bf39383ad 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -640,16 +640,14 @@ public class ConversationActivity extends BriarActivity && adapter.isScrolledToBottom(layoutManager); } + @UiThread private void updateMessageAttachment(MessageId m, AttachmentItem item) { - runOnUiThreadUnlessDestroyed(() -> { - Pair pair = - adapter.getMessageItem(m); - if (pair != null && pair.getSecond().updateAttachments(item)) { - boolean scroll = shouldScrollWhenUpdatingMessage(); - adapter.notifyItemChanged(pair.getFirst()); - if (scroll) scrollToBottom(); - } - }); + Pair pair = adapter.getMessageItem(m); + if (pair != null && pair.getSecond().updateAttachments(item)) { + boolean scroll = shouldScrollWhenUpdatingMessage(); + adapter.notifyItemChanged(pair.getFirst()); + if (scroll) scrollToBottom(); + } } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java index f0401b4cc..02f97730c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java @@ -59,7 +59,7 @@ class ImageAdapter extends Adapter { // get item requireNonNull(conversationItem); AttachmentItem item = items.get(position); - // set onClick listener, if not missing or error + // set onClick listener imageViewHolder.itemView.setOnClickListener(v -> listener.onAttachmentClicked(v, conversationItem, item) ); From c1cf6f61b90e231bfd4b6b812ba5c160fd9572e9 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 11 Feb 2020 10:11:09 -0300 Subject: [PATCH 007/582] [android] fix concurrency issues when attachments are received delayed Do not observe attachment live data multiple times and don't miss received attachments in ImageActivity resp. ImageViewModel. --- .../conversation/ConversationActivity.java | 4 ++ .../android/conversation/ImageActivity.java | 21 ++++++---- .../android/conversation/ImageFragment.java | 9 +++-- .../android/conversation/ImageViewModel.java | 40 ++++++++++++++++--- .../briar/android/viewmodel/LiveEvent.java | 16 ++++++++ .../android/viewmodel/MutableLiveEvent.java | 16 ++++++++ 6 files changed, 88 insertions(+), 18 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index bf39383ad..3bbf22011 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -1109,6 +1109,10 @@ public class ConversationActivity extends BriarActivity attachmentRetriever.getAttachmentItems(h); List items = new ArrayList<>(liveDataList.size()); for (LiveData liveData : liveDataList) { + // first remove all our observers to avoid having more than one + // in case we reload the conversation, e.g. after deleting messages + liveData.removeObservers(this); + // add a new observer liveData.observe(this, new AttachmentObserver(h.getId(), liveData)); items.add(requireNonNull(liveData.getValue())); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java index 874e4b95f..7f8e3bedb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -103,9 +103,20 @@ public class ImageActivity extends BriarActivity setSceneTransitionAnimation(transition, null, transition); } + // Intent Extras + Intent i = getIntent(); + attachments = + requireNonNull(i.getParcelableArrayListExtra(ATTACHMENTS)); + int position = i.getIntExtra(ATTACHMENT_POSITION, -1); + if (position == -1) throw new IllegalStateException(); + String name = i.getStringExtra(NAME); + long time = i.getLongExtra(DATE, 0); + byte[] messageIdBytes = requireNonNull(i.getByteArrayExtra(ITEM_ID)); + // get View Model viewModel = ViewModelProviders.of(this, viewModelFactory) .get(ImageViewModel.class); + viewModel.expectAttachments(attachments); viewModel.getSaveState().observeEvent(this, this::onImageSaveStateChanged); @@ -129,17 +140,11 @@ public class ImageActivity extends BriarActivity TextView contactName = toolbar.findViewById(R.id.contactName); TextView dateView = toolbar.findViewById(R.id.dateView); - // Intent Extras - Intent i = getIntent(); - attachments = i.getParcelableArrayListExtra(ATTACHMENTS); - int position = i.getIntExtra(ATTACHMENT_POSITION, -1); - if (position == -1) throw new IllegalStateException(); - String name = i.getStringExtra(NAME); - long time = i.getLongExtra(DATE, 0); + // Set contact name and message time String date = formatDateAbsolute(this, time); contactName.setText(name); dateView.setText(date); - conversationMessageId = new MessageId(i.getByteArrayExtra(ITEM_ID)); + conversationMessageId = new MessageId(messageIdBytes); // Set up image ViewPager viewPager = findViewById(R.id.viewPager); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java index 4a61b0dfb..09ec190eb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java @@ -95,8 +95,6 @@ public class ImageFragment extends Fragment viewModel = ViewModelProviders.of(requireNonNull(getActivity()), viewModelFactory).get(ImageViewModel.class); - viewModel.getOnAttachmentLoaded() - .observeEvent(this, this::onAttachmentLoaded); photoView = v.findViewById(R.id.photoView); photoView.setScaleLevels(1, 2, 4); @@ -111,6 +109,9 @@ public class ImageFragment extends Fragment } else { photoView.setImageResource(R.drawable.ic_image_missing); startPostponedTransition(); + // state is not final, so observe state changes + viewModel.getOnAttachmentReceived(attachment.getMessageId()) + .observeEvent(this, this::onAttachmentReceived); } return v; @@ -127,8 +128,8 @@ public class ImageFragment extends Fragment .into(photoView); } - private void onAttachmentLoaded(MessageId messageId) { - if (attachment.getMessageId().equals(messageId)) loadImage(); + private void onAttachmentReceived(Boolean received) { + if (received) loadImage(); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java index 58ef47dba..d53f26e7b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java @@ -27,7 +27,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.List; import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -40,6 +42,7 @@ import androidx.lifecycle.AndroidViewModel; import static android.media.MediaScannerConnection.scanFile; import static android.os.Environment.DIRECTORY_PICTURES; import static android.os.Environment.getExternalStoragePublicDirectory; +import static java.util.Objects.requireNonNull; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.IoUtils.copyAndClose; @@ -57,8 +60,10 @@ public class ImageViewModel extends AndroidViewModel implements EventListener { @IoExecutor private final Executor ioExecutor; - private final MutableLiveEvent attachmentLoaded = - new MutableLiveEvent<>(); + private volatile boolean receivedAttachmentsInitialized = false; + private ConcurrentHashMap> + receivedAttachments = new ConcurrentHashMap<>(); + /** * true means there was an error saving the image, false if image was saved. */ @@ -91,13 +96,36 @@ public class ImageViewModel extends AndroidViewModel implements EventListener { @Override public void eventOccurred(Event e) { if (e instanceof AttachmentReceivedEvent) { - attachmentLoaded - .postEvent(((AttachmentReceivedEvent) e).getMessageId()); + MessageId id = ((AttachmentReceivedEvent) e).getMessageId(); + MutableLiveEvent oldEvent; + if (receivedAttachmentsInitialized) { + oldEvent = receivedAttachments.get(id); + } else { + oldEvent = receivedAttachments + .putIfAbsent(id, new MutableLiveEvent<>(true)); + } + if (oldEvent != null) oldEvent.postEvent(true); } } - LiveEvent getOnAttachmentLoaded() { - return attachmentLoaded; + @UiThread + public void expectAttachments(List attachments) { + for (AttachmentItem item : attachments) { + // no need to track items that are in a final state already + if (item.getState().isFinal()) continue; + // add new live events, if not added concurrently by Event + MessageId id = item.getMessageId(); + receivedAttachments.putIfAbsent(id, new MutableLiveEvent<>()); + } + receivedAttachmentsInitialized = true; + } + + @UiThread + LiveEvent getOnAttachmentReceived(MessageId messageId) { + if (receivedAttachments.size() == 0) { + throw new IllegalStateException("expectAttachments() not called"); + } + return requireNonNull(receivedAttachments.get(messageId)); } void clickImage() { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveEvent.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveEvent.java index d7ff0cef5..d4e1b9cff 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveEvent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveEvent.java @@ -12,6 +12,22 @@ import androidx.lifecycle.Observer; @NotNullByDefault public class LiveEvent extends LiveData> { + /** + * Creates a LiveEvent initialized with the given {@code value}. + * + * @param value initial value + */ + public LiveEvent(T value) { + super(new ConsumableEvent<>(value)); + } + + /** + * Creates a LiveEvent with no value assigned to it. + */ + public LiveEvent() { + super(); + } + public void observeEvent(LifecycleOwner owner, LiveEventHandler handler) { LiveEventObserver observer = new LiveEventObserver<>(handler); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/MutableLiveEvent.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/MutableLiveEvent.java index dac33ecfc..fda307d5d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/MutableLiveEvent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/MutableLiveEvent.java @@ -5,6 +5,22 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault; @NotNullByDefault public class MutableLiveEvent extends LiveEvent { + /** + * Creates a MutableLiveEvent initialized with the given {@code value}. + * + * @param value initial value + */ + public MutableLiveEvent(T value) { + super(value); + } + + /** + * Creates a MutableLiveEvent with no value assigned to it. + */ + public MutableLiveEvent() { + super(); + } + public void postEvent(T value) { super.postValue(new ConsumableEvent<>(value)); } From 0f6f52c37a66270b2ee59c5e1d50b0e51cb59c22 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 11 Feb 2020 12:41:41 -0300 Subject: [PATCH 008/582] [android] Listen to AttachmentReceivedEvents when ConversationActivity is stopped This way Attachments get shown when the activity resumes. --- .../conversation/ConversationActivity.java | 14 ----------- .../conversation/ConversationViewModel.java | 24 ++++++++++++++++++- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 3bbf22011..4a73c33e8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -74,7 +74,6 @@ import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.PrivateMessageHeader; -import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; import java.util.ArrayList; @@ -652,13 +651,6 @@ public class ConversationActivity extends BriarActivity @Override 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) { ContactRemovedEvent c = (ContactRemovedEvent) e; if (c.getContactId().equals(contactId)) { @@ -718,12 +710,6 @@ public class ConversationActivity extends BriarActivity scrollToBottom(); } - @UiThread - private void onAttachmentReceived(MessageId attachmentId) { - runOnDbThread( - () -> attachmentRetriever.loadAttachmentItem(attachmentId)); - } - @UiThread private void onNewConversationMessage(ConversationMessageHeader h) { if (h instanceof ConversationRequest || diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java index 6670bf5ea..5becffce2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java @@ -11,6 +11,9 @@ import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.settings.Settings; @@ -30,6 +33,7 @@ import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessageFactory; import org.briarproject.briar.api.messaging.PrivateMessageHeader; +import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent; import java.util.Collection; import java.util.List; @@ -56,7 +60,7 @@ import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce; @NotNullByDefault public class ConversationViewModel extends AndroidViewModel - implements AttachmentManager { + implements EventListener, AttachmentManager { private static Logger LOG = getLogger(ConversationViewModel.class.getName()); @@ -69,6 +73,7 @@ public class ConversationViewModel extends AndroidViewModel @DatabaseExecutor private final Executor dbExecutor; private final TransactionManager db; + private final EventBus eventBus; private final MessagingManager messagingManager; private final ContactManager contactManager; private final SettingsManager settingsManager; @@ -101,6 +106,7 @@ public class ConversationViewModel extends AndroidViewModel ConversationViewModel(Application application, @DatabaseExecutor Executor dbExecutor, TransactionManager db, + EventBus eventBus, MessagingManager messagingManager, ContactManager contactManager, SettingsManager settingsManager, @@ -110,6 +116,7 @@ public class ConversationViewModel extends AndroidViewModel super(application); this.dbExecutor = dbExecutor; this.db = db; + this.eventBus = eventBus; this.messagingManager = messagingManager; this.contactManager = contactManager; this.settingsManager = settingsManager; @@ -119,12 +126,27 @@ public class ConversationViewModel extends AndroidViewModel messagingGroupId = Transformations .map(contact, c -> messagingManager.getContactGroup(c).getId()); contactDeleted.setValue(false); + + eventBus.addListener(this); } @Override protected void onCleared() { super.onCleared(); attachmentCreator.deleteUnsentAttachments(); + eventBus.removeListener(this); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof AttachmentReceivedEvent) { + AttachmentReceivedEvent a = (AttachmentReceivedEvent) e; + if (a.getContactId().equals(contactId)) { + LOG.info("Attachment received"); + dbExecutor.execute(() -> attachmentRetriever + .loadAttachmentItem(a.getMessageId())); + } + } } /** From 4d27828712db7f4a6dd492def966dc09716fecd8 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Fri, 24 Jan 2020 14:48:47 +0000 Subject: [PATCH 009/582] Check for concurrent cache updates. --- .../attachment/AttachmentRetrieverImpl.java | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java index 2fa8c5849..1faa7dd1e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java @@ -15,8 +15,8 @@ import java.io.BufferedInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -50,9 +50,9 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { private final int minWidth, maxWidth; private final int minHeight, maxHeight; - private final Map> + private final ConcurrentMap> itemsWithSize = new ConcurrentHashMap<>(); - private final Map> + private final ConcurrentMap> itemsWithoutSize = new ConcurrentHashMap<>(); @Inject @@ -99,15 +99,25 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { if (liveData == null) { AttachmentItem item = new AttachmentItem(h, defaultSize, defaultSize, LOADING); - final MutableLiveData finalLiveData = - new MutableLiveData<>(item); - // kick-off loading of attachment, will post to live data - dbExecutor.execute( - () -> loadAttachmentItem(h, needsSize, finalLiveData)); - // add new LiveData to cache - liveData = finalLiveData; - if (needsSize) itemsWithSize.put(h.getMessageId(), liveData); - else itemsWithoutSize.put(h.getMessageId(), liveData); + liveData = new MutableLiveData<>(item); + // add new LiveData to cache, checking for concurrent updates + MutableLiveData oldLiveData; + if (needsSize) { + oldLiveData = itemsWithSize.putIfAbsent(h.getMessageId(), + liveData); + } else { + oldLiveData = itemsWithoutSize.putIfAbsent(h.getMessageId(), + liveData); + } + if (oldLiveData == null) { + // kick-off loading of attachment, will post to live data + MutableLiveData finalLiveData = liveData; + dbExecutor.execute(() -> + loadAttachmentItem(h, needsSize, finalLiveData)); + } else { + // Concurrent cache update - use the existing live data + liveData = oldLiveData; + } } items.add(liveData); } @@ -118,12 +128,15 @@ class AttachmentRetrieverImpl implements AttachmentRetriever { @DatabaseExecutor public void cacheAttachmentItemWithSize(MessageId conversationMessageId, AttachmentHeader h) throws DbException { + // If a live data is already cached we don't need to do anything + if (itemsWithSize.containsKey(h.getMessageId())) return; try { Attachment a = messagingManager.getAttachment(h); AttachmentItem item = createAttachmentItem(a, true); MutableLiveData liveData = new MutableLiveData<>(item); - itemsWithSize.put(h.getMessageId(), liveData); + // If a live data was concurrently cached, don't replace it + itemsWithSize.putIfAbsent(h.getMessageId(), liveData); } catch (NoSuchMessageException e) { LOG.info("Attachment not received yet"); } From bded1edb2bf043431a2419c2d72be44092188430 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 13 Feb 2020 10:24:36 -0300 Subject: [PATCH 010/582] [android] Use ordinary HashMap for to be received attachments Also don't do list stacking from end for now. --- .../conversation/ConversationActivity.java | 1 - .../android/conversation/ImageViewModel.java | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 4a73c33e8..7a164c388 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -260,7 +260,6 @@ public class ConversationActivity extends BriarActivity adapter = new ConversationAdapter(this, this); list = findViewById(R.id.conversationView); layoutManager = new LinearLayoutManager(this); - layoutManager.setStackFromEnd(true); list.setLayoutManager(layoutManager); list.setAdapter(adapter); list.setEmptyText(getString(R.string.no_private_messages)); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java index d53f26e7b..1a59b02bf 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java @@ -27,9 +27,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -60,9 +60,9 @@ public class ImageViewModel extends AndroidViewModel implements EventListener { @IoExecutor private final Executor ioExecutor; - private volatile boolean receivedAttachmentsInitialized = false; - private ConcurrentHashMap> - receivedAttachments = new ConcurrentHashMap<>(); + private boolean receivedAttachmentsInitialized = false; + private HashMap> receivedAttachments = + new HashMap<>(); /** * true means there was an error saving the image, false if image was saved. @@ -93,6 +93,7 @@ public class ImageViewModel extends AndroidViewModel implements EventListener { eventBus.removeListener(this); } + @UiThread @Override public void eventOccurred(Event e) { if (e instanceof AttachmentReceivedEvent) { @@ -100,11 +101,10 @@ public class ImageViewModel extends AndroidViewModel implements EventListener { MutableLiveEvent oldEvent; if (receivedAttachmentsInitialized) { oldEvent = receivedAttachments.get(id); + if (oldEvent != null) oldEvent.postEvent(true); } else { - oldEvent = receivedAttachments - .putIfAbsent(id, new MutableLiveEvent<>(true)); + receivedAttachments.put(id, new MutableLiveEvent<>(true)); } - if (oldEvent != null) oldEvent.postEvent(true); } } @@ -113,18 +113,21 @@ public class ImageViewModel extends AndroidViewModel implements EventListener { for (AttachmentItem item : attachments) { // no need to track items that are in a final state already if (item.getState().isFinal()) continue; - // add new live events, if not added concurrently by Event + // add new live events, if not already added by eventOccurred() MessageId id = item.getMessageId(); - receivedAttachments.putIfAbsent(id, new MutableLiveEvent<>()); + if (!receivedAttachments.containsKey(id)) { + receivedAttachments.put(id, new MutableLiveEvent<>()); + } } receivedAttachmentsInitialized = true; } + /** + * Returns a LiveData for attachments in a non-final state. + * Note that you need to call {@link #expectAttachments(List)} first. + */ @UiThread LiveEvent getOnAttachmentReceived(MessageId messageId) { - if (receivedAttachments.size() == 0) { - throw new IllegalStateException("expectAttachments() not called"); - } return requireNonNull(receivedAttachments.get(messageId)); } From 61d3fe90551eb9c451241616a2ad3b3ea0349e5a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 5 Nov 2019 09:41:01 -0300 Subject: [PATCH 011/582] [android] fix IllegalStateException when creating attachments Injecting the non-singleton AttachmentCreator keeps an instance around that gets re-used with a different ViewModel. When backing out without sending or cancelling the attachments, we don't reset the state which leads us into an illegal state. --- .../android/attachment/AttachmentCreatorImpl.java | 15 +++++++++++---- .../conversation/ConversationViewModel.java | 4 +++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java index 60f498fdd..4eb79a02f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java @@ -76,8 +76,12 @@ class AttachmentCreatorImpl implements AttachmentCreator { @UiThread public LiveData storeAttachments( LiveData groupId, Collection newUris) { - if (task != null || result != null || !uris.isEmpty()) + if (task != null || result != null || !uris.isEmpty()) { + if (task != null) LOG.warning("Task already exists!"); + if (result != null) LOG.warning("Result already exists!"); + if (!uris.isEmpty()) LOG.warning("Uris available: " + uris); throw new IllegalStateException(); + } MutableLiveData result = new MutableLiveData<>(); this.result = result; uris.addAll(newUris); @@ -96,8 +100,12 @@ class AttachmentCreatorImpl implements AttachmentCreator { @UiThread public LiveData getLiveAttachments() { MutableLiveData result = this.result; - if (task == null || result == null || uris.isEmpty()) + if (task == null || result == null || uris.isEmpty()) { + if (task == null) LOG.warning("No Task!"); + if (result == null) LOG.warning("No Result!"); + if (uris.isEmpty()) LOG.warning("Uris empty!"); throw new IllegalStateException(); + } // A task is already running. It will update the result LiveData. // So nothing more to do here. return result; @@ -174,8 +182,7 @@ class AttachmentCreatorImpl implements AttachmentCreator { @Override @UiThread public void cancel() { - if (task == null) throw new AssertionError(); - task.cancel(); + if (task != null) task.cancel(); deleteUnsentAttachments(); resetState(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java index 5becffce2..8e65db26c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java @@ -133,7 +133,7 @@ public class ConversationViewModel extends AndroidViewModel @Override protected void onCleared() { super.onCleared(); - attachmentCreator.deleteUnsentAttachments(); + attachmentCreator.cancel(); // also deletes unsent attachments eventBus.removeListener(this); } @@ -274,6 +274,7 @@ public class ConversationViewModel extends AndroidViewModel settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE); } + @UiThread private void createMessage(GroupId groupId, @Nullable String text, List headers, long timestamp, boolean hasImageSupport) { @@ -292,6 +293,7 @@ public class ConversationViewModel extends AndroidViewModel } } + @UiThread private void storeMessage(PrivateMessage m) { attachmentCreator.onAttachmentsSent(m.getMessage().getId()); dbExecutor.execute(() -> { From cf8241e79c1f58a37bfbc0032272f308604ac26b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 23 Jan 2020 10:24:19 -0300 Subject: [PATCH 012/582] Fix IllegalStateException in RecyclerView when backing out very quickly after adding image attachments for preview before sending --- .../java/org/briarproject/briar/android/view/ImagePreview.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java index cb379b3d3..661ddaefa 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java @@ -79,7 +79,7 @@ public class ImagePreview extends ConstraintLayout { ((ImagePreviewAdapter) imageList.getAdapter()); int pos = requireNonNull(adapter).loadItemPreview(result); if (pos != NO_POSITION) { - imageList.smoothScrollToPosition(pos); + imageList.scrollToPosition(pos); } } From 8d55ea3f6f0f98f3ede87f607f24e6f572096703 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Fri, 25 Sep 2020 13:41:31 +0100 Subject: [PATCH 013/582] Update translations. --- .../src/main/res/values-de/strings.xml | 2 +- .../src/main/res/values-es/strings.xml | 6 ++--- .../src/main/res/values-fr/strings.xml | 4 +-- .../src/main/res/values-is/strings.xml | 21 +++++++++++++++ .../src/main/res/values-nl/strings.xml | 25 +++++++++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 1 + .../src/main/res/values-ru/strings.xml | 27 ++++++++++++++++++- .../src/main/res/values-zh-rCN/strings.xml | 27 +++++++++++++++++++ 8 files changed, 106 insertions(+), 7 deletions(-) diff --git a/briar-android/src/main/res/values-de/strings.xml b/briar-android/src/main/res/values-de/strings.xml index 52e875fec..9ebf87840 100644 --- a/briar-android/src/main/res/values-de/strings.xml +++ b/briar-android/src/main/res/values-de/strings.xml @@ -211,7 +211,7 @@ Wähle einen Spitznamen Gib einen Spitznamen ein Gib deinem Kontakt einen Spitznamen. Nur du kannst ihn sehen. - Gebe diesen Link dem Kontakt, den du hinzufügen möchtest: + Gib diesen Link dem Kontakt, den du hinzufügen möchtest Briar Link Link kopiert Es gab einen Fehler beim Hinzufügen des Kontaktes. diff --git a/briar-android/src/main/res/values-es/strings.xml b/briar-android/src/main/res/values-es/strings.xml index 3f9e7d729..a1bfb899a 100644 --- a/briar-android/src/main/res/values-es/strings.xml +++ b/briar-android/src/main/res/values-es/strings.xml @@ -157,12 +157,12 @@ ¿Estás seguro de que deseas eliminar todos los mensajes? No se pudieron eliminar todos los mensajes. Los mensajes relacionados con presentaciones o invitaciones en curso no se pueden eliminar hasta que finalicen. - Los mensajes relacionados con presentaciones o invitaciones en curso no se pueden eliminar hasta que finalicen. + Los mensajes relacionados con presentaciones no se pueden eliminar hasta que finalicen. Los mensajes relacionados a invitaciones en curso no pueden ser borrados hasta su conclusión. Los mensajes parcialmente descargados no se pueden eliminar hasta que haya finalizado la descarga. Para borrar una invitación o presentación, debes seleccionar la petición y la respuesta. - Para eliminar una introducción, debe seleccionar la solicitud y la respuesta. - Para eliminar una invitación, debe seleccionar la solicitud y la respuesta. + Para eliminar una introducción, debes seleccionar la solicitud y la respuesta. + Para eliminar una invitación, debes seleccionar la solicitud y la respuesta. Eliminar contacto Confirmar eliminación de contacto ¿Seguro que quieres eliminar este contacto y todos los mensajes intercambiados entre vosotros? diff --git a/briar-android/src/main/res/values-fr/strings.xml b/briar-android/src/main/res/values-fr/strings.xml index 266cb5cb7..a1b7ba891 100644 --- a/briar-android/src/main/res/values-fr/strings.xml +++ b/briar-android/src/main/res/values-fr/strings.xml @@ -198,9 +198,9 @@ Veuillez vérifier que vous êtes connecté au même réseau Wi-Fi. Si le problème persiste, veuillez nous envoyer une rétroaction pour nous aider à améliorer l\'appli. - Ajouter un contact éloigné + Ajouter un contact à distance Ajouter un contact à proximité - Ajouter un contact éloigné + Ajouter un contact à distance Saisissez ici le lien de votre contact Lien de votre contact Coller diff --git a/briar-android/src/main/res/values-is/strings.xml b/briar-android/src/main/res/values-is/strings.xml index 8d0a83552..d4bb6f5fc 100644 --- a/briar-android/src/main/res/values-is/strings.xml +++ b/briar-android/src/main/res/values-is/strings.xml @@ -79,8 +79,18 @@ Sama þráðlausa Wi-Fi netkerfið Síminn þinn er tengdur við Wi-Fi Síminn þinn er ekki tengdur við Wi-Fi + Briar er að tengjast við Wi-Fi-netið + Briar er tengt við við Wi-Fi-netið + Briar getur ekki tengst við Wi-Fi-netið + Briar er sett upp til að nota ekki Wi-Fi-netið Bluetooth + Kveikt er á Bluetooth-kerfi símans + Slökkt er á Bluetooth-kerfi símans + Briar er að tengjast við Bluetooth + Briar er tengt við Bluetooth + Briar getur ekki tengst við Bluetooth + Briar er sett upp til að nota ekki Bluetooth Skráð út úr Briar Ýttu til að skrá þig aftur inn. @@ -426,10 +436,20 @@ Sjálfvirkt (eftir tíma dags) Sjálfgefið í kerfinu + Tengingar + Tengjast tengiliðum í gegnum Bluetooth + Tengjast tengiliðum á sama þráðlausa Wi-Fi neti + Tengjast tengiliðum í gegnum internetið + Allar tengingar fara í gegnum Tor-netið til að vernda persónuupplýsingar + Aðferðir til tengingar við Tor-netið Sjálfvirkt byggt á staðsetningu + Nota Tor-netkerfið án brúa + Nota Tor-netkerfið með brúm + Ekki tengjast við internetið Sjálfvirkt: %1$s (eftir %2$s) Nota farsímagagnasamband + Tengjast í gegnum internetið aðeins þegar verið er í hleðslu Gerir internettengingu óvirka þegar tækið keyrir á rafhlöðu Öryggi @@ -540,6 +560,7 @@ Briar er læst Ýttu til að aflæsa + Briar getur tengst við tengiliðina þína í gegnum internet, Wi-Fi eða Bluetooth.\n\nAllar internettengingar fara í gegnum Tor-netkerfið til að gæta gagnaleyndar.\n\nEf hægt er að nálgast tengilið með mörgum leiðum, notar Briar þær samhliða. Lísa diff --git a/briar-android/src/main/res/values-nl/strings.xml b/briar-android/src/main/res/values-nl/strings.xml index 0b0f09e38..65f9619ff 100644 --- a/briar-android/src/main/res/values-nl/strings.xml +++ b/briar-android/src/main/res/values-nl/strings.xml @@ -61,12 +61,36 @@ App vergrendelen Instellingen Log uit + Tik hier om in te stellen hoe Briar een verbinding met je contacten opzet. Internet + Je apparaat heeft internettoegang via wifi + Je apparaat heeft internettoegang via mobiele dataverbinding + Je apparaat heeft geen internettoegang + Briar is aan het verbinden met internet + Briar is verbonden met het internet + Briar kan geen verbinding maken met het internet + Briar is geconfigureerd om internet niet te gebruiken + Briar is geconfigureerd om mobiele dataverbinding niet te gebruiken + Briar is geconfigureerd om het internet niet te gebruiken als apparaat op baterij loopt + Briar is geconfigureerd om internet niet te gebruiken in dit land Wifi + Hetzelfde wifinetwerk + Je apparaat is verbonden met wifi + Je apparaat is niet verbonden met wifi + Briar is verbinding aan het maken met het wifinetwerk + Briar is verbonden met het wifinetwerk + Briar kan geen verbinding maken met het wifinetwerk + Briar is geconfigureerd om het wifinetwerk niet te gebruiken Bluetooth + Bluetooth op je apparaat staat aan + Bluetooth op je apparaat staat uit + Briar is aan het verbinden met bluetooth + Briar is verbonden met bluetooth + Briar kan geen verbinding maken met bluetooth + Briar is geconfigureerd om bluetooth niet te gebruiken Uitgelogd van Briar Tik om opnieuw in te loggen. @@ -536,6 +560,7 @@ Briar is vergrendeld Tik om te ontgrendelen + Briar kan met je contacten verbinden via het internet, wifi of bluetooth.\n\nAlle internetverbindingen gaan voor privacy door het Tornetwerk.\n\nAls een contact via meerdere methoden te bereiken is, zal Briar die parallel gebruiken. Veerle diff --git a/briar-android/src/main/res/values-pt-rBR/strings.xml b/briar-android/src/main/res/values-pt-rBR/strings.xml index d68f6347a..696d7ac8d 100644 --- a/briar-android/src/main/res/values-pt-rBR/strings.xml +++ b/briar-android/src/main/res/values-pt-rBR/strings.xml @@ -61,6 +61,7 @@ Bloquear Aplicativo Configurações Sair + Clique aqui para controlar como o Briar se conecta aos seus contatos. Internet diff --git a/briar-android/src/main/res/values-ru/strings.xml b/briar-android/src/main/res/values-ru/strings.xml index afb225452..f0b74fbcc 100644 --- a/briar-android/src/main/res/values-ru/strings.xml +++ b/briar-android/src/main/res/values-ru/strings.xml @@ -65,12 +65,36 @@ Заблокировать приложение Настройки Выйти + Нажмите здесь, чтобы проверить, как Briar подключается к вашим контактам. Интернет + Ваш телефон имеет доступ в интернет через Wi-Fi + Ваш телефон имеет доступ в интернет через мобильную сеть. + Ваш телефон не имеет доступа в интернет + Briar подключается к интернету + Briar подключен к интернету + Briar не может подключиться к интернету. + Briar настроен не использовать интернет + Briar настроен не использовать мобильную сеть + Briar настроен не использовать интернет при работе от батареи + Briar настроен не использовать интернет в этой стране Wi-Fi + Та же сеть Wi-Fi + Ваш телефон подключен к Wi-Fi + Телефон не подключен к Wi-Fi + Briar подключается к сети Wi-Fi + Briar подключен к сети Wi-Fi + Briar не может подключиться к сети Wi-Fi. + Briar настроен не использовать сеть Wi-Fi. Bluetooth + Bluetooth вашего телефона включен. + Bluetooth вашего телефона отключен. + Briar подключается к Bluetooth + Briar подключен к Bluetooth + Briar не может подключиться к Bluetooth. + Briar настроен не использовать Bluetooth Вы не авторизованы в Briar Нажмите для входа @@ -199,7 +223,7 @@ Выберите ник Введите ник Дайте вашему контакту ник. Увидеть его сможете только вы. - Передайте эту ссылку контакту, которого вы хотите добавить. + Передайте эту ссылку контакту, который вы хотите добавить. Ссылка Briar Ссылка скопирована При добавлении контакта произошла ошибка. @@ -558,6 +582,7 @@ Briar заблокирован Нажмите для разблокировки + Briar может подключаться к контактам через интернет, Wi-Fi или Bluetooth.\n\nВсе интернет-соединения проходят через сеть Tor для обеспечения конфиденциальности.\n\nЕсли с контактом можно связаться несколькими способами, Briar использует их параллельно. Бузова diff --git a/briar-android/src/main/res/values-zh-rCN/strings.xml b/briar-android/src/main/res/values-zh-rCN/strings.xml index 89f21df1b..d7ce80b76 100644 --- a/briar-android/src/main/res/values-zh-rCN/strings.xml +++ b/briar-android/src/main/res/values-zh-rCN/strings.xml @@ -60,12 +60,36 @@ 锁定应用 设置 登出 + 点击这里来控制Briar如何连接到您的联系人 互联网 + 你的手机通过Wi-Fi上网 + 您的手机通过移动数据访问互联网 + 你的手机不能上网 + Briar正在连接互联网 + Briar 已联网 + Briar 无法联网 + Briar 配置为不使用互联网 + Briar 配置为不使用移动数据 + Briar配置为在用电池运行时不使用互联网 + Briar配置为在这个国家不使用互联网 无线局域网 + 同一 Wi-Fi 网络 + 你的手机连接到Wi-Fi + 你的手机未连接到Wi-Fi + Briar正在连接Wi-Fi网络 + Briar已连接到此Wi-Fi网络 + Briar不能连接到此Wi-Fi网络 + Briar 配置为不使用Wi-Fi 网络 蓝牙 + 你手机的蓝牙已打开 + 你手机额蓝牙已关闭 + Briar 正连接蓝牙 + Briar 已连接蓝牙 + Briar 不能连接蓝牙 + Briar 配置为不使用蓝牙 已登出 Briar 轻按以重新登录。 @@ -108,6 +132,7 @@ 帮助 抱歉 在您的系统上不可用 + 状态: 尚无联系人可供显示 轻按 + 号即可添加联系人 @@ -414,6 +439,7 @@ 自动选择:%1$s(在 %2$s) 使用移动数据 + 仅在充电时联网 当设备使用电池电量时关闭网络连接 安全 @@ -524,6 +550,7 @@ Briar 已锁定 轻按以解锁 + Briar可以通过互联网、Wi-Fi或蓝牙连接到您的联系人。\n\n为了保护隐私,所有的互联网连接都通过Tor网络。\n\n如果一个联系人可以通过多种方法联系到,Briar会并行地使用它们。 韩梅梅 From 54b852db701cc74e0e6073a2268169c8ebc7baaf Mon Sep 17 00:00:00 2001 From: akwizgran Date: Fri, 25 Sep 2020 13:42:28 +0100 Subject: [PATCH 014/582] Bump version numbers for 1.2.10 release. --- bramble-android/build.gradle | 4 ++-- briar-android/build.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bramble-android/build.gradle b/bramble-android/build.gradle index 9efb212de..81d73b7fe 100644 --- a/bramble-android/build.gradle +++ b/bramble-android/build.gradle @@ -11,8 +11,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 10209 - versionName "1.2.9" + versionCode 10210 + versionName "1.2.10" consumerProguardFiles 'proguard-rules.txt' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 0c3d613e0..e68700a55 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -22,8 +22,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 10209 - versionName "1.2.9" + versionCode 10210 + versionName "1.2.10" applicationId "org.briarproject.briar.android" buildConfigField "String", "GitHash", "\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\"" From ccbe6d4bb8d913578aa055eb3aa8bd192eec53a5 Mon Sep 17 00:00:00 2001 From: Nico Alt Date: Fri, 25 Sep 2020 12:40:43 +0200 Subject: [PATCH 015/582] Expose unread messages count in API's contacts list Fixes #1746 --- briar-headless/README.md | 3 ++- .../briar/headless/contact/ContactControllerImpl.kt | 3 ++- .../briarproject/briar/headless/contact/OutputContact.kt | 5 +++-- .../org/briarproject/briar/headless/ControllerTest.kt | 1 + .../briar/headless/contact/ContactControllerTest.kt | 8 +++++--- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/briar-headless/README.md b/briar-headless/README.md index 4a00e97ab..2acfdac3a 100644 --- a/briar-headless/README.md +++ b/briar-headless/README.md @@ -69,7 +69,8 @@ Returns a JSON array of contacts: "handshakePublicKey": "XnYRd7a7E4CTqgAvh4hCxh/YZ0EPscxknB9ZcEOpSzY=", "verified": true, "lastChatActivity": 1557838312175, - "connected": false + "connected": false, + "unreadCount": 7 } ``` diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt index 4cffe0d96..b7e542b61 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt @@ -77,7 +77,8 @@ constructor( val contacts = contactManager.contacts.map { contact -> val latestMsgTime = conversationManager.getGroupCount(contact.id).latestMsgTime val connected = connectionRegistry.isConnected(contact.id) - contact.output(latestMsgTime, connected) + val unreadCount = conversationManager.getGroupCount(contact.id).unreadCount + contact.output(latestMsgTime, connected, unreadCount) } return ctx.json(contacts) } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt index 2d8656047..d0fbd8dd5 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt @@ -7,12 +7,13 @@ import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent import org.briarproject.bramble.identity.output import org.briarproject.briar.headless.json.JsonDict -internal fun Contact.output(latestMsgTime: Long, connected: Boolean) = JsonDict( +internal fun Contact.output(latestMsgTime: Long, connected: Boolean, unreadCount: Int) = JsonDict( "contactId" to id.int, "author" to author.output(), "verified" to isVerified, "lastChatActivity" to latestMsgTime, - "connected" to connected + "connected" to connected, + "unreadCount" to unreadCount ).apply { alias?.let { put("alias", it) } handshakePublicKey?.let { put("handshakePublicKey", it.encoded) } diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt index 7e16d71af..ed4aa8384 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt @@ -46,6 +46,7 @@ abstract class ControllerTest { protected val message: Message = getMessage(group.id) protected val text: String = getRandomString(5) protected val timestamp = 42L + protected val unreadCount = 42 protected fun assertJsonEquals(json: String, obj: Any) { assertEquals(json, outputCtx.json(obj).resultString(), STRICT) diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt index 59ee3c06a..a169e8eef 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt @@ -58,7 +58,8 @@ internal class ContactControllerTest : ControllerTest() { every { contactManager.contacts } returns listOf(contact) every { conversationManager.getGroupCount(contact.id).latestMsgTime } returns timestamp every { connectionRegistry.isConnected(contact.id) } returns connected - every { ctx.json(listOf(contact.output(timestamp, connected))) } returns ctx + every { conversationManager.getGroupCount(contact.id).unreadCount } returns unreadCount + every { ctx.json(listOf(contact.output(timestamp, connected, unreadCount))) } returns ctx controller.list(ctx) } @@ -313,10 +314,11 @@ internal class ContactControllerTest : ControllerTest() { "handshakePublicKey": ${toJson(contact.handshakePublicKey!!.encoded)}, "verified": ${contact.isVerified}, "lastChatActivity": $timestamp, - "connected": $connected + "connected": $connected, + "unreadCount": $unreadCount } """ - assertJsonEquals(json, contact.output(timestamp, connected)) + assertJsonEquals(json, contact.output(timestamp, connected, unreadCount)) } @Test From 63d3a78ddaac2a3942e6c590863c6c81b730470c Mon Sep 17 00:00:00 2001 From: Nico Alt Date: Fri, 25 Sep 2020 13:51:28 +0200 Subject: [PATCH 016/582] Expose message delivery state changes to websockets API We already indicate whether a message was sent/acked, but we don't inform about updates. Needed for briar-gtk#69. Fixes #1779 --- briar-headless/README.md | 36 +++++++++ .../messaging/MessagingControllerImpl.kt | 10 +++ .../messaging/OutputConversationMessage.kt | 15 ++++ .../messaging/MessagingControllerImplTest.kt | 75 ++++++++++++++++++- 4 files changed, 135 insertions(+), 1 deletion(-) diff --git a/briar-headless/README.md b/briar-headless/README.md index 4a00e97ab..bf1c930bb 100644 --- a/briar-headless/README.md +++ b/briar-headless/README.md @@ -409,3 +409,39 @@ When the last connection is lost (the contact goes offline), it sends a `Contact "type": "event" } ``` + +### A message was sent + +When Briar sent a message to a contact, it sends a `MessagesSentEvent`. This is indicated in Briar +by showing one tick next to the message. + +```json +{ + "data": { + "contactId": 1, + "messageIds": [ + "+AIMMgOCPFF8HDEhiEHYjbfKrg7v0G94inKxjvjYzA8=" + ] + }, + "name": "MessagesSentEvent", + "type": "event" +} +``` + +### A message was acknowledged + +When a contact acknowledges that they received a message, Briar sends a `MessagesAckedEvent`. +This is indicated in Briar by showing two ticks next to the message. + +```json +{ + "data": { + "contactId": 1, + "messageIds": [ + "+AIMMgOCPFF8HDEhiEHYjbfKrg7v0G94inKxjvjYzA8=" + ] + }, + "name": "MessagesAckedEvent", + "type": "event" +} +``` diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt index d3ee6457e..df5607170 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt @@ -11,6 +11,8 @@ import org.briarproject.bramble.api.db.DatabaseExecutor import org.briarproject.bramble.api.db.NoSuchContactException import org.briarproject.bramble.api.event.Event import org.briarproject.bramble.api.event.EventListener +import org.briarproject.bramble.api.sync.event.MessagesAckedEvent +import org.briarproject.bramble.api.sync.event.MessagesSentEvent import org.briarproject.bramble.api.system.Clock import org.briarproject.bramble.util.StringUtils.utf8IsTooLong import org.briarproject.briar.api.blog.BlogInvitationRequest @@ -39,6 +41,8 @@ import javax.inject.Inject import javax.inject.Singleton internal const val EVENT_CONVERSATION_MESSAGE = "ConversationMessageReceivedEvent" +internal const val EVENT_MESSAGES_ACKED = "MessagesAckedEvent" +internal const val EVENT_MESSAGES_SENT = "MessagesSentEvent" @Immutable @Singleton @@ -90,6 +94,12 @@ constructor( webSocketController.sendEvent(EVENT_CONVERSATION_MESSAGE, e.output()) } } + is MessagesSentEvent -> { + webSocketController.sendEvent(EVENT_MESSAGES_SENT, e.output()) + } + is MessagesAckedEvent -> { + webSocketController.sendEvent(EVENT_MESSAGES_ACKED, e.output()) + } } } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputConversationMessage.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputConversationMessage.kt index ddd9c807c..3c42a39d0 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputConversationMessage.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputConversationMessage.kt @@ -1,6 +1,9 @@ package org.briarproject.briar.headless.messaging import org.briarproject.bramble.api.contact.ContactId +import org.briarproject.bramble.api.sync.MessageId +import org.briarproject.bramble.api.sync.event.MessagesAckedEvent +import org.briarproject.bramble.api.sync.event.MessagesSentEvent import org.briarproject.briar.api.conversation.ConversationMessageHeader import org.briarproject.briar.api.messaging.PrivateMessage import org.briarproject.briar.api.messaging.PrivateMessageHeader @@ -43,3 +46,15 @@ internal fun PrivateMessage.output(contactId: ContactId, text: String) = JsonDic "groupId" to message.groupId.bytes, "text" to text ) + +internal fun MessagesAckedEvent.output() = JsonDict( + "contactId" to contactId.int, + "messageIds" to messageIds.toJson() +) + +internal fun MessagesSentEvent.output() = JsonDict( + "contactId" to contactId.int, + "messageIds" to messageIds.toJson() +) + +internal fun Collection.toJson() = map { it.bytes } diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt index 4968f9849..6feac73e2 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt @@ -10,11 +10,13 @@ import org.briarproject.bramble.api.db.NoSuchContactException import org.briarproject.bramble.api.identity.AuthorInfo import org.briarproject.bramble.api.identity.AuthorInfo.Status.UNVERIFIED import org.briarproject.bramble.api.identity.AuthorInfo.Status.VERIFIED +import org.briarproject.bramble.api.sync.MessageId +import org.briarproject.bramble.api.sync.event.MessagesAckedEvent +import org.briarproject.bramble.api.sync.event.MessagesSentEvent import org.briarproject.bramble.test.ImmediateExecutor import org.briarproject.bramble.test.TestUtils.getRandomId import org.briarproject.bramble.util.StringUtils.getRandomString import org.briarproject.briar.api.client.SessionId -import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.api.introduction.IntroductionRequest import org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH import org.briarproject.briar.api.messaging.MessagingManager @@ -100,6 +102,40 @@ internal class MessagingControllerImplTest : ControllerTest() { testInvalidContactId { controller.list(ctx) } } + @Test + fun testMessagesAckedEvent() { + val messageId1 = MessageId(getRandomId()) + val messageId2 = MessageId(getRandomId()) + val messageIds = listOf(messageId1, messageId2) + val event = MessagesAckedEvent(contact.id, messageIds) + + every { + webSocketController.sendEvent( + EVENT_MESSAGES_ACKED, + event.output() + ) + } just runs + + controller.eventOccurred(event) + } + + @Test + fun testMessagesSentEvent() { + val messageId1 = MessageId(getRandomId()) + val messageId2 = MessageId(getRandomId()) + val messageIds = listOf(messageId1, messageId2) + val event = MessagesSentEvent(contact.id, messageIds) + + every { + webSocketController.sendEvent( + EVENT_MESSAGES_SENT, + event.output() + ) + } just runs + + controller.eventOccurred(event) + } + @Test fun listNonexistentContactId() { testNonexistentContactId { controller.list(ctx) } @@ -177,6 +213,43 @@ internal class MessagingControllerImplTest : ControllerTest() { controller.eventOccurred(event) } + @Test + fun testOutputMessagesAckedEvent() { + val messageId1 = MessageId(getRandomId()) + val messageId2 = MessageId(getRandomId()) + val messageIds = listOf(messageId1, messageId2) + val event = MessagesAckedEvent(contact.id, messageIds) + val json = """ + { + "contactId": ${contact.id.int}, + "messageIds": [ + ${toJson(messageId1.bytes)}, + ${toJson(messageId2.bytes)} + ] + } + """ + assertJsonEquals(json, event.output()) + } + + @Test + fun testOutputMessagesSentEvent() { + val messageId1 = MessageId(getRandomId()) + val messageId2 = MessageId(getRandomId()) + val messageIds = listOf(messageId1, messageId2) + val event = MessagesSentEvent(contact.id, messageIds) + + val json = """ + { + "contactId": ${contact.id.int}, + "messageIds": [ + ${toJson(messageId1.bytes)}, + ${toJson(messageId2.bytes)} + ] + } + """ + assertJsonEquals(json, event.output()) + } + @Test fun testOutputPrivateMessageHeader() { val json = """ From e9cdec95e0b1cb5af7b2c6fdeece2792727f3588 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 29 Sep 2020 13:39:46 +0100 Subject: [PATCH 017/582] Check whether Bluetooth adapter exists before trying to get address. --- .../bramble/plugin/bluetooth/AndroidBluetoothPlugin.java | 1 + 1 file changed, 1 insertion(+) diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java index ea6c82f97..478d59425 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java @@ -135,6 +135,7 @@ class AndroidBluetoothPlugin @Override @Nullable String getBluetoothAddress() { + if (adapter == null) return null; String address = AndroidUtils.getBluetoothAddress(app, adapter); return address.isEmpty() ? null : address; } From f3157e52766799ca8f1b2f6bed53bf94a79bd6db Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 1 Sep 2020 17:02:09 +0100 Subject: [PATCH 018/582] Raise target SDK version to 29. --- bramble-android/build.gradle | 6 +++--- briar-android/build.gradle | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bramble-android/build.gradle b/bramble-android/build.gradle index 81d73b7fe..cdb9d1000 100644 --- a/bramble-android/build.gradle +++ b/bramble-android/build.gradle @@ -5,12 +5,12 @@ apply plugin: 'witness' apply from: 'witness.gradle' android { - compileSdkVersion 29 - buildToolsVersion '29.0.2' + compileSdkVersion 30 + buildToolsVersion '30.0.2' defaultConfig { minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode 10210 versionName "1.2.10" consumerProguardFiles 'proguard-rules.txt' diff --git a/briar-android/build.gradle b/briar-android/build.gradle index e68700a55..ad7c335a6 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -16,12 +16,12 @@ def getStdout = { command, defaultValue -> } android { - compileSdkVersion 29 - buildToolsVersion '29.0.2' + compileSdkVersion 30 + buildToolsVersion '30.0.2' defaultConfig { minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode 10210 versionName "1.2.10" applicationId "org.briarproject.briar.android" From b72e8fa490f34298a97dd9d95e72ae1c40c087d4 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Mon, 23 Sep 2019 16:12:18 +0100 Subject: [PATCH 019/582] Package Tor binaries as libraries so we're allowed to execute them. --- bramble-android/.gitignore | 1 + bramble-android/build.gradle | 35 ++++- .../bramble/plugin/tor/AndroidTorPlugin.java | 135 ++++++++++++++++++ .../bramble/plugin/tor/TorPlugin.java | 65 +++++---- 4 files changed, 204 insertions(+), 32 deletions(-) diff --git a/bramble-android/.gitignore b/bramble-android/.gitignore index 6653bfd64..7b257d328 100644 --- a/bramble-android/.gitignore +++ b/bramble-android/.gitignore @@ -3,3 +3,4 @@ gen build .settings src/main/res/raw/*.zip +src/main/jniLibs \ No newline at end of file diff --git a/bramble-android/build.gradle b/bramble-android/build.gradle index cdb9d1000..a298a0ec6 100644 --- a/bramble-android/build.gradle +++ b/bramble-android/build.gradle @@ -53,10 +53,12 @@ dependencies { } def torBinariesDir = 'src/main/res/raw' +def torLibsDir = 'src/main/jniLibs' task cleanTorBinaries { doLast { delete fileTree(torBinariesDir) { include '*.zip' } + delete fileTree(torLibsDir) { include '**/*.so' } } } @@ -67,8 +69,36 @@ task unpackTorBinaries { copy { from configurations.tor.collect { zipTree(it) } into torBinariesDir - // TODO: Remove after next Tor upgrade, which won't include non-PIE binaries - include 'geoip.zip', '*_pie.zip' + include 'geoip.zip' + } + configurations.tor.each { outer -> + zipTree(outer).each { inner -> + if (inner.name.endsWith('_arm_pie.zip')) { + copy { + from zipTree(inner) + into torLibsDir + rename '(.*)', 'armeabi-v7a/lib$1.so' + } + } else if (inner.name.endsWith('_arm64_pie.zip')) { + copy { + from zipTree(inner) + into torLibsDir + rename '(.*)', 'arm64-v8a/lib$1.so' + } + } else if (inner.name.endsWith('_x86_pie.zip')) { + copy { + from zipTree(inner) + into torLibsDir + rename '(.*)', 'x86/lib$1.so' + } + } else if (inner.name.endsWith('_x86_64_pie.zip')) { + copy { + from zipTree(inner) + into torLibsDir + rename '(.*)', 'x86_64/lib$1.so' + } + } + } } } dependsOn cleanTorBinaries @@ -76,5 +106,6 @@ task unpackTorBinaries { tasks.withType(MergeResources) { inputs.dir torBinariesDir + inputs.dir torLibsDir dependsOn unpackTorBinaries } diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java index 7428fac0c..2efdc5986 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java @@ -16,19 +16,39 @@ import org.briarproject.bramble.api.system.AndroidWakeLockManager; import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.ResourceProvider; +import org.briarproject.bramble.util.AndroidUtils; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Executor; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import javax.net.SocketFactory; +import static android.os.Build.VERSION.SDK_INT; +import static java.util.Arrays.asList; +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; + @MethodsNotNullByDefault @ParametersNotNullByDefault class AndroidTorPlugin extends TorPlugin { + private static final List LIBRARY_ARCHITECTURES = + asList("armeabi-v7a", "arm64-v8a", "x86", "x86_64"); + + private static final Logger LOG = + getLogger(AndroidTorPlugin.class.getName()); + private final Application app; private final AndroidWakeLock wakeLock; + private final File torLib, obfs4Lib; AndroidTorPlugin(Executor ioExecutor, Executor wakefulIoExecutor, @@ -55,6 +75,9 @@ class AndroidTorPlugin extends TorPlugin { maxIdleTime, torDirectory); this.app = app; wakeLock = wakeLockManager.createWakeLock("TorPlugin"); + String nativeLibDir = app.getApplicationInfo().nativeLibraryDir; + torLib = new File(nativeLibDir, "libtor.so"); + obfs4Lib = new File(nativeLibDir, "libobfs4proxy.so"); } @Override @@ -85,4 +108,116 @@ class AndroidTorPlugin extends TorPlugin { super.stop(); wakeLock.release(); } + + @Override + protected File getTorExecutableFile() { + return torLib.exists() ? torLib : super.getTorExecutableFile(); + } + + @Override + protected File getObfs4ExecutableFile() { + return obfs4Lib.exists() ? obfs4Lib : super.getObfs4ExecutableFile(); + } + + @Override + protected void installTorExecutable() throws IOException { + File extracted = super.getTorExecutableFile(); + if (torLib.exists()) { + // If an older version left behind a Tor binary, delete it + if (extracted.exists()) { + if (extracted.delete()) LOG.info("Deleted Tor binary"); + else LOG.info("Failed to delete Tor binary"); + } + } else if (SDK_INT < 29) { + // The binary wasn't extracted at install time. Try to extract it + if (!extracted.exists()) { + extractLibraryFromApk("libtor.so", extracted); + } + } else { + // No point extracting the binary, we won't be allowed to execute it + throw new FileNotFoundException(torLib.getAbsolutePath()); + } + } + + @Override + protected void installObfs4Executable() throws IOException { + File extracted = super.getObfs4ExecutableFile(); + if (obfs4Lib.exists()) { + // If an older version left behind an obfs4 binary, delete it + if (extracted.exists()) { + if (extracted.delete()) LOG.info("Deleted obfs4 binary"); + else LOG.info("Failed to delete obfs4 binary"); + } + } else if (SDK_INT < 29) { + // The binary wasn't extracted at install time. Try to extract it + if (!extracted.exists()) { + extractLibraryFromApk("libobfs4proxy.so", extracted); + } + } else { + // No point extracting the binary, we won't be allowed to execute it + throw new FileNotFoundException(obfs4Lib.getAbsolutePath()); + } + } + + private void extractLibraryFromApk(String libName, File dest) + throws IOException { + File sourceDir = new File(app.getApplicationInfo().sourceDir); + if (sourceDir.isFile()) { + // Look for other APK files in the same directory, if we're allowed + File parent = sourceDir.getParentFile(); + if (parent != null) sourceDir = parent; + } + List libPaths = getSupportedLibraryPaths(libName); + for (File apk : findApkFiles(sourceDir)) { + ZipInputStream zin = new ZipInputStream(new FileInputStream(apk)); + for (ZipEntry e = zin.getNextEntry(); e != null; + e = zin.getNextEntry()) { + if (libPaths.contains(e.getName())) { + if (LOG.isLoggable(INFO)) { + LOG.info("Extracting " + e.getName() + + " from " + apk.getAbsolutePath()); + } + extract(zin, dest); // Zip input stream will be closed + return; + } + } + zin.close(); + } + throw new FileNotFoundException(libName); + } + + /** + * Returns all files with the extension .apk or .APK under the given root. + */ + private List findApkFiles(File root) { + List files = new ArrayList<>(); + findApkFiles(root, files); + return files; + } + + private void findApkFiles(File f, List files) { + if (f.isFile() && f.getName().toLowerCase().endsWith(".apk")) { + files.add(f); + } else if (f.isDirectory()) { + File[] children = f.listFiles(); + if (children != null) { + for (File child : children) findApkFiles(child, files); + } + } + } + + /** + * Returns the paths at which libraries with the given name would be found + * inside an APK file, for all architectures supported by the device, in + * order of preference. + */ + private List getSupportedLibraryPaths(String libName) { + List architectures = new ArrayList<>(); + for (String abi : AndroidUtils.getSupportedArchitectures()) { + if (LIBRARY_ARCHITECTURES.contains(abi)) { + architectures.add("lib/" + abi + "/" + libName); + } + } + return architectures; + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index 24816197b..5fc93f885 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -132,7 +132,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private final CircumventionProvider circumventionProvider; private final ResourceProvider resourceProvider; private final int maxLatency, maxIdleTime, socketTimeout; - private final File torDirectory, torFile, geoIpFile, obfs4File, configFile; + private final File torDirectory, geoIpFile, configFile; private final File doneFile, cookieFile; private final AtomicBoolean used = new AtomicBoolean(false); @@ -181,9 +181,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { socketTimeout = Integer.MAX_VALUE; else socketTimeout = maxIdleTime * 2; this.torDirectory = torDirectory; - torFile = new File(torDirectory, "tor"); geoIpFile = new File(torDirectory, "geoip"); - obfs4File = new File(torDirectory, "obfs4proxy"); configFile = new File(torDirectory, "torrc"); doneFile = new File(torDirectory, "done"); cookieFile = new File(torDirectory, ".tor/control_auth_cookie"); @@ -192,6 +190,14 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { new PoliteExecutor("TorPlugin", ioExecutor, 1); } + protected File getTorExecutableFile() { + return new File(torDirectory, "tor"); + } + + protected File getObfs4ExecutableFile() { + return new File(torDirectory, "obfs4proxy"); + } + @Override public TransportId getId() { return TorConstants.ID; @@ -224,6 +230,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { LOG.warning("Old auth cookie not deleted"); // Start a new Tor process LOG.info("Starting Tor"); + File torFile = getTorExecutableFile(); String torPath = torFile.getAbsolutePath(); String configPath = configFile.getAbsolutePath(); String pid = String.valueOf(getProcessId()); @@ -322,44 +329,43 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } private void installAssets() throws PluginException { - InputStream in = null; - OutputStream out = null; try { // The done file may already exist from a previous installation //noinspection ResultOfMethodCallIgnored doneFile.delete(); - // Unzip the Tor binary to the filesystem - in = getTorInputStream(); - out = new FileOutputStream(torFile); - copyAndClose(in, out); - // Make the Tor binary executable - if (!torFile.setExecutable(true, true)) throw new IOException(); - // Unzip the GeoIP database to the filesystem - in = getGeoIpInputStream(); - out = new FileOutputStream(geoIpFile); - copyAndClose(in, out); - // Unzip the Obfs4 proxy to the filesystem - in = getObfs4InputStream(); - out = new FileOutputStream(obfs4File); - copyAndClose(in, out); - // Make the Obfs4 proxy executable - if (!obfs4File.setExecutable(true, true)) throw new IOException(); - // Copy the config file to the filesystem - in = getConfigInputStream(); - out = new FileOutputStream(configFile); - copyAndClose(in, out); + installTorExecutable(); + installObfs4Executable(); + extract(getGeoIpInputStream(), geoIpFile); + extract(getConfigInputStream(), configFile); if (!doneFile.createNewFile()) LOG.warning("Failed to create done file"); } catch (IOException e) { - tryToClose(in, LOG, WARNING); - tryToClose(out, LOG, WARNING); throw new PluginException(e); } } - private InputStream getTorInputStream() throws IOException { + protected void extract(InputStream in, File dest) throws IOException { + OutputStream out = new FileOutputStream(dest); + copyAndClose(in, out); + } + + protected void installTorExecutable() throws IOException { if (LOG.isLoggable(INFO)) LOG.info("Installing Tor binary for " + architecture); + File torFile = getTorExecutableFile(); + extract(getTorInputStream(), torFile); + if (!torFile.setExecutable(true, true)) throw new IOException(); + } + + protected void installObfs4Executable() throws IOException { + if (LOG.isLoggable(INFO)) + LOG.info("Installing obfs4proxy binary for " + architecture); + File obfs4File = getObfs4ExecutableFile(); + extract(getObfs4InputStream(), obfs4File); + if (!obfs4File.setExecutable(true, true)) throw new IOException(); + } + + private InputStream getTorInputStream() throws IOException { InputStream in = resourceProvider .getResourceInputStream("tor_" + architecture, ".zip"); ZipInputStream zin = new ZipInputStream(in); @@ -376,8 +382,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } private InputStream getObfs4InputStream() throws IOException { - if (LOG.isLoggable(INFO)) - LOG.info("Installing obfs4proxy binary for " + architecture); InputStream in = resourceProvider .getResourceInputStream("obfs4proxy_" + architecture, ".zip"); ZipInputStream zin = new ZipInputStream(in); @@ -569,6 +573,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { if (enable) { Collection conf = new ArrayList<>(); conf.add("UseBridges 1"); + File obfs4File = getObfs4ExecutableFile(); if (needsMeek) { conf.add("ClientTransportPlugin meek_lite exec " + obfs4File.getAbsolutePath()); From 0e1fb406b51a73d99daed5112c2b1dadc6c17c56 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Fri, 4 Sep 2020 13:28:21 +0100 Subject: [PATCH 020/582] Extract library filenames into constants. --- .../bramble/plugin/tor/AndroidTorPlugin.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java index 2efdc5986..9be55eb79 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java @@ -43,6 +43,9 @@ class AndroidTorPlugin extends TorPlugin { private static final List LIBRARY_ARCHITECTURES = asList("armeabi-v7a", "arm64-v8a", "x86", "x86_64"); + private static final String TOR_LIB_NAME = "libtor.so"; + private static final String OBFS4_LIB_NAME = "libobfs4proxy.so"; + private static final Logger LOG = getLogger(AndroidTorPlugin.class.getName()); @@ -76,8 +79,8 @@ class AndroidTorPlugin extends TorPlugin { this.app = app; wakeLock = wakeLockManager.createWakeLock("TorPlugin"); String nativeLibDir = app.getApplicationInfo().nativeLibraryDir; - torLib = new File(nativeLibDir, "libtor.so"); - obfs4Lib = new File(nativeLibDir, "libobfs4proxy.so"); + torLib = new File(nativeLibDir, TOR_LIB_NAME); + obfs4Lib = new File(nativeLibDir, OBFS4_LIB_NAME); } @Override @@ -131,7 +134,7 @@ class AndroidTorPlugin extends TorPlugin { } else if (SDK_INT < 29) { // The binary wasn't extracted at install time. Try to extract it if (!extracted.exists()) { - extractLibraryFromApk("libtor.so", extracted); + extractLibraryFromApk(TOR_LIB_NAME, extracted); } } else { // No point extracting the binary, we won't be allowed to execute it @@ -151,7 +154,7 @@ class AndroidTorPlugin extends TorPlugin { } else if (SDK_INT < 29) { // The binary wasn't extracted at install time. Try to extract it if (!extracted.exists()) { - extractLibraryFromApk("libobfs4proxy.so", extracted); + extractLibraryFromApk(OBFS4_LIB_NAME, extracted); } } else { // No point extracting the binary, we won't be allowed to execute it From 37a2d9f9900d88ac2b179813423924ac25b5cdf0 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Fri, 4 Sep 2020 13:39:19 +0100 Subject: [PATCH 021/582] Extract binaries even if older versions already exist. --- .../briarproject/bramble/plugin/tor/AndroidTorPlugin.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java index 9be55eb79..a4b2a1d8f 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java @@ -133,9 +133,7 @@ class AndroidTorPlugin extends TorPlugin { } } else if (SDK_INT < 29) { // The binary wasn't extracted at install time. Try to extract it - if (!extracted.exists()) { - extractLibraryFromApk(TOR_LIB_NAME, extracted); - } + extractLibraryFromApk(TOR_LIB_NAME, extracted); } else { // No point extracting the binary, we won't be allowed to execute it throw new FileNotFoundException(torLib.getAbsolutePath()); @@ -153,9 +151,7 @@ class AndroidTorPlugin extends TorPlugin { } } else if (SDK_INT < 29) { // The binary wasn't extracted at install time. Try to extract it - if (!extracted.exists()) { - extractLibraryFromApk(OBFS4_LIB_NAME, extracted); - } + extractLibraryFromApk(OBFS4_LIB_NAME, extracted); } else { // No point extracting the binary, we won't be allowed to execute it throw new FileNotFoundException(obfs4Lib.getAbsolutePath()); From de9c6d4447fb7f8b3507091de6829954469b73f5 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Fri, 4 Sep 2020 13:54:29 +0100 Subject: [PATCH 022/582] Extract version constants into top-level build file. --- bramble-android/build.gradle | 12 ++++++------ briar-android/build.gradle | 12 ++++++------ build.gradle | 9 +++++++++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/bramble-android/build.gradle b/bramble-android/build.gradle index a298a0ec6..d2cce2d99 100644 --- a/bramble-android/build.gradle +++ b/bramble-android/build.gradle @@ -5,14 +5,14 @@ apply plugin: 'witness' apply from: 'witness.gradle' android { - compileSdkVersion 30 - buildToolsVersion '30.0.2' + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { - minSdkVersion 16 - targetSdkVersion 29 - versionCode 10210 - versionName "1.2.10" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode rootProject.ext.versionCode + versionName rootProject.ext.versionName consumerProguardFiles 'proguard-rules.txt' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/briar-android/build.gradle b/briar-android/build.gradle index ad7c335a6..8e07ab8bb 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -16,14 +16,14 @@ def getStdout = { command, defaultValue -> } android { - compileSdkVersion 30 - buildToolsVersion '30.0.2' + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { - minSdkVersion 16 - targetSdkVersion 29 - versionCode 10210 - versionName "1.2.10" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode rootProject.ext.versionCode + versionName rootProject.ext.versionName applicationId "org.briarproject.briar.android" buildConfigField "String", "GitHash", "\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\"" diff --git a/build.gradle b/build.gradle index 157f13f45..9f48c87fb 100644 --- a/build.gradle +++ b/build.gradle @@ -33,3 +33,12 @@ buildscript { classpath files('libs/gradle-witness.jar') } } + +project.ext { + buildToolsVersion = '30.0.2' + compileSdkVersion = 30 + minSdkVersion = 16 + targetSdkVersion = 29 + versionCode = 10210 + versionName = '1.2.10' +} \ No newline at end of file From 9738dd2838e554e1b6678ce0294580b58e332275 Mon Sep 17 00:00:00 2001 From: Nico Alt Date: Sat, 3 Oct 2020 19:29:39 +0200 Subject: [PATCH 023/582] Add method to mark message as read to REST API When exposing unread messages counters in https://code.briarproject.org/briar/briar/-/merge_requests/1283, I noticed that they were never set to 0. Fixes #1780 --- briar-headless/README.md | 13 +++++++++ .../org/briarproject/briar/headless/Router.kt | 3 ++ .../headless/messaging/MessagingController.kt | 2 ++ .../messaging/MessagingControllerImpl.kt | 23 +++++++++++++++ .../messaging/MessagingControllerImplTest.kt | 28 +++++++++++++++++++ 5 files changed, 69 insertions(+) diff --git a/briar-headless/README.md b/briar-headless/README.md index fbeb7c804..69b5c6137 100644 --- a/briar-headless/README.md +++ b/briar-headless/README.md @@ -234,6 +234,19 @@ The text of the message should be posted as JSON: } ``` +### Marking private messages as read + +`POST /v1/messages/{contactId}/read` + +The `messageId` of the message to be marked as read +needs to be provided in the request body as follows: + +```json +{ + "messageId": "+AIMMgOCPFF8HDEhiEHYjbfKrg7v0G94inKxjvjYzA8=" +} +``` + ### Listing blog posts `GET /v1/blogs/posts` diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt index 940eac8fe..4d226a559 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt @@ -86,6 +86,9 @@ constructor( get { ctx -> messagingController.list(ctx) } post { ctx -> messagingController.write(ctx) } } + path("/messages/:contactId/read") { + post { ctx -> messagingController.markMessageRead(ctx) } + } path("/forums") { get { ctx -> forumController.list(ctx) } post { ctx -> forumController.create(ctx) } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingController.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingController.kt index 55d7024f4..6fe4f23fa 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingController.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingController.kt @@ -8,4 +8,6 @@ interface MessagingController { fun write(ctx: Context): Context + fun markMessageRead(ctx: Context): Context + } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt index df5607170..533e519e6 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt @@ -13,6 +13,7 @@ import org.briarproject.bramble.api.event.Event import org.briarproject.bramble.api.event.EventListener import org.briarproject.bramble.api.sync.event.MessagesAckedEvent import org.briarproject.bramble.api.sync.event.MessagesSentEvent +import org.briarproject.bramble.api.sync.MessageId import org.briarproject.bramble.api.system.Clock import org.briarproject.bramble.util.StringUtils.utf8IsTooLong import org.briarproject.briar.api.blog.BlogInvitationRequest @@ -35,6 +36,8 @@ import org.briarproject.briar.headless.event.output import org.briarproject.briar.headless.getContactIdFromPathParam import org.briarproject.briar.headless.getFromJson import org.briarproject.briar.headless.json.JsonDict +import org.spongycastle.util.encoders.Base64 +import org.spongycastle.util.encoders.DecoderException import java.util.concurrent.Executor import javax.annotation.concurrent.Immutable import javax.inject.Inject @@ -83,6 +86,26 @@ constructor( return ctx.json(m.output(contact.id, text)) } + override fun markMessageRead(ctx: Context): Context { + val contact = getContact(ctx) + val groupId = messagingManager.getContactGroup(contact).id + + val messageIdString = ctx.getFromJson(objectMapper, "messageId") + val messageId = deserializeMessageId(messageIdString) + messagingManager.setReadFlag(groupId, messageId, true) + return ctx.json(messageIdString) + } + + private fun deserializeMessageId(idString: String): MessageId { + val idBytes = try { + Base64.decode(idString) + } catch (e: DecoderException) { + throw NotFoundResponse() + } + if (idBytes.size != MessageId.LENGTH) throw NotFoundResponse() + return MessageId(idBytes) + } + override fun eventOccurred(e: Event) { when (e) { is ConversationMessageReceivedEvent<*> -> { diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt index 6feac73e2..d827e50e6 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt @@ -26,10 +26,12 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent import org.briarproject.briar.headless.ControllerTest import org.briarproject.briar.headless.event.output +import org.briarproject.briar.headless.getFromJson import org.briarproject.briar.headless.json.JsonDict import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test +import org.spongycastle.util.encoders.Base64 internal class MessagingControllerImplTest : ControllerTest() { @@ -198,6 +200,32 @@ internal class MessagingControllerImplTest : ControllerTest() { assertThrows(BadRequestResponse::class.java) { controller.write(ctx) } } + @Test + fun markMessageRead() { + mockkStatic("org.briarproject.briar.headless.RouterKt") + mockkStatic("org.spongycastle.util.encoders.Base64") + expectGetContact() + + val messageIdString = message.id.bytes.toString() + every { messagingManager.getContactGroup(contact).id } returns group.id + every { ctx.getFromJson(objectMapper, "messageId") } returns messageIdString + every { Base64.decode(messageIdString) } returns message.id.bytes + every { messagingManager.setReadFlag(group.id, message.id, true) } just Runs + every { ctx.json(messageIdString) } returns ctx + + controller.markMessageRead(ctx) + } + + @Test + fun markMessageReadInvalidContactId() { + testInvalidContactId { controller.markMessageRead(ctx) } + } + + @Test + fun markMessageReadNonexistentId() { + testNonexistentContactId { controller.markMessageRead(ctx) } + } + @Test fun privateMessageEvent() { val event = PrivateMessageReceivedEvent(header, contact.id) From 1ac17cf8593d4049dbe7d6b84a82d37ab7510445 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 5 Oct 2020 09:54:35 -0300 Subject: [PATCH 024/582] [headless] Change coding style to not do star imports --- .idea/codeStyles/Project.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 011a1a146..7b9dcd72e 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -28,6 +28,20 @@ Érintse meg a zárolás feloldásához + A Briar Interneten, Wi-Fi-n vagy Bluetooth-on keresztül csatlakozhat kapcsolataihoz.\n\nAz összes internetkapcsolat a Tor hálózaton megy keresztül megy az adatvédelem érdekében.\n\nHa egy kapcsolatot több módszerrel is el lehet érni, Briar párhuzamosan használja azokat. Alice diff --git a/briar-android/src/main/res/values-is/strings.xml b/briar-android/src/main/res/values-is/strings.xml index d4bb6f5fc..c27fc3a58 100644 --- a/briar-android/src/main/res/values-is/strings.xml +++ b/briar-android/src/main/res/values-is/strings.xml @@ -456,9 +456,9 @@ Forritslæsing Notaðu skjálæsingu tækisins til að vernda Briar á meðan þú ert skráð/ur inn Til að nota þetta þarftu að setja upp skjálæsingu fyrir tækið þitt - Tímamörk forritslæsingar við aðgerðaleysi + Tímamörk læsingar forrits við aðgerðaleysi - Þehar ekki er verið að nota Briar, læsa því sjálfvirkt eftir %s + Þegar ekki er verið að nota Briar, læsa því sjálfvirkt eftir %s 1 mínúta diff --git a/briar-android/src/main/res/values-sv/strings.xml b/briar-android/src/main/res/values-sv/strings.xml index d4d288c25..2f36d2fcf 100644 --- a/briar-android/src/main/res/values-sv/strings.xml +++ b/briar-android/src/main/res/values-sv/strings.xml @@ -61,12 +61,36 @@ Lås appen Inställningar Logga ut + Klicka här för att styra hur Briar ansluter till dina kontakter. Internet + Din telefon har internetåtkomst via Wi-Fi + Din telefon har internetåtkomst via mobildata + Din telefon har inte internetåtkomst + Briar ansluter till internet + Briar är anslutet till internet + Briar kan inte ansluta till internet + Briar är inte konfigurerat för att använda internet + Briar är konfigurerat så att de inte använder mobildata + Briar är konfigurerat att inte använda internet vid batterianvändning + Briar är konfigurerat att inte använda internet i detta landet Wi-Fi + Samma Wi-Fi-nätverk + Din telefon är ansluten till Wi-Fi + Din telefon är inte ansluten till Wi-Fi + Briar ansluter till Wi-Fi-nätverket + Briar är anslutet till Wi-Fi nätverket + Briar kan inte ansluta till Wi-Fi-nätverket + Briar är konfigurerat för att inte använda Wi-Fi-nätverket Bluetooth + Din telefons Bluetooth är aktiverad + Din telefons Bluetooth är inaktiverad + Briar ansluter till Bluetooth + Briar är ansluten till Bluetooth + Briar kan inte ansluta till Bluetooth + Briar är konfigurerat för att inte använda Bluetooth Utloggad från Briar Tryck för att logga in igen. @@ -412,10 +436,20 @@ Automatisk (dagtid) Systemets förval + Anslutningar + Anslut till kontakter via Bluetooth + Anslut till kontakter på samma Wi-Fi-nätverk + Anslut till kontakter via internet + Alla anslutningar gå via Tor-nätverket av integritetsskäl + Anslutningsmetod för Tor-nätverket Automatisk, baserad på position + Använd Tor-nätverket utan bryggor + Anvädn Tor-nätverket med bryggor + Anslut inte till internet Automatisk: %1$s (i %2$s) Använd mobildata + Anslut till internet endast vid laddning Avaktiverar anslutning över Internet när enheten går på batteri Säkerhet @@ -526,6 +560,7 @@ Briar är låst Tryck för att låsa upp + Briar kan ansluta till dina kontakter via internet, Wi-Fi eller Bluetooth.\n\nAlla internetanslutningar går via Tor-nätverket av integritetsskäl.\n\nOm en kontakt kan nås via flera metoder kommer Briar att använda dem parallellt. Alice diff --git a/briar-android/src/main/res/values-tr/strings.xml b/briar-android/src/main/res/values-tr/strings.xml index 3af809580..6637ced45 100644 --- a/briar-android/src/main/res/values-tr/strings.xml +++ b/briar-android/src/main/res/values-tr/strings.xml @@ -61,12 +61,36 @@ Uygulamayı Kilitle Ayarlar Oturumu Kapat + Briar\'ın kişilerinizle nasıl bağlanacağını kontrol etmek için buraya tıklayın İnternet + Telefonunuzun Wi-Fi aracılığıyla İnternet erişimi var + Telefonunuzun mobil veri aracılığıyla İnternet erişimi var + Telefonunuzun İnternet erişimi yok + Briar İnternet\'e bağlanıyor + Briar İnternet\'e bağlandı + Briar İnternet\'e bağlanamıyor + Briar İnternet kullanmamak üzere yapılandırılmış + Briar mobil veri kullanmamak üzere yapılandırılmış + Briar pille çalışırken İnternet kullanmamak üzere yapılandırılmış + Briar bu ülkede İnternet kullanmamak üzere yapılandırılmış Wi-Fi + Aynı Wi-Fi Ağı + Telefonunuz Wi-Fi\'ye bağlı + Telefonunuz Wi-Fi\'ye bağlı değil + Briar Wi-Fi ağına bağlanıyor + Briar Wi-Fi ağına bağlandı + Briar Wi-Fi ağına bağlanamıyor + Briar Wi-Fi ağını kullanmamak üzere yapılandırılmış Bluetooth + Telefonunuzun Bluetooth\'u açık + Telefonunuzun Bluetooth\'u kapalı + Briar Bluetooth\'a bağlanıyor + Briar Bluetooth\'a bağlandı + Briar Bluetooth\'a bağlanamıyor + Briar Bluetooth kullanmamak üzere yapılandırılmış Briar oturumu kapatıldı Tekrar oturum açmak için dokunun @@ -536,6 +560,7 @@ Briar kilitli Kilidi açmak için dokunun + Briar kişilerinizle İnternet, Wi-Fi veya Bluetooth ile bağlanabilir.\n\nBütün İnternet bağlantıları gizliliğiniz için Tor Ağı üzerinden yapılıyor.\n\nEğer bir kişiniz birçok yöntemle erişilebiliyorsa, Briar bu yöntemleri paralel olarak kullanır. Alice From 8cbb38ee68b2a717848deae35770b092aa7ae8cc Mon Sep 17 00:00:00 2001 From: akwizgran Date: Wed, 14 Oct 2020 13:15:29 +0100 Subject: [PATCH 030/582] Bump version numbers for 1.2.11 release. --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 9f48c87fb..cbaf172bd 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,6 @@ project.ext { compileSdkVersion = 30 minSdkVersion = 16 targetSdkVersion = 29 - versionCode = 10210 - versionName = '1.2.10' -} \ No newline at end of file + versionCode = 10211 + versionName = '1.2.11' +} From 922a52bf830971e002e0293042e15f9eeae438d3 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 27 Oct 2020 14:22:30 +0000 Subject: [PATCH 031/582] Only Alice should perform Bluetooth discovery. --- .../bramble/api/plugin/duplex/DuplexPlugin.java | 4 +++- .../keyagreement/KeyAgreementConnector.java | 12 +++++++----- .../bramble/plugin/bluetooth/BluetoothPlugin.java | 15 +++++++++++---- .../bramble/plugin/tcp/LanTcpPlugin.java | 2 +- .../bramble/plugin/tcp/TcpPlugin.java | 2 +- .../bramble/plugin/tor/TorPlugin.java | 2 +- .../bramble/plugin/tcp/LanTcpPluginTest.java | 2 +- .../bramble/plugin/modem/ModemPlugin.java | 2 +- 8 files changed, 26 insertions(+), 15 deletions(-) diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPlugin.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPlugin.java index 9e14e7ab5..f37a8b5fe 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPlugin.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPlugin.java @@ -41,10 +41,12 @@ public interface DuplexPlugin extends Plugin { /** * Attempts to connect to the remote peer specified in the given descriptor. * Returns null if no connection can be established. + * + * @param alice True if the local party is Alice */ @Nullable DuplexTransportConnection createKeyAgreementConnection( - byte[] remoteCommitment, BdfList descriptor); + byte[] remoteCommitment, BdfList descriptor, boolean alice); /** * Returns true if the plugin supports rendezvous connections. diff --git a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java index e71137f7b..79bb700aa 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java @@ -118,8 +118,8 @@ class KeyAgreementConnector { DuplexPlugin plugin = (DuplexPlugin) p; byte[] commitment = remotePayload.getCommitment(); BdfList descriptor = d.getDescriptor(); - connectionChooser.submit(new ReadableTask( - new ConnectorTask(plugin, commitment, descriptor))); + connectionChooser.submit(new ReadableTask(new ConnectorTask( + plugin, commitment, descriptor, alice))); } } @@ -148,15 +148,17 @@ class KeyAgreementConnector { private class ConnectorTask implements Callable { + private final DuplexPlugin plugin; private final byte[] commitment; private final BdfList descriptor; - private final DuplexPlugin plugin; + private final boolean alice; private ConnectorTask(DuplexPlugin plugin, byte[] commitment, - BdfList descriptor) { + BdfList descriptor, boolean alice) { this.plugin = plugin; this.commitment = commitment; this.descriptor = descriptor; + this.alice = alice; } @Nullable @@ -166,7 +168,7 @@ class KeyAgreementConnector { while (!stopped) { DuplexTransportConnection conn = plugin.createKeyAgreementConnection(commitment, - descriptor); + descriptor, alice); if (conn != null) { if (LOG.isLoggable(INFO)) LOG.info(plugin.getId() + ": Outgoing connection"); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java index e8cf9b07f..7c1ee4b59 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java @@ -430,15 +430,22 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { @Override public DuplexTransportConnection createKeyAgreementConnection( - byte[] commitment, BdfList descriptor) { + byte[] commitment, BdfList descriptor, boolean alice) { if (getState() != ACTIVE) return null; // No truncation necessary because COMMIT_LENGTH = 16 String uuid = UUID.nameUUIDFromBytes(commitment).toString(); DuplexTransportConnection conn; if (descriptor.size() == 1) { - if (LOG.isLoggable(INFO)) - LOG.info("Discovering address for key agreement UUID " + uuid); - conn = discoverAndConnect(uuid); + if (alice) { + if (LOG.isLoggable(INFO)) { + LOG.info("Discovering address for key agreement UUID " + + uuid); + } + conn = discoverAndConnect(uuid); + } else { + LOG.info("No address in key agreement descriptor"); + return null; + } } else { String address; try { diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java index 87706b7e5..235ac5cb9 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java @@ -376,7 +376,7 @@ class LanTcpPlugin extends TcpPlugin { @Override public DuplexTransportConnection createKeyAgreementConnection( - byte[] commitment, BdfList descriptor) { + byte[] commitment, BdfList descriptor, boolean alice) { ServerSocket ss = state.getServerSocket(true); if (ss == null) return null; InterfaceAddress local = getLocalInterfaceAddress(ss.getInetAddress()); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java index f67121685..53ada12c1 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java @@ -367,7 +367,7 @@ abstract class TcpPlugin implements DuplexPlugin, EventListener { @Override public DuplexTransportConnection createKeyAgreementConnection( - byte[] commitment, BdfList descriptor) { + byte[] commitment, BdfList descriptor, boolean alice) { throw new UnsupportedOperationException(); } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index 5fc93f885..a2ba04ed2 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -708,7 +708,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { @Override public DuplexTransportConnection createKeyAgreementConnection( - byte[] commitment, BdfList descriptor) { + byte[] commitment, BdfList descriptor, boolean alice) { throw new UnsupportedOperationException(); } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java b/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java index cd560e3a6..59f3b1923 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java @@ -276,7 +276,7 @@ public class LanTcpPluginTest extends BrambleTestCase { descriptor.add(local.getPort()); // Connect to the port DuplexTransportConnection d = plugin.createKeyAgreementConnection( - new byte[COMMIT_LENGTH], descriptor); + new byte[COMMIT_LENGTH], descriptor, true); assertNotNull(d); // Check that the connection was accepted assertTrue(latch.await(5, SECONDS)); diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java index 53a668f20..4d2ee0e1e 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java @@ -198,7 +198,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { @Override public DuplexTransportConnection createKeyAgreementConnection( - byte[] commitment, BdfList descriptor) { + byte[] commitment, BdfList descriptor, boolean alice) { throw new UnsupportedOperationException(); } From ce1a57c2b4be522c84b75c996300311be6a27337 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 27 Oct 2020 12:21:57 +0000 Subject: [PATCH 032/582] Prefer Bluetooth for adding contacts. --- .../keyagreement/KeyAgreementConnector.java | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java index 79bb700aa..444b667fd 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java @@ -8,6 +8,8 @@ import org.briarproject.bramble.api.keyagreement.KeyAgreementListener; import org.briarproject.bramble.api.keyagreement.Payload; import org.briarproject.bramble.api.keyagreement.TransportDescriptor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.BluetoothConstants; +import org.briarproject.bramble.api.plugin.LanTcpConstants; import org.briarproject.bramble.api.plugin.Plugin; import org.briarproject.bramble.api.plugin.PluginManager; import org.briarproject.bramble.api.plugin.TransportId; @@ -19,7 +21,9 @@ import org.briarproject.bramble.api.record.RecordWriterFactory; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -28,8 +32,10 @@ import java.util.logging.Logger; import javax.annotation.Nullable; +import static java.util.Arrays.asList; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.CONNECTION_TIMEOUT; import static org.briarproject.bramble.util.LogUtils.logException; @@ -41,7 +47,10 @@ class KeyAgreementConnector { } private static final Logger LOG = - Logger.getLogger(KeyAgreementConnector.class.getName()); + getLogger(KeyAgreementConnector.class.getName()); + + private static final List PREFERRED_TRANSPORTS = + asList(BluetoothConstants.ID, LanTcpConstants.ID); private final Callbacks callbacks; private final KeyAgreementCrypto keyAgreementCrypto; @@ -105,24 +114,33 @@ class KeyAgreementConnector { this.alice = alice; aliceLatch.countDown(); - // Start connecting over supported transports + // Start connecting over best available transport if (LOG.isLoggable(INFO)) { LOG.info("Starting outgoing BQP connections as " + (alice ? "Alice" : "Bob")); } + Map descriptors = new HashMap<>(); for (TransportDescriptor d : remotePayload.getTransportDescriptors()) { - Plugin p = pluginManager.getPlugin(d.getId()); - if (p instanceof DuplexPlugin) { + descriptors.put(d.getId(), d); + } + for (TransportId id : PREFERRED_TRANSPORTS) { + TransportDescriptor d = descriptors.get(id); + Plugin p = pluginManager.getPlugin(id); + if (d != null && p instanceof DuplexPlugin) { if (LOG.isLoggable(INFO)) - LOG.info("Connecting via " + d.getId()); + LOG.info("Connecting via " + id); DuplexPlugin plugin = (DuplexPlugin) p; byte[] commitment = remotePayload.getCommitment(); BdfList descriptor = d.getDescriptor(); connectionChooser.submit(new ReadableTask(new ConnectorTask( plugin, commitment, descriptor, alice))); + break; } } + // TODO: If we don't have any transports in common with the peer, + // warn the user and give up (#1224) + // Get chosen connection try { KeyAgreementConnection chosen = From 948212103cc4a88d4d588a52e11e8e7acda73189 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 27 Oct 2020 12:31:32 +0000 Subject: [PATCH 033/582] Require Bluetooth permissions if device supports Bluetooth. --- .../keyagreement/KeyAgreementActivity.java | 111 +++++++++++------- briar-android/src/main/res/values/strings.xml | 1 + 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java index 568ed219b..ae6c74f93 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java @@ -26,7 +26,6 @@ import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.briar.android.keyagreement.IntroFragment.IntroScreenSeenListener; import org.briarproject.briar.android.keyagreement.KeyAgreementFragment.KeyAgreementEventListener; -import org.briarproject.briar.android.util.UiUtils; import java.util.logging.Logger; @@ -55,6 +54,7 @@ import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE; import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH_DISCOVERABLE; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION; +import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -133,6 +133,8 @@ public abstract class KeyAgreementActivity extends BriarActivity implements private Permission locationPermission = Permission.UNKNOWN; private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN; private BroadcastReceiver bluetoothReceiver = null; + private Plugin wifiPlugin = null, bluetoothPlugin = null; + private BluetoothAdapter bt = null; @Override public void injectActivity(ActivityComponent component) { @@ -152,6 +154,9 @@ public abstract class KeyAgreementActivity extends BriarActivity implements IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED); bluetoothReceiver = new BluetoothStateReceiver(); registerReceiver(bluetoothReceiver, filter); + wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID); + bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); + bt = BluetoothAdapter.getDefaultAdapter(); } @Override @@ -187,6 +192,7 @@ public abstract class KeyAgreementActivity extends BriarActivity implements showQrCodeFragmentIfAllowed(); } + @SuppressWarnings("StatementWithEmptyBody") private void showQrCodeFragmentIfAllowed() { if (isResumed && continueClicked && areEssentialPermissionsGranted()) { if (isWifiReady() && isBluetoothReady()) { @@ -200,6 +206,8 @@ public abstract class KeyAgreementActivity extends BriarActivity implements } if (bluetoothDecision == BluetoothDecision.UNKNOWN) { requestBluetoothDiscoverable(); + } else if (bluetoothDecision == BluetoothDecision.REFUSED) { + // Ask again when the user clicks "continue" } else if (shouldEnableBluetooth()) { LOG.info("Enabling Bluetooth plugin"); hasEnabledBluetooth = true; @@ -210,55 +218,50 @@ public abstract class KeyAgreementActivity extends BriarActivity implements } private boolean areEssentialPermissionsGranted() { - // If the camera permission has been granted, and the location - // permission has been granted or permanently denied, we can continue return cameraPermission == Permission.GRANTED && (locationPermission == Permission.GRANTED || - locationPermission == Permission.PERMANENTLY_DENIED); + !isBluetoothSupported()); + } + + private boolean isBluetoothSupported() { + return bt != null && bluetoothPlugin != null; } private boolean isWifiReady() { - Plugin p = pluginManager.getPlugin(LanTcpConstants.ID); - if (p == null) return true; // Continue without wifi - State state = p.getState(); + if (wifiPlugin == null) return true; // Continue without wifi + State state = wifiPlugin.getState(); // Wait for plugin to become enabled return state == ACTIVE || state == INACTIVE; } private boolean isBluetoothReady() { - if (bluetoothDecision == BluetoothDecision.UNKNOWN || - bluetoothDecision == BluetoothDecision.WAITING) { - // Wait for decision - return false; - } - if (bluetoothDecision == BluetoothDecision.NO_ADAPTER - || bluetoothDecision == BluetoothDecision.REFUSED) { + if (!isBluetoothSupported()) { // Continue without Bluetooth return true; } - BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); - if (bt == null) return true; // Continue without Bluetooth + if (bluetoothDecision == BluetoothDecision.UNKNOWN || + bluetoothDecision == BluetoothDecision.WAITING || + bluetoothDecision == BluetoothDecision.REFUSED) { + // Wait for user to accept + return false; + } if (bt.getScanMode() != SCAN_MODE_CONNECTABLE_DISCOVERABLE) { // Wait for adapter to become discoverable return false; } - Plugin p = pluginManager.getPlugin(BluetoothConstants.ID); - if (p == null) return true; // Continue without Bluetooth // Wait for plugin to become active - return p.getState() == ACTIVE; + return bluetoothPlugin.getState() == ACTIVE; } private boolean shouldEnableWifi() { if (hasEnabledWifi) return false; - Plugin p = pluginManager.getPlugin(LanTcpConstants.ID); - if (p == null) return false; - State state = p.getState(); + if (wifiPlugin == null) return false; + State state = wifiPlugin.getState(); return state == STARTING_STOPPING || state == DISABLED; } private void requestBluetoothDiscoverable() { - BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); - if (bt == null) { + if (!isBluetoothSupported()) { bluetoothDecision = BluetoothDecision.NO_ADAPTER; showQrCodeFragmentIfAllowed(); } else { @@ -277,9 +280,8 @@ public abstract class KeyAgreementActivity extends BriarActivity implements private boolean shouldEnableBluetooth() { if (bluetoothDecision != BluetoothDecision.ACCEPTED) return false; if (hasEnabledBluetooth) return false; - Plugin p = pluginManager.getPlugin(BluetoothConstants.ID); - if (p == null) return false; - State state = p.getState(); + if (!isBluetoothSupported()) return false; + State state = bluetoothPlugin.getState(); return state == STARTING_STOPPING || state == DISABLED; } @@ -298,6 +300,9 @@ public abstract class KeyAgreementActivity extends BriarActivity implements @Override public void showNextScreen() { continueClicked = true; + if (bluetoothDecision == BluetoothDecision.REFUSED) { + bluetoothDecision = BluetoothDecision.UNKNOWN; // Ask again + } if (checkPermissions()) showQrCodeFragmentIfAllowed(); } @@ -341,17 +346,17 @@ public abstract class KeyAgreementActivity extends BriarActivity implements private boolean checkPermissions() { if (areEssentialPermissionsGranted()) return true; - // If the camera permission has been permanently denied, ask the + // If an essential permission has been permanently denied, ask the // user to change the setting if (cameraPermission == Permission.PERMANENTLY_DENIED) { - Builder builder = new Builder(this, R.style.BriarDialogTheme); - builder.setTitle(R.string.permission_camera_title); - builder.setMessage(R.string.permission_camera_denied_body); - builder.setPositiveButton(R.string.ok, - UiUtils.getGoToSettingsListener(this)); - builder.setNegativeButton(R.string.cancel, - (dialog, which) -> supportFinishAfterTransition()); - builder.show(); + showDenialDialog(R.string.permission_camera_title, + R.string.permission_camera_denied_body); + return false; + } + if (isBluetoothSupported() && + locationPermission == Permission.PERMANENTLY_DENIED) { + showDenialDialog(R.string.permission_location_title, + R.string.permission_location_denied_body); return false; } // Should we show the rationale for one or both permissions? @@ -371,6 +376,16 @@ public abstract class KeyAgreementActivity extends BriarActivity implements return false; } + private void showDenialDialog(@StringRes int title, @StringRes int body) { + Builder builder = new Builder(this, R.style.BriarDialogTheme); + builder.setTitle(title); + builder.setMessage(body); + builder.setPositiveButton(R.string.ok, getGoToSettingsListener(this)); + builder.setNegativeButton(R.string.cancel, + (dialog, which) -> supportFinishAfterTransition()); + builder.show(); + } + private void showRationale(@StringRes int title, @StringRes int body) { Builder builder = new Builder(this, R.style.BriarDialogTheme); builder.setTitle(title); @@ -381,8 +396,13 @@ public abstract class KeyAgreementActivity extends BriarActivity implements } private void requestPermissions() { - ActivityCompat.requestPermissions(this, - new String[] {CAMERA, ACCESS_FINE_LOCATION}, + String[] permissions; + if (isBluetoothSupported()) { + permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION}; + } else { + permissions = new String[] {CAMERA}; + } + ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSION_CAMERA_LOCATION); } @@ -399,12 +419,15 @@ public abstract class KeyAgreementActivity extends BriarActivity implements } else { cameraPermission = Permission.PERMANENTLY_DENIED; } - if (gotPermission(ACCESS_FINE_LOCATION, permissions, grantResults)) { - locationPermission = Permission.GRANTED; - } else if (shouldShowRationale(ACCESS_FINE_LOCATION)) { - locationPermission = Permission.SHOW_RATIONALE; - } else { - locationPermission = Permission.PERMANENTLY_DENIED; + if (isBluetoothSupported()) { + if (gotPermission(ACCESS_FINE_LOCATION, permissions, + grantResults)) { + locationPermission = Permission.GRANTED; + } else if (shouldShowRationale(ACCESS_FINE_LOCATION)) { + locationPermission = Permission.SHOW_RATIONALE; + } else { + locationPermission = Permission.PERMANENTLY_DENIED; + } } // If a permission dialog has been shown, showing the QR code fragment // on this call path would cause a crash due to diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 8951beb2a..b603ca30f 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -589,6 +589,7 @@ Camera and location To scan the QR code, Briar needs access to the camera.\n\nTo discover Bluetooth devices, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone. You have denied access to the camera, but adding contacts requires using the camera.\n\nPlease consider granting access. + You have denied access to your location, but Briar needs this permission to discover Bluetooth devices.\n\nPlease consider granting access. QR code Show QR code fullscreen From 0caa522f07bf67b67d854c21ac70e4ada4ca330f Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 27 Oct 2020 17:37:22 +0000 Subject: [PATCH 034/582] Remove error message, return to intro fragment when retrying. --- .../keyagreement/ContactExchangeActivity.java | 7 ++----- .../ContactExchangeErrorFragment.java | 19 +++++++++++++------ briar-android/src/main/res/values/strings.xml | 1 - 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java index f18c41f75..222fba77b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java @@ -9,7 +9,6 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; -import org.briarproject.briar.android.fragment.BaseFragment; import javax.annotation.Nullable; import javax.inject.Inject; @@ -114,9 +113,7 @@ public class ContactExchangeActivity extends KeyAgreementActivity { return getString(R.string.exchanging_contact_details); } - protected void showErrorFragment() { - String errorMsg = getString(R.string.connection_error_explanation); - BaseFragment f = ContactExchangeErrorFragment.newInstance(errorMsg); - showNextFragment(f); + private void showErrorFragment() { + showNextFragment(new ContactExchangeErrorFragment()); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeErrorFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeErrorFragment.java index 733b50a6a..2ed8ce6e4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeErrorFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeErrorFragment.java @@ -1,5 +1,6 @@ package org.briarproject.briar.android.keyagreement; +import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -18,7 +19,10 @@ import org.briarproject.briar.android.util.UiUtils; import javax.inject.Inject; import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; +import static android.view.View.GONE; import static org.briarproject.briar.android.util.UiUtils.onSingleLinkClick; @MethodsNotNullByDefault @@ -58,13 +62,12 @@ public class ContactExchangeErrorFragment extends BaseFragment { View v = inflater.inflate(R.layout.fragment_error_contact_exchange, container, false); - // set humanized error message + // set optional error message TextView explanation = v.findViewById(R.id.errorMessage); Bundle args = getArguments(); - if (args == null) { - throw new IllegalArgumentException("Use newInstance()"); - } - explanation.setText(args.getString(ERROR_MSG)); + String errorMessage = args == null ? null : args.getString(ERROR_MSG); + if (errorMessage == null) explanation.setVisibility(GONE); + else explanation.setText(args.getString(ERROR_MSG)); // make feedback link clickable TextView sendFeedback = v.findViewById(R.id.sendFeedback); @@ -73,7 +76,11 @@ public class ContactExchangeErrorFragment extends BaseFragment { // buttons Button tryAgain = v.findViewById(R.id.tryAgainButton); tryAgain.setOnClickListener(view -> { - if (getActivity() != null) getActivity().onBackPressed(); + // Recreate the activity so we return to the intro fragment + FragmentActivity activity = requireActivity(); + Intent i = new Intent(activity, ContactExchangeActivity.class); + i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); + activity.startActivity(i); }); Button cancel = v.findViewById(R.id.cancelButton); cancel.setOnClickListener(view -> finish()); diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index b603ca30f..66cd51313 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -210,7 +210,6 @@ Connecting to device\u2026 Authenticating with device\u2026 Could not connect to your contact - Please check that you\'re both connected to the same Wi-Fi network. If this problem persists, please send feedback to help us improve the app. From e0f381a97318e4083721311f1889975c7c8e76ef Mon Sep 17 00:00:00 2001 From: akwizgran Date: Thu, 29 Oct 2020 11:48:10 +0000 Subject: [PATCH 035/582] Try all transports in order of preference. --- .../api/plugin/duplex/DuplexPlugin.java | 4 +- .../keyagreement/KeyAgreementConnector.java | 48 ++++++++++--------- .../plugin/bluetooth/BluetoothPlugin.java | 15 ++---- .../bramble/plugin/tcp/LanTcpPlugin.java | 2 +- .../bramble/plugin/tcp/TcpPlugin.java | 2 +- .../bramble/plugin/tor/TorPlugin.java | 2 +- .../bramble/plugin/tcp/LanTcpPluginTest.java | 2 +- .../bramble/plugin/modem/ModemPlugin.java | 2 +- 8 files changed, 37 insertions(+), 40 deletions(-) diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPlugin.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPlugin.java index f37a8b5fe..9e14e7ab5 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPlugin.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPlugin.java @@ -41,12 +41,10 @@ public interface DuplexPlugin extends Plugin { /** * Attempts to connect to the remote peer specified in the given descriptor. * Returns null if no connection can be established. - * - * @param alice True if the local party is Alice */ @Nullable DuplexTransportConnection createKeyAgreementConnection( - byte[] remoteCommitment, BdfList descriptor, boolean alice); + byte[] remoteCommitment, BdfList descriptor); /** * Returns true if the plugin supports rendezvous connections. diff --git a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java index 444b667fd..23af3589e 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java @@ -1,5 +1,6 @@ package org.briarproject.bramble.keyagreement; +import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.crypto.KeyAgreementCrypto; import org.briarproject.bramble.api.crypto.KeyPair; import org.briarproject.bramble.api.data.BdfList; @@ -114,7 +115,7 @@ class KeyAgreementConnector { this.alice = alice; aliceLatch.countDown(); - // Start connecting over best available transport + // Start connecting over supported transports in order of preference if (LOG.isLoggable(INFO)) { LOG.info("Starting outgoing BQP connections as " + (alice ? "Alice" : "Bob")); @@ -123,24 +124,26 @@ class KeyAgreementConnector { for (TransportDescriptor d : remotePayload.getTransportDescriptors()) { descriptors.put(d.getId(), d); } + List> transports = new ArrayList<>(); for (TransportId id : PREFERRED_TRANSPORTS) { TransportDescriptor d = descriptors.get(id); Plugin p = pluginManager.getPlugin(id); if (d != null && p instanceof DuplexPlugin) { if (LOG.isLoggable(INFO)) LOG.info("Connecting via " + id); - DuplexPlugin plugin = (DuplexPlugin) p; - byte[] commitment = remotePayload.getCommitment(); - BdfList descriptor = d.getDescriptor(); - connectionChooser.submit(new ReadableTask(new ConnectorTask( - plugin, commitment, descriptor, alice))); - break; + transports.add(new Pair<>((DuplexPlugin) p, d.getDescriptor())); } } // TODO: If we don't have any transports in common with the peer, // warn the user and give up (#1224) + if (!transports.isEmpty()) { + byte[] commitment = remotePayload.getCommitment(); + connectionChooser.submit(new ReadableTask(new ConnectorTask( + transports, commitment))); + } + // Get chosen connection try { KeyAgreementConnection chosen = @@ -166,17 +169,13 @@ class KeyAgreementConnector { private class ConnectorTask implements Callable { - private final DuplexPlugin plugin; + private final List> transports; private final byte[] commitment; - private final BdfList descriptor; - private final boolean alice; - private ConnectorTask(DuplexPlugin plugin, byte[] commitment, - BdfList descriptor, boolean alice) { - this.plugin = plugin; + private ConnectorTask(List> transports, + byte[] commitment) { + this.transports = transports; this.commitment = commitment; - this.descriptor = descriptor; - this.alice = alice; } @Nullable @@ -184,13 +183,18 @@ class KeyAgreementConnector { public KeyAgreementConnection call() throws Exception { // Repeat attempts until we connect, get stopped, or get interrupted while (!stopped) { - DuplexTransportConnection conn = - plugin.createKeyAgreementConnection(commitment, - descriptor, alice); - if (conn != null) { - if (LOG.isLoggable(INFO)) - LOG.info(plugin.getId() + ": Outgoing connection"); - return new KeyAgreementConnection(conn, plugin.getId()); + for (Pair pair : transports) { + if (stopped) return null; + DuplexPlugin plugin = pair.getFirst(); + BdfList descriptor = pair.getSecond(); + DuplexTransportConnection conn = + plugin.createKeyAgreementConnection(commitment, + descriptor); + if (conn != null) { + if (LOG.isLoggable(INFO)) + LOG.info(plugin.getId() + ": Outgoing connection"); + return new KeyAgreementConnection(conn, plugin.getId()); + } } // Wait 2s before retry (to circumvent transient failures) Thread.sleep(2000); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java index 7c1ee4b59..f90469d56 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java @@ -430,22 +430,17 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { @Override public DuplexTransportConnection createKeyAgreementConnection( - byte[] commitment, BdfList descriptor, boolean alice) { + byte[] commitment, BdfList descriptor) { if (getState() != ACTIVE) return null; // No truncation necessary because COMMIT_LENGTH = 16 String uuid = UUID.nameUUIDFromBytes(commitment).toString(); DuplexTransportConnection conn; if (descriptor.size() == 1) { - if (alice) { - if (LOG.isLoggable(INFO)) { - LOG.info("Discovering address for key agreement UUID " + - uuid); - } - conn = discoverAndConnect(uuid); - } else { - LOG.info("No address in key agreement descriptor"); - return null; + if (LOG.isLoggable(INFO)) { + LOG.info("Discovering address for key agreement UUID " + + uuid); } + conn = discoverAndConnect(uuid); } else { String address; try { diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java index 235ac5cb9..87706b7e5 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java @@ -376,7 +376,7 @@ class LanTcpPlugin extends TcpPlugin { @Override public DuplexTransportConnection createKeyAgreementConnection( - byte[] commitment, BdfList descriptor, boolean alice) { + byte[] commitment, BdfList descriptor) { ServerSocket ss = state.getServerSocket(true); if (ss == null) return null; InterfaceAddress local = getLocalInterfaceAddress(ss.getInetAddress()); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java index 53ada12c1..f67121685 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java @@ -367,7 +367,7 @@ abstract class TcpPlugin implements DuplexPlugin, EventListener { @Override public DuplexTransportConnection createKeyAgreementConnection( - byte[] commitment, BdfList descriptor, boolean alice) { + byte[] commitment, BdfList descriptor) { throw new UnsupportedOperationException(); } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index a2ba04ed2..5fc93f885 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -708,7 +708,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { @Override public DuplexTransportConnection createKeyAgreementConnection( - byte[] commitment, BdfList descriptor, boolean alice) { + byte[] commitment, BdfList descriptor) { throw new UnsupportedOperationException(); } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java b/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java index 59f3b1923..cd560e3a6 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java @@ -276,7 +276,7 @@ public class LanTcpPluginTest extends BrambleTestCase { descriptor.add(local.getPort()); // Connect to the port DuplexTransportConnection d = plugin.createKeyAgreementConnection( - new byte[COMMIT_LENGTH], descriptor, true); + new byte[COMMIT_LENGTH], descriptor); assertNotNull(d); // Check that the connection was accepted assertTrue(latch.await(5, SECONDS)); diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java index 4d2ee0e1e..53a668f20 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java @@ -198,7 +198,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { @Override public DuplexTransportConnection createKeyAgreementConnection( - byte[] commitment, BdfList descriptor, boolean alice) { + byte[] commitment, BdfList descriptor) { throw new UnsupportedOperationException(); } From 12a8907c8bebf261837b381347f340a535b74cdd Mon Sep 17 00:00:00 2001 From: akwizgran Date: Thu, 29 Oct 2020 14:34:10 +0000 Subject: [PATCH 036/582] Ignore missing location permission on API < 23 where it's not needed. --- .../briar/android/keyagreement/KeyAgreementActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java index ae6c74f93..a2795b2a0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java @@ -45,6 +45,7 @@ import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED; import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.Build.VERSION.SDK_INT; import static java.util.logging.Level.INFO; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; @@ -219,7 +220,7 @@ public abstract class KeyAgreementActivity extends BriarActivity implements private boolean areEssentialPermissionsGranted() { return cameraPermission == Permission.GRANTED && - (locationPermission == Permission.GRANTED || + (SDK_INT < 23 || locationPermission == Permission.GRANTED || !isBluetoothSupported()); } From 84657127b817aba95b5af191010aafc8f2de71fb Mon Sep 17 00:00:00 2001 From: akwizgran Date: Mon, 2 Nov 2020 14:50:06 +0000 Subject: [PATCH 037/582] Update translations. --- .../src/main/res/values-ar/strings.xml | 1 - .../src/main/res/values-az/strings.xml | 1 - .../src/main/res/values-bs/strings.xml | 1 - .../src/main/res/values-ca/strings.xml | 62 ++++++-- .../src/main/res/values-de/strings.xml | 2 +- .../src/main/res/values-es/strings.xml | 2 +- .../src/main/res/values-eu/strings.xml | 1 - .../src/main/res/values-fa/strings.xml | 12 +- .../src/main/res/values-fr/strings.xml | 2 +- .../src/main/res/values-gl/strings.xml | 2 +- .../src/main/res/values-he/strings.xml | 40 ++++- .../src/main/res/values-hi/strings.xml | 1 - .../src/main/res/values-hu/strings.xml | 1 - .../src/main/res/values-is/strings.xml | 1 - .../src/main/res/values-it/strings.xml | 1 - .../src/main/res/values-ja/strings.xml | 1 - .../src/main/res/values-ko/strings.xml | 140 ++++++++++++------ .../src/main/res/values-lt/strings.xml | 6 +- .../src/main/res/values-mk/strings.xml | 40 ++++- .../src/main/res/values-nl/strings.xml | 1 - .../src/main/res/values-oc/strings.xml | 1 - .../src/main/res/values-pl/strings.xml | 1 - .../src/main/res/values-pt-rBR/strings.xml | 1 - .../src/main/res/values-ro/strings.xml | 2 +- .../src/main/res/values-ru/strings.xml | 1 - .../src/main/res/values-sq/strings.xml | 2 +- .../src/main/res/values-sv/strings.xml | 1 - .../src/main/res/values-sw/strings.xml | 1 - .../src/main/res/values-tr/strings.xml | 1 - .../src/main/res/values-uk/strings.xml | 1 - .../src/main/res/values-zh-rCN/strings.xml | 2 +- .../src/main/res/values-zh-rTW/strings.xml | 1 - 32 files changed, 240 insertions(+), 93 deletions(-) diff --git a/briar-android/src/main/res/values-ar/strings.xml b/briar-android/src/main/res/values-ar/strings.xml index b73ce72fe..fe03be97c 100644 --- a/briar-android/src/main/res/values-ar/strings.xml +++ b/briar-android/src/main/res/values-ar/strings.xml @@ -215,7 +215,6 @@ يتم الإتصال بالجهاز\u2026 يتم التوثيق مع الجهاز\u2026 لم يمكن الإتصال بجهة إتصالك - رجاءًا التأكد أن كليكما متصل بنفس شبكة الواي فاي. إذا إستمرت المشكلة، رجاءًا أرسل تقرير لمساعدتنا على تحسين التطبيق. إضافة جهة اتصال عن بعد diff --git a/briar-android/src/main/res/values-az/strings.xml b/briar-android/src/main/res/values-az/strings.xml index 2f83e160a..b022f113b 100644 --- a/briar-android/src/main/res/values-az/strings.xml +++ b/briar-android/src/main/res/values-az/strings.xml @@ -162,7 +162,6 @@ Cihaza qoşulma /2026 Cihazla təsdiqlənir \u2026 Kontaktınıza qoşula bilmədi - Həmin Wi-Fi şəbəkəsinə qoşulduğunuzu yoxlayın. Bu problem davam edərsə, tətbiqin təkmilləşdirilməsinə kömək etmək üçün rəy göndərin. Məsafədə kontakt əlavə etmək diff --git a/briar-android/src/main/res/values-bs/strings.xml b/briar-android/src/main/res/values-bs/strings.xml index cb31535fd..df0729fe3 100644 --- a/briar-android/src/main/res/values-bs/strings.xml +++ b/briar-android/src/main/res/values-bs/strings.xml @@ -134,7 +134,6 @@ Povezujem se sa uređajem\u2026 Autentikacija sa uređajem\u2026 Nije moguće povezivanje sa vašim kontaktom - Provjerite jeste li oboje povezani na ist Wi-Fi mrežu. Ako se problem ponavlja, Molimo vas pošaljite povratne informacije kako bi pomogli poboljšanju aplikacije. Dodaj udaljeni konktakt diff --git a/briar-android/src/main/res/values-ca/strings.xml b/briar-android/src/main/res/values-ca/strings.xml index 8bcc77c48..dfa0ea9e3 100644 --- a/briar-android/src/main/res/values-ca/strings.xml +++ b/briar-android/src/main/res/values-ca/strings.xml @@ -27,7 +27,7 @@ Contrasenya La contrasenya és incorrecta, torneu a escriure-la No es pot verificar la contrasenya - El Briar no pot verificar la contrasenya. Proveu de reiniciar el vostre aparell per a solucionar aquest problema. + Briar no pot verificar la contrasenya. Proveu de reiniciar el vostre aparell per a solucionar aquest problema. Inicia la sessió No recordo la contrasenya Contrasenya perduda @@ -44,9 +44,9 @@ Aquesta és una versió de prova de Briar. El vostre compte caducarà en %d dies i no es podrà renovar. Aquesta versió de Briar ha caducat.\nGràcies per haver-lo provat! - Per continuar utilitzant el Briar, baixeu la darrera versió. + Per continuar utilitzant Briar, baixeu la darrera versió. Haureu de crear un compte nou, però podeu utilitzar el mateix sobrenom. - Baixa l\'última versió + Descarrega la darrera versió S\'està desxifrant la base de dades... S\'està actualitzant la base de dades... S\'està compactant la base de dades... @@ -61,12 +61,36 @@ Bloqueja l\'aplicació Configuració Tanca la sessió + Toqueu per decidir com es connecta Briar als vostres contactes. Internet + El mòbil té accés a Internet via WiFi + El mòbil té accés a Internet via dades + El mòbil no té accés a Internet + Briar s\'està connectant a Internet + Briar està connectat a Internet + Briar no pot connectar-se a Internet + Briar està configurat per a no emprar Internet + Briar està configurat per a no emprar dades mòbils + Briar està configurat per a no usar Internet mentre funciona amb bateria + Briar està configurat per a no emprar Internet en aquest país Wi-Fi + La mateixa xarxa WiFi + El mòbil està connectat a WiFi + El mòbil no està connectat a WiFi + Briar s\'està connectant a la xarxa WiFi + Briar està connectat a la xarxa WiFi + Briar no pot connectar-se a la xarxa WiFi + Briar està configurat per a no emprar la xarxa WiFi Bluetooth + El Bluetooth del mòbil està apagat + El Bluetooth del mòbil està engegat + Briar s\'està connectant a Bluetooth + Briar està connectat a Bluetooth + Briar no pot connectar-se a Bluetooth + Briar està configurat per a no emprar Bluetooth Heu sortit de Briar Toqueu per a reiniciar la sessió. @@ -112,7 +136,7 @@ Corregeix Ajuda Ens sap greu - No disponible al vostre sistema + No està disponible en el vostre sistema Estat No hi ha cap contacte per mostrar @@ -128,16 +152,16 @@ Canvia el nom del contacte Nom del contacte Canvia - Suprimeix tots els missatges - Confirmeu la supressió dels missatges - Esteu segur que voleu suprimir tots els missatges? - No s\'ha pogut suprimir tots els missatges - Els missatges relacionats amb introduccions i invitacions pendents no es poden suprimir fins que es finalitzin. - Els missatges relacionats amb introduccions pendents no es poden suprimir fins que es finalitzin. - Els missatges relacionats amb invitacions pendents no es poden suprimir fins que es finalitzin. + Esborra tots els missatges + Confirmeu l\'esborrat dels missatges + Segur que voleu esborrar tots els missatges? + No s\'han pogut esborrar tots els missatges + Els missatges relacionats amb presentacions i invitacions pendents no es poden suprimir fins que finalitzen. + Els missatges relacionats amb presentacions pendents no es poden suprimir fins que finalitzen. + Els missatges relacionats amb invitacions pendents no es poden suprimir fins que finalitzen. Els missatges baixats parcialment no es poden suprimir fins que s\'acabin de baixar. - Per a suprimir una invitació o una introducció, cal que seleccioneu la sol·licitdu i la resposta. - Per a suprimir una introducció, cal que seleccioneu la sol·licitud i la resposta. + Per a suprimir una invitació o una presentació, cal que seleccioneu la sol·licitud i la resposta. + Per a suprimir una presentació, cal que seleccioneu la sol·licitud i la resposta. Per a suprimir una invitació, cal que seleccioneu la sol·licitud i la resposta. Suprimeix aquest contacte Confirmeu la supressió del contacte @@ -172,7 +196,6 @@ Així que l\'actualitzi li veureu una icona diferent . Connectant-se al dispositiu\u2026 Autenticant-se amb el dispositiu\u2026 No ha pogut connectar-se al vostre contacte - Comproveu que esteu connectats a la mateixa xarxa Wi-Fi. Si aquest problema persisteix, envieu-nos un comentari per ajudar-nos a millorar l\'aplicació. Afegeix un contacte llunyà @@ -413,10 +436,20 @@ Així que l\'actualitzi li veureu una icona diferent . Automàtic (segons l\'hora) Valor per defecte del sistema + Connexions + Connectat als contactes via Bluetooth + Connectat als contactes en la mateixa xarxa WiFi + Connectat als contactes via Internet + Totes les connexions van via la xarxa Tor per augmentar privacitat + Mètode de connexió per a la xarxa Tor Automàtic, basat en la posició + Usa la xarxa Tor sense ponts + Usa la xarxa Tor amb ponts + No connectis a Internet Automàtic: %1$s (a %2$s) Usa dades mòbils + Només connecta\'t a Internet mentre s\'està carregant Desactiva la connexió a Internet quan el dispositiu estigui funcionant amb la bateria Seguretat @@ -527,6 +560,7 @@ Així que l\'actualitzi li veureu una icona diferent . Briar està bloquejat Toqueu per desbloquejar-lo + Briar pot contactar els vostres contactes via Internet, WiFi o Bluetooth.\n\nTotes les connexions d\'Internet van via la xarxa Tor per privacitat.\n\nSi es pot arribar a un contacte per diversos mètodes, Briar els usa tots simultàniament. Alba diff --git a/briar-android/src/main/res/values-de/strings.xml b/briar-android/src/main/res/values-de/strings.xml index 9ebf87840..15654b7ca 100644 --- a/briar-android/src/main/res/values-de/strings.xml +++ b/briar-android/src/main/res/values-de/strings.xml @@ -195,7 +195,6 @@ Verbinde mit Gerät\u2026 Authentifiziere Gerät\u2026 Keine Verbindung zum Kontakt - Überprüfe, ob ihr beide mit demselben WLAN-Netzwerk verbunden seid. Wenn das Problem weiterbesteht, hilf uns die App zu verbessern und schicke Feedback. Kontakt aus der Ferne hinzufügen @@ -550,6 +549,7 @@ Kamera und Standort Um den QR-Code zu scannen, braucht Briar Zugriff auf die Kamera.\n\nUm Bluetooth-Geräte zu finden, braucht Briar Zugriff auf deinen Standort.\n\nBriar speichert weder deinen Standort noch gibt es ihn an andere weiter. Du hast den Zugriff auf die Kamera verweigert, aber das Hinzufügen von Kontakten erfordert die Verwendung der Kamera.\n\nBitte gewähre den Zugriff. + Du hast den Zugriff auf deinen Standort verweigert, aber Briar benötigt diese Berechtigung, um Bluetooth-Geräte zu erkennen.\n\nBitte gewähre den Zugriff. QR-Code QR-Code im Vollbildmodus anzeigen diff --git a/briar-android/src/main/res/values-es/strings.xml b/briar-android/src/main/res/values-es/strings.xml index a1bfb899a..b6f0d9a66 100644 --- a/briar-android/src/main/res/values-es/strings.xml +++ b/briar-android/src/main/res/values-es/strings.xml @@ -195,7 +195,6 @@ Conectando al dispositivo\u2026 Autentificándose con el dispositivo\u2026 No se pudo conectar a tu contacto - Por favor comprobar que ambos estén conectados a la misma red Wi-Fi. Si este problema persiste, por favor envía tus comentarios para ayudarnos a mejorar la aplicación. Añadir un Contacto a Distancia @@ -550,6 +549,7 @@ Cámara y ubicación Para escanear el código QR, Briar necesita acceso a la cámara.\n\nPara descubrir dispositivos Bluetooth, Briar necesita permiso para acceder tu ubicación.\n\nBriar no la almacena o la comparte con nadie. Has denegado el acceso a la cámara, pero para añadir contactos se requiere el uso de la cámara.\n\nPor favor considera la posibilidad de conceder el acceso. + Has denegado el acceso a tu ubicación, pero Briar necesita este permiso para descubrir dispositivos Bluetooth.\n\nPor favor considera la posibilidad de conceder el acceso. Código QR Mostrar código QR a pantalla completa diff --git a/briar-android/src/main/res/values-eu/strings.xml b/briar-android/src/main/res/values-eu/strings.xml index 44b39c07b..9631d3b26 100644 --- a/briar-android/src/main/res/values-eu/strings.xml +++ b/briar-android/src/main/res/values-eu/strings.xml @@ -171,7 +171,6 @@ Gailura konektatzen\u2026 Gailuarekin autentifikatzen\u2026 Ezin izan da zure kontaktuarekin konektatu - Egiaztatu biak Wi-Fi sare berera konektatuta zaudetela. Arazoa mantentzen bada, mesedez bidali iruzkin bat aplikazioa hobetzen laguntzeko. Gehitu urruneko kontaktua diff --git a/briar-android/src/main/res/values-fa/strings.xml b/briar-android/src/main/res/values-fa/strings.xml index 8c7db8dbd..3a0fc1e6a 100644 --- a/briar-android/src/main/res/values-fa/strings.xml +++ b/briar-android/src/main/res/values-fa/strings.xml @@ -69,10 +69,21 @@ خروج اینترنت + Briar در حال اتصال به اینترنت می باشد + Briar به اینترنت متصل شد + Briar نمی تواند به اینترنت متصل شود وای فای + همان شبکه وای-فای + موبایل شما به وای-فای وصل می باشد + موبایل شما به وای-فای وصل نیست + Briar در حال اتصال به شبکه وای-فای می باشد بلوتوث + بلوتوث موبایل شما روشن می باشد + بلوتوث موبایل شما خاموش می باشد + Briar نمی تواند به بلوتوث وصل شود + Briar طوری پیکربندی شده که از بلوتوث استفاده نکند از Briar (برایر) خارج شد برای وارد شدن دوباره ضربه بزنید. @@ -185,7 +196,6 @@ اتصال به دستگاهu2026\ تصدیق سازی با دستگاه u2026\ اتصال به مخاطب شما برقرار نشد - لطفا مطمئن شوید که هر دو شما به شبکه وای فای یکسان متصل هستید. اگر این مشکل ادامه پیدا کرد، لطفا بازخورد ارسال کنید تا به ما در بهبود برنامه کمک کنید. افزودن مخاطب از دور diff --git a/briar-android/src/main/res/values-fr/strings.xml b/briar-android/src/main/res/values-fr/strings.xml index 25456a2a7..89f8a0e36 100644 --- a/briar-android/src/main/res/values-fr/strings.xml +++ b/briar-android/src/main/res/values-fr/strings.xml @@ -195,7 +195,6 @@ Connexion à l’appareil\u2026 Autentification avec l’appareil\u2026 Impossible de se connecter à votre contact - Veuillez vérifier que vous êtes connecté au même réseau Wi-Fi. Si le problème persiste, veuillez nous envoyer une rétroaction pour nous aider à améliorer l\'appli. Ajouter un contact éloigné @@ -550,6 +549,7 @@ Appareil photo et position Afin de balayer le code QR, Briar doit accéder à l’appareil photo.\n\nAfin de découvrir des périphériques Bluetooth, Briar doit accéder à votre position.\n\nBriar n’enregistre pas votre position et ne la partage avec personne. Vous avez refusé l’accès à la caméra, mais l’ajout de contacts exige l’utilisation de celle-ci.\n\nVeuillez envisager d’y accorder l’accès. + Vous avez refusé l’accès à votre position géographique, mais Briar en a besoin pour découvrir les appareils Bluetooth.\n\nVeuillez envisager d’y accorder l’accès. Code QR Afficher le code QR en plein écran diff --git a/briar-android/src/main/res/values-gl/strings.xml b/briar-android/src/main/res/values-gl/strings.xml index f73085f8c..d8e3dc1af 100644 --- a/briar-android/src/main/res/values-gl/strings.xml +++ b/briar-android/src/main/res/values-gl/strings.xml @@ -195,7 +195,6 @@ Conectando co dispositivo\u2026 Autenticándose co dispositivo\u2026 Non se puido conectar co contacto - Por favor, comprobe que ambas están conectadas a mesma rede Wi-Fi. Si persiste o problema,envíe un informe para axudarnos a mellorar a app. Engadir contacto a distancia @@ -550,6 +549,7 @@ Cámara e localización Para escanear o código QR, Briar precisa acceso a cámara.\n\nPara descubrir dispositivos Bluetooth, Briar precisa permiso a súa localización.\n\nBriar non garda a súa localización nin a comparte con ninguén. Denegou o permiso de acceso a cámara, pero é necesario para engadir contactos.\n\nPor favor, considere conceder o permiso. + Denegache o acceso á localización, mais Briar precisa este permiso para descubrir dispositivos Bluetooth.\n\nConsidera conceder o permiso. Código QR Amosar o código QR a pantalla completa diff --git a/briar-android/src/main/res/values-he/strings.xml b/briar-android/src/main/res/values-he/strings.xml index 3a75097dd..37f0e9341 100644 --- a/briar-android/src/main/res/values-he/strings.xml +++ b/briar-android/src/main/res/values-he/strings.xml @@ -63,12 +63,36 @@ נעל יישום הגדרות התנתק + הקש כאן כדי לשלוט איך Briar מתחבר אל אנשי הקשר שלך. אינטרנט + לטלפון שלך יש גישת אינטרנט באמצעות Wi-Fi + לטלפון שלך יש חיבור אינטרנט באמצעות נתונים סלולריים + לטלפון שלך אין גישת אינטרנט + Briar מתחבר אל האינטרנט + Briar מחובר אל האינטרנט + Briar אינו יכול להתחבר אל האינטרנט + Briar מתוצר לא להשתמש באינטרנט + Briar מתוצר לא להשתמש בנתונים סלולריים + Briar מתוצר לא להשתמש באינטרנט בעת הרצה על סוללה + Briar מתוצר לא להשתמש באינטרנט במדינה זו Wi-Fi + אותה רשת Wi-Fi + הטלפון שלך מחובר אל Wi-Fi + הטלפון שלך אינו מחובר אל Wi-Fi + Briar מתחבר אל רשת ה־Wi-Fi + Briar מחובר אל רשת ה־Wi-Fi + Briar אינו יכול להתחבר אל רשת ה־Wi-Fi + Briar מתוצר לא להשתמש ברשת ה־Wi-Fi - שן כחולה + Bluetooth + Bluetooth של הטלפון שלך מופעל + Bluetooth של הטלפון שלך מכובה + Briar מתחבר אל Bluetooth + Briar מחובר אל Bluetooth + Briar אינו יכול להתחבר אל Bluetooth + Briar מתוצר לא להשתמש ב־Bluetooth נותקת מן Briar הקש כדי להתחבר חזרה. @@ -181,7 +205,6 @@ מתחבר אל מכשיר\u2026 מזדהה מול המכשיר\u2026 לא היה ניתן להתחבר אל איש הקשר שלך - אנא בדוק ששניכם מחוברים אל אותה רשת Wi-Fi. אם בעיה נמשכת, אנא שלח משוב כדי לעזור לנו לשפר את היישום. הוסף איש קשר בקרבה @@ -436,10 +459,20 @@ אוטומטי (שעות יום) ברירת מחדל + חיבורים + התחבר אל אנשי קשר באמצעות Bluetooth + התחבר אל אנשי קשר באותה רשת Wi-Fi + התחבר אל אנשי קשר באמצעות האינטרנט + כל החיבורים עוברים דרך רשת Tor למען פרטיות + שיטת חיבור עבור רשת Tor אוטומטי על סמך מיקום + השתמש ברשת Tor בלי גשרים + השתמש ברשת Tor עם גשרים + אל תתחבר אל האינטרנט אוטומטי: %1$s (תוך %2$s) השתמש בנתונים סלולריים + התחבר אל האינטרנט רק בעת טעינה משבית חיבור אינטרנט כשהמכשיר עובד על סוללה אבטחה @@ -536,7 +569,7 @@ הרשאת מצלמה כדי לסרוק את קוד ה־QR, היישום Briar צריך גישה אל המצלמה. הרשאת מיקום - כדי לגלות מכשירי שן־כחולה, Briar צריך הרשאה להשיג גישה אל מיקומך.\n\nBriar אינו מאחסן את מיקומך או משתף אותו עם אף אחד. + כדי לגלות מכשירי Bluetooth, היישום Briar צריך הרשאה להשיג גישה אל מיקומך.\n\nBriar אינו מאחסן את מיקומך או משתף אותו עם אף אחד. מצלמה ומיקום כדי לסרוק את קוד ה־QR, היישום Briar צריך הרשאה אל המצלמה.\n\nכדי לגלות מכשירי שן־כחולה, Briar צריך הרשאה להשיג גישה אל מיקומך.\n\nBriar אינו מאחסן את מיקומך או משתף אותו עם אף אחד. דחית גישה אל המצלמה, אבל הוספת אנשי קשר דורשת שימוש במצלמה.\n\nאנא שקול הענקת גישה. @@ -550,6 +583,7 @@ Briar נעול הקש כדי לבטל נעילה + Briar יכול להתחבר אל אנשי הקשר שלך באמצעות האינטרנט, Wi-Fi או Bluetooth.\n\nכל חיבורי האינטרנט עוברים דרך רשת Tor למען פרטיות.\n\nאם איש קשר ניתן להשגה באמצעות שיטות רבות, Briar משתמש בהן במקביל. נועה diff --git a/briar-android/src/main/res/values-hi/strings.xml b/briar-android/src/main/res/values-hi/strings.xml index 2b426c6d9..73484fbad 100644 --- a/briar-android/src/main/res/values-hi/strings.xml +++ b/briar-android/src/main/res/values-hi/strings.xml @@ -154,7 +154,6 @@ उपकरण \ u2026 से कनेक्ट हो रहा है डिवाइस के साथ प्रमाणीकरण \ u2026 आपके संपर्क से कनेक्ट नहीं हो सका - कृपया जांचें कि आप दोनों एक ही वाई-फाई नेटवर्क से जुड़े हुए हैं। यदि यह समस्या बनी रहती है, तो कृपया ऐप को बेहतर बनाने में हमारी सहायता के लिए फ़ीडबैक भेजें दूरी वाले संपर्क जोड़ें diff --git a/briar-android/src/main/res/values-hu/strings.xml b/briar-android/src/main/res/values-hu/strings.xml index fe546eb51..c4438c20f 100644 --- a/briar-android/src/main/res/values-hu/strings.xml +++ b/briar-android/src/main/res/values-hu/strings.xml @@ -197,7 +197,6 @@ Biztosan szeretné menteni? Csatlakozás az eszközhöz\u2026 Azonosítás az eszközzel\u2026 Nem sikerült csatlakozni a kapcsolatához - Ellenőrizzék, hogy mindketten ugyanahhoz a Wi-Fi hálózathoz vannak-e csatlakozva. Ha ez a probléma tartósan fennáll, kérjük küldjön visszajelzést nekünk, hogy segítsen fejleszteni az appot. Távoli kapcsolat hozzá adása diff --git a/briar-android/src/main/res/values-is/strings.xml b/briar-android/src/main/res/values-is/strings.xml index c27fc3a58..7a3e10fc5 100644 --- a/briar-android/src/main/res/values-is/strings.xml +++ b/briar-android/src/main/res/values-is/strings.xml @@ -195,7 +195,6 @@ Tengist við tæki\u2026 Auðkenni við tæki\u2026 Gat ekki tengst við tengiliðinn þinn - Athugaðu hvort þið séuð ekki báðir tengdir við sama þráðlausa Wi-Fi netið. Ef þetta vandamál er viðvarandi, ættirðu að senda umsögn til að hjálpa okkur að bæta forritið. Bæta við fjarlægum tengilið diff --git a/briar-android/src/main/res/values-it/strings.xml b/briar-android/src/main/res/values-it/strings.xml index 835965282..6f2c13fca 100644 --- a/briar-android/src/main/res/values-it/strings.xml +++ b/briar-android/src/main/res/values-it/strings.xml @@ -195,7 +195,6 @@ Connessione al dispositivo\u2026 Autenticazione con il dispositivo\u2026 Impossibile connettersi al tuo contatto - Verifica di essere entrambi connessi alla stessa rete Wi-Fi. Se il problema persiste, invia un feedback per aiutarci a migliorare l\'app. Aggiungi contatto distante diff --git a/briar-android/src/main/res/values-ja/strings.xml b/briar-android/src/main/res/values-ja/strings.xml index c3c2b1ca5..280bed255 100644 --- a/briar-android/src/main/res/values-ja/strings.xml +++ b/briar-android/src/main/res/values-ja/strings.xml @@ -149,7 +149,6 @@ デバイスに接続中\u2026 デバイス同士での認証中\u2026 連絡先に接続できませんでした - あなたと連絡相手、両方が同じWi-Fiネットワークに接続していることを確認してください。 この問題が解決しない場合、アプリを改善するためにフィードバックを送信してください。 離れた場所にいる相手を連絡先に追加 diff --git a/briar-android/src/main/res/values-ko/strings.xml b/briar-android/src/main/res/values-ko/strings.xml index 4d5e33376..adcf287fb 100644 --- a/briar-android/src/main/res/values-ko/strings.xml +++ b/briar-android/src/main/res/values-ko/strings.xml @@ -26,6 +26,8 @@ 비밀번호 잘못된 비밀번호입니다. 다시 시도하세요. + 비밀번호를 확인할 수 없습니다 + 비밀번호를 확인할 수 없었습니다. 이 문제를 해결하기 위해 기기를 재시작 해 보세요. 로그인 비밀번호를 잊어버렸습니다 비밀번호 분실 @@ -41,7 +43,9 @@ 이 Briar는 테스트용입니다. %d일 내에 계정이 만료되고 다시 되돌리지 못합니다. 이 소프트웨어는 만료되었습니다. \ n 테스트 해 주셔서 감사합니다! + Briar를 계속 이용하기 위해서는 최신 버전을 다운로드 해 주세요. 새로운 계정을 만들어야 하지만, 같은 별명을 사용할 수 있습니다. + 최신 버전 다운로드 받기 데이터베이스를 복호화하고 있습니다.... 데이터베이스를 업그레이드 하고 있습니다... 데이터베이스를 작게 만들고 있습니다... @@ -56,12 +60,36 @@ 앱 잠그기 설정 로그아웃 + 여기를 눌러 Briar가 연락처와 연결하는 방법을 설정하세요. 인터넷 + 와이파이를 통해 인터넷에 연결하고 있습니다 + 모바일 데이터로 인터넷에 연결하고 있습니다 + 인터넷에 연결돼 있지 않습니다 + 인터넷에 연결하고 있습니다 + 인터넷에 연결했습니다 + 인터넷에 연결하지 못했습니다 + 인터넷을 사용하지 않도록 설정돼 있습니다 + 모바일 데이터를 사용하지 않도록 설정돼 있습니다 + 충전하지 않고 있을 때에는 인터넷을 사용하지 않도록 설정돼 있습니다 + 이 국가에서는 인터넷을 사용하지 않도록 설정돼 있습니다 Wi-Fi + 동일한 와이파이 네트워크입니다 + 와이파이에 연결돼 있습니다 + 와이파이에 연결돼 있지 않습니다 + 와이파이에 연결 중입니다 + 와이파이에 연결됐습니다 + 와이파이에 연결하지 못했습니다 + 와이파이를 사용하지 않도록 설정돼 있습니다 블루투스 + 블루투스가 켜져 있습니다 + 블루투스가 꺼져 있습니다 + 블루투스에 연결 중입니다 + 블루투스에 연결했습니다 + 블루투스에 연결하지 못했습니다 + 블루투스를 사용하지 않도록 설정돼 있습니다 Briar에서 로그아웃 됨 눌러서 다시 로그인하세요. @@ -103,6 +131,7 @@ 고치기 도움 죄송합니다 + 이 시스템에서는 지원되지 않습니다 상태 저장된 연락처가 없습니다 @@ -118,6 +147,17 @@ 연락처 이름 바꾸기 연락처 이름 바꾸기 + 모든 메시지 지우기 + 메시지 삭제 확인 + 정말로 모든 메시지를 지우려고 하시나요? + 모든 메시지를 지울 수는 없었습니다 + 진행 중인 초대와 소개와 관련된 메시지는 완료되기 전에는 지울 수 없습니다. + 진행 중인 소개와 관련된 메시지는 완료되기 전에는 지울 수 없습니다. + 진행 중인 초대와 관련된 메시지는 완료되기 전에는 지울 수 없습니다. + 부분적으로 다운로드 된 메시지는 다운로드가 완료되기 전까지는 지울 수 없습니다. + 초대나 소개를 지우기 위해서는 요청을 선택하고 답을 주셔야 합니다. + 소개를 지우기 위해서는 요청을 선택하고 답을 주셔야 합니다. + 초대를 지우기 위해서는 요청을 선택하고 답을 주셔야 합니다. 연락처 삭제하기 연락처 삭제 확인 정말로 이 연락처, 그리고 이 연락처와의 메시지를 모두 제거하시겠어요? @@ -133,8 +173,9 @@ 지인 분의 Briar가 이미지 첨부를 지원하지 않습니다. 이 분이 업그레이드하면 다른 상징을 볼 수 있습니다. 이제 이 분에게 이미지를 보낼 수 있습니다 이 상징을 눌러 이미지를 첨부하세요. + 첫 %d개의 이미지만 보내질 것입니다 - 주위의 지인 추가하기 + 근처의 연락처 추가하기 연락처를 추가하려는 사람과 먼저 만나야 합니다.\n\n나중에 누군가 당신인 척 하거나 메시지를 훔쳐보는 것을 방지할 수 있습니다. 계속하기 다시 시도하기 @@ -149,12 +190,11 @@ \u2026 기기에 연결하고 있습니다 \u2026 기기와 인증하고 있습니다 연락처에 연결하지 못했습니다 - 둘 다 같은 Wi-Fi 네트워크에 연결돼 있는지 확인해 보세요. 문제가 계속된다면, 부디 피드백을 남겨서 앱이 좋아지게 도와주세요. - 멀리서 지인 추가하기 - 주위의 지인 추가하기 - 멀리서 지인 추가하기 + 원거리에서 연락처 추가하기 + 근처의 연락처 추가하기 + 원거리에서 연락처 추가하기 지인 분에게서 받은 링크를 여기에 입력하세요 지인 분의 링크 붙여 넣기 @@ -194,8 +234,9 @@ 인터넷에 연결되지 않음 중복되는 링크입니다 %s: 이미 이 링크를 통한 연락 요청이 있습니다. + %s: 이 링크의 연락처가 이미 있습니다. - %s과(와) %s이(가) 같은 사람인가요? + %s 님과 %s 님이 같은 사람인가요? @@ -204,7 +245,7 @@ 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--> 다른 사람 - %s과(와) %s이(가) 같은 링크를 보냈습니다.\n\n둘 중 하나가 당신의 지인이 누군지 알아내려고 하는 것일 수도 있습니다.\n\n둘 중 누구에게라도 다른 사람에게서 같은 링크를 받았다고 알리지 마십시오. + %s 님과 %s 님이 동일한 링크를 보냈습니다.\n\n누군가 당신의 연락처를 찾으려고 하는 것일 수도 있습니다.\n\n둘 중 누구에게라도 다른 사람에게서 동일한 링크를 받았다고 알리지 마십시오. 남은 연락 요청이 있는지 확인했습니다 지인 소개하기 @@ -218,19 +259,19 @@ 소개시켰습니다. 소개시키는 과정에서 문제가 있었습니다. %2$s에게 %1$s을(를) 소개하고 싶다고 전했습니다. - %1$s이(가) %2$s에게 소개하고 싶다고 합니다. %2$s을(를) 연락처 목록에 추가하고 싶으십니까? - %1$s이(가) %2$s에게 소개하고 싶다고 물었지만, %2$s은(는) 이미 연락처 목록에 있습니다. %1$s이(가) 아직 이 부분을 모르고 있을 수 있으니, 아직 응할 수 있습니다: - %1$s이(가) %2$s에게 소개시키고 싶다고 물었습니다. + %1$s 님이 %2$s 님에게 소개하고 싶다고 합니다. %2$s 님을 연락처 목록에 추가하고 싶으십니까? + %1$s 님이 당신을 %2$s 님에게 소개하고 싶다고 하지만, %2$s 님은 이미 연락처 목록에 있습니다. %1$s 님이 아직 모르고 있을 수 있으니, 아직 응할 수 있습니다: + %1$s 님이 당신을 %2$s 님에게 소개시키고 싶다고 합니다. %1$s에게 소개되고 싶다고 했습니다. - %1$s이(가) 당신의 연락처를 받기 전에, 그 쪽에서도 소개를 승락해야 합니다. 여기서 시간이 좀 걸릴 수 있습니다. + %1$s 님이 소개를 승락해야 연락처에 추가될 수 있습니다. 여기서 시간이 좀 걸릴 수 있습니다. %1$s에게 소개되고 싶지 않다고 했습니다. - %1$s이(가) %2$s에게 소개해도 괜찮다고 했습니다. - %1$s이(가) %2$s에게 소개되고 싶지 않다고 했습니다. - %1$s이(가) %2$s이(가) 소개되고 싶지 않다고 합니다. + %1$s 님이 %2$s 님과의 소개를 승락했습니다. + %1$s 님이 %2$s 님과의 소개를 거절했습니다. + %1$s 님은 %2$s 님이 소개를 거절했다고 합니다. 모임이 없습니다. - + 상징을 눌러 모임을 만들거나, 연락하는 분에게 모임을 공유해 달라고 물어보세요 - %s이(가) 만듦 + + 상징을 눌러 모임을 만들거나, 연락하는 분에게 모임을 공유해 달라고 여쭤보세요 + %s 님이 만듦 메시지 %d개 @@ -245,9 +286,9 @@ 구성원 목록 구성원 초대하기 모임을 만들었습니다 - %s이(가) 모임을 만들었습니다 + %s 님이 모임을 만들었습니다 모임에 참가했습니다 - %s이(가) 모임에 참가했습니다 + %s 님이 모임에 참가했습니다 모임 나가기 모임 떠나기 확인 정말로 이 모임을 나가고 싶으신가요? @@ -260,7 +301,7 @@ 모임 초대장 %1$s을(를) \"%2$s\" 모임에 참가하도록 초대했습니다. - %1$s이(가) \"%2$s\" 모임에 참가하도록 초대했습니다. + %1$s 님이 \"%2$s\" 모임에 참가하도록 초대했습니다. 모임에 참가했습니다 모임 초대장 거절함 @@ -268,19 +309,19 @@ %s(으)로부터 받은 초대장을 승락했습니다. %s(으)로부터의 모임 초대장을 거절했습니다. - %s이(가) 모임 초대장을 승락했습니다. - %s이(가) 모임 초대장을 거절했습니다. + %s 님이 모임 초대장을 승락했습니다. + %s 님이 모임 초대장을 거절했습니다. 모임을 만든 분만 새로운 구성원을 초대할 수 있습니다. 다음은 모임의 모든 현재 구성원입니다. - 연락처 보이기 + 연락처 공개하기 이 모임의 지금과 앞으로 들어올 구성원에게 연락처를 공개할지를 정할 수 있습니다.\n\n연락처를 공개하면, 모임을 만든 사람이 오프라인이어도 공개된 연락처와 연결할 수 있기 때문에 모임에 더 빠르고 안정적으로 연결할 수 있습니다. 연락처 관계가 모임에서 보입니다 - 연락처 관계가 모임에서 보입니다(본인이 공개) - 연락처 관계가 모임에서 보입니다(%s이(가) 공개) - 연락처 관계가 모임에서 보이지 않습니다 + (당신이) 모임에게 지인 관계 공개합니다 + (%s님이) 모임에게 지인 관계를 공개합니다 + 모임에게 지인 관계를 공개하지 않습니다 보여드릴 포럼이 없습니다 - + 상징을 눌러 포럼을 만들거나, 연락하는 분에게 포럼을 공유해 달라고 물어보세요 + + 상징을 눌러 포럼을 만들거나, 연락하는 분에게 포럼을 공유해 달라고 여쭤보세요 포럼 만들기 만드실 포럼 이름을 정해주세요 포럼 만들기 @@ -295,7 +336,7 @@ 답장 포럼 떠나기 포럼 떠나기 확인 - 정말로 이 포럼을 떠나려고 하세요?\n\n이 포럼과 공유한 연락처에서 업데이트가 되지 않을 수 있습니다. + 정말로 이 포럼을 떠나려고 하세요?\n\n이 포럼과 공유한 지인이 업데이트를 받지 못할 수 있습니다. 떠나기 포럼을 떠났습니다 @@ -303,11 +344,11 @@ 선택한 연락처 연락처 선택하기 저장된 연락처가 없습니다 - 연락처를 추가한 후에 돌아오길 바랍니다 - 선택한 지인 분과 공유하는 포럼 + 연락처를 추가한 후에 돌아오세요 + 선택한 지인과 공유하는 포럼 메시지 추가하기(선택 사항) 포럼을 공유하는 과정에서 문제가 있었습니다 - %1$s이(가) \"%2$s\" 포럼을 공유했습니다. + %1$s님이 \"%2$s\" 포럼을 공유했습니다. \"%1$s\" 포럼을 %2$s과(와) 공유했습니다. 포럼 초대장 이미 이 포럼에 초대를 승락했습니다.\n\n초대를 더 많이 승락할수록 더 빠르고 안정적으로 포럼에 연결할 수 있습니다. @@ -317,10 +358,10 @@ 이미 공유하고 있습니다 %s(으)로부터의 포럼 초대를 승락했습니다. %s(으)로부터의 포럼 초대를 거절했습니다. - %s이(가) 포럼 초대장을 승락했습니다. - %s이(가) 포럼 초대장을 거절했습니다. + %s님이 포럼 초대장을 승락했습니다. + %s 님이 포럼 초대장을 거절했습니다. 공유 상태 - 포럼은 참가한 누구나 지인과 공유할 수 있습니다. 다음 연락처와 포럼을 공유하고 있습니다. 이외 다른 구성원이 더 있을 수 있습니다. + 포럼은 참가한 누구나 자신의 지인과 공유할 수 있습니다. 현재 다음 연락처와 포럼을 공유하고 있습니다. 이외 다른 구성원이 더 있을 수 있습니다. 와(과) 공유하고 있습니다.(%1$d %2$d명 온라인) 지인과 공유한 %d개의 포럼 @@ -336,9 +377,9 @@ 새로운 블로그 게시물을 받았습니다 스크롤 게시물이 없습니다 - 연락처와 구독한 블로그의 게시물이 여기에 나타납니다.\n\n게시물을 쓰려면 펜 상징을 누르세요 + 지인이 올린 글과 구독한 블로그의 게시물이 여기에 올라옵니다.\n\n게시물을 작성하려면 펜 상징을 누르세요 블로그 제거하기 - 정말로 이 블로그를 제거하시겠어요?\n\n기기에서 게시물은 제거되지만 다른 사람의 기기에서는 제거되지 않습니다.\n\n이 블로그를 공유하던 연락처에서 업데이트가 되지 않을 수 있습니다. + 정말로 이 블로그를 제거하시겠어요?\n\n게시물이 이 기기에서는 제거되지만 다른 사람의 기기에서는 제거되지 않습니다.\n\n이 블로그를 공유하던 지인이 업데이트를 받지 못하게 될 수 있습니다. 제거하기 블로그를 제거했습니다 글 추가하기(선택 사항) @@ -347,17 +388,17 @@ 블로그 공유하기 블로그를 공유하는 과정에서 문제가 있었습니다. 블로그 공유하기 - 선택한 지인 분과 블로그 공유함 + 선택한 지인과 블로그 공유함 %s(으)로부터 받은 블로그 초대장을 승락했습니다. %s(으)로부터의 블로그 초대장을 거절했습니다. - %s이(가) 블로그 초대를 승락했습니다. - %s이(가) 블로그 초대장을 거절했습니다. - %1$s이(가) \"%2$s\" 블로그를 공유했습니다. + %s 님이 블로그 초대를 승락했습니다. + %s 님이 블로그 초대장을 거절했습니다. + %1$s 님이 \"%2$s\" 블로그를 공유했습니다. %2$s와(과) \"%1$s\" 블로그를 공유했습니다. 블로그 초대장 블로그에 구독함 모임 초대장 거절함 - 블로그는 구독한 누구나 지인과 공유할 수 있습니다. 다음 지인 분과 이 블로그를 공유하고 있습니다. 이외 다른 구독자가 더 있을 수 있습니다. + 블로그는 구독한 누구나 자신의 지인과 공유할 수 있습니다. 현재 다음 연락처와 이 블로그를 공유하고 있습니다. 이외 다른 구독자가 더 있을 수 있습니다. RSS 피드 불러오기 가져오기 @@ -368,13 +409,13 @@ 작성자: 최종 업데이트: 피드 제거하기 - 정말로 이 피드를 제거하시겠어요?\n\n기기에서 게시물은 제거되지만 다른 사람의 기기에서는 제거되지 않습니다.\n\n이 피드를 공유하던 연락처에서 업데이트가 되지 않을 수 있습니다. + 정말로 이 피드를 제거하시겠어요?\n\n기기에서 게시물은 제거되지만 다른 사람의 기기에서는 제거되지 않습니다.\n\n이 피드를 공유하던 지인이 업데이트를 받지 못하게 될 수 있습니다. 제거하기 피드를 삭제할 수 없었습니다! 보여드릴 RSS 피드가 없습니다\n\n+ 상징을 눌러 피드를 불러오세요 피드를 불러오는 과정에서 문제가 있었습니다. 나중에 다시 시도해 주세요. - 언어 & 지역 + Language & region Briar를 다시 시작한 후에 설정이 적용됩니다. 부디 로그아웃하고 Briar를 다시 시작해 주세요. 기본 설정 화면 @@ -384,10 +425,20 @@ 자동(시간에 따라) 기본 설정 + 연결 + 블루투스로 지인과 연결하기 + 같은 와이파이 네트워크에 연결된 지인과 연결하기 + 인터넷을 통해 지인과 연결하기 + 프라이버시를 위해 모든 연결은 Tor 네트워크를 거칩니다 + Tor 네트워크 연결 방식 장소에 따라 자동으로 + 브리지 없이 Tor 네트워크 사용하기 + 브지를 통해 Tor 네트워크 사용하기 + 인터넷에 연결하지 않기 자동: %1$s(%2$s에서) 모바일 데이터 사용하기 + 충전할 때만 인터넷에 연결하기 충전 중이지 않을 때에는 인터넷 연결을 비활성화 보안 @@ -422,12 +473,12 @@ 설정된 앱이 없습니다 없음 패닉 앱 확인 - %1$s이(가) 파기 권한을 지닌 패닉 버튼 동작을 작동시킬 수 있게 허용하시겠어요? + %1$s 님이 파기 권한을 지닌 패닉 버튼 동작을 작동시킬 수 있게 허용하시겠어요? 파기하기 로그아웃 패닉 버튼이 눌리면 Briar에서 로그아웃하기 계정 삭제하기 - 패닉 버튼이 눌리면 Briar 계정을 삭제하기 (주의: 계정, 연락처와 메시지가 영구적으로 지워집니다) + 패닉 버튼이 눌리면 Briar 계정을 삭제하기. 주의: 계정, 연락처와 메시지가 영구적으로 지워집니다 알림 로그인 알려주기 @@ -498,6 +549,7 @@ Briar가 잠겼습니다 눌러 잠금해제 + Briar는 인터넷이나 Wi-Fi, 블루투스를 통해 지인과 연결할 수 있습니다.\n\n인터넷에 연결할 때는 프라이버시를 위해 언제나 Tor 네트워크를 거칩니다.\n\nBriar는 가능한 연락처에 닿을 수 있는 여러 방식을 병용합니다. 영희 diff --git a/briar-android/src/main/res/values-lt/strings.xml b/briar-android/src/main/res/values-lt/strings.xml index ced83f263..7d5407ff2 100644 --- a/briar-android/src/main/res/values-lt/strings.xml +++ b/briar-android/src/main/res/values-lt/strings.xml @@ -31,7 +31,7 @@ Prisijungti Aš pamiršau savo slaptažodį Prarastas slaptažodis - Jūsų Briar paskyra yra saugoma šifruotu pavidalu jūsų įrenginyje, o ne debesijoje, taigi, negalime atstatyti jūsų slaptažodžio Ar norėtumėte ištrinti savo paskyrą ir pradėti iš naujo?\n\nDėmesio: Jūsų tapatybės, žinutės ir adresatai bus prarasti visiems laikams. + Jūsų Briar paskyra yra saugoma šifruotu pavidalu jūsų įrenginyje, o ne debesijoje, taigi, negalime atstatyti jūsų slaptažodžio. Ar norėtumėte ištrinti savo paskyrą ir pradėti iš naujo?\n\nDėmesio: Jūsų tapatybės, žinutės ir adresatai bus prarasti visiems laikams. Nepavyko paleisti Briar Bakstelėkite išsamesnei informacijai. Briar paleidimo nesėkmė @@ -191,7 +191,7 @@ Bus išsiųsti tik %d pirmi paveikslai Pridėti šalia esantį adresatą - Jūs privalote susitikti gyvai su asmeniu, kurį norite pridėti kaip adresatą.\n\nTai neleis bet kam apsimetinėti jumis ar ateityje skaityti jūsų žinutes. + Jūs privalote susitikti gyvai su asmeniu, kurį norite pridėti kaip adresatą.\n\nTai neleis bet kam apsimetinėti jumis ar ateityje skaityti jūsų žinučių. Tęsti Bandyti dar kartą Laukiama, kol adresatas nuskenuos ir prisijungs\u2026 @@ -205,7 +205,6 @@ Jungiamasi prie įrenginio\u2026 Tapatybės nustatymas su įrenginiu\u2026 Nepavyko prisijungti prie jūsų adresato - Įsitikinkite, kad abu esate prisijungę prie to paties belaidžio (Wi-Fi) tinklo. Jei ši problema išlieka, atsiųskite mums atsiliepimą, kad padėtumėte mums patobulinti programėlę. Pridėti adresatą per atstumą @@ -570,6 +569,7 @@ Kamera ir įrenginio vietovė Tam, kad galėtų nuskenuoti QR kodą, Briar reikia gauti prieigą prie jūsų kameros.\n\nTam, kad galėtų atrasti Bluetooth įrenginius, Briar reikia gauti prieigą prie jūsų įrenginio vietovės.\n\nBriar nesaugo jūsų įrenginio vietovės ir su niekuo jos nebendrina. Jūs uždraudėte prieigą prie kameros, tačiau norint pridėti adresatus, reikia naudoti kamerą.\n\nApsvarstykite galimybę sutekti prieigą prie kameros. + Jūs uždraudėte prieigą prie įrenginio vietovės, tačiau norint atrasti Bluetooth įrenginius, Briar reikia šio leidimo.\n\nApsvarstykite galimybę sutekti prieigą prie įrenginio vietovės. QR kodas Rodyti QR kodą visame ekrane diff --git a/briar-android/src/main/res/values-mk/strings.xml b/briar-android/src/main/res/values-mk/strings.xml index dd0a18353..4bfe80053 100644 --- a/briar-android/src/main/res/values-mk/strings.xml +++ b/briar-android/src/main/res/values-mk/strings.xml @@ -61,12 +61,36 @@ Заклучи ја апликацијата Поставки Одјави се + Допрете овде за да контролирате како Briar се поврзува со вашите контакти. Интернет + Вашиот телефон има интернет пристап преку Wi-Fi + Вашиот телефон има интернет пристап преку мобилни податоци + Вашиот телефон нема интернет пристап + Briar се поврзува на Интернет + Briar е поврзан на Интернет + Briar не може да се поврзе на Интернет + Briar е конфигуриран да не користи Интернет + Briar е конфигуриран да не користи мобилни податоци + Briar е конфигуриран да не користи Интернет додека е работи на батерија + Briar е конфигуриран да не користи Интернет во оваа држава Wi-Fi + Иста Wi-Fi мрежа + Вашиот телефон е поврзан на Wi-Fi + Вашиот телефон не е поврзан на Wi-Fi + Briar се поврзува на Wi-Fi мрежа + Briar е поврзан на Wi-Fi мрежата + Briar сне може да се поврзе на Wi-Fi мрежата + Briar е конфигуриран да не ја користи Wi-Fi мрежата Bluetooth + Bluetooth-от на вашиот телефон е вклучен + Bluetooth-от на вашиот телефон е исклучен + Briar се поврзува со Bluetooth + Briar е поврзан со Bluetooth + Briar не може да се поврзе со Bluetooth + Briar е конфигуриран да не користи Bluetooth Одјави се од Briar Допрете за да се најавите. @@ -113,6 +137,7 @@ Помош Жал ни е Недостапно на вашиот систем + Статус: Нема контакти за прикажување Допрете ја + иконата за да додадете контакт @@ -170,7 +195,6 @@ Поврзување со уред\u2026 Автентикација со уред\u2026 Не може да се поврзе со твојот контакт - Ве молиме проверете дека и двајцата сте поврзани на истата Wi-Fi мрежа. Ако овој проблем останува, ве молиме испратете повратна информација за да ни помогнете да ја подобриме апликацијата. Додај контакт НА РАСТОЈАНИЕ @@ -411,10 +435,20 @@ Автоматски (Дневно) Систем стандардно поставен + Поврзувања + Поврзете се контактите преку Bluetooth + Поврзете се со контактите на истата Wi-Fi мрежа + Поврзете се контактите преку Интернет + Сите поврзувања одат преку Tor мрежата поради приватност + Метод на поврзување на Tor мрежата Автоматски базирано на локација + Користете Tor мрежа без мостови + Користете Tor мрежа со мостови + Не се поврзувај на Интернет Автоматски: %1$s (во %2$s) Користи мобилни податоци + Поврзи на Интернет само за време на полнење Оневозможи го Интернет поврзувањето кога уредот не е приклучен на полнач Безбедност @@ -514,7 +548,8 @@ За да открие Bluetooth уреди, на Briar му е потребна дозвола за пристап до вашата локација.\n\nBriar не ја зачувува вашата локација или не ја споделува со никого. Камера и локација За да го скенира QR кодот, на Briar му е потребен пристап до камерата.\n\nЗа да открие Bluetooth уреди, на Briar му е потребна дозвола за вашата локација.\n\nBriar не ја зачувува вашата локација или не ја споделува со никого. - Го одбивте пристапот до камерата, но за додавање на контакти потребно е користење на камерата.\n\nВе молиме размислете за давање дозвола. + Го одбивте пристапот до камерата, но за додавање на контакти потребно е користење на камерата.\n\nВе молиме размислете за давање пристап. + Го одбивте пристапот до вашата локација, но на Briar му е потребна оваа дозвола за да ги открие Bluetooth уредите.\n\nВе молиме размислете за давање пристап. QR код Покажи го QR кодот на цел екран @@ -525,6 +560,7 @@ Briar е заклучен Допрете за отклучување + Briar може да се поврзе со вашите контакти преку Интернет, Wi-Fi или Bluetooth.\n\nСите Интернет поврзувања одат преку Tor Мрежата поради приватност.\n\nАко контактот може да биде достапен преку повеќе начини, Briar ќе ги користи паралелно. Жарко diff --git a/briar-android/src/main/res/values-nl/strings.xml b/briar-android/src/main/res/values-nl/strings.xml index 65f9619ff..7e55ff377 100644 --- a/briar-android/src/main/res/values-nl/strings.xml +++ b/briar-android/src/main/res/values-nl/strings.xml @@ -195,7 +195,6 @@ Aan het verbinden met apparaat\u2026 Aan het authenticeren met apparaat\u2026 Kon geen verbinding maken met je contact - Controleer alsjeblieft dat jullie beiden met hetzelfde wifinetwerk zijn verbonden. Als dit probleem aanhoudt, stuur feedback alsjeblieft om ons te helpen de app te verbeteren. Voeg een contact ver weg toe diff --git a/briar-android/src/main/res/values-oc/strings.xml b/briar-android/src/main/res/values-oc/strings.xml index 35ee6e305..3c9e5e8ab 100644 --- a/briar-android/src/main/res/values-oc/strings.xml +++ b/briar-android/src/main/res/values-oc/strings.xml @@ -169,7 +169,6 @@ Volètz suprimir vòstre compte e ne crear un nòu ?\n Connexion a l’aparelh\u2026 Autentificacion amb l’aparelh\u2026 Connexion impossibla al contacte - Verificatz que sètz los dos connectats al meteis ret wifi. S’aqueste problèma dura, mercés d’enviar un comentari per nos ajudar a melhorar l’aplicacion. Ajustar un contacte a distància diff --git a/briar-android/src/main/res/values-pl/strings.xml b/briar-android/src/main/res/values-pl/strings.xml index 2e64f8574..d44c3871a 100644 --- a/briar-android/src/main/res/values-pl/strings.xml +++ b/briar-android/src/main/res/values-pl/strings.xml @@ -165,7 +165,6 @@ Łączenie z urządzeniem\u2026 Autoryzowanie z urządzeniem\u2026 Nie udało się połączyć z kontaktem - Sprawdź czy obaj jesteście połączeni z tą samą siecią Wi-Fi. Jeśli problem będzie występować dalej, proszę wysłać zgłoszenie aby pomóc nam ulepszyć aplikację. Dodaj Kontakt na odległość diff --git a/briar-android/src/main/res/values-pt-rBR/strings.xml b/briar-android/src/main/res/values-pt-rBR/strings.xml index 696d7ac8d..4b6298024 100644 --- a/briar-android/src/main/res/values-pt-rBR/strings.xml +++ b/briar-android/src/main/res/values-pt-rBR/strings.xml @@ -172,7 +172,6 @@ Conectando a device\u2026 Autenticando com o dispositivo\u2026 Não foi possível conectar-se ao seu contato - Por favor, certifique se ambos estão conectados na mesma rede Wi-Fi. Se o problema persistir, por favor envie um feedback para nos ajudar a melhorar o app. Adicionar contato à distância diff --git a/briar-android/src/main/res/values-ro/strings.xml b/briar-android/src/main/res/values-ro/strings.xml index 18fbebaaa..153a80b78 100644 --- a/briar-android/src/main/res/values-ro/strings.xml +++ b/briar-android/src/main/res/values-ro/strings.xml @@ -200,7 +200,6 @@ Conectare la dispozitiv\u2026 Autentificare cu dispozitivul\u2026 Nu s-a putut face conexiunea la contactul dumneavoastră - Verificați să fiți conectați la aceeași rețea Wi-Fi. Dacă problema persistă, vă rugăm să trimiteți feedback pentru a ne ajuta să îmbunătățim aplicația. Adaugă un contact la distanță @@ -560,6 +559,7 @@ Cameră foto și locație Pentru a scana codul QR, Briar are nevoie să acceseze camera foto.\n\nPentru a putea descoperi dispozitive Bluetooth, Briar are nevoie de permisiunea de a vă accesa locația.\n\nBriar nu vă stochează locația și nici nu o partajează cu nimeni. Ați refuzat accesul la camera foto, dar pentru a adăuga contacte este necesară folosirea camerei foto.\n\nVă rugăm să luați în considerare acordarea accesului. + Ați refuzat accesul la locație, dar Briar are nevoie de această permisiune pentru a descoperi dispozitive Bluetooth.\n\nVă rugăm să luați în considerare acordarea accesului. Cod QR Arată codul QR pe tot ecranul diff --git a/briar-android/src/main/res/values-ru/strings.xml b/briar-android/src/main/res/values-ru/strings.xml index f0b74fbcc..9eb451fad 100644 --- a/briar-android/src/main/res/values-ru/strings.xml +++ b/briar-android/src/main/res/values-ru/strings.xml @@ -207,7 +207,6 @@ Подключение к устройству\u2026 Аутентификация с устройством\u2026 Не удалось подключиться к контакту - Убедитесь, что вы оба подключены к той же сети Wi-Fi. Если эта проблема сохраняется, пожалуйста отправьте отзыв, чтобы помочь нам улучшить приложение. Добавление контакта на расстоянии diff --git a/briar-android/src/main/res/values-sq/strings.xml b/briar-android/src/main/res/values-sq/strings.xml index 2a581642e..47c62ffad 100644 --- a/briar-android/src/main/res/values-sq/strings.xml +++ b/briar-android/src/main/res/values-sq/strings.xml @@ -195,7 +195,6 @@ Po lidhet me pajisjen\u2026 Po bëhet mirëfilltësimi me pajisjen\u2026 S’u lidh dot te kontakti juaj - Ju lutemi, kontrolloni se jeni të dy të lidhur në të njëjtin rrjet Wi-Fi. Nëse problemi vazhdon, ju lutemi, na njoftoni, që të mund të përmirësojmë aplikacionin. Shtoni Kontakt në Largësi @@ -550,6 +549,7 @@ Kamera dhe vend Që të skanojë kodin QR, Briar-it i duhet të përdorë kamerën.\n\nQë të pikasë pajisje Bluetooth, Briar-i lyp leje të njohë vendin tuaj.\n\nBriar-i nuk e depoziton vendin tuaj, as e ndan me dikë. Keni mohuar hyrjen në kamera, por shtimi i kontakteve lyp përdorimin e kamerës.\n\nJu lutemi, shihni mundësinë e akordimit të hyrjes. + Keni mohuar hyrje te vendndodhja juaj, por Briar-i ka nevojë për këtë leje, që të mund të zbulojë pajisje Bluetooth.\n\nJu lutemi, shihni mundësinë e akordimit të hyrjes. Kod QR Shfaqe kodin QR sa tërë ekrani diff --git a/briar-android/src/main/res/values-sv/strings.xml b/briar-android/src/main/res/values-sv/strings.xml index 2f36d2fcf..f4a5ebf55 100644 --- a/briar-android/src/main/res/values-sv/strings.xml +++ b/briar-android/src/main/res/values-sv/strings.xml @@ -195,7 +195,6 @@ Ansluter till enhet\u2026 Autentiserar med enhet\u2026 Kunde ej ansluta till din kontakt - Kontrollera att ni båda är anslutna på samma Wi-Fi-nätverk. Om det här problemet kvarstår, vänligen lämna synpunkter så vi kan förbättra appen. Lägg till en kontakt på avstånd diff --git a/briar-android/src/main/res/values-sw/strings.xml b/briar-android/src/main/res/values-sw/strings.xml index 8993fc374..225fce739 100644 --- a/briar-android/src/main/res/values-sw/strings.xml +++ b/briar-android/src/main/res/values-sw/strings.xml @@ -187,7 +187,6 @@ Umepoteza nenosiri. Unganisha kwenye kifaa\u2026 Uhalisishaji wa kifaa\u2026 Imeshindwa unganishwa kwenye mawasiliano - Tafathali Angalia kama wote Mmeunganishwa kwenye Mtandao wa WI-FI mmoja . Kama hilo tatizo linaendela , Tafathali tutumie maoniilituboreshe Programu . Ongeza mawasiliano kwambali diff --git a/briar-android/src/main/res/values-tr/strings.xml b/briar-android/src/main/res/values-tr/strings.xml index 6637ced45..f01de4223 100644 --- a/briar-android/src/main/res/values-tr/strings.xml +++ b/briar-android/src/main/res/values-tr/strings.xml @@ -195,7 +195,6 @@ Aygıta bağlanıyor\u2026 Aygıtla kimlik doğrulama\u2026 Kişinizle bağlantı kurulamadı - Lütfen her ikinizin de aynı Wi-Fi ağına bağlı olup olmadığınızı kontrol edin. Sorun devam ederse, uygulamayı geliştirmemize yardımcı olmak için geri bildirim gönderin. Uzaktaki Kişiyi Ekle diff --git a/briar-android/src/main/res/values-uk/strings.xml b/briar-android/src/main/res/values-uk/strings.xml index b419333c1..e1f210f2d 100644 --- a/briar-android/src/main/res/values-uk/strings.xml +++ b/briar-android/src/main/res/values-uk/strings.xml @@ -165,7 +165,6 @@ З\'єднання з пристроєм\u2026 Автентифікація з пристроєм\u2026 Неможливо з\'єднатися з вашим контактом - Будь ласка, впевніться, що ви обоє під\'єднані до однієї Wi-Fi-мережі. Якщо ця проблема збережеться, будь ласка, надішліть відгук, щоб допомогти нам вдосконалити цей додаток. Додати контакт віддалено diff --git a/briar-android/src/main/res/values-zh-rCN/strings.xml b/briar-android/src/main/res/values-zh-rCN/strings.xml index d7ce80b76..d771acf81 100644 --- a/briar-android/src/main/res/values-zh-rCN/strings.xml +++ b/briar-android/src/main/res/values-zh-rCN/strings.xml @@ -190,7 +190,6 @@ 正在连接至设备\u2026 正在验证设备\u2026 无法连接到您的联系人 - 请确保你们连接到了同一个无线局域网中。 如果该问题仍存在,请 发送反馈 帮助我们改善应用。 添加远处的联系人 @@ -540,6 +539,7 @@ 相机和位置 Briar 需要相机权限以扫描二维码。\n\nBriar 需要位置信息权限以发现蓝牙设备。\n\nBriar 不会存储您的位置或将它分享给任何人。 您已拒绝相机权限,而添加联系人需要使用相机。\n\n请考虑授予相机权限。 + 您已拒绝访问您的位置,但Briar需要此权限才能发现蓝牙设备。\n\n请考虑授予访问权限。 二维码 全屏显示二维码 diff --git a/briar-android/src/main/res/values-zh-rTW/strings.xml b/briar-android/src/main/res/values-zh-rTW/strings.xml index 4eeb2e48f..5b05103b6 100644 --- a/briar-android/src/main/res/values-zh-rTW/strings.xml +++ b/briar-android/src/main/res/values-zh-rTW/strings.xml @@ -149,7 +149,6 @@ 正在連接至裝置\u2026 正在驗證裝置\u2026 無法連接到您的聯絡人 - 請確保您們連接到了同一個 Wi-Fi中。 如果該問題仍存在,請 發送反饋 幫助我們改善程式。 添加遠處的聯絡人 From 54893d2716286420f4ac4c5fc25c243115ef0e37 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Mon, 2 Nov 2020 14:51:34 +0000 Subject: [PATCH 038/582] Bump version numbers for 1.2.12 release. --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index cbaf172bd..40dd5a584 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,6 @@ project.ext { compileSdkVersion = 30 minSdkVersion = 16 targetSdkVersion = 29 - versionCode = 10211 - versionName = '1.2.11' + versionCode = 10212 + versionName = '1.2.12' } From 993502add08d2e3c222b340b20ff7c1d0b9e1d24 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Mon, 2 Nov 2020 17:13:14 +0000 Subject: [PATCH 039/582] Add adaptive icon for API 26+ and Play Store icon. --- .../src/main/ic_launcher-playstore.png | Bin 0 -> 10095 bytes .../res/drawable/ic_launcher_foreground.xml | 43 ++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 ++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1366 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1161 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 1937 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 2928 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 4028 bytes .../res/values/ic_launcher_background.xml | 4 ++ 10 files changed, 57 insertions(+) create mode 100644 briar-android/src/main/ic_launcher-playstore.png create mode 100644 briar-android/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 briar-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 briar-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 briar-android/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 briar-android/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 briar-android/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 briar-android/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 briar-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 briar-android/src/main/res/values/ic_launcher_background.xml diff --git a/briar-android/src/main/ic_launcher-playstore.png b/briar-android/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..5be757102b665e28129faaf2b34b0b8d11e2ee65 GIT binary patch literal 10095 zcmb_?c|6oz`~PPKZL+jc$j#zv?3HtpU7 zK@i`W(@xAlwAri7a)|%pSN~K!UPAp`_r{(xx`}m!qFlSnt z%90YR%?$!Bd9#l>j!RFDr?`G5>`B9GsmRe2VPw|LRBaCc)Y3IqFT2?}mNaTYl ztL+2Q%-TO3A}3}=OO-__}}nj38pi(z}} zFzsn%&v%olVZO_fuS5qzO-+{M6h&Ce1u>`Pz8b%hvJ33I~ul zo8o9OhLscRw^D@Ar;|*)!_ODwu%PMUn(qctyxj4Ds&+QkX}Xp-8AiQ+1HH(r=)5qI zMZ#V>=EG&veUQvZlcxVK{7j=uCt1Fhi;ZrXUf&X?^M;hEv~8cYZ)e*vDdsIQuOT_& z$|5oAIjOCv3@hnXd{ul~A+r+%VvzpFVr&q*n3pxcuJ834c#~g7?Vie*j_q;(a>Kow zT~dv0rq9x`kGneD4Go(4Ljkl@6ZEkg&UazFB9RA-(x< zfhx6B{VV2*K}%FHX#YS#Lp{#ZRi-qEEgRdrF*j|Lm9P|>E2$Kp7$7*K{oC|c#-&l8OH{VI@dC-Q&B#Z@dHj5+{r9YJM7i=$?-+5 zQGN0W&l}ok2%-1Fg|xp-bWZQ5beLbEkyOc##zH>wY1b3SXATgl*qp^sN?M7HmC2P! zB5j@u&{0OQFfruSjQ_hawF&m}p@G2jvCM^-vgg|E`O25}(57R|(WGfs2c<5aaQPTy zd}p1s$k0sstU@Amnot(_ak235hQ`PFi8GWC{|D~r_^O0Q*{nlMk09u|^?~9B!ST1>xTFl*dj z?vgm+?EzAoJe|C?e5m6G0l?#?!M5(m>e;8m_RMT1$V!#kZr2Fr5e1=g{uG5&E)Ey*K|*;K;Fqd6~n5--b(*!u^pFP_3D*%C)%* z3muZ>7SbLyl?#RIb2~ue)2YkeCXdNyX&sGoGZD9}gCGe7KBLrdut3e`lRf8yyM=cX z9e$I$Uvrx!1@LB4l9=IDY$-e>rV{~4SW(gK)yFSlugxb@qn23qM}OCK1#Jxh@Hw8P zNWVns^q?`aCo{Cs&kQbOgm}S=a%}4WSX6Yh6{6#8k@_$% z%~9}xkpG~!DuYM%rwsf={1cyHXSr7mu8oy@t$Uz^dHABPnzMBUE`N%_OHb$udn6tV zI|lavRk_+@1i&4^7owoJ2@hP={hGih3 zecSe{yzyP|lct-Sd7;iN7O}D>rgXiPtL@8dnmv@c^|F#u#UpLit`r8?4YoASAYvg#%h>IwZDaA{h<&@EA4 z2JT*)nk7ihkX-`^DQBH`4Ge?1h8Mw(-bNNG3W6D^=#kcjD*3!w;`%0m%dj$XYSk2c~lA2WqAN zwl`_XnF9CnHh4=kBT=f&YGW{jh<;RUuVw2nTI@Uz%c6sLxnuEBQdW8Up!u`z(!^^i zkQkA8a{mKB9};CNy|n__{w zogFMfQy<(QZa}Vriz*&D6OulzPuhU9Q&GPt-qS;f>94CFaR~<@I_DC+@}pVNfrgQA)ZCHH&HtYf$mC726)`umlG+M<+v+i#_>6!4p)fQ$5(~g zywMDX-;Iop-_AgCDGz^S+Bi2oIC__3xVq~q_RPD8X)oig&^g=N7)6kkPb@=jud4n` z?s)1N8G5fs0i`O4h_($JA7{7pG{Q3=Au#eX5*zv>(SE%&BHBMa8W&8uEHfz{F<8yg&cP+=D>&#{!FH?S}Umkjh z=qt8`*YaO4(2u%vl+rcY{XGm?1hh_X#Pw>Z_U%-7hy8q#6IkCP{-wD_sONhjcYEvX z!|#i)+3n|y`y)-V0#)i<&*<$GAKoFhmLDex)%xk;o%DRUd7 zqqYh6Y+Z$@WN~hcN+Fj?5&s3GM7#qOS)(Fv2K@AWgD_^kS8w--g=%j>y_SB6uZJKo zDn&30s~8Pu7!0e(3OFw~ptuvH)+*}3W8uJ+HJlPi81%zIEIF8J&G8ySz!0g#G4~I@ zM%aErIOvxg)PfX*crvirn(vD+m0&=b2*2IdVoi~euGLxNC)XcDL~9*yb}bUEiHqO6R-EAhyYb;_5TnYF{aIyb(c4O7n@1o#U~hZn{asCy>p+uc zNS!PCl*dPQGo>XGs(tZ5M*M>sfR?~}da>NHZlya}wdO6?xngE4%L`yHUH zrbNP9-my_pt2ToONOP7(yN8E?9cdF%V#y34*LZ6B>t{FbCfA;?5pz4hvZ%H&qtoHM zziV|8l4~VFr)Z2abS<2HeDAOoJ9z2Vu!acFU;N(zk=LaGy=%_cy z07od!`ebxZLo2pN%De7jI&(Q&4^Amr=z+rTe_b6`8M z%*hRiHSFiWk)o_@OJuwz>tGrAXr@&qz_DZY*ALS&vxiFF=Y0tcRary9SL@ko)r(WC zZZ(UieXb6oce(X2XZn5mww?Mm%KZU_Q$kkg!>O@>>h;5fXz34;ch_^Eh z&bD7kGYi=Bz+soohIj*_y*_C6;2!USl5$&;?#qdL4^bgc30vU>gEI@PXFra`Zm|n= zEO2~Ic4hMDy-4Lth>ZP=u37ASZDZTVz5^-3%L9j3kv-HLY$&ZOsNfZ#(p~LEYgd3m z2Ltr{@^IOehI0lD>IDYM{U(|(`IIGM;I48 z%uG)6VCeZ_C%a#)2R6aF6!tG=NqTuRD-zNY1N%rP8pik$(dnQ*QF9gVNfFMYrslpL z(p#c-NtCFR<>_oU+~1StICv+#`CSi%2T&gKM)-&b%ne{lF#N1iB4h;{4lVe%3QiCL z#w`fElYk&@93V)mnD)GVv%$0A8ppig8$k%8(N8#U~wg1!S|3+)MSAG7!sQCXI z1fak%2!ZZ7L4Xkd=(8Zy8T%ha3Iq)p;r@3c1>&<74eQTB5b~e;@jqk7jiVm;hZ&Q( z4WGMt{Hz)3XjDtrm28SX!gB`23@d~ z=L_zFSItDJ$s$y1AnlYBEJz3^079*j+_kyimpn+K#+aYe=t! zFaPDZ%AE0MJO^IDvi(wBx>Q5!_O2=I4lD5ZQGlj?Wj?k@*a2y5!lehw7Ur#p4_7%l zHx4%GkL$+eOy98T0TfF+V1EDH-Gv=)AC@}DcJ>IXEbQlBhkSnb2)}Ct;GYpSyHSq` z2WPTY`phkSy?W2jJ^Yw5#PqIWdUK7|Fm^9ETcHJL-WQ2Fmu`?ZJebee0U;|At~P?*VQ6Mz zp{)?F;K?w#i{9f|AqZ7nzkXeJT_*aE2XyPtc%dmN1qiT^5?z%$4jny%KVrorxO7mF z`CZNCwX6q_%<00!{8wV#d(cM$HR&O2#(j*(CJ15Oc)@VK=%#y$UR&+r6hUR$dLOiD z(gf>X4Ff(5Si*Az&hS0ZF`EYR=9(% zrN3am9d`IL3A z2Rs$Z$qg>Q0P5O!Fdv1|45i^gt|&*TVw5#&1PIwZvHRD^X%VJmjC=>~Bo>9*qAk04 z_gadj06=J=x`VIkEBV~as9M}GVpM~AATJR8Y!60rMA-!97~@ofxGIyT`1mi$h9KW< z=bbHYvQIusa=EJrsJm=Qlk3k48s&v9Fb>Hkd?Zm+Au_8e`L0F5h<8zxKzY$<;NhedonyA9O5U?Cx zmn_01dI#osEEaZeISuRCkqqVpjC8oN_sT-<&7J*&kR;^pcvMB~k)P|~614=5a5e?e zavGK3Yx)+h7dm6Lw1^x!ttb^WV%=h>q>8Qo?JP9h3~G2#A*rqo!uLp-CQ&$DWg2F) zkBCr0He3&J5D@6upQ=&o6EcT}FU{WY)oz>jMsh+ne-qRWJO1SSh&$RMkXc-BvTR9f z^|3}?+#Vji`I{yn$!~G%y@rR&QTlUx0E8a?kKhpcRs00(YHerWR0rIg!LDsRYt;e# z>fiji$%1@C-^rPIwXVDb)V(2%pK>_BFiBHGScgACY{Uf|RlK;Zr<*W*3x}*|gCgqE zeF@A-r6YX<5+JS)cS?0rUFq4J9uu^a;x&n)0Pi?-|D=PKjl-z-s)xg^QHtSw?$KZ>3*Rc@Ew>N>T>?XebJ7}A0`i~|@WTDK zN?=Vf0p!A|hOsbCZj?$#egWqIe`Wwkp$imrhq^i_??gbMAJ~Tq-Lb;~fC>nxMYV;+ zO11hoZ-0X(0S&lG11JUhzV3HJH_dMRvxyab=H~7$A>J+`D7Go86|LXDF?IoUIHv<> zWKoExA|~mXNbvsh;;QU=wWp_{UU|rUH$>V9*>` zT?lDo*e#sySML5~!*G?EAtL%acg6e}vBy2yr8?kn<`kSV)&|X^;h;J=-l~C-C|8k1 zT3Z-4;X~)|`G@&&33FCm0;mq6K;dpXbCFOi2x+JR&;kn~B{gX)goME)%!KVw(@N-R zv%0<)9BRx5wu5@LI&@#ah36pp)pDZ;NEXv$%QZWbr`_LffmUwaEY%frT2@(&Pni0Z zB!#fnM6dN=hinl7XI@IFH)*nKLPcP(Jc3g)FfUTG>LeEZF&)0MchE?+U?N>B)?X!v1<4#cC!Jg>M=oU z@E+~lrV#=H=hyVlHYP>5Ik~U*6!ov<<4l1XeJz)K&YoH>I~^CbNeO z@sWIw;hBg5{^fmWfcSf*)8c?qs{GJn z3jOWRR5O32NXW+-p%~9sIc)OvZtBPWYPsqIj+$LHd&YPmNpP6<9s)@KG7XY|mm&Zv zkd}Zl!vXwfJWGaqsIXi~xzBQ5MMEuG7tPH9y6xp9ji!5U66N3H8TNH(-V7E^dMD^353u-I?T604_aNu@F4CIba7m1``1L@+T!-^#HtFbqE3b$U$mY zcMWF$FFwt&1`lYh%2*@spo>+5-0*`VhaYPO|2|{q$Y9t0lKpd^asU4d4&0{>JRc5R zDb+t@x_lEN>6+3~!>C3xdm_ zwl7ZDDaW#`VYSKRr4-Q?HB8TabM7lf0ZK04=~uul48M2onY<7Pp=X)YWN_l*Y>=B_ z2)>|vXkev_&;Kb;{swSOBT_8pbFL2i(iHVUbp${dH@Gx0=^Cy1sfIB9aKCBBR{OOg=@TC;l$1n%Q`mvSGQ-LqBc6>}hw^xi>t+S(pfLVX_peI za>6=p$XC9E|9Qr2YfQPjf5!{nv>eBj2XBMq~%r^)V1& zc8nIX-;)yf(mTJiK@FO~t6$#&F5Kv|o_6Ag>hcgrpIGhA%t(?)g8R_x0q+&OiWskk zF?`Wi8OyzOt`?BSFDE8x=wZ3T6LVvh`<@B&EhZjtk%jvd&Di}Me5v4~bI0Y3^Y_Vy z!$w%1%0WksqQc=+-nSE9XRE%Nn$RMV2)#=9>xUG1)S(w>w!?=NbZ!gs2;=yfzU^bi zHT4Op;?z(3AsOw=jLU<~`)3b7#@`OqTqlk8YxR)v{oc;pfwkN^dCO#_=3Cx3J;*pZ zMk^lNfuEa=udwW+}4ccbEblfX^(r=vP* zd+0qbhr63-aZK0HK6z59;be^dPt%X>jL%c-C67=q!>3D5poXK=R5aV@7)#@%jdS# zijql+&@5?f9V)^{5auQ*Ej8bAGgIe6eLm2z8K|eSajI0k4r$wf_aw738=%m0^MUrH zu~u`pG4IfkG$Obs>OzrnguN{~+nnIfHyJv$J>v5|^gC zEjqxx--q$-?t<*Q*M)e^xtC5~_82Lg+%K@t8nf^6qowMhBC`zAjqTlmEb%uby_MqN z!v=l5`R@bM(!?F$iuSdHZn>m`{9;kx6Uzq0*;uUQndUDP!}c%mw<22nsaArSjM%ap zGtT9E%e{XOLLqT6D%yQQMND*tFzV-IW-(ZdvCMY%&zm~~j+QHhHEE*c{J`|NZaIt z9$>nt8;zOpt|uxUAn}0Zr4L|!&Zo&?sFT^i)(&;_V!LxbAo%%{`h>C6q~P;W0SR5G z(`kvFJ@(pP{LJhMf%dCxu(Xd@IxKvn&tpo!_Eg(aA^t$|A5qklCWGI}H-vjvaDXkL ziT2sk{@SUo3?tgM_Lqc_VVVvxmE~^*J~?={)8|n6)469N3<+q!;hgR+KEtEcb(N!o zdUWUU%wPUdff6pLJC-zwgyFV!8{LZ0$e`I^YK=n38>&^<=M=}Z@%D3I{~AA + + + + + + + + + + + diff --git a/briar-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/briar-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..7353dbd1f --- /dev/null +++ b/briar-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/briar-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/briar-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..7353dbd1f --- /dev/null +++ b/briar-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/briar-android/src/main/res/mipmap-hdpi/ic_launcher.png b/briar-android/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9f6c7e3fc112b0f98ce492ba74155d0635c3b693 GIT binary patch literal 1366 zcmV-c1*!UpP)u2@#AI}$lT(E(Rnq4CfrE881W7wcXNp*!i5()^+sKcF)ksQZX;t0i=r^U zMhuEYra}ZWftIoH5kAIg3x)AfTA-yLa1N)XJ?$wxzE9H<2CWC$lAd$=KFKeqP5Qj) z|Hpgi+x9)p%A!S!7A;!JE&#|TH?lFdB`gcV<3_plq+#Tdh_>1Th#4g2`mMj#(2Cl*4h{w>X#@ z1z{#qDCkGbnuwrWm&;YJRzWy}P>^1){|U1uDySg?1l>$nki+4)p>{zG!!)K@PzX5R zA`DsUK{tOf%((3r@CvSdkKqlt;l8a2w4@hdY|Y>cu<8)x_Rc`V(93X9m;VsW%o*?q zPRV}WZ--x*(|h_ghBsj&u&R7Pw;z1ECtQ?qGApF|2ds5*`^T0Wl`p8(SQ$59pZSVp zf7|4Har--`IBMcjaQ_95u6iFe}mcT9am1*u+8+vHiecK1X~`Ym5&jvz4<0Kd4t z2fIfP3v6FZ+#547d!mFkMM*)SP%!*hA%YkZL|--Q_P2Pmouil%NT>CjEl$Emu=P|0if_^-o!zV&5<)V2K|}zaaca z*lB(j>c*dwjUbexUHlvZqE{a4AYYJ$>9sZ(vR_Xro`6GA5a+k->6_=eq~yqBC5>w& zD>notXM0Q>p2^I9t09tAu15bx+<-lE7pNdAhzg>Ds30mx#e!I$1?)T%6Z|?!5rR0s z6$Wfqq04e^8|~tGSo51D1>xs8JNr_>$Vz>9ktqv7ykLjs>9Re`{qGDfg55JFDQJf6 zJe2ow?Q@mk6@J8{elK;HL?{Xi;p{~APGT}?iN`JsvCb6EbG1U z_&lg>u0nQ#x-1`qAbRBSGQSaA1*5we`j)=f22VYF`Ii!eUm5M}O9j(w?L-cr84E?H z#{~1Mx|q1VvveihZu%f@f7k4JDu@coWI?$3pvCZ3+n?3RBL% zCHqH~euyjRzU^n_3-SmK_$_>zJ-HpMy7xHz^WYN@f*$D|u7Vr>n0Om5=?b>{FZC9X zne~3MJ6+|n4R{m$!F8B*cZD}?B35<~0;2B#{XCdgd$;>7>^-v1@v37RFx^O=;er}6 zKv3e>YPei3)y|;kC6TT#q=Xq#q zYC4NqM@B~S4ti1VBZOmSvv~}sA`Xb6)Bt9|OrxWt49WO3WQ@PfiQ8Od_Au(0q6%$W3VTo6flWHuck$9ytqfs(-tkt}&xhfvunffg-Vv=mYQ Y0X@=EYTg)mPyhe`07*qoM6N<$f)=@dvj6}9 literal 0 HcmV?d00001 diff --git a/briar-android/src/main/res/mipmap-mdpi/ic_launcher.png b/briar-android/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..5d26977fb2ee7d6f98bbca9abcb7d68c9e17c935 GIT binary patch literal 1161 zcmV;41a|w0P)E)Ae)2@$JlU@mbF_eZ9!;R3s?RZDD7SEt}S%EUdzwlbD@_WTp5rZcSN4#o2Tjh zo_oHZr@iO--Sh0)wNngS0H6R>sIv+-WBgGf0}bT*{p1GX=Y6PWQHOLA>3g4=w>8aU z{RFCRX=!OblgUK%)Ut;<;^~19*^B9P`aCuSY-kwqJbw|bJ(3q7`>@1fvHb`TCXKKb zu+Ctuid)9+m<4_V)Ipxn5NPtXAt0 z1YnH_(9+U!Y`p-6`~?~t8;@%OKri^AZ}vBE1TI2=cVIhFHBL+?VUo2i z2Z-JeLcQY^_^M~Wq#y1ahVe*`YV2^h6TY<;$*~Q6ufhFPOkV&$^EaHe9F$EFb&sA_ zjqMuwL=pGhof4Rh_UjAa3txvbmS+^kZVZ2{8cV)c09cD*hIi=;Fv@hpx!#wc)_y{k zY3xhm0>p(NjPuE}sRB%KHaVs*bOGk#eNsPLHjPg6z0m4;2QItcmgNZ4Z=ZEw+b?800NV**Fn9?% zhd-3{7j+8Q#fhy0Ao;wXy9wsN7r3&126tz_hM~~aRRGt0AHYDkL5kUk$bBcHx3!yv z0x=t0a39T?vtI_!eCsNJ4&VFGG4}4J){9?vA3}>m;0;|pf&>OHU8>y+ODIp%sW^>_MM@+jZiuwZ~NIM`PdX;S7f*^p) z<+_62_N?cOtf0KS{Fu|}{FA0>mgBf6((|O=N@e?G7>2dm?bpi6%E;fRV7(`mdr-v{ z6&0^mS69DLRaK>nNZ%*15HAlL{gn`vm6ea<$Gxa0b=F?uWm5<54#Q@q%$_^5 z_XY$|P*6}%P*6}%PzWagU?Ec=$*G0DG~U@Lb-`EQ3u|D}l%E$ftz}xz6y~Q|&v~$T zCXaY0%pI3A9b*zR4KnGNwA|63Gy!0sG{&+!ERn->OfPnzG2ViJu$e5Fp^C5c2 z2Y`il?%ata0PX`A92_)cWo7L^@Av=^SX^8jMF2bkU`$U>--+H400KY&1O&hVW7ZMS znft(MH}aNu%sR|+s=;U(0>`+W06Z(RO$W7#EV%G67BXt0;B0jyWY%tnYpq{_N+)Kf zIXup3cUa-kz;(DN-pl3O0%vQsvNk?}2g=_V;4uP#LL+WTzkw4KYv8By71NYny%~B& zt2`b_P3i(Tbw6~*oF^;SLPggJ2FXSMx<<;GQ6V!LHK{+E4}isPa62bnZgT4PxXg)Q z`}M5^ph|w)rBSG}HIXnf*5!6iwXT7UsMRiWrj@T`m#7;A;I{PWtN>0|g~O1s!|j}M zP4%ofQ4Q)V1i%{r6xv$105q!e2><~g0R9c2MxFt=;y9NoXi5N$NkIU-QwNy9WYfA7 z7HXbUumoUAk=3IsgZjR7sObH{CmJ<(dIZ2^(>(cG<Ew#S3sq7r5yHf(B#d-4?c zz3pST(7N3xQX1+!0-(33XUvl?`3y=GM_FOlCG4pPtR%6QjhbX^RCr%leR~1`pB>MX z$6)Pu{?0ERN#5^1>%fNgG-~hd~5BiLaAk+K<9iyWE)up}Xd~VFH9l)e6KH3EXV=cl1aI-Vn<%tOe0P4Q#xbOhrGl2O5;D+o= z0)PNohVowtfWKv55P&BD3TOnle-Ah`Z!STV%45OLhgH3$(5bool(0_lxDvMXPRrvhVF89O z6VP1H3(N)C92U@;dS{i{rl-j`ZvepW3A-~JTBpb&00e*l5C8(;rvSvNTmn$vn?0*h zS#>*{pIY3_iL0QTtPFOU6BqN44&5dIXcjJ{k2LRlD zBQQETdZpQHZlx_uX5YdDJ(5Ty?NL!t!CdEkxqW1^v$JuNvNzk>+RoYScHFAThV78y z&pZRS+b7T)yQ}TGrFkqg5)EV@yWw=udK|<)X}>=T&mBErUhF|n z&>PO9C4Bb&$-qxUuZM+&tzr?ze%`>ei7CR*g&u4`PtY6mi1W%TfmHFP{6=ta@Y|uG zp{qkeLe{KXw=T#Zh3AeQpcm)~=goou9|SfM$X(|a`?(fzUiid#3JMAe3JMB}`4RsC X%HmJwa4^o<00000NkvXXu0mjf9!-QC literal 0 HcmV?d00001 diff --git a/briar-android/src/main/res/mipmap-xxhdpi/ic_launcher.png b/briar-android/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2289069cde7152ebb4a6bff8b4370c6f16e3e84c GIT binary patch literal 2928 zcmZ`*c{mho*T2VJh_YoABg-givTu>y*w-+!H%xkrb?l5mDcP3@Wyv7>nq(gIMA^42 z*+TX`j3tCu|9szbUGMdt>zs4n=UnHn^Skf!JCTO^nvCb6=K%nWT1YkHvorpW(b1gw zMU9y{0O+N))RayA#@1g48k#P0eo=HM^h539f>=KUDm6M|a?e|3*k0)nQ zB+)Pt(ne*5Bjt%v8$%rLqFowmG;3u7JRySD{TQ+2`hz zXAO|vqtV7yTjr~PlIKuX%`e@Z=uWl9rg?+&F+<7===8Y?CSAV<_jLEt3gwX-E};z$ z#fi$LiR}UMclNplsd`m@XnXcG1n%3t7*T$a$bZwAT5S(9;!-Plp$~a{eC#r;V8OOw zE|ws}=aE-dR_1NIg}-}6QFkXaO;@m}xHxb~SRtVysmW*EGM6lL4Yh#gAot~b)z{n+~JFTlw z6+Mc{`up#{F`q&79lE>$O`)->Ah)T(LG<&uxU9~!05p4j(1Fu^+69kaOTQhSa7<2>z1n`X5}Iw`Px4Y^%=B; zAj<5Kk&#r{0ql6K!n`OUF)69({8<(T>G=kjORf`CKzuKRm4aXgs3>8VfinlJ-6xJAZJ$d*axj2fIAkp$JK62<2tixl^#(dr~EU0 zY-6r4b8Euqy7FU5FI-D)`|;^BWq4=g?*5{>$cCFoRbFoiq7CAtg~|`~uoLU~ zQ!iMQ<;En(%!p%`lZvkIM#ij~M2A5L}L> z>aitxTn%~pr13!tf`*(Dd%IepW3jW_o^C<#)?t>_BmOU6_0m&?mh5Q?B zivJmGI}(09ducMix~z24sV`6RKmh16__3O4O2_QhUoqCT-9rk+2D#TI7lTUL^7&x$ zh9;1p^4*s-@>#tV(6m5{Q`={hv#Oj^ylU4;j&#WfPfaSELAo{JA9DjDfOneiK};9M#d1ocre!OON&#- ztb}8)mQkQ9Cf>}%Hir+B^P}W17-7HpADEh%8wfT>h`9(+Dm79uPk{;1VUr{4*8>}( z?;)_YNU8pWPtp;nQ{R96<9{LBhopG6PtAj{kt+eGxu@YeTm#nBOUCOyfEbaq(z}G0 zECJ;7lOHsMo4EQJOmmU#gwGXF2+8hx4+mZJy0YqQOqa_qu4yWsCj{{lP3X^A+DnW&uD=4w}pj~YQk0F)vUsu76yp^`FEA~qzx;$?M2M3!P8Y%_utwqkRefsn%-y=mkpBvI7Eu@!FHzH%#aZmAQ=jD8sWKggm^uEA?6*!!9kqezr2G zI}As1aBhibjcPk3y?{T=Z9F>Z&ZS?F;mn>F) z$`trdv015QgUe_Yk0QBCJW5@#{}m&Kh-NE8bF3|owtBFiH_X4wjkIugcON8bB{{`# ziaW*r@{grpS#$Jae{~Mi%;{mK?Zm4cae}u=>?*Ysi0s5XfdTkAZLTYZ! zXKOI(>#Hjd9V;HN#ywGZbhLU>lc-*is7Fw>DM|{q%VnkgSX80FG!#ab$+r6m4TI7B czgm4g1<3jx`w?V_CO8u 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 + + #FFFFFF + \ No newline at end of file From f6bdbb1b80b67f42c990e0a5e2a0ea44002efc3e Mon Sep 17 00:00:00 2001 From: akwizgran Date: Wed, 14 Oct 2020 11:07:02 +0100 Subject: [PATCH 040/582] Let Tor know if we're on an IPv6-only network. --- .../network/AndroidNetworkManager.java | 98 ++++++++++++++++--- .../bramble/api/network/NetworkStatus.java | 9 +- .../bramble/plugin/tor/TorPlugin.java | 10 +- .../bramble/network/JavaNetworkManager.java | 28 +++--- 4 files changed, 119 insertions(+), 26 deletions(-) diff --git a/bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkManager.java b/bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkManager.java index 3ae99173a..dd6f91018 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkManager.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkManager.java @@ -1,11 +1,15 @@ package org.briarproject.bramble.network; +import android.annotation.TargetApi; import android.app.Application; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; import android.net.NetworkInfo; import org.briarproject.bramble.api.event.EventBus; @@ -19,6 +23,11 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.system.TaskScheduler; import org.briarproject.bramble.api.system.TaskScheduler.Cancellable; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -36,16 +45,22 @@ import static android.net.ConnectivityManager.TYPE_WIFI; import static android.net.wifi.p2p.WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION; import static android.os.Build.VERSION.SDK_INT; import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED; +import static java.net.NetworkInterface.getNetworkInterfaces; +import static java.util.Collections.list; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +import static org.briarproject.bramble.util.LogUtils.logException; @MethodsNotNullByDefault @ParametersNotNullByDefault class AndroidNetworkManager implements NetworkManager, Service { private static final Logger LOG = - Logger.getLogger(AndroidNetworkManager.class.getName()); + getLogger(AndroidNetworkManager.class.getName()); // See android.net.wifi.WifiManager private static final String WIFI_AP_STATE_CHANGED_ACTION = @@ -54,7 +69,8 @@ class AndroidNetworkManager implements NetworkManager, Service { private final TaskScheduler scheduler; private final EventBus eventBus; private final Executor eventExecutor; - private final Context appContext; + private final Application app; + private final ConnectivityManager connectivityManager; private final AtomicReference connectivityCheck = new AtomicReference<>(); private final AtomicBoolean used = new AtomicBoolean(false); @@ -67,7 +83,9 @@ class AndroidNetworkManager implements NetworkManager, Service { this.scheduler = scheduler; this.eventBus = eventBus; this.eventExecutor = eventExecutor; - this.appContext = app.getApplicationContext(); + this.app = app; + connectivityManager = (ConnectivityManager) + requireNonNull(app.getSystemService(CONNECTIVITY_SERVICE)); } @Override @@ -82,24 +100,82 @@ class AndroidNetworkManager implements NetworkManager, Service { filter.addAction(WIFI_AP_STATE_CHANGED_ACTION); filter.addAction(WIFI_P2P_THIS_DEVICE_CHANGED_ACTION); if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED); - appContext.registerReceiver(networkStateReceiver, filter); + app.registerReceiver(networkStateReceiver, filter); } @Override public void stopService() { if (networkStateReceiver != null) - appContext.unregisterReceiver(networkStateReceiver); + app.unregisterReceiver(networkStateReceiver); } @Override public NetworkStatus getNetworkStatus() { - ConnectivityManager cm = (ConnectivityManager) - appContext.getSystemService(CONNECTIVITY_SERVICE); - if (cm == null) throw new AssertionError(); - NetworkInfo net = cm.getActiveNetworkInfo(); + NetworkInfo net = connectivityManager.getActiveNetworkInfo(); boolean connected = net != null && net.isConnected(); - boolean wifi = connected && net.getType() == TYPE_WIFI; - return new NetworkStatus(connected, wifi); + boolean wifi = false, ipv6Only = false; + if (connected) { + wifi = net.getType() == TYPE_WIFI; + if (SDK_INT >= 23) ipv6Only = isActiveNetworkIpv6Only(); + else ipv6Only = areAllAvailableNetworksIpv6Only(); + } + return new NetworkStatus(connected, wifi, ipv6Only); + } + + /** + * Returns true if the + * {@link ConnectivityManager#getActiveNetwork() active network} has an + * IPv6 unicast address and no IPv4 addresses. The active network is + * assumed not to be a loopback interface. + */ + @TargetApi(23) + private boolean isActiveNetworkIpv6Only() { + Network net = connectivityManager.getActiveNetwork(); + if (net == null) { + LOG.info("No active network"); + return false; + } + LinkProperties props = connectivityManager.getLinkProperties(net); + if (props == null) { + LOG.info("No link properties for active network"); + return false; + } + boolean hasIpv6Unicast = false; + for (LinkAddress linkAddress : props.getLinkAddresses()) { + InetAddress addr = linkAddress.getAddress(); + if (addr instanceof Inet4Address) return false; + if (!addr.isMulticastAddress()) hasIpv6Unicast = true; + } + return hasIpv6Unicast; + } + + /** + * Returns true if the device has at least one network interface with an + * IPv6 unicast address and no interfaces with IPv4 addresses, excluding + * loopback interfaces and interfaces that are + * {@link NetworkInterface#isUp() down}. If this method returns true and + * the device has internet access then it's via IPv6 only. + */ + private boolean areAllAvailableNetworksIpv6Only() { + try { + Enumeration interfaces = getNetworkInterfaces(); + if (interfaces == null) { + LOG.info("No network interfaces"); + return false; + } + boolean hasIpv6Unicast = false; + for (NetworkInterface i : list(interfaces)) { + if (i.isLoopback() || !i.isUp()) continue; + for (InetAddress addr : list(i.getInetAddresses())) { + if (addr instanceof Inet4Address) return false; + if (!addr.isMulticastAddress()) hasIpv6Unicast = true; + } + } + return hasIpv6Unicast; + } catch (SocketException e) { + logException(LOG, WARNING, e); + return false; + } } private void updateConnectionStatus() { diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkStatus.java b/bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkStatus.java index 2cdb22e76..348c29f0d 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkStatus.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkStatus.java @@ -8,11 +8,12 @@ import javax.annotation.concurrent.Immutable; @NotNullByDefault public class NetworkStatus { - private final boolean connected, wifi; + private final boolean connected, wifi, ipv6Only; - public NetworkStatus(boolean connected, boolean wifi) { + public NetworkStatus(boolean connected, boolean wifi, boolean ipv6Only) { this.connected = connected; this.wifi = wifi; + this.ipv6Only = ipv6Only; } public boolean isConnected() { @@ -22,4 +23,8 @@ public class NetworkStatus { public boolean isWifi() { return wifi; } + + public boolean isIpv6Only() { + return ipv6Only; + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index 5fc93f885..d624346e0 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -863,6 +863,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { if (!state.isTorRunning()) return; boolean online = status.isConnected(); boolean wifi = status.isWifi(); + boolean ipv6Only = status.isIpv6Only(); String country = locationUtils.getCurrentCountry(); boolean blocked = circumventionProvider.isTorProbablyBlocked(country); @@ -879,7 +880,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC; if (LOG.isLoggable(INFO)) { - LOG.info("Online: " + online + ", wifi: " + wifi); + LOG.info("Online: " + online + ", wifi: " + wifi + + ", IPv6 only: " + ipv6Only); if (country.isEmpty()) LOG.info("Country code unknown"); else LOG.info("Country code: " + country); LOG.info("Charging: " + charging); @@ -942,6 +944,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { if (enableNetwork) { enableBridges(enableBridges, useMeek); enableConnectionPadding(enableConnectionPadding); + useIpv6(ipv6Only); } enableNetwork(enableNetwork); } catch (IOException e) { @@ -954,6 +957,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { controlConnection.setConf("ConnectionPadding", enable ? "1" : "0"); } + private void useIpv6(boolean ipv6Only) throws IOException { + controlConnection.setConf("ClientUseIPv4", ipv6Only ? "0" : "1"); + controlConnection.setConf("ClientUseIPv6", ipv6Only ? "1" : "0"); + } + @ThreadSafe @NotNullByDefault protected class PluginState { diff --git a/bramble-java/src/main/java/org/briarproject/bramble/network/JavaNetworkManager.java b/bramble-java/src/main/java/org/briarproject/bramble/network/JavaNetworkManager.java index 75bd3f969..ee46bd11b 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/network/JavaNetworkManager.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/network/JavaNetworkManager.java @@ -5,6 +5,8 @@ import org.briarproject.bramble.api.network.NetworkStatus; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import java.net.Inet4Address; +import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; @@ -14,8 +16,8 @@ import javax.inject.Inject; import static java.net.NetworkInterface.getNetworkInterfaces; import static java.util.Collections.list; -import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.LogUtils.logException; @MethodsNotNullByDefault @@ -23,7 +25,7 @@ import static org.briarproject.bramble.util.LogUtils.logException; class JavaNetworkManager implements NetworkManager { private static final Logger LOG = - Logger.getLogger(JavaNetworkManager.class.getName()); + getLogger(JavaNetworkManager.class.getName()); @Inject JavaNetworkManager() { @@ -31,26 +33,28 @@ class JavaNetworkManager implements NetworkManager { @Override public NetworkStatus getNetworkStatus() { - boolean connected = false; + boolean connected = false, hasIpv4 = false, hasIpv6Unicast = false; try { Enumeration interfaces = getNetworkInterfaces(); - if (interfaces != null) { + if (interfaces == null) { + LOG.info("No network interfaces"); + } else { for (NetworkInterface i : list(interfaces)) { - if (i.isLoopback()) continue; - if (i.isUp() && i.getInetAddresses().hasMoreElements()) { - if (LOG.isLoggable(INFO)) { - LOG.info("Interface " + i.getDisplayName() + - " is up with at least one address."); - } + if (i.isLoopback() || !i.isUp()) continue; + for (InetAddress addr : list(i.getInetAddresses())) { connected = true; - break; + if (addr instanceof Inet4Address) { + hasIpv4 = true; + } else if (!addr.isMulticastAddress()) { + hasIpv6Unicast = true; + } } } } } catch (SocketException e) { logException(LOG, WARNING, e); } - return new NetworkStatus(connected, false); + return new NetworkStatus(connected, false, !hasIpv4 && hasIpv6Unicast); } } From 02ee678bab3cdeb1ad9cd97b63ebcd53f59cd9d6 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 3 Nov 2020 13:52:12 +0000 Subject: [PATCH 041/582] If using bridges, use meek if the network is IPv6-only. --- .../java/org/briarproject/bramble/plugin/tor/TorPlugin.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index d624346e0..17bb73c79 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -918,7 +918,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { enableNetwork = true; if (network == PREF_TOR_NETWORK_WITH_BRIDGES || (automatic && bridgesWork)) { - if (circumventionProvider.needsMeek(country)) { + if (ipv6Only || + circumventionProvider.needsMeek(country)) { LOG.info("Using meek bridges"); enableBridges = true; useMeek = true; From a17b154024f03f4cb34ea915635ca1e6558c2c22 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Mon, 9 Nov 2020 12:42:13 +0000 Subject: [PATCH 042/582] Update translations. --- briar-android/src/main/res/values-it/strings.xml | 3 ++- briar-android/src/main/res/values-tr/strings.xml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/briar-android/src/main/res/values-it/strings.xml b/briar-android/src/main/res/values-it/strings.xml index 6f2c13fca..17dd3f69f 100644 --- a/briar-android/src/main/res/values-it/strings.xml +++ b/briar-android/src/main/res/values-it/strings.xml @@ -1,5 +1,5 @@ - + Benvenuto su Briar Il tuo nickname sarà accanto ad ogni contenuto pubblicato. Non potrai cambiarlo dopo aver creato l\'account. @@ -549,6 +549,7 @@ Fotocamera e geolocalizzazione Per scansionare il codice QR, Briar ha bisogno di accedere alla fotocamera.\n\nPer trovare dispositivi Bluetooth, Briar ha bisogno di accedere alla tua posizione.\n\nBriar non memorizza la tua posizione, nè la condivide con terzi. Hai negato l\'accesso alla fotocamera, ma questa serve per aggiungere i contatti.\n\nConsidera la possibilità di concedere l\'accesso. + Hai negato l\'accesso alla tua posizione, ma Briar ha bisogno di questa autorizzazione per trovare i dispositivi Bluetooth.\n\nConsidera la possibilità di concedere l\'accesso. Codice QR Mostra codice QR a tutto schermo diff --git a/briar-android/src/main/res/values-tr/strings.xml b/briar-android/src/main/res/values-tr/strings.xml index f01de4223..9ab47570b 100644 --- a/briar-android/src/main/res/values-tr/strings.xml +++ b/briar-android/src/main/res/values-tr/strings.xml @@ -1,5 +1,5 @@ - + Briar\'a Hoşgeldiniz Takma adınız, gönderdiğiniz herhangi bir içeriğin yanında gösterilecek. Hesabınızı oluşturduktan sonra onu değiştiremezsiniz. @@ -549,6 +549,7 @@ Kamera ve konum QR kodunu taramak için, Briar\'ın kameranıza erişmesi gerekiyor.\n\nBluetooth aygıtlarını keşfetmek için Briar\'ın konumunuza erişmesi gerekiyor.\n\nBriar konumunuzu saklamaz ve hiç kimseyle paylaşmaz. Kameraya erişimi engellediniz, ancak kişi eklemek için kamerayı kullanmanız gerekiyor.\n\nLütfen erişim izni vermeyi düşünün. + Konuma erişimi engellediniz, ancak Briar\'ın Bluetooth aygıtlarını bulabilmek için bu izne ihtiyacı var.\n\nLütfen erişim izni vermeyi düşünün. QR kodu QR kodunu tam ekran göster From 47ae59492136d7b463057af3bb029802beb19a65 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Mon, 9 Nov 2020 12:43:29 +0000 Subject: [PATCH 043/582] Update run configurations for Android Studio 4.1. --- .idea/runConfigurations/All_tests.xml | 2 +- .../All_tests_in_bramble_android.xml | 14 ++++---------- .../runConfigurations/All_tests_in_bramble_api.xml | 14 ++++---------- .../All_tests_in_bramble_core.xml | 14 ++++---------- .../All_tests_in_bramble_java.xml | 13 ++++--------- .../All_tests_in_briar_android.xml | 14 ++++---------- .../runConfigurations/All_tests_in_briar_core.xml | 14 ++++---------- .../All_tests_in_briar_headless.xml | 2 +- .idea/runConfigurations/H2_Performance_Test.xml | 14 ++++---------- .../HyperSQL_Performance_Test.xml | 14 ++++---------- .idea/runConfigurations/briar_headless.xml | 9 ++++----- 11 files changed, 38 insertions(+), 86 deletions(-) diff --git a/.idea/runConfigurations/All_tests.xml b/.idea/runConfigurations/All_tests.xml index 102bdba54..7d53e9dcc 100644 --- a/.idea/runConfigurations/All_tests.xml +++ b/.idea/runConfigurations/All_tests.xml @@ -1,6 +1,6 @@ - +

CDN3J4HG%Y_~k*Bl2Ja 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 From 33331dee3ed62a6c95d17e3f41e01fe5b5314b4e Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 10 Nov 2020 17:30:53 +0000 Subject: [PATCH 046/582] Fix some lint errors and warnings. --- .../briar/android/account/AuthorNameFragment.java | 3 +-- .../briar/android/account/DozeFragment.java | 14 +++++--------- .../briar/android/account/SetPasswordFragment.java | 3 +-- .../briar/android/blog/BasePostFragment.java | 3 +-- .../briar/android/blog/BlogFragment.java | 8 ++++---- .../briar/android/blog/FeedFragment.java | 6 +++--- .../briar/android/blog/FeedPostFragment.java | 3 +-- .../briar/android/blog/ReblogFragment.java | 2 +- .../briar/android/contact/ContactListFragment.java | 9 ++++----- .../contact/add/remote/LinkExchangeFragment.java | 9 ++++----- .../contact/add/remote/NicknameFragment.java | 4 +++- .../BaseContactSelectorFragment.java | 5 ++--- .../briar/android/conversation/ImageFragment.java | 8 +++++--- .../briar/android/forum/ForumListFragment.java | 3 +-- .../introduction/ContactChooserFragment.java | 6 ++---- .../introduction/IntroductionMessageFragment.java | 3 +-- .../android/keyagreement/KeyAgreementFragment.java | 5 ++--- .../briar/android/login/OpenDatabaseFragment.java | 4 +++- .../briar/android/login/PasswordFragment.java | 4 +++- .../privategroup/creation/GroupInviteFragment.java | 3 +-- .../privategroup/list/GroupListFragment.java | 3 +-- .../briar/android/reporting/CrashFragment.java | 4 +--- .../android/reporting/ReportFormFragment.java | 4 ++-- .../briar/android/widget/LinkDialogFragment.java | 4 ++-- 24 files changed, 54 insertions(+), 66 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/account/AuthorNameFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/account/AuthorNameFragment.java index 1df351d55..fee24208d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/account/AuthorNameFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/account/AuthorNameFragment.java @@ -17,7 +17,6 @@ import org.briarproject.briar.android.activity.ActivityComponent; import javax.annotation.Nullable; -import static java.util.Objects.requireNonNull; 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; @@ -45,7 +44,7 @@ public class AuthorNameFragment extends SetupFragment { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - requireNonNull(getActivity()).setTitle(getString(R.string.setup_title)); + requireActivity().setTitle(getString(R.string.setup_title)); View v = inflater.inflate(R.layout.fragment_setup_author_name, container, false); authorNameWrapper = v.findViewById(R.id.nickname_entry_wrapper); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/account/DozeFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/account/DozeFragment.java index 454b8bd3a..611cc1e18 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/account/DozeFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/account/DozeFragment.java @@ -20,7 +20,6 @@ import androidx.annotation.Nullable; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; -import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING; import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog; @@ -50,10 +49,10 @@ public class DozeFragment extends SetupFragment public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - requireNonNull(getActivity()).setTitle(getString(R.string.setup_doze_title)); + requireActivity().setTitle(getString(R.string.setup_doze_title)); setHasOptionsMenu(false); View v = inflater.inflate(R.layout.fragment_setup_doze, container, - false); + false); dozeView = v.findViewById(R.id.dozeView); dozeView.setOnCheckedChangedListener(this); huaweiView = v.findViewById(R.id.huaweiView); @@ -78,7 +77,8 @@ public class DozeFragment extends SetupFragment } @Override - public void onActivityResult(int request, int result, Intent data) { + public void onActivityResult(int request, int result, + @Nullable Intent data) { super.onActivityResult(request, result, data); if (request == REQUEST_DOZE_WHITELISTING) { if (!dozeView.needsToBeShown() || secondAttempt) { @@ -92,11 +92,7 @@ public class DozeFragment extends SetupFragment @Override public void onCheckedChanged() { - if (dozeView.isChecked() && huaweiView.isChecked()) { - next.setEnabled(true); - } else { - next.setEnabled(false); - } + next.setEnabled(dozeView.isChecked() && huaweiView.isChecked()); } @SuppressLint("BatteryLife") diff --git a/briar-android/src/main/java/org/briarproject/briar/android/account/SetPasswordFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/account/SetPasswordFragment.java index 854971c4e..19958ad3f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/account/SetPasswordFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/account/SetPasswordFragment.java @@ -24,7 +24,6 @@ import static android.content.Context.INPUT_METHOD_SERVICE; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static java.util.Objects.requireNonNull; import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK; import static org.briarproject.briar.android.util.UiUtils.setError; @@ -55,7 +54,7 @@ public class SetPasswordFragment extends SetupFragment { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - requireNonNull(getActivity()).setTitle(getString(R.string.setup_password_intro)); + requireActivity().setTitle(getString(R.string.setup_password_intro)); View v = inflater.inflate(R.layout.fragment_setup_password, container, false); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java index 35d0ea653..01d40619e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java @@ -25,7 +25,6 @@ import androidx.annotation.UiThread; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; -import static java.util.Objects.requireNonNull; import static java.util.logging.Logger.getLogger; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION; @@ -55,7 +54,7 @@ abstract class BasePostFragment extends BaseFragment { @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { // retrieve MessageId of blog post from arguments - byte[] p = requireNonNull(getArguments()).getByteArray(POST_ID); + byte[] p = requireArguments().getByteArray(POST_ID); if (p == null) throw new IllegalStateException("No post ID in args"); postId = new MessageId(p); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index eb2540677..a9e3da6d3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -46,7 +46,6 @@ import static android.app.Activity.RESULT_OK; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.widget.Toast.LENGTH_SHORT; import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; -import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST; @@ -97,14 +96,14 @@ public class BlogFragment extends BaseFragment public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - Bundle args = requireNonNull(getArguments()); + Bundle args = requireArguments(); byte[] b = args.getByteArray(GROUP_ID); if (b == null) throw new IllegalStateException("No group ID in args"); groupId = new GroupId(b); View v = inflater.inflate(R.layout.fragment_blog, container, false); - adapter = new BlogPostAdapter(requireNonNull(getActivity()), this, + adapter = new BlogPostAdapter(requireActivity(), this, getFragmentManager()); list = v.findViewById(R.id.postList); layoutManager = new LinearLayoutManager(getActivity()); @@ -196,7 +195,8 @@ public class BlogFragment extends BaseFragment } @Override - public void onActivityResult(int request, int result, Intent data) { + public void onActivityResult(int request, int result, + @Nullable Intent data) { super.onActivityResult(request, result, data); if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index 2d01394a3..cbb686e9d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -35,7 +35,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import static android.app.Activity.RESULT_OK; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; -import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST; @@ -79,7 +78,7 @@ public class FeedFragment extends BaseFragment implements public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - requireNonNull(getActivity()).setTitle(R.string.blogs_button); + requireActivity().setTitle(R.string.blogs_button); View v = inflater.inflate(R.layout.fragment_blog, container, false); @@ -103,7 +102,8 @@ public class FeedFragment extends BaseFragment implements } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public void onActivityResult(int requestCode, int resultCode, + @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); // The BlogPostAddedEvent arrives when the controller is not listening diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java index 032445c0e..fb397898d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java @@ -18,7 +18,6 @@ import javax.inject.Inject; import androidx.annotation.UiThread; -import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; @UiThread @@ -54,7 +53,7 @@ public class FeedPostFragment extends BasePostFragment { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - Bundle args = requireNonNull(getArguments()); + Bundle args = requireArguments(); byte[] b = args.getByteArray(GROUP_ID); if (b == null) throw new IllegalStateException("No group ID in args"); blogId = new GroupId(b); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java index 3fcdddaa0..058c09a2e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java @@ -74,7 +74,7 @@ public class ReblogFragment extends BaseFragment implements SendListener { @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - Bundle args = requireNonNull(getArguments()); + Bundle args = requireArguments(); GroupId blogId = new GroupId(requireNonNull(args.getByteArray(GROUP_ID))); MessageId postId = diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java index f76a05cac..46941ffe6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java @@ -62,7 +62,6 @@ import static android.os.Build.VERSION.SDK_INT; import static androidx.core.app.ActivityOptionsCompat.makeSceneTransitionAnimation; import static androidx.core.view.ViewCompat.getTransitionName; import static com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_INDEFINITE; -import static java.util.Objects.requireNonNull; import static java.util.logging.Level.WARNING; import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; @@ -122,7 +121,7 @@ public class ContactListFragment extends BaseFragment implements EventListener, public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - requireNonNull(getActivity()).setTitle(R.string.contact_list_button); + requireActivity().setTitle(R.string.contact_list_button); View contentView = inflater.inflate(R.layout.fragment_contact_list, container, false); @@ -274,8 +273,8 @@ public class ContactListFragment extends BaseFragment implements EventListener, removeItem(((ContactRemovedEvent) e).getContactId()); } else if (e instanceof ConversationMessageReceivedEvent) { LOG.info("Conversation message received, updating item"); - ConversationMessageReceivedEvent p = - (ConversationMessageReceivedEvent) e; + ConversationMessageReceivedEvent p = + (ConversationMessageReceivedEvent) e; ConversationMessageHeader h = p.getMessageHeader(); updateItem(p.getContactId(), h); } else if (e instanceof PendingContactAddedEvent || @@ -317,7 +316,7 @@ public class ContactListFragment extends BaseFragment implements EventListener, @UiThread private void showSnackBar() { if (snackbar != null) return; - View v = requireNonNull(getView()); + View v = requireView(); int stringRes = R.string.pending_contact_requests_snackbar; snackbar = new BriarSnackbarBuilder() .setAction(R.string.show, view -> showPendingContactList()) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java index 2e4cad254..20566613b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java @@ -33,7 +33,6 @@ import androidx.lifecycle.ViewModelProviders; import static android.content.Context.CLIPBOARD_SERVICE; import static android.widget.Toast.LENGTH_SHORT; -import static java.util.Objects.requireNonNull; import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_REGEX; import static org.briarproject.briar.android.util.UiUtils.observeOnce; @@ -83,8 +82,8 @@ public class LinkExchangeFragment extends BaseFragment linkInput.setText(viewModel.getRemoteHandshakeLink()); } - clipboard = (ClipboardManager) requireNonNull( - getContext().getSystemService(CLIPBOARD_SERVICE)); + clipboard = (ClipboardManager) + requireContext().getSystemService(CLIPBOARD_SERVICE); Button pasteButton = v.findViewById(R.id.pasteButton); pasteButton.setOnClickListener(view -> { @@ -107,7 +106,7 @@ public class LinkExchangeFragment extends BaseFragment @Override public void onGlobalLayout() { - ScrollView scrollView = (ScrollView) requireNonNull(getView()); + ScrollView scrollView = (ScrollView) requireView(); View layout = scrollView.getChildAt(0); int scrollBy = layout.getHeight() - scrollView.getHeight(); if (scrollBy > 0) { @@ -121,7 +120,7 @@ public class LinkExchangeFragment extends BaseFragment } private void onHandshakeLinkLoaded(String link) { - View v = requireNonNull(getView()); + View v = requireView(); TextView linkView = v.findViewById(R.id.linkView); linkView.setText(link); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java index aec20a01d..db435c061 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java @@ -31,6 +31,7 @@ import javax.inject.Inject; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog.Builder; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProviders; @@ -117,7 +118,8 @@ public class NicknameFragment extends BaseFragment { addButton.setVisibility(INVISIBLE); progressBar.setVisibility(VISIBLE); - viewModel.getAddContactResult().observe(this, result -> { + LifecycleOwner owner = getViewLifecycleOwner(); + viewModel.getAddContactResult().observe(owner, result -> { if (result == null) return; if (result.hasError()) handleException(name, requireNonNull(result.getException())); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contactselection/BaseContactSelectorFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contactselection/BaseContactSelectorFragment.java index 704bf984c..3c6052c77 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contactselection/BaseContactSelectorFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contactselection/BaseContactSelectorFragment.java @@ -26,7 +26,6 @@ import javax.annotation.Nullable; import androidx.annotation.CallSuper; import androidx.recyclerview.widget.LinearLayoutManager; -import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.contactselection.ContactSelectorActivity.CONTACTS; import static org.briarproject.briar.android.contactselection.ContactSelectorActivity.getContactsFromIds; @@ -55,7 +54,7 @@ public abstract class BaseContactSelectorFragment { + LifecycleOwner owner = getViewLifecycleOwner(); + viewModel.getPasswordValidated().observeEvent(owner, result -> { if (result != SUCCESS) onPasswordInvalid(result); }); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/creation/GroupInviteFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/creation/GroupInviteFragment.java index afc963b83..ba1dda030 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/creation/GroupInviteFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/creation/GroupInviteFragment.java @@ -15,7 +15,6 @@ import javax.inject.Inject; import androidx.annotation.Nullable; -import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; @MethodsNotNullByDefault @@ -43,7 +42,7 @@ public class GroupInviteFragment extends ContactSelectorFragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - requireNonNull(getActivity()).setTitle(R.string.groups_invite_members); + requireActivity().setTitle(R.string.groups_invite_members); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java index fcc3027d6..98cb53f90 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java @@ -39,7 +39,6 @@ import androidx.annotation.UiThread; import androidx.recyclerview.widget.LinearLayoutManager; import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE; -import static java.util.Objects.requireNonNull; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -72,7 +71,7 @@ public class GroupListFragment extends BaseFragment implements @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - requireNonNull(getActivity()).setTitle(R.string.groups_button); + requireActivity().setTitle(R.string.groups_button); View v = inflater.inflate(R.layout.list, container, false); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashFragment.java index beaf7f3c5..892daf0b8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/CrashFragment.java @@ -12,8 +12,6 @@ import org.briarproject.briar.R; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import static java.util.Objects.requireNonNull; - @MethodsNotNullByDefault @ParametersNotNullByDefault public class CrashFragment extends Fragment { @@ -35,7 +33,7 @@ public class CrashFragment extends Fragment { } private DevReportActivity getDevReportActivity() { - return (DevReportActivity) requireNonNull(getActivity()); + return (DevReportActivity) requireActivity(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportFormFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportFormFragment.java index 32edfede3..4dd3f9d2f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportFormFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/ReportFormFragment.java @@ -113,7 +113,7 @@ public class ReportFormFragment extends Fragment report = v.findViewById(R.id.report_content); progress = v.findViewById(R.id.progress_wheel); - Bundle args = requireNonNull(getArguments()); + Bundle args = requireArguments(); isFeedback = args.getBoolean(IS_FEEDBACK); reportFile = (File) requireNonNull(args.getSerializable(EXTRA_REPORT_FILE)); @@ -285,7 +285,7 @@ public class ReportFormFragment extends Fragment } private DevReportActivity getDevReportActivity() { - return (DevReportActivity) requireNonNull(getActivity()); + return (DevReportActivity) requireActivity(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/widget/LinkDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/widget/LinkDialogFragment.java index 791a2601f..481c298a7 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/widget/LinkDialogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/widget/LinkDialogFragment.java @@ -49,7 +49,7 @@ public class LinkDialogFragment extends DialogFragment { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Bundle args = requireNonNull(getArguments()); + Bundle args = requireArguments(); url = requireNonNull(args.getString("url")); setStyle(STYLE_NO_TITLE, R.style.BriarDialogTheme); @@ -70,7 +70,7 @@ public class LinkDialogFragment extends DialogFragment { Context ctx = requireContext(); Intent i = new Intent(ACTION_VIEW, Uri.parse(url)); PackageManager packageManager = ctx.getPackageManager(); - List activities = + List activities = packageManager.queryIntentActivities(i, MATCH_DEFAULT_ONLY); boolean choice = activities.size() > 1; Intent intent = choice ? Intent.createChooser(i, From f3bffb6aa63dd53a771d224dcc6fa2b7d1f818dc Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 10 Nov 2020 17:48:48 +0000 Subject: [PATCH 047/582] Fix some more lint errors. --- .../util/BriarNotificationBuilder.java | 6 ++++-- .../layout-land/fragment_keyagreement_id.xml | 21 +++++++++---------- .../main/res/layout/activity_nav_drawer.xml | 4 ++-- .../res/layout/fragment_keyagreement_id.xml | 4 ++-- .../res/layout/fragment_open_database.xml | 2 +- .../src/main/res/layout/fragment_sign_out.xml | 2 +- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/BriarNotificationBuilder.java b/briar-android/src/main/java/org/briarproject/briar/android/util/BriarNotificationBuilder.java index b3a4daa6a..13d771130 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/BriarNotificationBuilder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/BriarNotificationBuilder.java @@ -11,11 +11,13 @@ import androidx.core.content.ContextCompat; import static android.os.Build.VERSION.SDK_INT; import static androidx.core.app.NotificationCompat.VISIBILITY_PRIVATE; - public class BriarNotificationBuilder extends NotificationCompat.Builder { + private final Context context; + public BriarNotificationBuilder(Context context, String channelId) { super(context, channelId); + this.context = context; // Auto-cancel does not fire the delete intent, see // https://issuetracker.google.com/issues/36961721 setAutoCancel(true); @@ -26,7 +28,7 @@ public class BriarNotificationBuilder extends NotificationCompat.Builder { } public BriarNotificationBuilder setColorRes(@ColorRes int res) { - setColor(ContextCompat.getColor(mContext, res)); + setColor(ContextCompat.getColor(context, res)); return this; } diff --git a/briar-android/src/main/res/layout-land/fragment_keyagreement_id.xml b/briar-android/src/main/res/layout-land/fragment_keyagreement_id.xml index 1f62190f1..91b1da901 100644 --- a/briar-android/src/main/res/layout-land/fragment_keyagreement_id.xml +++ b/briar-android/src/main/res/layout-land/fragment_keyagreement_id.xml @@ -1,9 +1,8 @@ - @@ -22,12 +21,12 @@ android:padding="@dimen/margin_medium" android:scaleType="fitCenter" android:src="@drawable/qr_code_intro" - android:tint="@color/color_primary" app:layout_constraintBottom_toBottomOf="@id/explanationText" app:layout_constraintEnd_toStartOf="@id/explanationText" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:ignore="ContentDescription"/> + app:tint="@color/color_primary" + tools:ignore="ContentDescription" /> + tools:ignore="ContentDescription" /> + app:layout_constraintTop_toBottomOf="@id/explanationImage" /> + app:layout_constraintTop_toTopOf="@id/explanationImage" /> + app:constraint_referenced_ids="diagram,explanationBorder" />