From 0e49cdfe8faab06c1be6beda1cc89a08fbf07fb3 Mon Sep 17 00:00:00 2001 From: M66B Date: Sat, 12 Jan 2019 10:45:57 +0000 Subject: [PATCH] Observe messages for previous/next navigation --- .../java/eu/faircode/email/ActivityView.java | 4 - .../eu/faircode/email/FragmentMessages.java | 363 +++++++++--------- .../eu/faircode/email/ViewModelMessages.java | 57 ++- 3 files changed, 223 insertions(+), 201 deletions(-) diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index 15024ce370..3220c620c7 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -100,7 +100,6 @@ import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProviders; import androidx.localbroadcastmanager.content.LocalBroadcastManager; public class ActivityView extends ActivityBilling implements FragmentManager.OnBackStackChangedListener { @@ -384,9 +383,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB onDebugInfo(); else if (action.startsWith("thread")) { - ViewModelMessages model = ViewModelProviders.of(ActivityView.this).get(ViewModelMessages.class); - model.setMessages(null); - intent.putExtra("thread", action.split(":", 2)[1]); onViewThread(intent); } diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index 6bd1780879..588f7d5806 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -69,7 +69,7 @@ import androidx.appcompat.widget.SearchView; import androidx.constraintlayout.widget.Group; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LiveData; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProviders; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -130,7 +130,6 @@ public class FragmentMessages extends FragmentEx { private AdapterMessage.ViewType viewType; private SelectionTracker selectionTracker = null; - private LiveData> messages = null; private int autoCloseCount = 0; private boolean autoExpand = true; @@ -449,17 +448,16 @@ public class FragmentMessages extends FragmentEx { @Override public void onSelectionChanged() { try { + ViewModelMessages modelMessages = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); + LifecycleOwner owner = (viewType == AdapterMessage.ViewType.THREAD ? getViewLifecycleOwner() : getActivity()); if (selectionTracker.hasSelection()) { swipeRefresh.setEnabled(false); - if (getViewLifecycleOwner().getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) - if (messages != null) - messages.removeObservers(getViewLifecycleOwner()); + modelMessages.removeObservers(viewType, owner); fabMore.show(); } else { predicate.clearAccount(); fabMore.hide(); - if (getViewLifecycleOwner().getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) - loadMessages(); + modelMessages.observe(viewType, owner, observer); swipeRefresh.setEnabled(pull); } } catch (IllegalStateException ex) { @@ -625,8 +623,6 @@ public class FragmentMessages extends FragmentEx { bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { - ViewModelMessages.Target[] pn = (ViewModelMessages.Target[]) bottom_navigation.getTag(); - switch (menuItem.getItemId()) { case R.id.action_delete: onActionMove(EntityFolder.TRASH); @@ -637,11 +633,11 @@ public class FragmentMessages extends FragmentEx { return true; case R.id.action_prev: - onActionNavigate(pn[0]); + navigate(false); return true; case R.id.action_next: - onActionNavigate(pn[1]); + navigate(true); return true; default: @@ -704,15 +700,6 @@ public class FragmentMessages extends FragmentEx { } }.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() { @@ -1755,10 +1742,8 @@ public class FragmentMessages extends FragmentEx { } private void loadMessages() { - final DB db = DB.getInstance(getContext()); - - ViewModelBrowse model = ViewModelProviders.of(getActivity()).get(ViewModelBrowse.class); - model.set(getContext(), folder, search, REMOTE_PAGE_SIZE); + ViewModelBrowse modelBrowse = ViewModelProviders.of(getActivity()).get(ViewModelBrowse.class); + modelBrowse.set(getContext(), folder, search, REMOTE_PAGE_SIZE); // Observe folder/messages/search SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); @@ -1768,9 +1753,11 @@ public class FragmentMessages extends FragmentEx { Log.i("Load messages type=" + viewType + " sort=" + sort + " debug=" + debug); // Sort changed - if (messages != null) - messages.removeObservers(getViewLifecycleOwner()); + final ViewModelMessages modelMessages = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); + LifecycleOwner owner = (viewType == AdapterMessage.ViewType.THREAD ? getViewLifecycleOwner() : getActivity()); + modelMessages.removeObservers(viewType, owner); + DB db = DB.getInstance(getContext()); LivePagedListBuilder builder = null; switch (viewType) { case UNIFIED: @@ -1780,7 +1767,7 @@ public class FragmentMessages extends FragmentEx { case FOLDER: if (searchCallback == null) - searchCallback = new BoundaryCallbackMessages(this, model, + searchCallback = new BoundaryCallbackMessages(this, modelBrowse, new BoundaryCallbackMessages.IBoundaryCallbackMessages() { @Override public void onLoading() { @@ -1819,7 +1806,7 @@ public class FragmentMessages extends FragmentEx { case SEARCH: if (searchCallback == null) - searchCallback = new BoundaryCallbackMessages(this, model, + searchCallback = new BoundaryCallbackMessages(this, modelBrowse, new BoundaryCallbackMessages.IBoundaryCallbackMessages() { @Override public void onLoading() { @@ -1830,8 +1817,7 @@ public class FragmentMessages extends FragmentEx { @Override public void onLoaded() { pbWait.setVisibility(View.GONE); - if (messages.getValue() == null || messages.getValue().size() == 0) - tvNoEmail.setVisibility(View.VISIBLE); + tvNoEmail.setVisibility(modelMessages.isEmpty(viewType) ? View.VISIBLE : View.GONE); } @Override @@ -1857,195 +1843,171 @@ public class FragmentMessages extends FragmentEx { builder.setFetchExecutor(executor); - messages = builder.build(); - messages.observe(getViewLifecycleOwner(), new Observer>() { - @Override - public void onChanged(@Nullable PagedList messages) { - if (messages == null || - (viewType == AdapterMessage.ViewType.THREAD && messages.size() == 0 && - (autoclose || autonext))) { - autoCloseNext(); - return; - } + modelMessages.setMessages(viewType, builder.build()); + modelMessages.observe(viewType, owner, observer); + } - if (viewType == AdapterMessage.ViewType.THREAD) { - if (autoExpand) { - autoExpand = false; + private Observer> observer = new Observer>() { + @Override + public void onChanged(@Nullable PagedList messages) { + if (messages == null || + (viewType == AdapterMessage.ViewType.THREAD && messages.size() == 0 && + (autoclose || autonext))) { + handleAutoClose(); + return; + } - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - long download = prefs.getInt("download", 32768); - if (download == 0) - download = Long.MAX_VALUE; + if (viewType == AdapterMessage.ViewType.THREAD) { + if (autoExpand) { + autoExpand = false; + + 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); - boolean metered = (isMetered == null || isMetered); + if (!EntityFolder.ARCHIVE.equals(message.folderType) && + !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()); + values.get("expanded").add(expand.id); + handleExpand(expand.id); + } + } else { + if (autoCloseCount > 0 && (autoclose || autonext)) { 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; - } - } - + 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)) - 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()); - values.get("expanded").add(expand.id); - handleExpand(expand.id); + count++; } - } else { - 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); + Log.i("Auto close=" + count); - // Auto close/next when: - // - no more non archived/trashed/sent messages + // Auto close/next when: + // - no more non archived/trashed/sent messages - if (count == 0) { - autoCloseNext(); - return; - } + if (count == 0) { + handleAutoClose(); + return; } } + } - if (actionbar) { - ViewModelMessages model = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); - ViewModelMessages.Target[] pn = model.getPrevNext(thread); - bottom_navigation.setTag(pn); - bottom_navigation.getMenu().findItem(R.id.action_prev).setEnabled(pn[0] != null); - bottom_navigation.getMenu().findItem(R.id.action_next).setEnabled(pn[1] != null); + if (actionbar) { + Bundle args = new Bundle(); + args.putLong("account", account); + args.putString("thread", thread); + args.putLong("id", id); - Bundle args = new Bundle(); - args.putLong("account", account); - args.putString("thread", thread); - args.putLong("id", id); + new SimpleTask() { + @Override + protected Boolean[] onExecute(Context context, Bundle args) { + long account = args.getLong("account"); + String thread = args.getString("thread"); + long id = args.getLong("id"); - new SimpleTask() { - @Override - protected Boolean[] onExecute(Context context, Bundle args) { - long account = args.getLong("account"); - String thread = args.getString("thread"); - long id = args.getLong("id"); + DB db = DB.getInstance(context); - List messages = db.message().getMessageByThread( - 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; - } + List messages = db.message().getMessageByThread( + account, thread, threading ? null : id, null); - 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 - 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); - } + return new Boolean[]{trashable, archivable}; + } - @Override - protected void onException(Bundle args, Throwable ex) { - Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); - } - }.execute(FragmentMessages.this, args, "messages:navigation"); - } + @Override + 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); + } - } else { - ViewModelMessages model = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); - model.setMessages(messages); + @Override + protected void onException(Bundle args, Throwable ex) { + 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) - pbWait.setVisibility(View.GONE); - grpReady.setVisibility(View.VISIBLE); + boolean searching = (searchCallback != null && searchCallback.isSearching()); - if (messages.size() == 0) { - tvNoEmail.setVisibility(searching ? View.GONE : View.VISIBLE); - rvMessage.setVisibility(View.GONE); - } else { - tvNoEmail.setVisibility(View.GONE); - rvMessage.setVisibility(View.VISIBLE); - } - } - }); - } + if (!searching) + pbWait.setVisibility(View.GONE); + grpReady.setVisibility(View.VISIBLE); - private void autoCloseNext() { - if (autoclose) - finish(); - else if (autonext) { - ViewModelMessages model = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); - ViewModelMessages.Target[] pn = model.getPrevNext(thread); - 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)); + if (messages.size() == 0) { + tvNoEmail.setVisibility(searching ? View.GONE : View.VISIBLE); + rvMessage.setVisibility(View.GONE); + } else { + tvNoEmail.setVisibility(View.GONE); + rvMessage.setVisibility(View.VISIBLE); } } - } + }; private void handleExpand(long id) { Bundle args = new Bundle(); @@ -2084,6 +2046,29 @@ public class FragmentMessages extends FragmentEx { }.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) { if (result.target == null) return; diff --git a/app/src/main/java/eu/faircode/email/ViewModelMessages.java b/app/src/main/java/eu/faircode/email/ViewModelMessages.java index 91e9497b43..a028960b9d 100644 --- a/app/src/main/java/eu/faircode/email/ViewModelMessages.java +++ b/app/src/main/java/eu/faircode/email/ViewModelMessages.java @@ -19,35 +19,76 @@ package eu.faircode.email; 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.paging.PagedList; public class ViewModelMessages extends ViewModel { - private PagedList messages = null; + private Map>> messages = new HashMap<>(); + + void setMessages(AdapterMessage.ViewType viewType, LiveData> messages) { + boolean thread = (viewType == AdapterMessage.ViewType.THREAD); + this.messages.put(thread, messages); + } + + void observe(AdapterMessage.ViewType viewType, LifecycleOwner owner, Observer> 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> list = messages.get(thread); + if (list != null) + list.removeObservers(owner); + } + } - void setMessages(PagedList messages) { - this.messages = messages; + boolean isEmpty(AdapterMessage.ViewType viewType) { + boolean thread = (viewType == AdapterMessage.ViewType.THREAD); + LiveData> list = messages.get(thread); + return (list == null || list.getValue() == null || list.getValue().size() == 0); } @Override protected void onCleared() { - messages = null; + messages.clear(); } Target[] getPrevNext(String thread) { - if (messages == null) + LiveData> list = messages.get(false); + if (list == null || list.getValue() == null || list.getValue().size() == 0) return new Target[]{null, null}; boolean found = false; TupleMessageEx prev = null; TupleMessageEx next = null; - for (int i = 0; i < messages.size(); i++) { - TupleMessageEx item = messages.get(i); + for (int i = 0; i < list.getValue().size(); i++) { + TupleMessageEx item = list.getValue().get(i); if (item == null) continue; if (found) { prev = item; - messages.loadAround(i); + list.getValue().loadAround(i); break; } if (thread.equals(item.thread))