Compare commits

..

12 Commits

Author SHA1 Message Date
akwizgran
35d035b19f Remove observer when load completes or is cancelled. 2025-05-02 15:03:20 +01:00
akwizgran
9ecac899fb Reset bound message ID when view is recycled. 2025-05-02 14:35:46 +01:00
akwizgran
16104b84b2 Load private group text lazily too, what the heck. 2025-05-02 14:35:46 +01:00
akwizgran
1542be20db Load forum post text lazily. 2025-05-02 14:35:41 +01:00
akwizgran
070a0181d9 Merge branch 'remove-forum-without-opening' into 'master'
Allow forums to be removed without opening them

See merge request briar/briar!1841
2025-05-01 08:24:38 +00:00
akwizgran
d83ae3a3b4 Use long click to open menu, clean up some cruft. 2025-04-30 15:25:14 +01:00
akwizgran
143f04bf1b Merge branch 'mark-db-clean-after-compacting' into 'master'
Mark DB as clean after compacting, keep foreground service until shutdown completes

See merge request briar/briar!1842
2025-04-30 10:30:48 +00:00
akwizgran
138fa6f39d Allow forums to be removed without opening them. 2025-04-29 10:17:40 +01:00
akwizgran
8e1371acf0 Keep foreground service until lifecycle shutdown completes.
This ensures our background threads keep running.
2025-04-29 10:17:09 +01:00
akwizgran
29f0b9d3c0 Mark DB as clean after compacting.
This ensures we compact the DB at the next startup if we didn't finish
compacting it at shutdown.
2025-04-29 10:17:09 +01:00
akwizgran
eb45ccfe9e Merge branch 'fix-problem-from-recent-fix-for-annotations-processor' into 'master'
Fix problem in AS after 8962fefd

See merge request briar/briar!1840
2025-04-29 08:49:15 +00:00
Sebastian Kürten
e98b5a9882 Fix problem in AS after 8962fefd
Commit 8962fefd introduced a problem while loading the project into
Android Studio. Apparently the fix from that commit did not handly
updated types of the more recent Gradle API. This update should fix it.
2025-04-03 08:30:16 +02:00
29 changed files with 230 additions and 152 deletions

View File

@@ -89,11 +89,17 @@ class H2Database extends JdbcDatabase {
try { try {
c = createConnection(); c = createConnection();
closeAllConnections(); closeAllConnections();
setDirty(c, false); LOG.info("Compacting DB");
s = c.createStatement(); s = c.createStatement();
s.execute("SHUTDOWN COMPACT"); s.execute("SHUTDOWN COMPACT");
LOG.info("Finished compacting DB");
s.close(); s.close();
c.close(); c.close();
// Reopen the DB to mark it as clean after compacting
c = createConnection();
setDirty(c, false);
LOG.info("Marked DB as clean");
c.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
tryToClose(c, LOG, WARNING); tryToClose(c, LOG, WARNING);
@@ -126,6 +132,7 @@ class H2Database extends JdbcDatabase {
closeAllConnections(); closeAllConnections();
s = c.createStatement(); s = c.createStatement();
s.execute("SHUTDOWN COMPACT"); s.execute("SHUTDOWN COMPACT");
LOG.info("Finished compacting DB");
s.close(); s.close();
c.close(); c.close();
} catch (SQLException e) { } catch (SQLException e) {

View File

@@ -217,19 +217,14 @@ public class BriarService extends Service {
@Override @Override
public void onDestroy() { public void onDestroy() {
// Hold a wake lock during shutdown super.onDestroy();
wakeLockManager.runWakefully(() -> { LOG.info("Destroyed");
super.onDestroy(); // Stop the lifecycle, if not already stopped
LOG.info("Destroyed"); shutdown(false);
stopForeground(true); stopForeground(true);
if (receiver != null) { if (receiver != null) {
getApplicationContext().unregisterReceiver(receiver); getApplicationContext().unregisterReceiver(receiver);
} }
// Stop the services in a background thread
wakeLockManager.executeWakefully(() -> {
if (started) lifecycleManager.stopServices();
}, "LifecycleShutdown");
}, "LifecycleShutdown");
} }
@Override @Override
@@ -299,8 +294,8 @@ public class BriarService extends Service {
private void shutdownFromBackground() { private void shutdownFromBackground() {
// Hold a wake lock during shutdown // Hold a wake lock during shutdown
wakeLockManager.runWakefully(() -> { wakeLockManager.runWakefully(() -> {
// Stop the service // Begin lifecycle shutdown
stopSelf(); shutdown(true);
// Hide the UI // Hide the UI
hideUi(); hideUi();
// Wait for shutdown to complete, then exit // Wait for shutdown to complete, then exit
@@ -335,8 +330,18 @@ public class BriarService extends Service {
/** /**
* Starts the shutdown process. * Starts the shutdown process.
*/ */
public void shutdown() { public void shutdown(boolean stopAndroidService) {
stopSelf(); // This will call onDestroy() // Hold a wake lock during shutdown
wakeLockManager.runWakefully(() -> {
// Stop the lifecycle services in a background thread,
// then stop this Android service if needed
wakeLockManager.executeWakefully(() -> {
if (started) lifecycleManager.stopServices();
if (stopAndroidService) {
androidExecutor.runOnUiThread(() -> stopSelf());
}
}, "LifecycleShutdown");
}, "LifecycleShutdown");
} }
public class BriarBinder extends Binder { public class BriarBinder extends Binder {

View File

@@ -147,7 +147,7 @@ public class BriarControllerImpl implements BriarController {
service.waitForStartup(); service.waitForStartup();
// Shut down the service and wait for it to shut down // Shut down the service and wait for it to shut down
LOG.info("Shutting down service"); LOG.info("Shutting down service");
service.shutdown(); service.shutdown(true);
service.waitForShutdown(); service.waitForShutdown();
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOG.warning("Interrupted while waiting for service"); LOG.warning("Interrupted while waiting for service");

View File

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

View File

@@ -13,15 +13,18 @@ import androidx.recyclerview.widget.ListAdapter;
@NotNullByDefault @NotNullByDefault
class ForumListAdapter extends ListAdapter<ForumListItem, ForumViewHolder> { class ForumListAdapter extends ListAdapter<ForumListItem, ForumViewHolder> {
ForumListAdapter() { private final ForumListViewModel viewModel;
ForumListAdapter(ForumListViewModel viewModel) {
super(new ForumListCallback()); super(new ForumListCallback());
this.viewModel = viewModel;
} }
@Override @Override
public ForumViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public ForumViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate( View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.list_item_forum, parent, false); R.layout.list_item_forum, parent, false);
return new ForumViewHolder(v); return new ForumViewHolder(v, viewModel);
} }
@Override @Override

View File

@@ -40,7 +40,7 @@ public class ForumListFragment extends BaseFragment implements
private ForumListViewModel viewModel; private ForumListViewModel viewModel;
private BriarRecyclerView list; private BriarRecyclerView list;
private Snackbar snackbar; private Snackbar snackbar;
private final ForumListAdapter adapter = new ForumListAdapter(); private ForumListAdapter adapter;
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
@@ -54,6 +54,7 @@ public class ForumListFragment extends BaseFragment implements
component.inject(this); component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory) viewModel = new ViewModelProvider(this, viewModelFactory)
.get(ForumListViewModel.class); .get(ForumListViewModel.class);
adapter = new ForumListAdapter(viewModel);
} }
@Nullable @Nullable

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.forum; package org.briarproject.briar.android.forum;
import android.app.Application; import android.app.Application;
import android.widget.Toast;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
@@ -15,6 +16,7 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.event.GroupAddedEvent; import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.viewmodel.DbViewModel; import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveResult; import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
@@ -40,6 +42,7 @@ import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
@@ -180,4 +183,17 @@ class ForumListViewModel extends DbViewModel implements EventListener {
return numInvitations; return numInvitations;
} }
void deleteForum(GroupId groupId) {
runOnDbThread(() -> {
try {
Forum f = forumManager.getForum(groupId);
forumManager.removeForum(f);
androidExecutor.runOnUiThread(() -> Toast
.makeText(getApplication(), R.string.forum_left_toast,
LENGTH_SHORT).show());
} catch (DbException e) {
handleException(e);
}
});
}
} }

View File

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

View File

@@ -4,6 +4,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.PopupMenu;
import android.widget.TextView; import android.widget.TextView;
import org.briarproject.briar.R; import org.briarproject.briar.R;
@@ -20,6 +21,7 @@ import static org.briarproject.briar.android.activity.BriarActivity.GROUP_NAME;
class ForumViewHolder extends RecyclerView.ViewHolder { class ForumViewHolder extends RecyclerView.ViewHolder {
private final ForumListViewModel viewModel;
private final Context ctx; private final Context ctx;
private final ViewGroup layout; private final ViewGroup layout;
private final TextAvatarView avatar; private final TextAvatarView avatar;
@@ -27,8 +29,9 @@ class ForumViewHolder extends RecyclerView.ViewHolder {
private final TextView postCount; private final TextView postCount;
private final TextView date; private final TextView date;
ForumViewHolder(View v) { ForumViewHolder(View v, ForumListViewModel viewModel) {
super(v); super(v);
this.viewModel = viewModel;
ctx = v.getContext(); ctx = v.getContext();
layout = (ViewGroup) v; layout = (ViewGroup) v;
avatar = v.findViewById(R.id.avatarView); avatar = v.findViewById(R.id.avatarView);
@@ -64,6 +67,21 @@ class ForumViewHolder extends RecyclerView.ViewHolder {
date.setVisibility(VISIBLE); date.setVisibility(VISIBLE);
} }
// Open popup menu on long click
layout.setOnLongClickListener(v -> {
PopupMenu pm = new PopupMenu(ctx, v);
pm.getMenuInflater().inflate(R.menu.forum_list_item_actions,
pm.getMenu());
pm.setOnMenuItemClickListener(it -> {
if (it.getItemId() == R.id.action_forum_delete) {
viewModel.deleteForum(item.getForum().getId());
}
return true;
});
pm.show();
return true;
});
// Open Forum on Click // Open Forum on Click
layout.setOnClickListener(v -> { layout.setOnClickListener(v -> {
Intent i = new Intent(ctx, ForumActivity.class); Intent i = new Intent(ctx, ForumActivity.class);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,35 +4,46 @@ import android.animation.Animator;
import android.animation.ArgbEvaluator; import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.content.Context; import android.content.Context;
import android.text.util.Linkify;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator; import android.view.animation.AccelerateInterpolator;
import android.widget.TextView; import android.widget.TextView;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener; import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
import org.briarproject.briar.android.view.AuthorView; import org.briarproject.briar.android.view.AuthorView;
import org.briarproject.nullsafety.NotNullByDefault; import org.briarproject.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.RecyclerView; 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 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.briar.android.util.UiUtils.makeLinksClickable;
import static org.briarproject.nullsafety.NullSafety.requireNonNull;
@UiThread @UiThread
@NotNullByDefault @NotNullByDefault
public abstract class BaseThreadItemViewHolder<I extends ThreadItem> public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
extends RecyclerView.ViewHolder { extends RecyclerView.ViewHolder implements Observer<String> {
private final static int ANIMATION_DURATION = 5000; private final static int ANIMATION_DURATION = 5000;
protected final TextView textView; protected final TextView textView;
private final ViewGroup layout; private final ViewGroup layout;
private final AuthorView author; private final AuthorView author;
@Nullable
private ThreadItemListener<I> listener = null;
@Nullable
private LiveData<String> textLiveData = null;
public BaseThreadItemViewHolder(View v) { public BaseThreadItemViewHolder(View v) {
super(v); super(v);
@@ -43,10 +54,9 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
} }
@CallSuper @CallSuper
public void bind(I item, ThreadItemListener<I> listener) { public void bind(I item, LifecycleOwner lifecycleOwner,
textView.setText(StringUtils.trim(item.getText())); ThreadItemListener<I> listener) {
Linkify.addLinks(textView, Linkify.WEB_URLS); setText(item, lifecycleOwner, listener);
makeLinksClickable(textView, listener::onLinkClick);
author.setAuthor(item.getAuthor(), item.getAuthorInfo()); author.setAuthor(item.getAuthor(), item.getAuthorInfo());
author.setDate(item.getTimestamp()); author.setDate(item.getTimestamp());
@@ -61,6 +71,20 @@ 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() { private void animateFadeOut() {
setIsRecyclable(false); setIsRecyclable(false);
ValueAnimator anim = new ValueAnimator(); ValueAnimator anim = new ValueAnimator();
@@ -73,6 +97,7 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
@Override @Override
public void onAnimationStart(Animator animation) { public void onAnimationStart(Animator animation) {
} }
@Override @Override
public void onAnimationEnd(Animator animation) { public void onAnimationEnd(Animator animation) {
layout.setBackgroundResource( layout.setBackgroundResource(
@@ -80,9 +105,11 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
layout.setActivated(false); layout.setActivated(false);
setIsRecyclable(true); setIsRecyclable(true);
} }
@Override @Override
public void onAnimationCancel(Animator animation) { public void onAnimationCancel(Animator animation) {
} }
@Override @Override
public void onAnimationRepeat(Animator animation) { public void onAnimationRepeat(Animator animation) {
} }
@@ -97,4 +124,24 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
return textView.getContext(); 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,7 +19,6 @@ public abstract class ThreadItem implements MessageNode {
private final MessageId messageId; private final MessageId messageId;
@Nullable @Nullable
private final MessageId parentId; private final MessageId parentId;
private final String text;
private final long timestamp; private final long timestamp;
private final Author author; private final Author author;
private final AuthorInfo authorInfo; private final AuthorInfo authorInfo;
@@ -27,11 +26,10 @@ public abstract class ThreadItem implements MessageNode {
private boolean isRead, highlighted; private boolean isRead, highlighted;
public ThreadItem(MessageId messageId, @Nullable MessageId parentId, public ThreadItem(MessageId messageId, @Nullable MessageId parentId,
String text, long timestamp, Author author, AuthorInfo authorInfo, long timestamp, Author author, AuthorInfo authorInfo,
boolean isRead) { boolean isRead) {
this.messageId = messageId; this.messageId = messageId;
this.parentId = parentId; this.parentId = parentId;
this.text = text;
this.timestamp = timestamp; this.timestamp = timestamp;
this.author = author; this.author = author;
this.authorInfo = authorInfo; this.authorInfo = authorInfo;
@@ -39,10 +37,6 @@ public abstract class ThreadItem implements MessageNode {
this.highlighted = false; this.highlighted = false;
} }
public String getText() {
return text;
}
public int getLevel() { public int getLevel() {
return level; return level;
} }

View File

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

View File

@@ -85,6 +85,9 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
scrollListener = new ThreadScrollListener<>(adapter, viewModel, scrollListener = new ThreadScrollListener<>(adapter, viewModel,
upButton, downButton); upButton, downButton);
list.getRecyclerView().addOnScrollListener(scrollListener); 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 -> { upButton.setOnClickListener(v -> {
int position = adapter.getVisibleUnreadPosTop(layoutManager); int position = adapter.getVisibleUnreadPosTop(layoutManager);
@@ -257,4 +260,8 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
protected abstract int getMaxTextLength(); protected abstract int getMaxTextLength();
@Override
public LiveData<String> loadItemText(MessageId m) {
return getViewModel().loadMessageText(m);
}
} }

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchGroupException; 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.db.TransactionManager;
import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
@@ -260,4 +261,14 @@ public abstract class ThreadListViewModel<I extends ThreadItem>
return scrollToItem.getAndSet(null); 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,6 +10,7 @@ import org.briarproject.nullsafety.NotNullByDefault;
import java.util.Locale; import java.util.Locale;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.lifecycle.LifecycleOwner;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
@@ -40,8 +41,9 @@ public class ThreadPostViewHolder<I extends ThreadItem>
} }
@Override @Override
public void bind(I item, ThreadItemListener<I> listener) { public void bind(I item, LifecycleOwner lifecycleOwner,
super.bind(item, listener); ThreadItemListener<I> listener) {
super.bind(item, lifecycleOwner, listener);
for (int i = 0; i < lvls.length; i++) { for (int i = 0; i < lvls.length; i++) {
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE); lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);

View File

@@ -11,7 +11,6 @@
android:layout_width="@dimen/listitem_picture_frame_size" android:layout_width="@dimen/listitem_picture_frame_size"
android:layout_height="@dimen/listitem_picture_frame_size" android:layout_height="@dimen/listitem_picture_frame_size"
android:layout_marginStart="@dimen/listitem_horizontal_margin" android:layout_marginStart="@dimen/listitem_horizontal_margin"
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
app:layout_constraintBottom_toTopOf="@+id/divider" app:layout_constraintBottom_toTopOf="@+id/divider"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@@ -38,7 +37,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium" android:layout_marginTop="@dimen/margin_medium"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_small" android:textSize="@dimen/text_size_small"
app:layout_constraintEnd_toStartOf="@+id/dateView" app:layout_constraintEnd_toStartOf="@+id/dateView"
@@ -51,7 +49,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/listitem_horizontal_margin" android:layout_marginEnd="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_small" android:textSize="@dimen/text_size_small"
app:layout_constraintBaseline_toBaselineOf="@+id/postCountView" app:layout_constraintBaseline_toBaselineOf="@+id/postCountView"
@@ -63,7 +60,6 @@
style="@style/Divider.ThreadItem" style="@style/Divider.ThreadItem"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_marginStart="@dimen/margin_medium" android:layout_marginStart="@dimen/margin_medium"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_marginTop="@dimen/listitem_horizontal_margin" android:layout_marginTop="@dimen/listitem_horizontal_margin"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_forum_delete"
android:title="@string/forum_leave" />
</menu>

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,9 +6,9 @@ sourceSets.configureEach { sourceSet ->
idea { idea {
module { module {
sourceDirs += compileJava.options.generatedSourceOutputDirectory sourceDirs += compileJava.options.generatedSourceOutputDirectory.get().getAsFile()
generatedSourceDirs += compileJava.options.generatedSourceOutputDirectory generatedSourceDirs += compileJava.options.generatedSourceOutputDirectory.get().getAsFile()
testSourceDirs += compileTestJava.options.generatedSourceOutputDirectory testSourceDirs += compileTestJava.options.generatedSourceOutputDirectory.get().getAsFile()
generatedSourceDirs += compileTestJava.options.generatedSourceOutputDirectory generatedSourceDirs += compileTestJava.options.generatedSourceOutputDirectory.get().getAsFile()
} }
} }