FairEmail/app/src/main/java/eu/faircode/email/ViewModelMessages.java

470 lines
18 KiB
Java
Raw Normal View History

package eu.faircode.email;
2018-10-29 08:09:56 +00:00
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
2018-10-29 10:46:49 +00:00
FairEmail is distributed in the hope that it will be useful,
2018-10-29 08:09:56 +00:00
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
2018-10-29 10:46:49 +00:00
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
2018-10-29 08:09:56 +00:00
2020-01-05 17:32:53 +00:00
Copyright 2018-2020 by Marcel Bokhorst (M66B)
2018-10-29 08:09:56 +00:00
*/
2019-05-14 16:34:09 +00:00
import android.content.Context;
import android.content.SharedPreferences;
2020-03-27 09:47:49 +00:00
import android.os.Build;
2019-07-28 08:09:00 +00:00
import android.os.Bundle;
2019-05-14 16:34:09 +00:00
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
2019-05-14 16:34:09 +00:00
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
2019-05-14 16:34:09 +00:00
import androidx.preference.PreferenceManager;
2019-07-28 08:09:00 +00:00
import androidx.room.paging.LimitOffsetDataSource;
2019-05-14 16:34:09 +00:00
import java.util.ArrayList;
import java.util.HashMap;
2019-07-28 08:09:00 +00:00
import java.util.List;
import java.util.Map;
2019-05-14 16:34:09 +00:00
import java.util.Objects;
import java.util.concurrent.ExecutorService;
public class ViewModelMessages extends ViewModel {
2019-05-14 16:34:09 +00:00
private AdapterMessage.ViewType last = AdapterMessage.ViewType.UNIFIED;
2020-04-09 19:19:44 +00:00
private Map<AdapterMessage.ViewType, Model> models = new HashMap<AdapterMessage.ViewType, Model>() {
@Nullable
@Override
public Model put(AdapterMessage.ViewType key, Model value) {
Model existing = this.get(key);
if (existing != null && existing.boundary != null)
existing.boundary.destroy();
return super.put(key, value);
}
@Nullable
@Override
public Model remove(@Nullable Object key) {
Model existing = this.get(key);
if (existing != null && existing.boundary != null)
existing.boundary.destroy();
return super.remove(key);
}
};
2019-05-14 16:34:09 +00:00
2019-10-10 11:26:44 +00:00
private ExecutorService executor = Helper.getBackgroundExecutor(2, "model");
2019-05-14 16:34:09 +00:00
2020-04-26 14:30:41 +00:00
private static final int LOCAL_PAGE_SIZE = 50;
2019-05-14 16:34:09 +00:00
private static final int REMOTE_PAGE_SIZE = 10;
2019-10-29 10:35:44 +00:00
private static final int SEARCH_PAGE_SIZE = 10;
private static final int LOW_MEM_MB = 32;
2019-05-14 16:34:09 +00:00
2019-05-15 15:05:11 +00:00
Model getModel(
final Context context, final LifecycleOwner owner,
2019-05-14 16:34:09 +00:00
final AdapterMessage.ViewType viewType,
2019-07-19 06:27:44 +00:00
String type, long account, long folder,
String thread, long id, boolean filter_archive,
2020-04-09 15:50:29 +00:00
BoundaryCallbackMessages.SearchCriteria criteria, boolean server) {
2019-05-14 16:34:09 +00:00
2020-04-09 15:50:29 +00:00
Args args = new Args(context, viewType, type, account, folder, thread, id, filter_archive, criteria, server);
2019-12-07 16:02:42 +00:00
Log.d("Get model=" + viewType + " " + args);
2019-05-14 16:34:09 +00:00
dump();
Model model = models.get(viewType);
if (model == null || !model.args.equals(args)) {
2019-12-07 16:02:42 +00:00
Log.d("Creating model=" + viewType + " replace=" + (model != null));
2019-08-09 17:29:17 +00:00
if (model != null)
2019-06-20 05:25:38 +00:00
model.list.removeObservers(owner);
2019-01-14 07:21:48 +00:00
2019-05-14 16:34:09 +00:00
DB db = DB.getInstance(context);
BoundaryCallbackMessages boundary = null;
2019-10-02 19:13:10 +00:00
if (viewType == AdapterMessage.ViewType.FOLDER)
2019-05-14 16:34:09 +00:00
boundary = new BoundaryCallbackMessages(context,
2020-04-09 15:50:29 +00:00
args.account, args.folder, true, args.criteria, REMOTE_PAGE_SIZE);
2019-10-02 19:13:10 +00:00
else if (viewType == AdapterMessage.ViewType.SEARCH)
boundary = new BoundaryCallbackMessages(context,
2020-04-09 15:50:29 +00:00
args.account, args.folder, args.server, args.criteria,
2019-10-02 19:13:10 +00:00
args.server ? REMOTE_PAGE_SIZE : SEARCH_PAGE_SIZE);
2019-05-14 16:34:09 +00:00
LivePagedListBuilder<Integer, TupleMessageEx> builder = null;
switch (viewType) {
case UNIFIED:
builder = new LivePagedListBuilder<>(
2019-07-19 06:27:44 +00:00
db.message().pagedUnified(
args.type,
2019-05-14 16:34:09 +00:00
args.threading,
args.sort, args.ascending,
args.filter_seen,
args.filter_unflagged,
args.filter_unknown,
args.filter_snoozed,
2020-03-27 09:47:49 +00:00
args.filter_language,
2019-05-14 16:34:09 +00:00
false,
args.debug),
LOCAL_PAGE_SIZE);
break;
case FOLDER:
PagedList.Config configFolder = new PagedList.Config.Builder()
2019-06-07 07:53:29 +00:00
.setInitialLoadSizeHint(LOCAL_PAGE_SIZE)
2019-05-14 16:34:09 +00:00
.setPageSize(LOCAL_PAGE_SIZE)
.setPrefetchDistance(REMOTE_PAGE_SIZE)
.build();
builder = new LivePagedListBuilder<>(
db.message().pagedFolder(
args.folder, args.threading,
args.sort, args.ascending,
args.filter_seen,
args.filter_unflagged,
args.filter_unknown,
args.filter_snoozed,
2020-03-27 09:47:49 +00:00
args.filter_language,
2019-05-14 16:34:09 +00:00
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.filter_archive,
args.ascending,
2019-05-14 16:34:09 +00:00
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<>(
2019-07-19 06:27:44 +00:00
db.message().pagedUnified(
null,
2019-05-14 16:34:09 +00:00
args.threading,
"time", false,
false, false, false, false,
2020-03-27 09:47:49 +00:00
null,
2019-05-14 16:34:09 +00:00
true,
args.debug),
configSearch);
else
builder = new LivePagedListBuilder<>(
db.message().pagedFolder(
args.folder, args.threading,
"time", false,
false, false, false, false,
2020-03-27 09:47:49 +00:00
null,
2019-05-14 16:34:09 +00:00
true,
args.debug),
configSearch);
builder.setBoundaryCallback(boundary);
break;
}
2019-02-05 08:15:05 +00:00
2019-05-14 16:34:09 +00:00
builder.setFetchExecutor(executor);
model = new Model(args, builder.build(), boundary);
models.put(viewType, model);
2019-01-31 08:57:28 +00:00
}
2019-05-17 14:44:37 +00:00
owner.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
2019-05-22 13:41:54 +00:00
int free_mb = Log.getFreeMemMb();
2019-05-17 14:44:37 +00:00
boolean lowmem = (free_mb < LOW_MEM_MB);
2019-12-07 16:02:42 +00:00
Log.d("Destroy model=" + viewType +
" lowmem=" + lowmem + " free=" + free_mb + " MB");
2019-05-17 14:44:37 +00:00
Model model = models.get(viewType);
if (model != null) {
2019-12-07 16:02:42 +00:00
Log.d("Remove observer model=" + viewType);
2019-05-17 14:44:37 +00:00
model.list.removeObservers(owner);
}
if (viewType == AdapterMessage.ViewType.THREAD || lowmem) {
2019-12-07 16:02:42 +00:00
Log.d("Remove model=" + viewType);
2019-08-09 17:29:17 +00:00
models.remove(viewType);
2019-05-17 14:44:37 +00:00
}
dump();
}
});
2019-05-14 16:34:09 +00:00
if (viewType == AdapterMessage.ViewType.UNIFIED) {
2019-08-09 17:29:17 +00:00
models.remove(AdapterMessage.ViewType.FOLDER);
models.remove(AdapterMessage.ViewType.SEARCH);
2019-05-14 16:34:09 +00:00
} else if (viewType == AdapterMessage.ViewType.FOLDER)
2019-08-09 17:29:17 +00:00
models.remove(AdapterMessage.ViewType.SEARCH);
2019-05-16 09:24:58 +00:00
if (viewType != AdapterMessage.ViewType.THREAD) {
last = viewType;
2019-12-07 16:02:42 +00:00
Log.d("Last model=" + last);
2019-05-16 09:24:58 +00:00
}
2019-02-05 08:15:05 +00:00
2019-12-07 16:02:42 +00:00
Log.d("Returning model=" + viewType);
2019-05-14 16:34:09 +00:00
dump();
2019-05-15 15:05:11 +00:00
return model;
}
2020-03-24 12:20:27 +00:00
void retry(AdapterMessage.ViewType viewType) {
Model model = models.get(viewType);
if (model != null && model.boundary != null)
model.boundary.retry();
}
2018-10-21 14:07:45 +00:00
@Override
protected void onCleared() {
2019-05-14 16:34:09 +00:00
for (AdapterMessage.ViewType viewType : new ArrayList<>(models.keySet()))
models.remove(viewType);
}
void observePrevNext(LifecycleOwner owner, final long id, final IPrevNext intf) {
2019-12-07 16:02:42 +00:00
Log.d("Observe prev/next model=" + last);
2019-02-05 08:15:05 +00:00
2019-05-14 16:34:09 +00:00
Model model = models.get(last);
if (model == null) {
// When showing accounts or folders
intf.onPrevious(false, null);
intf.onNext(false, null);
intf.onFound(-1, 0);
2019-01-26 14:32:52 +00:00
return;
}
2019-01-26 11:56:23 +00:00
2019-12-07 16:02:42 +00:00
Log.d("Observe previous/next id=" + id);
2019-05-14 16:34:09 +00:00
model.list.observe(owner, new Observer<PagedList<TupleMessageEx>>() {
2019-01-26 14:32:52 +00:00
@Override
public void onChanged(PagedList<TupleMessageEx> messages) {
2019-12-07 16:02:42 +00:00
Log.d("Observe previous/next id=" + id + " messages=" + messages.size());
for (int pos = 0; pos < messages.size(); pos++) {
TupleMessageEx item = messages.get(pos);
if (item != null && id == item.id) {
2019-01-26 14:32:52 +00:00
if (pos - 1 >= 0) {
TupleMessageEx next = messages.get(pos - 1);
2019-02-02 12:58:28 +00:00
intf.onNext(true, next == null ? null : next.id);
2019-01-26 14:32:52 +00:00
} else
2019-02-02 12:58:28 +00:00
intf.onNext(false, null);
2019-01-26 14:32:52 +00:00
if (pos + 1 < messages.size()) {
TupleMessageEx prev = messages.get(pos + 1);
2019-02-02 12:58:28 +00:00
intf.onPrevious(true, prev == null ? null : prev.id);
2019-01-26 14:32:52 +00:00
} else
2019-02-02 12:58:28 +00:00
intf.onPrevious(false, null);
intf.onFound(pos, messages.size());
2019-01-26 14:32:52 +00:00
return;
2019-01-26 14:32:52 +00:00
}
}
Log.w("Observe previous/next gone id=" + id);
2019-01-26 14:32:52 +00:00
}
});
2018-10-20 19:04:05 +00:00
}
2019-07-28 08:09:00 +00:00
void getIds(Context context, LifecycleOwner owner, final Observer<List<Long>> observer) {
final Model model = models.get(last);
if (model == null) {
Log.w("Get IDs without model");
observer.onChanged(new ArrayList<Long>());
return;
}
new SimpleTask<List<Long>>() {
@Override
protected List<Long> onExecute(Context context, Bundle args) {
List<Long> ids = new ArrayList<>();
2019-08-11 08:18:23 +00:00
PagedList<TupleMessageEx> plist = model.list.getValue();
if (plist == null)
return ids;
2019-10-04 17:17:33 +00:00
2019-08-11 08:18:23 +00:00
LimitOffsetDataSource<TupleMessageEx> ds = (LimitOffsetDataSource<TupleMessageEx>) plist.getDataSource();
2019-07-28 08:09:00 +00:00
int count = ds.countItems();
for (int i = 0; i < count; i += 100)
for (TupleMessageEx message : ds.loadRange(i, Math.min(100, count - i)))
2019-11-23 12:48:59 +00:00
if ((message.uid != null && !message.folderReadOnly) ||
message.accountProtocol != EntityAccount.TYPE_IMAP)
ids.add(message.id);
2019-07-28 08:09:00 +00:00
Log.i("Loaded messages #" + ids.size());
return ids;
}
@Override
protected void onExecuted(Bundle args, List<Long> ids) {
observer.onChanged(ids);
}
@Override
protected void onException(Bundle args, Throwable ex) {
observer.onChanged(new ArrayList<Long>());
}
}.execute(context, owner, new Bundle(), "model:ids");
}
2019-05-14 16:34:09 +00:00
private class Args {
private long account;
2019-07-19 06:27:44 +00:00
private String type;
2019-05-14 16:34:09 +00:00
private long folder;
private String thread;
private long id;
2020-04-09 15:50:29 +00:00
private BoundaryCallbackMessages.SearchCriteria criteria;
2019-05-14 16:34:09 +00:00
private boolean server;
private boolean threading;
private String sort;
2019-09-03 08:33:52 +00:00
private boolean ascending;
2019-05-14 16:34:09 +00:00
private boolean filter_seen;
private boolean filter_unflagged;
private boolean filter_unknown;
2019-05-14 16:34:09 +00:00
private boolean filter_snoozed;
private boolean filter_archive;
2020-03-27 09:47:49 +00:00
private String filter_language;
2019-05-14 16:34:09 +00:00
private boolean debug;
Args(Context context,
2019-09-09 07:46:54 +00:00
AdapterMessage.ViewType viewType,
2019-07-19 06:27:44 +00:00
String type, long account, long folder,
String thread, long id, boolean filter_archive,
2020-04-09 15:50:29 +00:00
BoundaryCallbackMessages.SearchCriteria criteria, boolean server) {
2019-05-14 16:34:09 +00:00
2019-07-19 06:27:44 +00:00
this.type = type;
2019-05-14 16:34:09 +00:00
this.account = account;
this.folder = folder;
this.thread = thread;
this.id = id;
this.filter_archive = filter_archive;
2020-04-09 15:50:29 +00:00
this.criteria = criteria;
2019-05-14 16:34:09 +00:00
this.server = server;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
this.threading = prefs.getBoolean("threading", true);
this.sort = prefs.getString("sort", "time");
2019-09-09 07:46:54 +00:00
this.ascending = prefs.getBoolean(
viewType == AdapterMessage.ViewType.THREAD ? "ascending_thread" : "ascending_list", false);
2019-05-14 16:34:09 +00:00
this.filter_seen = prefs.getBoolean("filter_seen", false);
this.filter_unflagged = prefs.getBoolean("filter_unflagged", false);
this.filter_unknown = prefs.getBoolean("filter_unknown", false);
2019-05-14 16:34:09 +00:00
this.filter_snoozed = prefs.getBoolean("filter_snoozed", true);
boolean language_detection = prefs.getBoolean("language_detection", false);
if (!language_detection || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
2020-03-27 09:47:49 +00:00
this.filter_language = null;
else
this.filter_language = prefs.getString("filter_language", null);
2019-05-14 16:34:09 +00:00
this.debug = prefs.getBoolean("debug", false);
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof Args) {
Args other = (Args) obj;
2019-07-19 06:27:44 +00:00
return (Objects.equals(this.type, other.type) &&
this.account == other.account &&
2019-05-14 16:34:09 +00:00
this.folder == other.folder &&
Objects.equals(this.thread, other.thread) &&
this.id == other.id &&
2020-04-09 15:50:29 +00:00
Objects.equals(this.criteria, other.criteria) &&
2019-05-14 16:34:09 +00:00
this.server == other.server &&
this.threading == other.threading &&
Objects.equals(this.sort, other.sort) &&
2019-09-03 08:33:52 +00:00
this.ascending == other.ascending &&
2019-05-14 16:34:09 +00:00
this.filter_seen == other.filter_seen &&
this.filter_unflagged == other.filter_unflagged &&
this.filter_unknown == other.filter_unknown &&
2019-05-14 16:34:09 +00:00
this.filter_snoozed == other.filter_snoozed &&
this.filter_archive == other.filter_archive &&
2020-03-27 09:47:49 +00:00
Objects.equals(this.filter_language, other.filter_language) &&
2019-05-14 16:34:09 +00:00
this.debug == other.debug);
} else
return false;
}
@NonNull
@Override
public String toString() {
2019-07-19 06:27:44 +00:00
return "folder=" + type + ":" + account + ":" + folder +
" thread=" + thread + ":" + id +
2020-04-09 15:50:29 +00:00
" criteria=" + criteria + ":" + server + "" +
2019-05-14 16:34:09 +00:00
" threading=" + threading +
" sort=" + sort + ":" + ascending +
" filter seen=" + filter_seen +
" unflagged=" + filter_unflagged +
" unknown=" + filter_unknown +
" snoozed=" + filter_snoozed +
" archive=" + filter_archive +
2020-03-27 09:47:49 +00:00
" language=" + filter_language +
2019-05-14 16:34:09 +00:00
" debug=" + debug;
}
}
private void dump() {
2019-12-07 16:02:42 +00:00
Log.d("Current models=" + TextUtils.join(", ", models.keySet()));
2019-05-14 16:34:09 +00:00
}
2019-05-15 15:05:11 +00:00
class Model {
2019-05-14 16:34:09 +00:00
private Args args;
private LiveData<PagedList<TupleMessageEx>> list;
private BoundaryCallbackMessages boundary;
Model(Args args, LiveData<PagedList<TupleMessageEx>> list, BoundaryCallbackMessages boundary) {
this.args = args;
this.list = list;
this.boundary = boundary;
}
2019-08-09 17:29:17 +00:00
void setCallback(LifecycleOwner owner, BoundaryCallbackMessages.IBoundaryCallbackMessages callback) {
if (boundary != null) {
2019-05-15 15:05:11 +00:00
boundary.setCallback(callback);
2019-08-09 17:29:17 +00:00
owner.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
2020-03-22 09:14:36 +00:00
boundary.destroy();
2019-08-09 17:29:17 +00:00
}
});
}
2019-05-15 15:05:11 +00:00
}
void setObserver(LifecycleOwner owner, @NonNull Observer<PagedList<TupleMessageEx>> observer) {
2019-05-16 09:24:58 +00:00
//list.removeObservers(owner);
2019-05-15 15:05:11 +00:00
list.observe(owner, observer);
}
2019-05-14 16:34:09 +00:00
}
2019-01-26 14:32:52 +00:00
interface IPrevNext {
2019-01-29 10:08:22 +00:00
void onPrevious(boolean exists, Long id);
2019-01-26 14:32:52 +00:00
2019-01-29 10:08:22 +00:00
void onNext(boolean exists, Long id);
2019-02-02 12:58:28 +00:00
void onFound(int position, int size);
}
}