Observe messages for previous/next navigation

pull/147/head
M66B 6 years ago
parent 2f6b18cd61
commit 0e49cdfe8f

@ -100,7 +100,6 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
public class ActivityView extends ActivityBilling implements FragmentManager.OnBackStackChangedListener { public class ActivityView extends ActivityBilling implements FragmentManager.OnBackStackChangedListener {
@ -384,9 +383,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
onDebugInfo(); onDebugInfo();
else if (action.startsWith("thread")) { else if (action.startsWith("thread")) {
ViewModelMessages model = ViewModelProviders.of(ActivityView.this).get(ViewModelMessages.class);
model.setMessages(null);
intent.putExtra("thread", action.split(":", 2)[1]); intent.putExtra("thread", action.split(":", 2)[1]);
onViewThread(intent); onViewThread(intent);
} }

@ -69,7 +69,7 @@ import androidx.appcompat.widget.SearchView;
import androidx.constraintlayout.widget.Group; import androidx.constraintlayout.widget.Group;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@ -130,7 +130,6 @@ public class FragmentMessages extends FragmentEx {
private AdapterMessage.ViewType viewType; private AdapterMessage.ViewType viewType;
private SelectionTracker<Long> selectionTracker = null; private SelectionTracker<Long> selectionTracker = null;
private LiveData<PagedList<TupleMessageEx>> messages = null;
private int autoCloseCount = 0; private int autoCloseCount = 0;
private boolean autoExpand = true; private boolean autoExpand = true;
@ -449,17 +448,16 @@ public class FragmentMessages extends FragmentEx {
@Override @Override
public void onSelectionChanged() { public void onSelectionChanged() {
try { try {
ViewModelMessages modelMessages = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class);
LifecycleOwner owner = (viewType == AdapterMessage.ViewType.THREAD ? getViewLifecycleOwner() : getActivity());
if (selectionTracker.hasSelection()) { if (selectionTracker.hasSelection()) {
swipeRefresh.setEnabled(false); swipeRefresh.setEnabled(false);
if (getViewLifecycleOwner().getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) modelMessages.removeObservers(viewType, owner);
if (messages != null)
messages.removeObservers(getViewLifecycleOwner());
fabMore.show(); fabMore.show();
} else { } else {
predicate.clearAccount(); predicate.clearAccount();
fabMore.hide(); fabMore.hide();
if (getViewLifecycleOwner().getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) modelMessages.observe(viewType, owner, observer);
loadMessages();
swipeRefresh.setEnabled(pull); swipeRefresh.setEnabled(pull);
} }
} catch (IllegalStateException ex) { } catch (IllegalStateException ex) {
@ -625,8 +623,6 @@ public class FragmentMessages extends FragmentEx {
bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override @Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
ViewModelMessages.Target[] pn = (ViewModelMessages.Target[]) bottom_navigation.getTag();
switch (menuItem.getItemId()) { switch (menuItem.getItemId()) {
case R.id.action_delete: case R.id.action_delete:
onActionMove(EntityFolder.TRASH); onActionMove(EntityFolder.TRASH);
@ -637,11 +633,11 @@ public class FragmentMessages extends FragmentEx {
return true; return true;
case R.id.action_prev: case R.id.action_prev:
onActionNavigate(pn[0]); navigate(false);
return true; return true;
case R.id.action_next: case R.id.action_next:
onActionNavigate(pn[1]); navigate(true);
return true; return true;
default: default:
@ -704,15 +700,6 @@ public class FragmentMessages extends FragmentEx {
} }
}.execute(FragmentMessages.this, args, "messages:move"); }.execute(FragmentMessages.this, args, "messages:move");
} }
private void onActionNavigate(ViewModelMessages.Target target) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getContext());
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_THREAD)
.putExtra("account", target.account)
.putExtra("thread", target.thread)
.putExtra("id", target.id));
}
}); });
fab.setOnClickListener(new View.OnClickListener() { fab.setOnClickListener(new View.OnClickListener() {
@ -1755,10 +1742,8 @@ public class FragmentMessages extends FragmentEx {
} }
private void loadMessages() { private void loadMessages() {
final DB db = DB.getInstance(getContext()); ViewModelBrowse modelBrowse = ViewModelProviders.of(getActivity()).get(ViewModelBrowse.class);
modelBrowse.set(getContext(), folder, search, REMOTE_PAGE_SIZE);
ViewModelBrowse model = ViewModelProviders.of(getActivity()).get(ViewModelBrowse.class);
model.set(getContext(), folder, search, REMOTE_PAGE_SIZE);
// Observe folder/messages/search // Observe folder/messages/search
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
@ -1768,9 +1753,11 @@ public class FragmentMessages extends FragmentEx {
Log.i("Load messages type=" + viewType + " sort=" + sort + " debug=" + debug); Log.i("Load messages type=" + viewType + " sort=" + sort + " debug=" + debug);
// Sort changed // Sort changed
if (messages != null) final ViewModelMessages modelMessages = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class);
messages.removeObservers(getViewLifecycleOwner()); LifecycleOwner owner = (viewType == AdapterMessage.ViewType.THREAD ? getViewLifecycleOwner() : getActivity());
modelMessages.removeObservers(viewType, owner);
DB db = DB.getInstance(getContext());
LivePagedListBuilder<Integer, TupleMessageEx> builder = null; LivePagedListBuilder<Integer, TupleMessageEx> builder = null;
switch (viewType) { switch (viewType) {
case UNIFIED: case UNIFIED:
@ -1780,7 +1767,7 @@ public class FragmentMessages extends FragmentEx {
case FOLDER: case FOLDER:
if (searchCallback == null) if (searchCallback == null)
searchCallback = new BoundaryCallbackMessages(this, model, searchCallback = new BoundaryCallbackMessages(this, modelBrowse,
new BoundaryCallbackMessages.IBoundaryCallbackMessages() { new BoundaryCallbackMessages.IBoundaryCallbackMessages() {
@Override @Override
public void onLoading() { public void onLoading() {
@ -1819,7 +1806,7 @@ public class FragmentMessages extends FragmentEx {
case SEARCH: case SEARCH:
if (searchCallback == null) if (searchCallback == null)
searchCallback = new BoundaryCallbackMessages(this, model, searchCallback = new BoundaryCallbackMessages(this, modelBrowse,
new BoundaryCallbackMessages.IBoundaryCallbackMessages() { new BoundaryCallbackMessages.IBoundaryCallbackMessages() {
@Override @Override
public void onLoading() { public void onLoading() {
@ -1830,8 +1817,7 @@ public class FragmentMessages extends FragmentEx {
@Override @Override
public void onLoaded() { public void onLoaded() {
pbWait.setVisibility(View.GONE); pbWait.setVisibility(View.GONE);
if (messages.getValue() == null || messages.getValue().size() == 0) tvNoEmail.setVisibility(modelMessages.isEmpty(viewType) ? View.VISIBLE : View.GONE);
tvNoEmail.setVisibility(View.VISIBLE);
} }
@Override @Override
@ -1857,195 +1843,171 @@ public class FragmentMessages extends FragmentEx {
builder.setFetchExecutor(executor); builder.setFetchExecutor(executor);
messages = builder.build(); modelMessages.setMessages(viewType, builder.build());
messages.observe(getViewLifecycleOwner(), new Observer<PagedList<TupleMessageEx>>() { modelMessages.observe(viewType, owner, observer);
@Override }
public void onChanged(@Nullable PagedList<TupleMessageEx> messages) {
if (messages == null ||
(viewType == AdapterMessage.ViewType.THREAD && messages.size() == 0 &&
(autoclose || autonext))) {
autoCloseNext();
return;
}
if (viewType == AdapterMessage.ViewType.THREAD) { private Observer<PagedList<TupleMessageEx>> observer = new Observer<PagedList<TupleMessageEx>>() {
if (autoExpand) { @Override
autoExpand = false; public void onChanged(@Nullable PagedList<TupleMessageEx> messages) {
if (messages == null ||
(viewType == AdapterMessage.ViewType.THREAD && messages.size() == 0 &&
(autoclose || autonext))) {
handleAutoClose();
return;
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); if (viewType == AdapterMessage.ViewType.THREAD) {
long download = prefs.getInt("download", 32768); if (autoExpand) {
if (download == 0) autoExpand = false;
download = Long.MAX_VALUE;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
long download = prefs.getInt("download", 32768);
if (download == 0)
download = Long.MAX_VALUE;
Boolean isMetered = Helper.isMetered(getContext(), false);
boolean metered = (isMetered == null || isMetered);
int count = 0;
int unseen = 0;
TupleMessageEx single = null;
TupleMessageEx see = null;
for (TupleMessageEx message : messages) {
if (!message.duplicate &&
!EntityFolder.DRAFTS.equals(message.folderType) &&
!EntityFolder.TRASH.equals(message.folderType)) {
count++;
single = message;
if (!message.ui_seen) {
unseen++;
see = message;
}
}
Boolean isMetered = Helper.isMetered(getContext(), false); if (!EntityFolder.ARCHIVE.equals(message.folderType) &&
boolean metered = (isMetered == null || isMetered); !EntityFolder.SENT.equals(message.folderType) &&
!EntityFolder.TRASH.equals(message.folderType) &&
!EntityFolder.JUNK.equals(message.folderType))
autoCloseCount++;
}
// Auto expand when:
// - single, non archived/trashed/sent message
// - one unread, non archived/trashed/sent message in conversation
// - sole message
TupleMessageEx expand = null;
if (count == 1)
expand = single;
else if (unseen == 1)
expand = see;
else if (messages.size() == 1)
expand = messages.get(0);
if (expand != null &&
(expand.content || !metered || (expand.size != null && expand.size < download))) {
if (!values.containsKey("expanded"))
values.put("expanded", new ArrayList<Long>());
values.get("expanded").add(expand.id);
handleExpand(expand.id);
}
} else {
if (autoCloseCount > 0 && (autoclose || autonext)) {
int count = 0; int count = 0;
int unseen = 0; for (int i = 0; i < messages.size(); i++) {
TupleMessageEx single = null; TupleMessageEx message = messages.get(i);
TupleMessageEx see = null;
for (TupleMessageEx message : messages) {
if (!message.duplicate &&
!EntityFolder.DRAFTS.equals(message.folderType) &&
!EntityFolder.TRASH.equals(message.folderType)) {
count++;
single = message;
if (!message.ui_seen) {
unseen++;
see = message;
}
}
if (!EntityFolder.ARCHIVE.equals(message.folderType) && if (!EntityFolder.ARCHIVE.equals(message.folderType) &&
!EntityFolder.SENT.equals(message.folderType) && !EntityFolder.SENT.equals(message.folderType) &&
!EntityFolder.TRASH.equals(message.folderType) && !EntityFolder.TRASH.equals(message.folderType) &&
!EntityFolder.JUNK.equals(message.folderType)) !EntityFolder.JUNK.equals(message.folderType))
autoCloseCount++; count++;
}
// Auto expand when:
// - single, non archived/trashed/sent message
// - one unread, non archived/trashed/sent message in conversation
// - sole message
TupleMessageEx expand = null;
if (count == 1)
expand = single;
else if (unseen == 1)
expand = see;
else if (messages.size() == 1)
expand = messages.get(0);
if (expand != null &&
(expand.content || !metered || (expand.size != null && expand.size < download))) {
if (!values.containsKey("expanded"))
values.put("expanded", new ArrayList<Long>());
values.get("expanded").add(expand.id);
handleExpand(expand.id);
} }
} else { Log.i("Auto close=" + count);
if (autoCloseCount > 0 && (autoclose || autonext)) {
int count = 0;
for (int i = 0; i < messages.size(); i++) {
TupleMessageEx message = messages.get(i);
if (!EntityFolder.ARCHIVE.equals(message.folderType) &&
!EntityFolder.SENT.equals(message.folderType) &&
!EntityFolder.TRASH.equals(message.folderType) &&
!EntityFolder.JUNK.equals(message.folderType))
count++;
}
Log.i("Auto close=" + count);
// Auto close/next when: // Auto close/next when:
// - no more non archived/trashed/sent messages // - no more non archived/trashed/sent messages
if (count == 0) { if (count == 0) {
autoCloseNext(); handleAutoClose();
return; return;
}
} }
} }
}
if (actionbar) { if (actionbar) {
ViewModelMessages model = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); Bundle args = new Bundle();
ViewModelMessages.Target[] pn = model.getPrevNext(thread); args.putLong("account", account);
bottom_navigation.setTag(pn); args.putString("thread", thread);
bottom_navigation.getMenu().findItem(R.id.action_prev).setEnabled(pn[0] != null); args.putLong("id", id);
bottom_navigation.getMenu().findItem(R.id.action_next).setEnabled(pn[1] != null);
Bundle args = new Bundle(); new SimpleTask<Boolean[]>() {
args.putLong("account", account); @Override
args.putString("thread", thread); protected Boolean[] onExecute(Context context, Bundle args) {
args.putLong("id", id); long account = args.getLong("account");
String thread = args.getString("thread");
long id = args.getLong("id");
new SimpleTask<Boolean[]>() { DB db = DB.getInstance(context);
@Override
protected Boolean[] onExecute(Context context, Bundle args) {
long account = args.getLong("account");
String thread = args.getString("thread");
long id = args.getLong("id");
List<EntityMessage> messages = db.message().getMessageByThread( List<EntityMessage> messages = db.message().getMessageByThread(
account, thread, threading ? null : id, null); account, thread, threading ? null : id, null);
boolean trashable = false;
boolean archivable = false;
for (EntityMessage message : messages)
if (message.uid != null) {
EntityFolder folder = db.folder().getFolder(message.folder);
if (!EntityFolder.DRAFTS.equals(folder.type) &&
!EntityFolder.OUTBOX.equals(folder.type) &&
// allow sent
!EntityFolder.TRASH.equals(folder.type) &&
!EntityFolder.JUNK.equals(folder.type))
trashable = true;
if (!EntityFolder.isOutgoing(folder.type) &&
!EntityFolder.TRASH.equals(folder.type) &&
!EntityFolder.JUNK.equals(folder.type) &&
!EntityFolder.ARCHIVE.equals(folder.type))
archivable = true;
}
return new Boolean[]{trashable, archivable}; boolean trashable = false;
} boolean archivable = false;
for (EntityMessage message : messages)
if (message.uid != null) {
EntityFolder folder = db.folder().getFolder(message.folder);
if (!EntityFolder.DRAFTS.equals(folder.type) &&
!EntityFolder.OUTBOX.equals(folder.type) &&
// allow sent
!EntityFolder.TRASH.equals(folder.type) &&
!EntityFolder.JUNK.equals(folder.type))
trashable = true;
if (!EntityFolder.isOutgoing(folder.type) &&
!EntityFolder.TRASH.equals(folder.type) &&
!EntityFolder.JUNK.equals(folder.type) &&
!EntityFolder.ARCHIVE.equals(folder.type))
archivable = true;
}
@Override return new Boolean[]{trashable, archivable};
protected void onExecuted(Bundle args, Boolean[] data) { }
bottom_navigation.getMenu().findItem(R.id.action_delete).setVisible(trashes.size() > 0 && data[0]);
bottom_navigation.getMenu().findItem(R.id.action_archive).setVisible(archives.size() > 0 && data[1]);
bottom_navigation.setVisibility(View.VISIBLE);
}
@Override @Override
protected void onException(Bundle args, Throwable ex) { protected void onExecuted(Bundle args, Boolean[] data) {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); bottom_navigation.getMenu().findItem(R.id.action_delete).setVisible(trashes.size() > 0 && data[0]);
} bottom_navigation.getMenu().findItem(R.id.action_archive).setVisible(archives.size() > 0 && data[1]);
}.execute(FragmentMessages.this, args, "messages:navigation"); bottom_navigation.setVisibility(View.VISIBLE);
} }
} else { @Override
ViewModelMessages model = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); protected void onException(Bundle args, Throwable ex) {
model.setMessages(messages); Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(FragmentMessages.this, args, "messages:navigation");
} }
Log.i("Submit messages=" + messages.size()); }
adapter.submitList(messages);
boolean searching = (searchCallback != null && searchCallback.isSearching()); Log.i("Submit messages=" + messages.size());
adapter.submitList(messages);
if (!searching) boolean searching = (searchCallback != null && searchCallback.isSearching());
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
if (messages.size() == 0) { if (!searching)
tvNoEmail.setVisibility(searching ? View.GONE : View.VISIBLE); pbWait.setVisibility(View.GONE);
rvMessage.setVisibility(View.GONE); grpReady.setVisibility(View.VISIBLE);
} else {
tvNoEmail.setVisibility(View.GONE);
rvMessage.setVisibility(View.VISIBLE);
}
}
});
}
private void autoCloseNext() { if (messages.size() == 0) {
if (autoclose) tvNoEmail.setVisibility(searching ? View.GONE : View.VISIBLE);
finish(); rvMessage.setVisibility(View.GONE);
else if (autonext) { } else {
ViewModelMessages model = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); tvNoEmail.setVisibility(View.GONE);
ViewModelMessages.Target[] pn = model.getPrevNext(thread); rvMessage.setVisibility(View.VISIBLE);
if (pn[1] == null)
finish();
else {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getContext());
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_THREAD)
.putExtra("account", pn[1].account)
.putExtra("thread", pn[1].thread)
.putExtra("id", pn[1].id));
} }
} }
} };
private void handleExpand(long id) { private void handleExpand(long id) {
Bundle args = new Bundle(); Bundle args = new Bundle();
@ -2084,6 +2046,29 @@ public class FragmentMessages extends FragmentEx {
}.execute(this, args, "messages:expand"); }.execute(this, args, "messages:expand");
} }
private void handleAutoClose() {
if (autoclose)
finish();
else if (autonext)
navigate(true);
}
private void navigate(boolean next) {
ViewModelMessages model = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class);
ViewModelMessages.Target target = model.getPrevNext(thread)[next ? 1 : 0];
if (target == null)
finish();
else {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getContext());
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_THREAD)
.putExtra("account", target.account)
.putExtra("thread", target.thread)
.putExtra("id", target.id));
}
}
private void moveAsk(final MessageTarget result) { private void moveAsk(final MessageTarget result) {
if (result.target == null) if (result.target == null)
return; return;

@ -19,35 +19,76 @@ package eu.faircode.email;
Copyright 2018-2019 by Marcel Bokhorst (M66B) Copyright 2018-2019 by Marcel Bokhorst (M66B)
*/ */
import java.util.HashMap;
import java.util.Map;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import androidx.paging.PagedList; import androidx.paging.PagedList;
public class ViewModelMessages extends ViewModel { public class ViewModelMessages extends ViewModel {
private PagedList<TupleMessageEx> messages = null; private Map<Boolean, LiveData<PagedList<TupleMessageEx>>> messages = new HashMap<>();
void setMessages(AdapterMessage.ViewType viewType, LiveData<PagedList<TupleMessageEx>> messages) {
boolean thread = (viewType == AdapterMessage.ViewType.THREAD);
this.messages.put(thread, messages);
}
void observe(AdapterMessage.ViewType viewType, LifecycleOwner owner, Observer<PagedList<TupleMessageEx>> observer) {
if (owner.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
final boolean thread = (viewType == AdapterMessage.ViewType.THREAD);
messages.get(thread).observe(owner, observer);
owner.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
Log.i("Removed model thread=" + thread);
messages.remove(thread);
}
});
}
}
void removeObservers(AdapterMessage.ViewType viewType, LifecycleOwner owner) {
if (owner.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
boolean thread = (viewType == AdapterMessage.ViewType.THREAD);
LiveData<PagedList<TupleMessageEx>> list = messages.get(thread);
if (list != null)
list.removeObservers(owner);
}
}
void setMessages(PagedList<TupleMessageEx> messages) { boolean isEmpty(AdapterMessage.ViewType viewType) {
this.messages = messages; boolean thread = (viewType == AdapterMessage.ViewType.THREAD);
LiveData<PagedList<TupleMessageEx>> list = messages.get(thread);
return (list == null || list.getValue() == null || list.getValue().size() == 0);
} }
@Override @Override
protected void onCleared() { protected void onCleared() {
messages = null; messages.clear();
} }
Target[] getPrevNext(String thread) { Target[] getPrevNext(String thread) {
if (messages == null) LiveData<PagedList<TupleMessageEx>> list = messages.get(false);
if (list == null || list.getValue() == null || list.getValue().size() == 0)
return new Target[]{null, null}; return new Target[]{null, null};
boolean found = false; boolean found = false;
TupleMessageEx prev = null; TupleMessageEx prev = null;
TupleMessageEx next = null; TupleMessageEx next = null;
for (int i = 0; i < messages.size(); i++) { for (int i = 0; i < list.getValue().size(); i++) {
TupleMessageEx item = messages.get(i); TupleMessageEx item = list.getValue().get(i);
if (item == null) if (item == null)
continue; continue;
if (found) { if (found) {
prev = item; prev = item;
messages.loadAround(i); list.getValue().loadAround(i);
break; break;
} }
if (thread.equals(item.thread)) if (thread.equals(item.thread))

Loading…
Cancel
Save