From 4a3d18104dd9b593cc968086ede8bbdb0b7d2e93 Mon Sep 17 00:00:00 2001 From: M66B Date: Tue, 14 May 2019 18:34:09 +0200 Subject: [PATCH] Cache message list view models --- .../email/BoundaryCallbackMessages.java | 98 +++--- .../eu/faircode/email/FragmentMessages.java | 172 +++-------- .../eu/faircode/email/ViewModelMessages.java | 280 +++++++++++++++--- 3 files changed, 328 insertions(+), 222 deletions(-) diff --git a/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java b/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java index aeb393c082..22aa9d01fd 100644 --- a/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java +++ b/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java @@ -25,10 +25,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Handler; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.OnLifecycleEvent; import androidx.paging.PagedList; import androidx.preference.PreferenceManager; @@ -72,6 +68,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback messages = null; @@ -96,39 +92,17 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback> attachments = new LongSparseArray<>(); private LongSparseArray accountSwipes = new LongSparseArray<>(); - private BoundaryCallbackMessages boundaryCallback = null; - - private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory); - private final int action_seen = 1; private final int action_unseen = 2; private final int action_snooze = 3; @@ -199,8 +193,6 @@ public class FragmentMessages extends FragmentBase { private NumberFormat nf = NumberFormat.getNumberInstance(); - private static final int LOCAL_PAGE_SIZE = 100; - private static final int REMOTE_PAGE_SIZE = 10; private static final int UNDO_TIMEOUT = 5000; // milliseconds private static final int SWIPE_DISABLE_SELECT_DURATION = 1500; // milliseconds @@ -679,7 +671,7 @@ public class FragmentMessages extends FragmentBase { if (viewType == AdapterMessage.ViewType.THREAD) { ViewModelMessages model = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); - model.observePrevNext(getViewLifecycleOwner(), id, found, new ViewModelMessages.IPrevNext() { + model.observePrevNext(getViewLifecycleOwner(), id, new ViewModelMessages.IPrevNext() { @Override public void onPrevious(boolean exists, Long id) { previous = id; @@ -2540,7 +2532,7 @@ public class FragmentMessages extends FragmentBase { private void loadMessages(final boolean top) { if (viewType == AdapterMessage.ViewType.THREAD && autonext) { ViewModelMessages model = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); - model.observePrevNext(getViewLifecycleOwner(), id, found, new ViewModelMessages.IPrevNext() { + model.observePrevNext(getViewLifecycleOwner(), id, new ViewModelMessages.IPrevNext() { boolean once = false; @Override @@ -2568,126 +2560,48 @@ public class FragmentMessages extends FragmentBase { loadMessagesNext(top); } - private void loadMessagesNext(final boolean top) { - if (viewType == AdapterMessage.ViewType.FOLDER || viewType == AdapterMessage.ViewType.SEARCH) - if (boundaryCallback == null) - boundaryCallback = new BoundaryCallbackMessages( - getContext(), getViewLifecycleOwner(), - folder, server || viewType == AdapterMessage.ViewType.FOLDER, - query, REMOTE_PAGE_SIZE, - new BoundaryCallbackMessages.IBoundaryCallbackMessages() { - @Override - public void onLoading() { - pbWait.setVisibility(View.VISIBLE); - } - - @Override - public void onLoaded(int fetched) { - pbWait.setVisibility(View.GONE); - - Integer submitted = (Integer) rvMessage.getTag(); - if (submitted == null) - submitted = 0; - if (submitted + fetched == 0) - tvNoEmail.setVisibility(View.VISIBLE); - } + private boolean loading = false; - @Override - public void onError(Throwable ex) { - if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) - if (ex instanceof IllegalArgumentException) - Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show(); - else - new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner()) - .setMessage(Helper.formatThrowable(ex)) - .setPositiveButton(android.R.string.cancel, null) - .create() - .show(); - } - }); - - // Observe folder/messages/search - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - String sort = prefs.getString("sort", "time"); - boolean filter_seen = prefs.getBoolean("filter_seen", false); - boolean filter_unflagged = prefs.getBoolean("filter_unflagged", false); - boolean filter_snoozed = prefs.getBoolean("filter_snoozed", true); - boolean debug = prefs.getBoolean("debug", false); - Log.i("Load messages type=" + viewType + - " sort=" + sort + - " filter seen=" + filter_seen + " unflagged=" + filter_unflagged + " snoozed=" + filter_snoozed + - " debug=" + debug); - - // Sort changed - final ViewModelMessages modelMessages = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); - modelMessages.removeObservers(viewType, getViewLifecycleOwner()); - - DB db = DB.getInstance(getContext()); - LivePagedListBuilder builder = null; - switch (viewType) { - case UNIFIED: - builder = new LivePagedListBuilder<>( - db.message().pagedUnifiedInbox( - threading, - sort, - filter_seen, filter_unflagged, filter_snoozed, - false, - debug), - LOCAL_PAGE_SIZE); - break; - - case FOLDER: - PagedList.Config configFolder = new PagedList.Config.Builder() - .setPageSize(LOCAL_PAGE_SIZE) - .setPrefetchDistance(REMOTE_PAGE_SIZE) - .build(); - builder = new LivePagedListBuilder<>( - db.message().pagedFolder( - folder, threading, - sort, - filter_seen, filter_unflagged, filter_snoozed, - false, - debug), - configFolder); - builder.setBoundaryCallback(boundaryCallback); - break; + private void loadMessagesNext(final boolean top) { + ViewModelMessages model = ViewModelProviders.of(getActivity()).get(ViewModelMessages.class); - case THREAD: - builder = new LivePagedListBuilder<>( - db.message().pagedThread(account, thread, threading ? null : id, debug), LOCAL_PAGE_SIZE); - break; + LiveData> liveMessages = model.getPagedList( + getContext(), getViewLifecycleOwner(), + viewType, account, folder, thread, id, query, server, + new BoundaryCallbackMessages.IBoundaryCallbackMessages() { + @Override + public void onLoading() { + loading = true; + pbWait.setVisibility(View.VISIBLE); + } - case SEARCH: - PagedList.Config configSearch = new PagedList.Config.Builder() - .setPageSize(LOCAL_PAGE_SIZE) - .setPrefetchDistance(REMOTE_PAGE_SIZE) - .build(); - if (folder < 0) - builder = new LivePagedListBuilder<>( - db.message().pagedUnifiedInbox( - threading, - "time", - false, false, false, - true, - debug), - configSearch); - else - builder = new LivePagedListBuilder<>( - db.message().pagedFolder( - folder, threading, - "time", - false, false, false, - true, - debug), - configSearch); - builder.setBoundaryCallback(boundaryCallback); - break; - } + @Override + public void onLoaded(int fetched) { + loading = false; + pbWait.setVisibility(View.GONE); + + Integer submitted = (Integer) rvMessage.getTag(); + if (submitted == null) + submitted = 0; + if (submitted + fetched == 0) + tvNoEmail.setVisibility(View.VISIBLE); + } - builder.setFetchExecutor(executor); + @Override + public void onError(Throwable ex) { + if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) + if (ex instanceof IllegalArgumentException) + Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show(); + else + new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner()) + .setMessage(Helper.formatThrowable(ex)) + .setPositiveButton(android.R.string.cancel, null) + .create() + .show(); + } + }); - modelMessages.setMessages(viewType, getViewLifecycleOwner(), builder.build()); - modelMessages.observe(viewType, getViewLifecycleOwner(), new Observer>() { + liveMessages.observe(getViewLifecycleOwner(), new Observer>() { private boolean topped = false; @Override @@ -2714,9 +2628,11 @@ public class FragmentMessages extends FragmentBase { rvMessage.setTag(messages.size()); - if (boundaryCallback == null || !boundaryCallback.isLoading()) + boolean hasBoundary = (viewType == AdapterMessage.ViewType.FOLDER || viewType == AdapterMessage.ViewType.SEARCH); + + if (!hasBoundary || !loading) pbWait.setVisibility(View.GONE); - if (boundaryCallback == null && messages.size() == 0) + if (!hasBoundary && messages.size() == 0) tvNoEmail.setVisibility(View.VISIBLE); if (messages.size() > 0) { tvNoEmail.setVisibility(View.GONE); diff --git a/app/src/main/java/eu/faircode/email/ViewModelMessages.java b/app/src/main/java/eu/faircode/email/ViewModelMessages.java index 5174578ea4..451883ab46 100644 --- a/app/src/main/java/eu/faircode/email/ViewModelMessages.java +++ b/app/src/main/java/eu/faircode/email/ViewModelMessages.java @@ -19,6 +19,12 @@ package eu.faircode.email; Copyright 2018-2019 by Marcel Bokhorst (M66B) */ +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; @@ -26,77 +32,184 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.OnLifecycleEvent; import androidx.lifecycle.ViewModel; +import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; +import androidx.preference.PreferenceManager; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class ViewModelMessages extends ViewModel { - private Map>> messages = new HashMap<>(); + private AdapterMessage.ViewType last = AdapterMessage.ViewType.UNIFIED; + private Map models = new HashMap<>(); + + private ExecutorService executor = Executors.newCachedThreadPool(Helper.backgroundThreadFactory); + + private static final int LOCAL_PAGE_SIZE = 100; + private static final int REMOTE_PAGE_SIZE = 10; + + LiveData> getPagedList( + Context context, LifecycleOwner owner, + final AdapterMessage.ViewType viewType, + long account, long folder, String thread, long id, + String query, boolean server, + BoundaryCallbackMessages.IBoundaryCallbackMessages callback) { + + Args args = new Args(context, account, folder, thread, id, query, server); + Log.i("Get model " + viewType + " " + args); + dump(); + + Model model = models.get(viewType); + if (model == null || !model.args.equals(args)) { + Log.i("Creating model"); - void setMessages(AdapterMessage.ViewType viewType, LifecycleOwner owner, final LiveData> messages) { - if (viewType == AdapterMessage.ViewType.UNIFIED) - viewType = AdapterMessage.ViewType.FOLDER; + if (model != null) + model.clear(); - this.messages.put(viewType, messages); + DB db = DB.getInstance(context); + + BoundaryCallbackMessages boundary = null; + if (viewType == AdapterMessage.ViewType.FOLDER || viewType == AdapterMessage.ViewType.SEARCH) + boundary = new BoundaryCallbackMessages(context, + args.folder, args.server || viewType == AdapterMessage.ViewType.FOLDER, + args.query, REMOTE_PAGE_SIZE); + + LivePagedListBuilder builder = null; + switch (viewType) { + case UNIFIED: + builder = new LivePagedListBuilder<>( + db.message().pagedUnifiedInbox( + args.threading, + args.sort, + args.filter_seen, args.filter_unflagged, args.filter_snoozed, + false, + args.debug), + LOCAL_PAGE_SIZE); + break; + + case FOLDER: + PagedList.Config configFolder = new PagedList.Config.Builder() + .setPageSize(LOCAL_PAGE_SIZE) + .setPrefetchDistance(REMOTE_PAGE_SIZE) + .build(); + builder = new LivePagedListBuilder<>( + db.message().pagedFolder( + args.folder, args.threading, + args.sort, + args.filter_seen, args.filter_unflagged, args.filter_snoozed, + false, + args.debug), + configFolder); + builder.setBoundaryCallback(boundary); + break; + + case THREAD: + builder = new LivePagedListBuilder<>( + db.message().pagedThread( + args.account, args.thread, + args.threading ? null : args.id, + args.debug), LOCAL_PAGE_SIZE); + break; + + case SEARCH: + PagedList.Config configSearch = new PagedList.Config.Builder() + .setPageSize(LOCAL_PAGE_SIZE) + .setPrefetchDistance(REMOTE_PAGE_SIZE) + .build(); + if (args.folder < 0) + builder = new LivePagedListBuilder<>( + db.message().pagedUnifiedInbox( + args.threading, + "time", + false, false, false, + true, + args.debug), + configSearch); + else + builder = new LivePagedListBuilder<>( + db.message().pagedFolder( + args.folder, args.threading, + "time", + false, false, false, + true, + args.debug), + configSearch); + builder.setBoundaryCallback(boundary); + break; + } - if (viewType == AdapterMessage.ViewType.FOLDER) - this.messages.remove(AdapterMessage.ViewType.SEARCH); + builder.setFetchExecutor(executor); + + model = new Model(args, builder.build(), boundary); + models.put(viewType, model); - if (viewType == AdapterMessage.ViewType.THREAD) owner.getLifecycle().addObserver(new LifecycleObserver() { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) public void onDestroyed() { - Log.i("Removed model thread"); - ViewModelMessages.this.messages.remove(AdapterMessage.ViewType.THREAD); - } - }); - else { - // Keep list up-to-date for previous/next navigation - messages.observeForever(new Observer>() { - @Override - public void onChanged(PagedList messages) { + Log.i("Destroy model " + viewType); + + if (viewType == AdapterMessage.ViewType.THREAD) + remove(viewType); + + dump(); } }); + + if (viewType != AdapterMessage.ViewType.THREAD) + // Keep list up-to-date for previous/next navigation + model.list.observeForever(new Observer>() { + @Override + public void onChanged(PagedList messages) { + } + }); } - } - void observe(AdapterMessage.ViewType viewType, LifecycleOwner owner, Observer> observer) { - if (viewType == AdapterMessage.ViewType.UNIFIED) - viewType = AdapterMessage.ViewType.FOLDER; + if (model.boundary != null) + model.boundary.setCallback(callback); - if (owner.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.INITIALIZED)) - messages.get(viewType).observe(owner, observer); - } + if (viewType == AdapterMessage.ViewType.UNIFIED) { + remove(AdapterMessage.ViewType.FOLDER); + remove(AdapterMessage.ViewType.SEARCH); + } else if (viewType == AdapterMessage.ViewType.FOLDER) + remove(AdapterMessage.ViewType.SEARCH); - void removeObservers(AdapterMessage.ViewType viewType, LifecycleOwner owner) { - if (viewType == AdapterMessage.ViewType.UNIFIED) - viewType = AdapterMessage.ViewType.FOLDER; + last = viewType; - if (owner.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.INITIALIZED)) { - LiveData> list = messages.get(viewType); - if (list != null) - list.removeObservers(owner); - } + Log.i("Returning model=" + viewType); + dump(); + + return model.list; } @Override protected void onCleared() { - messages.clear(); + for (AdapterMessage.ViewType viewType : new ArrayList<>(models.keySet())) + remove(viewType); } - void observePrevNext(LifecycleOwner owner, final long id, boolean found, final IPrevNext intf) { - AdapterMessage.ViewType viewType = - (found ? AdapterMessage.ViewType.SEARCH : AdapterMessage.ViewType.FOLDER); + private void remove(AdapterMessage.ViewType viewType) { + Model model = models.get(viewType); + if (model != null) { + model.clear(); + models.remove(viewType); + } + } + + void observePrevNext(LifecycleOwner owner, final long id, final IPrevNext intf) { + Log.i("Observe prev/next model=" + last); - LiveData> list = messages.get(viewType); - if (list == null) { + Model model = models.get(last); + if (model == null) { Log.w("Observe previous/next without list"); return; } Log.i("Observe previous/next id=" + id); - list.observe(owner, new Observer>() { + model.list.observe(owner, new Observer>() { @Override public void onChanged(PagedList messages) { Log.i("Observe previous/next id=" + id + " messages=" + messages.size()); @@ -136,6 +249,95 @@ public class ViewModelMessages extends ViewModel { }); } + private class Args { + private long account; + private long folder; + private String thread; + private long id; + private String query; + private boolean server; + + private boolean threading; + private String sort; + private boolean filter_seen; + private boolean filter_unflagged; + private boolean filter_snoozed; + private boolean debug; + + Args(Context context, + long account, long folder, String thread, long id, + String query, boolean server) { + + this.account = account; + this.folder = folder; + this.thread = thread; + this.id = id; + this.query = query; + this.server = server; + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + this.threading = prefs.getBoolean("threading", true); + this.sort = prefs.getString("sort", "time"); + this.filter_seen = prefs.getBoolean("filter_seen", false); + this.filter_unflagged = prefs.getBoolean("filter_unflagged", false); + this.filter_snoozed = prefs.getBoolean("filter_snoozed", true); + this.debug = prefs.getBoolean("debug", false); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof Args) { + Args other = (Args) obj; + return (this.account == other.account && + this.folder == other.folder && + Objects.equals(this.thread, other.thread) && + this.id == other.id && + Objects.equals(this.query, other.query) && + this.server == other.server && + + this.threading == other.threading && + Objects.equals(this.sort, other.sort) && + this.filter_seen == other.filter_seen && + this.filter_unflagged == other.filter_unflagged && + this.filter_snoozed == other.filter_snoozed && + this.debug == other.debug); + } else + return false; + } + + @NonNull + @Override + public String toString() { + return "folder=" + account + ":" + folder + " thread=" + thread + ":" + id + + " query=" + query + ":" + server + "" + + " threading=" + threading + + " sort=" + sort + + " filter seen=" + filter_seen + " unflagged=" + filter_unflagged + " snoozed=" + filter_snoozed + + " debug=" + debug; + } + } + + private void dump() { + Log.i("Current models=" + TextUtils.join(", ", models.keySet())); + } + + private class Model { + private Args args; + private LiveData> list; + private BoundaryCallbackMessages boundary; + + Model(Args args, LiveData> list, BoundaryCallbackMessages boundary) { + this.args = args; + this.list = list; + this.boundary = boundary; + } + + private void clear() { + if (this.boundary != null) + this.boundary.clear(); + } + } + interface IPrevNext { void onPrevious(boolean exists, Long id);