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 {
c = createConnection();
closeAllConnections();
setDirty(c, false);
LOG.info("Compacting DB");
s = c.createStatement();
s.execute("SHUTDOWN COMPACT");
LOG.info("Finished compacting DB");
s.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) {
tryToClose(s, LOG, WARNING);
tryToClose(c, LOG, WARNING);
@@ -126,6 +132,7 @@ class H2Database extends JdbcDatabase {
closeAllConnections();
s = c.createStatement();
s.execute("SHUTDOWN COMPACT");
LOG.info("Finished compacting DB");
s.close();
c.close();
} catch (SQLException e) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.forum;
import android.app.Application;
import android.widget.Toast;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
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.GroupRemovedEvent;
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.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
@@ -40,6 +42,7 @@ import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
@@ -180,4 +183,17 @@ class ForumListViewModel extends DbViewModel implements EventListener {
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;
@NotThreadSafe
class ForumPostItem extends ThreadItem {
public class ForumPostItem extends ThreadItem {
ForumPostItem(ForumPostHeader h, String text) {
super(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
ForumPostItem(ForumPostHeader h) {
super(h.getId(), h.getParentId(), h.getTimestamp(), h.getAuthor(),
h.getAuthorInfo(), h.isRead());
}

View File

@@ -4,6 +4,7 @@ import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import android.widget.TextView;
import org.briarproject.briar.R;
@@ -20,6 +21,7 @@ import static org.briarproject.briar.android.activity.BriarActivity.GROUP_NAME;
class ForumViewHolder extends RecyclerView.ViewHolder {
private final ForumListViewModel viewModel;
private final Context ctx;
private final ViewGroup layout;
private final TextAvatarView avatar;
@@ -27,8 +29,9 @@ class ForumViewHolder extends RecyclerView.ViewHolder {
private final TextView postCount;
private final TextView date;
ForumViewHolder(View v) {
ForumViewHolder(View v, ForumListViewModel viewModel) {
super(v);
this.viewModel = viewModel;
ctx = v.getContext();
layout = (ViewGroup) v;
avatar = v.findViewById(R.id.avatarView);
@@ -64,6 +67,21 @@ class ForumViewHolder extends RecyclerView.ViewHolder {
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
layout.setOnClickListener(v -> {
Intent i = new Intent(ctx, ForumActivity.class);

View File

@@ -91,9 +91,7 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
if (f.getGroupId().equals(groupId)) {
LOG.info("Forum post received, adding...");
ForumPostItem item =
new ForumPostItem(f.getHeader(), f.getText());
addItem(item, false);
addItem(new ForumPostItem(f.getHeader()), false);
}
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
ForumInvitationResponseReceivedEvent f =
@@ -139,22 +137,14 @@ 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(loadItem(txn, header));
items.add(new ForumPostItem(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) {
@@ -175,21 +165,17 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
@Nullable MessageId parentId, LocalAuthor author) {
cryptoExecutor.execute(() -> {
LOG.info("Creating forum post...");
ForumPost msg = forumManager.createLocalPost(groupId, text,
timestamp, parentId, author);
storePost(msg, text);
storePost(forumManager.createLocalPost(groupId, text,
timestamp, parentId, author));
});
}
private void storePost(ForumPost msg, String text) {
private void storePost(ForumPost msg) {
runOnDbThread(false, txn -> {
long start = now();
ForumPostHeader header = forumManager.addLocalPost(txn, msg);
logDuration(LOG, "Storing forum post", start);
txn.attach(() -> {
ForumPostItem item = new ForumPostItem(header, text);
addItem(item, true);
});
txn.attach(() -> addItem(new ForumPostItem(header), true));
}, 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.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;
@@ -56,7 +55,7 @@ public class GroupActivity extends
@Override
protected GroupMessageAdapter createAdapter() {
return new GroupMessageAdapter(this);
return new GroupMessageAdapter(this, this);
}
@Override
@@ -160,12 +159,6 @@ 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,16 +11,19 @@ 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
class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
private boolean isCreator = false;
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener) {
super(listener);
GroupMessageAdapter(LifecycleOwner lifecycleOwner,
ThreadItemListener<GroupMessageItem> listener) {
super(lifecycleOwner, listener);
}
@LayoutRes
@@ -30,6 +33,7 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
return item.getLayout();
}
@NonNull
@Override
public BaseThreadItemViewHolder<GroupMessageItem> onCreateViewHolder(
ViewGroup parent, int type) {

View File

@@ -1,14 +1,10 @@
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;
@@ -16,20 +12,14 @@ import androidx.annotation.UiThread;
@UiThread
@NotThreadSafe
class GroupMessageItem extends ThreadItem {
public class GroupMessageItem extends ThreadItem {
private final GroupId groupId;
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());
GroupMessageItem(GroupMessageHeader h) {
super(h.getId(), h.getParentId(), h.getTimestamp(), h.getAuthor(),
h.getAuthorInfo(), h.isRead());
this.groupId = h.getGroupId();
}
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(), g.getText());
GroupMessageItem item = buildItem(g.getHeader());
addItem(item, false);
// In case the join message comes from the creator,
// we need to reload the sharing contacts
@@ -167,33 +167,19 @@ 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(loadItem(txn, header));
items.add(buildItem(header));
}
logDuration(LOG, "Loading bodies and creating items", start);
return items;
}, this::setItems);
}
private GroupMessageItem loadItem(Transaction txn,
GroupMessageHeader header) throws DbException {
String text;
private GroupMessageItem buildItem(GroupMessageHeader header) {
if (header instanceof JoinMessageHeader) {
// will be looked up later
text = "";
} else {
text = privateGroupManager.getMessageText(txn, header.getId());
return new JoinMessageItem((JoinMessageHeader) 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);
return new GroupMessageItem(header);
}
@Override
@@ -221,19 +207,17 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
LOG.info("Creating group message...");
GroupMessage msg = groupMessageFactory.createGroupMessage(groupId,
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 -> {
long start = now();
GroupMessageHeader header =
privateGroupManager.addLocalMessage(txn, msg);
logDuration(LOG, "Storing group message", start);
txn.attach(() ->
addItem(buildItem(header, text), true)
);
txn.attach(() -> addItem(buildItem(header), true));
}, this::handleException);
}
@@ -284,4 +268,9 @@ 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, String text) {
super(h, text);
JoinMessageItem(JoinMessageHeader h) {
super(h);
isInitial = h.isInitial();
}

View File

@@ -9,6 +9,7 @@ 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;
@@ -25,10 +26,8 @@ class JoinMessageItemViewHolder
}
@Override
public void bind(GroupMessageItem item,
protected void setText(GroupMessageItem item, LifecycleOwner lifecycleOwner,
ThreadItemListener<GroupMessageItem> listener) {
super.bind(item, listener);
if (isCreator) bindForCreator((JoinMessageItem) item);
else bind((JoinMessageItem) item);
}

View File

@@ -4,35 +4,46 @@ 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 {
extends RecyclerView.ViewHolder implements Observer<String> {
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);
@@ -43,10 +54,9 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
}
@CallSuper
public void bind(I item, ThreadItemListener<I> listener) {
textView.setText(StringUtils.trim(item.getText()));
Linkify.addLinks(textView, Linkify.WEB_URLS);
makeLinksClickable(textView, listener::onLinkClick);
public void bind(I item, LifecycleOwner lifecycleOwner,
ThreadItemListener<I> listener) {
setText(item, lifecycleOwner, listener);
author.setAuthor(item.getAuthor(), item.getAuthorInfo());
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() {
setIsRecyclable(false);
ValueAnimator anim = new ValueAnimator();
@@ -73,6 +97,7 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
layout.setBackgroundResource(
@@ -80,9 +105,11 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
layout.setActivated(false);
setIsRecyclable(true);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@@ -97,4 +124,24 @@ 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,7 +19,6 @@ 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;
@@ -27,11 +26,10 @@ public abstract class ThreadItem implements MessageNode {
private boolean isRead, highlighted;
public ThreadItem(MessageId messageId, @Nullable MessageId parentId,
String text, long timestamp, Author author, AuthorInfo authorInfo,
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;
@@ -39,10 +37,6 @@ public abstract class ThreadItem implements MessageNode {
this.highlighted = false;
}
public String getText() {
return text;
}
public int getLevel() {
return level;
}

View File

@@ -13,6 +13,8 @@ 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;
@@ -27,9 +29,11 @@ public class ThreadItemAdapter<I extends ThreadItem>
static final int UNDEFINED = -1;
private final LifecycleOwner lifecycleOwner;
private final ThreadItemListener<I> listener;
public ThreadItemAdapter(ThreadItemListener<I> listener) {
public ThreadItemAdapter(LifecycleOwner lifecycleOwner,
ThreadItemListener<I> listener) {
super(new DiffUtil.ItemCallback<I>() {
@Override
public boolean areItemsTheSame(I a, I b) {
@@ -42,6 +46,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
a.isRead() == b.isRead();
}
});
this.lifecycleOwner = lifecycleOwner;
this.listener = listener;
}
@@ -58,7 +63,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
public void onBindViewHolder(@NonNull BaseThreadItemViewHolder<I> ui,
int position) {
I item = getItem(position);
ui.bind(item, listener);
ui.bind(item, lifecycleOwner, listener);
}
int findItemPosition(MessageId id) {
@@ -135,9 +140,19 @@ 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,6 +85,9 @@ 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);
@@ -257,4 +260,8 @@ 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,6 +6,7 @@ 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;
@@ -260,4 +261,14 @@ 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,6 +10,7 @@ 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;
@@ -40,8 +41,9 @@ public class ThreadPostViewHolder<I extends ThreadItem>
}
@Override
public void bind(I item, ThreadItemListener<I> listener) {
super.bind(item, listener);
public void bind(I item, LifecycleOwner lifecycleOwner,
ThreadItemListener<I> listener) {
super.bind(item, lifecycleOwner, listener);
for (int i = 0; i < lvls.length; i++) {
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);

View File

@@ -11,7 +11,6 @@
android:layout_width="@dimen/listitem_picture_frame_size"
android:layout_height="@dimen/listitem_picture_frame_size"
android:layout_marginStart="@dimen/listitem_horizontal_margin"
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
app:layout_constraintBottom_toTopOf="@+id/divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -38,7 +37,6 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_small"
app:layout_constraintEnd_toStartOf="@+id/dateView"
@@ -51,7 +49,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_small"
app:layout_constraintBaseline_toBaselineOf="@+id/postCountView"
@@ -63,7 +60,6 @@
style="@style/Divider.ThreadItem"
android:layout_width="0dp"
android:layout_marginStart="@dimen/margin_medium"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_marginTop="@dimen/listitem_horizontal_margin"
app:layout_constraintBottom_toBottomOf="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 ForumPostHeader header;
private final String text;
public ForumPostReceivedEvent(GroupId groupId, ForumPostHeader header,
String text) {
public ForumPostReceivedEvent(GroupId groupId, ForumPostHeader header) {
this.groupId = groupId;
this.header = header;
this.text = text;
}
public GroupId getGroupId() {
@@ -32,8 +29,4 @@ public class ForumPostReceivedEvent extends Event {
public ForumPostHeader getHeader() {
return header;
}
public String getText() {
return text;
}
}

View File

@@ -17,14 +17,12 @@ 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,
String text, boolean local) {
boolean local) {
this.groupId = groupId;
this.header = header;
this.text = text;
this.local = local;
}
@@ -36,10 +34,6 @@ public class GroupMessageAddedEvent extends Event {
return header;
}
public String getText() {
return text;
}
public boolean isLocal() {
return local;
}

View File

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

View File

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

View File

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