Compare commits

..

1 Commits

27 changed files with 135 additions and 309 deletions

View File

@@ -54,38 +54,6 @@ public interface CryptoComponent {
KeyPair ourKeyPair, byte[]... inputs)
throws GeneralSecurityException;
/**
* Derives a shared secret from two static and two ephemeral key pairs.
* <p>
* Do not use this method for new protocols. The shared secret can be
* re-derived using the ephemeral public keys and both static private
* keys, so keys derived from the shared secret should not be used if
* forward secrecy is required. Use {@link #deriveSharedSecret(String,
* PublicKey, PublicKey, KeyPair, KeyPair, boolean, byte[]...)} instead.
* <p>
* TODO: Remove this after a reasonable migration period (added 2023-03-10).
* <p>
*
* @param label A namespaced label indicating the purpose of this shared
* secret, to prevent it from being repurposed or colliding with a shared
* secret derived for another purpose
* @param theirStaticPublicKey The static public key of the remote party
* @param theirEphemeralPublicKey The ephemeral public key of the remote
* party
* @param ourStaticKeyPair The static key pair of the local party
* @param ourEphemeralKeyPair The ephemeral key pair of the local party
* @param alice True if the local party is Alice
* @param inputs Additional inputs that will be included in the
* derivation of the shared secret
* @return The shared secret
*/
@Deprecated
SecretKey deriveSharedSecretBadly(String label,
PublicKey theirStaticPublicKey, PublicKey theirEphemeralPublicKey,
KeyPair ourStaticKeyPair, KeyPair ourEphemeralKeyPair,
boolean alice, byte[]... inputs)
throws GeneralSecurityException;
/**
* Derives a shared secret from two static and two ephemeral key pairs.
*

View File

@@ -14,16 +14,6 @@ interface HandshakeConstants {
*/
byte PROTOCOL_MINOR_VERSION = 1;
/**
* Label for deriving the master key when using the deprecated v0.0 key
* derivation method.
* <p>
* TODO: Remove this after a reasonable migration period (added 2023-03-10).
*/
@Deprecated
String MASTER_KEY_LABEL_0_0 =
"org.briarproject.bramble.handshake/MASTER_KEY";
/**
* Label for deriving the master key when using the v0.1 key derivation
* method.

View File

@@ -12,20 +12,6 @@ interface HandshakeCrypto {
KeyPair generateEphemeralKeyPair();
/**
* Derives the master key from the given static and ephemeral keys using
* the deprecated v0.0 key derivation method.
* <p>
* TODO: Remove this after a reasonable migration period (added 2023-03-10).
*
* @param alice Whether the local peer is Alice
*/
@Deprecated
SecretKey deriveMasterKey_0_0(PublicKey theirStaticPublicKey,
PublicKey theirEphemeralPublicKey, KeyPair ourStaticKeyPair,
KeyPair ourEphemeralKeyPair, boolean alice)
throws GeneralSecurityException;
/**
* Derives the master key from the given static and ephemeral keys using
* the v0.1 key derivation method.

View File

@@ -13,7 +13,6 @@ import javax.inject.Inject;
import static org.briarproject.bramble.contact.HandshakeConstants.ALICE_PROOF_LABEL;
import static org.briarproject.bramble.contact.HandshakeConstants.BOB_PROOF_LABEL;
import static org.briarproject.bramble.contact.HandshakeConstants.MASTER_KEY_LABEL_0_0;
import static org.briarproject.bramble.contact.HandshakeConstants.MASTER_KEY_LABEL_0_1;
@Immutable
@@ -32,27 +31,6 @@ class HandshakeCryptoImpl implements HandshakeCrypto {
return crypto.generateAgreementKeyPair();
}
@Override
@Deprecated
public SecretKey deriveMasterKey_0_0(PublicKey theirStaticPublicKey,
PublicKey theirEphemeralPublicKey, KeyPair ourStaticKeyPair,
KeyPair ourEphemeralKeyPair, boolean alice) throws
GeneralSecurityException {
byte[] theirStatic = theirStaticPublicKey.getEncoded();
byte[] theirEphemeral = theirEphemeralPublicKey.getEncoded();
byte[] ourStatic = ourStaticKeyPair.getPublic().getEncoded();
byte[] ourEphemeral = ourEphemeralKeyPair.getPublic().getEncoded();
byte[][] inputs = {
alice ? ourStatic : theirStatic,
alice ? theirStatic : ourStatic,
alice ? ourEphemeral : theirEphemeral,
alice ? theirEphemeral : ourEphemeral
};
return crypto.deriveSharedSecretBadly(MASTER_KEY_LABEL_0_0,
theirStaticPublicKey, theirEphemeralPublicKey,
ourStaticKeyPair, ourEphemeralKeyPair, alice, inputs);
}
@Override
public SecretKey deriveMasterKey_0_1(PublicKey theirStaticPublicKey,
PublicKey theirEphemeralPublicKey, KeyPair ourStaticKeyPair,

View File

@@ -2,6 +2,7 @@ package org.briarproject.bramble.contact;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.HandshakeManager;
import org.briarproject.bramble.api.contact.PendingContact;
@@ -111,21 +112,12 @@ class HandshakeManagerImpl implements HandshakeManager {
sendMinorVersion(recordWriter);
sendPublicKey(recordWriter, ourEphemeralKeyPair.getPublic());
}
byte theirMinorVersion = theirMinorVersionAndKey.getFirst();
PublicKey theirEphemeralPublicKey = theirMinorVersionAndKey.getSecond();
SecretKey masterKey;
try {
if (theirMinorVersion > 0) {
masterKey = handshakeCrypto.deriveMasterKey_0_1(
theirStaticPublicKey, theirEphemeralPublicKey,
ourStaticKeyPair, ourEphemeralKeyPair, alice);
} else {
// TODO: Remove this branch after a reasonable migration
// period (added 2023-03-10).
masterKey = handshakeCrypto.deriveMasterKey_0_0(
theirStaticPublicKey, theirEphemeralPublicKey,
ourStaticKeyPair, ourEphemeralKeyPair, alice);
}
masterKey = handshakeCrypto.deriveMasterKey_0_1(
theirStaticPublicKey, theirEphemeralPublicKey,
ourStaticKeyPair, ourEphemeralKeyPair, alice);
} catch (GeneralSecurityException e) {
throw new FormatException();
}
@@ -187,10 +179,11 @@ class HandshakeManagerImpl implements HandshakeManager {
} else {
// The remote peer did not send a minor version record, so the
// remote peer's protocol minor version is assumed to be zero
// TODO: Remove this branch after a reasonable migration period
// (added 2023-03-10).
theirMinorVersion = 0;
theirEphemeralPublicKey = parsePublicKey(first);
// TODO: How communicate to user that contact seems to use a version
// of Briar that is too old? (be aware of MITM attacks)
// `RendezvousPollerImpl` broadcasts PendingContactState FAILED via EventBus
throw new UnsupportedVersionException(true);
}
return new Pair<>(theirMinorVersion, theirEphemeralPublicKey);
}

View File

@@ -222,36 +222,6 @@ class CryptoComponentImpl implements CryptoComponent {
return new SecretKey(hash);
}
@Override
@Deprecated
public SecretKey deriveSharedSecretBadly(String label,
PublicKey theirStaticPublicKey, PublicKey theirEphemeralPublicKey,
KeyPair ourStaticKeyPair, KeyPair ourEphemeralKeyPair,
boolean alice, byte[]... inputs) throws GeneralSecurityException {
PrivateKey ourStaticPrivateKey = ourStaticKeyPair.getPrivate();
PrivateKey ourEphemeralPrivateKey = ourEphemeralKeyPair.getPrivate();
byte[][] hashInputs = new byte[inputs.length + 3][];
// Alice static/Bob static
hashInputs[0] = performRawKeyAgreement(ourStaticPrivateKey,
theirStaticPublicKey);
// Alice static/Bob ephemeral, Bob static/Alice ephemeral
if (alice) {
hashInputs[1] = performRawKeyAgreement(ourStaticPrivateKey,
theirEphemeralPublicKey);
hashInputs[2] = performRawKeyAgreement(ourEphemeralPrivateKey,
theirStaticPublicKey);
} else {
hashInputs[1] = performRawKeyAgreement(ourEphemeralPrivateKey,
theirStaticPublicKey);
hashInputs[2] = performRawKeyAgreement(ourStaticPrivateKey,
theirEphemeralPublicKey);
}
arraycopy(inputs, 0, hashInputs, 3, inputs.length);
byte[] hash = hash(label, hashInputs);
if (hash.length != SecretKey.LENGTH) throw new IllegalStateException();
return new SecretKey(hash);
}
@Override
public SecretKey deriveSharedSecret(String label,
PublicKey theirStaticPublicKey, PublicKey theirEphemeralPublicKey,

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.contact;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.HandshakeManager.HandshakeResult;
import org.briarproject.bramble.api.contact.PendingContact;
@@ -27,6 +28,7 @@ import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
@@ -123,12 +125,12 @@ public class HandshakeManagerImplTest extends BrambleMockTestCase {
assertEquals(alice, result.isAlice());
}
@Test
@Test(expected = UnsupportedVersionException.class)
public void testHandshakeAsAliceWithPeerVersion_0_0() throws Exception {
testHandshakeWithPeerVersion_0_0(true);
}
@Test
@Test(expected = UnsupportedVersionException.class)
public void testHandshakeAsBobWithPeerVersion_0_0() throws Exception {
testHandshakeWithPeerVersion_0_0(false);
}
@@ -140,20 +142,8 @@ public class HandshakeManagerImplTest extends BrambleMockTestCase {
expectSendKey();
// Remote peer does not send minor version, so use old key derivation
expectReceiveKey();
expectDeriveMasterKey_0_0(alice);
expectDeriveProof(alice);
expectSendProof();
expectReceiveProof();
expectSendEof();
expectReceiveEof();
expectVerifyOwnership(alice, true);
HandshakeResult result = handshakeManager.handshake(
pendingContact.getId(), in, streamWriter);
assertArrayEquals(masterKey.getBytes(),
result.getMasterKey().getBytes());
assertEquals(alice, result.isAlice());
handshakeManager.handshake(pendingContact.getId(), in, streamWriter);
}
@Test(expected = FormatException.class)
@@ -241,15 +231,6 @@ public class HandshakeManagerImplTest extends BrambleMockTestCase {
}});
}
private void expectDeriveMasterKey_0_0(boolean alice) throws Exception {
context.checking(new Expectations() {{
oneOf(handshakeCrypto).deriveMasterKey_0_0(theirStaticPublicKey,
theirEphemeralPublicKey, ourStaticKeyPair,
ourEphemeralKeyPair, alice);
will(returnValue(masterKey));
}});
}
private void expectDeriveProof(boolean alice) {
context.checking(new Expectations() {{
oneOf(handshakeCrypto).proveOwnership(masterKey, alice);

View File

@@ -60,22 +60,6 @@ public class KeyAgreementTest extends BrambleTestCase {
assertArrayEquals(aShared.getBytes(), bShared.getBytes());
}
@Test
public void testDerivesStaticEphemeralSharedSecretBadly() throws Exception {
String label = getRandomString(123);
KeyPair aStatic = crypto.generateAgreementKeyPair();
KeyPair aEphemeral = crypto.generateAgreementKeyPair();
KeyPair bStatic = crypto.generateAgreementKeyPair();
KeyPair bEphemeral = crypto.generateAgreementKeyPair();
SecretKey aShared = crypto.deriveSharedSecretBadly(label,
bStatic.getPublic(), bEphemeral.getPublic(), aStatic,
aEphemeral, true, inputs);
SecretKey bShared = crypto.deriveSharedSecretBadly(label,
aStatic.getPublic(), aEphemeral.getPublic(), bStatic,
bEphemeral, false, inputs);
assertArrayEquals(aShared.getBytes(), bShared.getBytes());
}
@Test
public void testDerivesStaticEphemeralSharedSecret() throws Exception {
String label = getRandomString(123);

View File

@@ -54,7 +54,7 @@ public class ForumActivity extends
@Override
protected ThreadItemAdapter<ForumPostItem> createAdapter() {
return new ThreadItemAdapter<>(this, this);
return new ThreadItemAdapter<>(this);
}
@Override

View File

@@ -6,10 +6,10 @@ import org.briarproject.briar.api.forum.ForumPostHeader;
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
public class ForumPostItem extends ThreadItem {
class ForumPostItem extends ThreadItem {
ForumPostItem(ForumPostHeader h) {
super(h.getId(), h.getParentId(), h.getTimestamp(), h.getAuthor(),
ForumPostItem(ForumPostHeader h, String text) {
super(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
h.getAuthorInfo(), h.isRead());
}

View File

@@ -91,7 +91,9 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
if (f.getGroupId().equals(groupId)) {
LOG.info("Forum post received, adding...");
addItem(new ForumPostItem(f.getHeader()), false);
ForumPostItem item =
new ForumPostItem(f.getHeader(), f.getText());
addItem(item, false);
}
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
ForumInvitationResponseReceivedEvent f =
@@ -137,14 +139,22 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
List<ForumPostHeader> headers =
forumManager.getPostHeaders(txn, groupId);
logDuration(LOG, "Loading headers", start);
start = now();
List<ForumPostItem> items = new ArrayList<>();
for (ForumPostHeader header : headers) {
items.add(new ForumPostItem(header));
items.add(loadItem(txn, header));
}
logDuration(LOG, "Loading bodies and creating items", start);
return items;
}, this::setItems);
}
private ForumPostItem loadItem(Transaction txn, ForumPostHeader header)
throws DbException {
String text = forumManager.getPostText(txn, header.getId());
return new ForumPostItem(header, text);
}
@Override
public void createAndStoreMessage(String text,
@Nullable MessageId parentId) {
@@ -165,17 +175,21 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
@Nullable MessageId parentId, LocalAuthor author) {
cryptoExecutor.execute(() -> {
LOG.info("Creating forum post...");
storePost(forumManager.createLocalPost(groupId, text,
timestamp, parentId, author));
ForumPost msg = forumManager.createLocalPost(groupId, text,
timestamp, parentId, author);
storePost(msg, text);
});
}
private void storePost(ForumPost msg) {
private void storePost(ForumPost msg, String text) {
runOnDbThread(false, txn -> {
long start = now();
ForumPostHeader header = forumManager.addLocalPost(txn, msg);
logDuration(LOG, "Storing forum post", start);
txn.attach(() -> addItem(new ForumPostItem(header), true));
txn.attach(() -> {
ForumPostItem item = new ForumPostItem(header, text);
addItem(item, true);
});
}, this::handleException);
}
@@ -215,9 +229,4 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
});
}
@Override
protected String getMessageText(Transaction txn, MessageId m)
throws DbException {
return forumManager.getPostText(txn, m);
}
}

View File

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

View File

@@ -11,19 +11,16 @@ import org.briarproject.briar.android.threaded.ThreadPostViewHolder;
import org.briarproject.nullsafety.NotNullByDefault;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import androidx.lifecycle.LifecycleOwner;
@UiThread
@NotNullByDefault
public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
private boolean isCreator = false;
GroupMessageAdapter(LifecycleOwner lifecycleOwner,
ThreadItemListener<GroupMessageItem> listener) {
super(lifecycleOwner, listener);
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener) {
super(listener);
}
@LayoutRes
@@ -33,7 +30,6 @@ public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
return item.getLayout();
}
@NonNull
@Override
public BaseThreadItemViewHolder<GroupMessageItem> onCreateViewHolder(
ViewGroup parent, int type) {

View File

@@ -1,10 +1,14 @@
package org.briarproject.briar.android.privategroup.conversation;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.threaded.ThreadItem;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
@@ -12,14 +16,20 @@ import androidx.annotation.UiThread;
@UiThread
@NotThreadSafe
public class GroupMessageItem extends ThreadItem {
class GroupMessageItem extends ThreadItem {
private final GroupId groupId;
GroupMessageItem(GroupMessageHeader h) {
super(h.getId(), h.getParentId(), h.getTimestamp(), h.getAuthor(),
h.getAuthorInfo(), h.isRead());
this.groupId = h.getGroupId();
private GroupMessageItem(MessageId messageId, GroupId groupId,
@Nullable MessageId parentId, String text, long timestamp,
Author author, AuthorInfo authorInfo, boolean isRead) {
super(messageId, parentId, text, timestamp, author, authorInfo, isRead);
this.groupId = groupId;
}
GroupMessageItem(GroupMessageHeader h, String text) {
this(h.getId(), h.getGroupId(), h.getParentId(), text, h.getTimestamp(),
h.getAuthor(), h.getAuthorInfo(), h.isRead());
}
public GroupId getGroupId() {

View File

@@ -99,7 +99,7 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
// only act on non-local messages in this group
if (!g.isLocal() && g.getGroupId().equals(groupId)) {
LOG.info("Group message received, adding...");
GroupMessageItem item = buildItem(g.getHeader());
GroupMessageItem item = buildItem(g.getHeader(), g.getText());
addItem(item, false);
// In case the join message comes from the creator,
// we need to reload the sharing contacts
@@ -167,19 +167,33 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
List<GroupMessageHeader> headers =
privateGroupManager.getHeaders(txn, groupId);
logDuration(LOG, "Loading headers", start);
start = now();
List<GroupMessageItem> items = new ArrayList<>();
for (GroupMessageHeader header : headers) {
items.add(buildItem(header));
items.add(loadItem(txn, header));
}
logDuration(LOG, "Loading bodies and creating items", start);
return items;
}, this::setItems);
}
private GroupMessageItem buildItem(GroupMessageHeader header) {
private GroupMessageItem loadItem(Transaction txn,
GroupMessageHeader header) throws DbException {
String text;
if (header instanceof JoinMessageHeader) {
return new JoinMessageItem((JoinMessageHeader) header);
// will be looked up later
text = "";
} else {
text = privateGroupManager.getMessageText(txn, header.getId());
}
return new GroupMessageItem(header);
return buildItem(header, text);
}
private GroupMessageItem buildItem(GroupMessageHeader header, String text) {
if (header instanceof JoinMessageHeader) {
return new JoinMessageItem((JoinMessageHeader) header, text);
}
return new GroupMessageItem(header, text);
}
@Override
@@ -207,17 +221,19 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
LOG.info("Creating group message...");
GroupMessage msg = groupMessageFactory.createGroupMessage(groupId,
timestamp, parentId, author, text, previousMsgId);
storePost(msg);
storePost(msg, text);
});
}
private void storePost(GroupMessage msg) {
private void storePost(GroupMessage msg, String text) {
runOnDbThread(false, txn -> {
long start = now();
GroupMessageHeader header =
privateGroupManager.addLocalMessage(txn, msg);
logDuration(LOG, "Storing group message", start);
txn.attach(() -> addItem(buildItem(header), true));
txn.attach(() ->
addItem(buildItem(header, text), true)
);
}, this::handleException);
}
@@ -268,9 +284,4 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
return isDissolved;
}
@Override
protected String getMessageText(Transaction txn, MessageId m)
throws DbException {
return privateGroupManager.getMessageText(txn, m);
}
}

View File

@@ -14,8 +14,8 @@ class JoinMessageItem extends GroupMessageItem {
private final boolean isInitial;
JoinMessageItem(JoinMessageHeader h) {
super(h);
JoinMessageItem(JoinMessageHeader h, String text) {
super(h, text);
isInitial = h.isInitial();
}

View File

@@ -9,7 +9,6 @@ import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListe
import org.briarproject.nullsafety.NotNullByDefault;
import androidx.annotation.UiThread;
import androidx.lifecycle.LifecycleOwner;
import static org.briarproject.briar.api.identity.AuthorInfo.Status.OURSELVES;
@@ -26,8 +25,10 @@ class JoinMessageItemViewHolder
}
@Override
protected void setText(GroupMessageItem item, LifecycleOwner lifecycleOwner,
public void bind(GroupMessageItem item,
ThreadItemListener<GroupMessageItem> listener) {
super.bind(item, listener);
if (isCreator) bindForCreator((JoinMessageItem) item);
else bind((JoinMessageItem) item);
}

View File

@@ -4,46 +4,35 @@ import android.animation.Animator;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.text.util.Linkify;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.widget.TextView;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
import org.briarproject.briar.android.view.AuthorView;
import org.briarproject.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import androidx.annotation.CallSuper;
import androidx.annotation.UiThread;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.RecyclerView;
import static android.text.util.Linkify.WEB_URLS;
import static android.text.util.Linkify.addLinks;
import static androidx.core.content.ContextCompat.getColor;
import static org.briarproject.bramble.util.StringUtils.trim;
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
import static org.briarproject.nullsafety.NullSafety.requireNonNull;
@UiThread
@NotNullByDefault
public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
extends RecyclerView.ViewHolder implements Observer<String> {
extends RecyclerView.ViewHolder {
private final static int ANIMATION_DURATION = 5000;
protected final TextView textView;
private final ViewGroup layout;
private final AuthorView author;
@Nullable
private ThreadItemListener<I> listener = null;
@Nullable
private LiveData<String> textLiveData = null;
public BaseThreadItemViewHolder(View v) {
super(v);
@@ -54,9 +43,10 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
}
@CallSuper
public void bind(I item, LifecycleOwner lifecycleOwner,
ThreadItemListener<I> listener) {
setText(item, lifecycleOwner, listener);
public void bind(I item, ThreadItemListener<I> listener) {
textView.setText(StringUtils.trim(item.getText()));
Linkify.addLinks(textView, Linkify.WEB_URLS);
makeLinksClickable(textView, listener::onLinkClick);
author.setAuthor(item.getAuthor(), item.getAuthorInfo());
author.setDate(item.getTimestamp());
@@ -71,20 +61,6 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
}
}
protected void setText(I item, LifecycleOwner lifecycleOwner,
ThreadItemListener<I> listener) {
// Clear any existing text while we asynchronously load the new text
textView.setText(null);
// Remember the listener so we can use it to create links later
this.listener = listener;
// If the view has been re-bound and we're already asynchronously
// loading text for another item, stop observing it
if (textLiveData != null) textLiveData.removeObserver(this);
// Asynchronously load the text for this item and observe the result
textLiveData = listener.loadItemText(item.getId());
textLiveData.observe(lifecycleOwner, this);
}
private void animateFadeOut() {
setIsRecyclable(false);
ValueAnimator anim = new ValueAnimator();
@@ -97,7 +73,6 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
layout.setBackgroundResource(
@@ -105,11 +80,9 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
layout.setActivated(false);
setIsRecyclable(true);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@@ -124,24 +97,4 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
return textView.getContext();
}
void onViewRecycled() {
textView.setText(null);
if (textLiveData != null) {
textLiveData.removeObserver(this);
textLiveData = null;
listener = null;
}
}
@Override
public void onChanged(String s) {
if (textLiveData != null) {
textLiveData.removeObserver(this);
textLiveData = null;
textView.setText(trim(s));
addLinks(textView, WEB_URLS);
makeLinksClickable(textView, requireNonNull(listener)::onLinkClick);
listener = null;
}
}
}

View File

@@ -19,6 +19,7 @@ public abstract class ThreadItem implements MessageNode {
private final MessageId messageId;
@Nullable
private final MessageId parentId;
private final String text;
private final long timestamp;
private final Author author;
private final AuthorInfo authorInfo;
@@ -26,10 +27,11 @@ public abstract class ThreadItem implements MessageNode {
private boolean isRead, highlighted;
public ThreadItem(MessageId messageId, @Nullable MessageId parentId,
long timestamp, Author author, AuthorInfo authorInfo,
String text, long timestamp, Author author, AuthorInfo authorInfo,
boolean isRead) {
this.messageId = messageId;
this.parentId = parentId;
this.text = text;
this.timestamp = timestamp;
this.author = author;
this.authorInfo = authorInfo;
@@ -37,6 +39,10 @@ public abstract class ThreadItem implements MessageNode {
this.highlighted = false;
}
public String getText() {
return text;
}
public int getLevel() {
return level;
}

View File

@@ -13,8 +13,6 @@ import javax.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.ListAdapter;
@@ -29,11 +27,9 @@ public class ThreadItemAdapter<I extends ThreadItem>
static final int UNDEFINED = -1;
private final LifecycleOwner lifecycleOwner;
private final ThreadItemListener<I> listener;
public ThreadItemAdapter(LifecycleOwner lifecycleOwner,
ThreadItemListener<I> listener) {
public ThreadItemAdapter(ThreadItemListener<I> listener) {
super(new DiffUtil.ItemCallback<I>() {
@Override
public boolean areItemsTheSame(I a, I b) {
@@ -46,7 +42,6 @@ public class ThreadItemAdapter<I extends ThreadItem>
a.isRead() == b.isRead();
}
});
this.lifecycleOwner = lifecycleOwner;
this.listener = listener;
}
@@ -63,7 +58,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
public void onBindViewHolder(@NonNull BaseThreadItemViewHolder<I> ui,
int position) {
I item = getItem(position);
ui.bind(item, lifecycleOwner, listener);
ui.bind(item, listener);
}
int findItemPosition(MessageId id) {
@@ -140,19 +135,9 @@ public class ThreadItemAdapter<I extends ThreadItem>
return getItem(position);
}
@Override
public void onViewRecycled(BaseThreadItemViewHolder<I> viewHolder) {
super.onViewRecycled(viewHolder);
viewHolder.onViewRecycled();
}
public interface ThreadItemListener<I> {
void onReplyClick(I item);
void onLinkClick(String url);
LiveData<String> loadItemText(MessageId m);
}
}

View File

@@ -85,9 +85,6 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
scrollListener = new ThreadScrollListener<>(adapter, viewModel,
upButton, downButton);
list.getRecyclerView().addOnScrollListener(scrollListener);
// This is a tradeoff between memory consumption for cached views
// and the cost of loading message text from the database
list.getRecyclerView().setItemViewCacheSize(20);
upButton.setOnClickListener(v -> {
int position = adapter.getVisibleUnreadPosTop(layoutManager);
@@ -260,8 +257,4 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
protected abstract int getMaxTextLength();
@Override
public LiveData<String> loadItemText(MessageId m) {
return getViewModel().loadMessageText(m);
}
}

View File

@@ -6,7 +6,6 @@ import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
@@ -261,14 +260,4 @@ public abstract class ThreadListViewModel<I extends ThreadItem>
return scrollToItem.getAndSet(null);
}
public LiveData<String> loadMessageText(MessageId m) {
MutableLiveData<String> textLiveData = new MutableLiveData<>();
runOnDbThread(true, txn ->
textLiveData.postValue(getMessageText(txn, m)),
this::handleException);
return textLiveData;
}
protected abstract String getMessageText(Transaction txn, MessageId m)
throws DbException;
}

View File

@@ -10,7 +10,6 @@ import org.briarproject.nullsafety.NotNullByDefault;
import java.util.Locale;
import androidx.annotation.UiThread;
import androidx.lifecycle.LifecycleOwner;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
@@ -41,9 +40,8 @@ public class ThreadPostViewHolder<I extends ThreadItem>
}
@Override
public void bind(I item, LifecycleOwner lifecycleOwner,
ThreadItemListener<I> listener) {
super.bind(item, lifecycleOwner, listener);
public void bind(I item, ThreadItemListener<I> listener) {
super.bind(item, listener);
for (int i = 0; i < lvls.length; i++) {
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);

View File

@@ -16,10 +16,13 @@ public class ForumPostReceivedEvent extends Event {
private final GroupId groupId;
private final ForumPostHeader header;
private final String text;
public ForumPostReceivedEvent(GroupId groupId, ForumPostHeader header) {
public ForumPostReceivedEvent(GroupId groupId, ForumPostHeader header,
String text) {
this.groupId = groupId;
this.header = header;
this.text = text;
}
public GroupId getGroupId() {
@@ -29,4 +32,8 @@ public class ForumPostReceivedEvent extends Event {
public ForumPostHeader getHeader() {
return header;
}
public String getText() {
return text;
}
}

View File

@@ -17,12 +17,14 @@ public class GroupMessageAddedEvent extends Event {
private final GroupId groupId;
private final GroupMessageHeader header;
private final String text;
private final boolean local;
public GroupMessageAddedEvent(GroupId groupId, GroupMessageHeader header,
boolean local) {
String text, boolean local) {
this.groupId = groupId;
this.header = header;
this.text = text;
this.local = local;
}
@@ -34,6 +36,10 @@ public class GroupMessageAddedEvent extends Event {
return header;
}
public String getText() {
return text;
}
public boolean isLocal() {
return local;
}

View File

@@ -83,7 +83,10 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
messageTracker.trackIncomingMessage(txn, m);
ForumPostHeader header = getForumPostHeader(txn, m.getId(), meta);
txn.attach(new ForumPostReceivedEvent(m.getGroupId(), header));
String text = getPostText(body);
ForumPostReceivedEvent event =
new ForumPostReceivedEvent(m.getGroupId(), header, text);
txn.attach(event);
return ACCEPT_SHARE;
}

View File

@@ -170,7 +170,6 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
txn -> getPreviousMsgId(txn, g));
}
@Override
public MessageId getPreviousMsgId(Transaction txn, GroupId g)
throws DbException {
try {
@@ -606,7 +605,9 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
throws DbException, FormatException {
GroupMessageHeader header = getGroupMessageHeader(txn, m.getGroupId(),
m.getId(), meta, Collections.emptyMap());
txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, local));
String text = getMessageText(clientHelper.toList(m));
txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, text,
local));
}
private void attachJoinMessageAddedEvent(Transaction txn, Message m,
@@ -614,7 +615,8 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
throws DbException, FormatException {
JoinMessageHeader header = getJoinMessageHeader(txn, m.getGroupId(),
m.getId(), meta, Collections.emptyMap());
txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, local));
txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, "",
local));
}
private void addMember(Transaction txn, GroupId g, Author a, Visibility v)