diff --git a/app/build.gradle b/app/build.gradle index 200012693..6f0b77183 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -136,6 +136,7 @@ dependencies { annotationProcessor "androidx.room:room-compiler:${ROOM_VERSION}" implementation "androidx.lifecycle:lifecycle-extensions:2.0.0" implementation "io.reactivex.rxjava2:rxandroid:2.1.1" + implementation "androidx.paging:paging-runtime:2.1.0" annotationProcessor "com.jakewharton:butterknife-compiler:${BUTTERKNIFE_VERSION}" implementation "com.jakewharton:butterknife:${BUTTERKNIFE_VERSION}" diff --git a/app/licenses.yml b/app/licenses.yml index 7edc9f655..47dc757dc 100644 --- a/app/licenses.yml +++ b/app/licenses.yml @@ -702,3 +702,15 @@ license: The Apache Software License, Version 2.0 licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt url: https://github.com/census-instrumentation/opencensus-java +- artifact: androidx.paging:paging-common:+ + name: Android Paging-Common + copyrightHolder: Android Open Source Project + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.paging:paging-runtime:+ + name: Android Paging-Runtime + copyrightHolder: Android Open Source Project + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html diff --git a/app/src/main/assets/licenses.json b/app/src/main/assets/licenses.json index ab98024d2..652bba604 100644 --- a/app/src/main/assets/licenses.json +++ b/app/src/main/assets/licenses.json @@ -1887,6 +1887,38 @@ "group": "io.opencensus", "version": "0.18.0" } + }, + { + "notice": null, + "copyrightHolder": "Android Open Source Project", + "copyrightStatement": "Copyright © Android Open Source Project. All rights reserved.", + "license": "The Apache Software License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "https://developer.android.com/topic/libraries/architecture/index.html", + "libraryName": "Android Paging-Common", + "artifactId": { + "name": "paging-common", + "group": "androidx.paging", + "version": "2.1.0" + } + }, + { + "notice": null, + "copyrightHolder": "Android Open Source Project", + "copyrightStatement": "Copyright © Android Open Source Project. All rights reserved.", + "license": "The Apache Software License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "https://developer.android.com/topic/libraries/architecture/index.html", + "libraryName": "Android Paging-Runtime", + "artifactId": { + "name": "paging-runtime", + "group": "androidx.paging", + "version": "2.1.0" + } } ] } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java index 460d643c1..809a35bb2 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java @@ -29,7 +29,6 @@ import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.lifecycle.ViewModelProviders; import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -83,6 +82,8 @@ import org.tasks.preferences.Device; import org.tasks.preferences.Preferences; import org.tasks.sync.SyncAdapters; import org.tasks.tasklist.ActionModeProvider; +import org.tasks.tasklist.ManualSortRecyclerAdapter; +import org.tasks.tasklist.PagedListRecyclerAdapter; import org.tasks.tasklist.TaskListRecyclerAdapter; import org.tasks.tasklist.ViewHolderFactory; import org.tasks.ui.CheckBoxes; @@ -237,12 +238,22 @@ public final class TaskListFragment extends InjectingFragment taskListViewModel.setFilter(filter, taskAdapter.isManuallySorted()); recyclerAdapter = - new TaskListRecyclerAdapter( - taskAdapter, viewHolderFactory, this, actionModeProvider, taskListViewModel.getValue()); + taskAdapter.isManuallySorted() + ? new ManualSortRecyclerAdapter( + taskAdapter, + recyclerView, + viewHolderFactory, + this, + actionModeProvider, + taskListViewModel.getValue()) + : new PagedListRecyclerAdapter( + taskAdapter, + viewHolderFactory, + this, + actionModeProvider, + taskListViewModel.getValue()); taskAdapter.setHelper(recyclerAdapter); ((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false); - new ItemTouchHelper(recyclerAdapter.getItemTouchHelperCallback()) - .attachToRecyclerView(recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(context)); taskListViewModel.observe( diff --git a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java index bb116cd42..ddb427606 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java @@ -10,6 +10,7 @@ import static com.google.common.collect.Lists.newArrayList; import static com.todoroo.andlib.utility.DateUtilities.now; import android.database.Cursor; +import androidx.paging.DataSource; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.Query; @@ -26,6 +27,9 @@ import com.todoroo.astrid.helper.UUIDHelper; import java.util.ArrayList; import java.util.List; import org.tasks.BuildConfig; +import org.tasks.data.CaldavTask; +import org.tasks.data.GoogleTask; +import org.tasks.data.Tag; import org.tasks.data.TaskContainer; import org.tasks.jobs.WorkManager; import timber.log.Timber; @@ -131,6 +135,9 @@ public abstract class TaskDao { @RawQuery public abstract List fetchTasks(SimpleSQLiteQuery query); + @RawQuery(observedEntities = {Task.class, GoogleTask.class, CaldavTask.class, Tag.class}) + public abstract DataSource.Factory getTaskFactory(SimpleSQLiteQuery query); + /** * Saves the given task to the database.getDatabase(). Task must already exist. Returns true on * success. diff --git a/app/src/main/java/org/tasks/tasklist/ItemCallback.java b/app/src/main/java/org/tasks/tasklist/ItemCallback.java new file mode 100644 index 000000000..03297217a --- /dev/null +++ b/app/src/main/java/org/tasks/tasklist/ItemCallback.java @@ -0,0 +1,19 @@ +package org.tasks.tasklist; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import org.tasks.data.TaskContainer; + +public class ItemCallback extends DiffUtil.ItemCallback { + + @Override + public boolean areItemsTheSame(@NonNull TaskContainer oldItem, @NonNull TaskContainer newItem) { + return oldItem.getId() == newItem.getId(); + } + + @Override + public boolean areContentsTheSame( + @NonNull TaskContainer oldItem, @NonNull TaskContainer newItem) { + return oldItem.equals(newItem); + } +} diff --git a/app/src/main/java/org/tasks/tasklist/ItemTouchHelperCallback.java b/app/src/main/java/org/tasks/tasklist/ItemTouchHelperCallback.java index b7ea44eb2..94cea53ce 100644 --- a/app/src/main/java/org/tasks/tasklist/ItemTouchHelperCallback.java +++ b/app/src/main/java/org/tasks/tasklist/ItemTouchHelperCallback.java @@ -15,14 +15,14 @@ import org.tasks.data.TaskContainer; public class ItemTouchHelperCallback extends ItemTouchHelper.Callback { private final TaskAdapter adapter; - private final TaskListRecyclerAdapter recyclerAdapter; + private final ManualSortRecyclerAdapter recyclerAdapter; private final Runnable onClear; private int from = -1; private int to = -1; private boolean dragging; ItemTouchHelperCallback( - TaskAdapter adapter, TaskListRecyclerAdapter recyclerAdapter, Runnable onClear) { + TaskAdapter adapter, ManualSortRecyclerAdapter recyclerAdapter, Runnable onClear) { this.adapter = adapter; this.recyclerAdapter = recyclerAdapter; this.onClear = onClear; diff --git a/app/src/main/java/org/tasks/tasklist/ManualSortRecyclerAdapter.java b/app/src/main/java/org/tasks/tasklist/ManualSortRecyclerAdapter.java new file mode 100644 index 000000000..2da160348 --- /dev/null +++ b/app/src/main/java/org/tasks/tasklist/ManualSortRecyclerAdapter.java @@ -0,0 +1,109 @@ +package org.tasks.tasklist; + +import static com.todoroo.andlib.utility.AndroidUtilities.assertMainThread; +import static com.todoroo.andlib.utility.AndroidUtilities.assertNotMainThread; + +import androidx.annotation.NonNull; +import androidx.core.util.Pair; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.DiffUtil.DiffResult; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.ListUpdateCallback; +import androidx.recyclerview.widget.RecyclerView; +import com.todoroo.astrid.activity.TaskListFragment; +import com.todoroo.astrid.adapter.TaskAdapter; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.PublishSubject; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import org.tasks.data.TaskContainer; + +public class ManualSortRecyclerAdapter extends TaskListRecyclerAdapter { + + private final ItemTouchHelperCallback itemTouchHelperCallback; + private List list; + private PublishSubject> publishSubject = PublishSubject.create(); + private CompositeDisposable disposables = new CompositeDisposable(); + private Queue, DiffResult>> updates = new LinkedList<>(); + + public ManualSortRecyclerAdapter( + TaskAdapter adapter, + RecyclerView recyclerView, ViewHolderFactory viewHolderFactory, + TaskListFragment taskList, + ActionModeProvider actionModeProvider, + List list) { + super(adapter, viewHolderFactory, taskList, actionModeProvider); + this.list = list; + itemTouchHelperCallback = new ItemTouchHelperCallback(adapter, this, this::drainQueue); + new ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(recyclerView); + Pair, DiffResult> initial = Pair.create(list, null); + disposables.add( + publishSubject + .observeOn(Schedulers.computation()) + .scan(initial, this::calculateDiff) + .skip(1) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::applyDiff)); + } + + private Pair, DiffResult> calculateDiff( + Pair, DiffResult> last, List next) { + assertNotMainThread(); + + DiffCallback cb = new DiffCallback(last.first, next, adapter); + DiffResult result = DiffUtil.calculateDiff(cb, true); + + return Pair.create(next, result); + } + + private void applyDiff(Pair, DiffResult> update) { + assertMainThread(); + + updates.add(update); + + if (!itemTouchHelperCallback.isDragging()) { + drainQueue(); + } + } + + private void drainQueue() { + assertMainThread(); + + Pair, DiffResult> update = updates.poll(); + while (update != null) { + list = update.first; + update.second.dispatchUpdatesTo((ListUpdateCallback) this); + update = updates.poll(); + } + } + + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + disposables.dispose(); + } + + @Override + public int getItemCount() { + return list.size(); + } + + @Override + public TaskContainer getItem(int position) { + return list.get(position); + } + + @Override + public void submitList(List list) { + publishSubject.onNext(list); + } + + void moved(int from, int to, int indent) { + adapter.moved(from, to, indent); + TaskContainer task = list.remove(from); + list.add(from < to ? to - 1 : to, task); + taskList.loadTaskListContent(); + } +} diff --git a/app/src/main/java/org/tasks/tasklist/PagedListRecyclerAdapter.java b/app/src/main/java/org/tasks/tasklist/PagedListRecyclerAdapter.java new file mode 100644 index 000000000..db71ff97b --- /dev/null +++ b/app/src/main/java/org/tasks/tasklist/PagedListRecyclerAdapter.java @@ -0,0 +1,45 @@ +package org.tasks.tasklist; + +import androidx.paging.AsyncPagedListDiffer; +import androidx.paging.PagedList; +import androidx.recyclerview.widget.AsyncDifferConfig; +import com.todoroo.astrid.activity.TaskListFragment; +import com.todoroo.astrid.adapter.TaskAdapter; +import java.util.List; +import org.tasks.data.TaskContainer; + +public class PagedListRecyclerAdapter extends TaskListRecyclerAdapter { + + private AsyncPagedListDiffer differ; + + public PagedListRecyclerAdapter( + TaskAdapter adapter, + ViewHolderFactory viewHolderFactory, + TaskListFragment taskList, + ActionModeProvider actionModeProvider, + List list) { + super(adapter, viewHolderFactory, taskList, actionModeProvider); + + differ = + new AsyncPagedListDiffer<>( + this, new AsyncDifferConfig.Builder<>(new ItemCallback()).build()); + if (list instanceof PagedList) { + differ.submitList((PagedList) list); + } + } + + @Override + public int getItemCount() { + return differ.getItemCount(); + } + + @Override + public TaskContainer getItem(int position) { + return differ.getItem(position); + } + + @Override + public void submitList(List list) { + differ.submitList((PagedList) list); + } +} diff --git a/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java b/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java index 7ea4e23f7..9d7d69771 100644 --- a/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java +++ b/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java @@ -1,17 +1,11 @@ package org.tasks.tasklist; -import static com.todoroo.andlib.utility.AndroidUtilities.assertMainThread; -import static com.todoroo.andlib.utility.AndroidUtilities.assertNotMainThread; - import android.os.Bundle; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.view.ActionMode; -import androidx.core.util.Pair; import androidx.fragment.app.FragmentActivity; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.DiffUtil.DiffResult; import androidx.recyclerview.widget.ListUpdateCallback; import androidx.recyclerview.widget.RecyclerView; import com.google.common.primitives.Longs; @@ -20,92 +14,32 @@ import com.todoroo.astrid.adapter.TaskAdapter; import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.GtasksFilter; import com.todoroo.astrid.utility.Flags; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import io.reactivex.subjects.PublishSubject; -import java.util.LinkedList; import java.util.List; -import java.util.Queue; import org.tasks.data.TaskContainer; import org.tasks.intents.TaskIntents; -public class TaskListRecyclerAdapter extends RecyclerView.Adapter +public abstract class TaskListRecyclerAdapter extends RecyclerView.Adapter implements ViewHolder.ViewHolderCallbacks, ListUpdateCallback { private static final String EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids"; - private final TaskAdapter adapter; + protected final TaskAdapter adapter; + final TaskListFragment taskList; private final ViewHolderFactory viewHolderFactory; - private final TaskListFragment taskList; private final ActionModeProvider actionModeProvider; - private final ItemTouchHelperCallback itemTouchHelperCallback; private final boolean isGoogleTaskList; private ActionMode mode = null; - private List list; - private PublishSubject> publishSubject = PublishSubject.create(); - private Queue, DiffResult>> updates = new LinkedList<>(); - private CompositeDisposable disposables = new CompositeDisposable(); - public TaskListRecyclerAdapter( + TaskListRecyclerAdapter( TaskAdapter adapter, ViewHolderFactory viewHolderFactory, TaskListFragment taskList, - ActionModeProvider actionModeProvider, - List list) { + ActionModeProvider actionModeProvider) { this.adapter = adapter; this.viewHolderFactory = viewHolderFactory; this.taskList = taskList; this.actionModeProvider = actionModeProvider; - this.list = list; - itemTouchHelperCallback = - new ItemTouchHelperCallback(adapter, this, this::drainQueue); isGoogleTaskList = taskList.getFilter() instanceof GtasksFilter; - Pair, DiffResult> initial = Pair.create(list, null); - disposables.add( - publishSubject - .observeOn(Schedulers.computation()) - .scan(initial, this::calculateDiff) - .skip(1) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::applyDiff)); - } - - private Pair, DiffResult> calculateDiff( - Pair, DiffResult> last, List next) { - assertNotMainThread(); - - DiffCallback cb = new DiffCallback(last.first, next, adapter); - DiffResult result = DiffUtil.calculateDiff(cb, true); - - return Pair.create(next, result); - } - - private void drainQueue() { - assertMainThread(); - - Pair, DiffResult> update = updates.poll(); - Bundle selections = getSaveState(); - while (update != null) { - list = update.first; - update.second.dispatchUpdatesTo((ListUpdateCallback) this); - update = updates.poll(); - } - restoreSaveState(selections); - } - - private void applyDiff(Pair, DiffResult> update) { - assertMainThread(); - - updates.add(update); - - if (!itemTouchHelperCallback.isDragging()) { - drainQueue(); - } - } - - public ItemTouchHelperCallback getItemTouchHelperCallback() { - return itemTouchHelperCallback; } public Bundle getSaveState() { @@ -125,13 +59,14 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter } } + @NonNull @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return viewHolderFactory.newViewHolder(parent, this); } @Override - public void onBindViewHolder(ViewHolder holder, int position) { + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { TaskContainer task = getItem(position); if (task != null) { holder.bindView(task, isGoogleTaskList); @@ -143,11 +78,6 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter } } - @Override - public int getItemCount() { - return list.size(); - } - @Override public void onCompletedTask(TaskContainer task, boolean newState) { adapter.onCompletedTask(task, newState); @@ -173,11 +103,6 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter } } - @Override - public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { - disposables.dispose(); - } - @Override public boolean onLongPress(ViewHolder viewHolder) { if (!adapter.isManuallySorted()) { @@ -230,13 +155,9 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter mode = null; } - public TaskContainer getItem(int position) { - return list.get(position); - } + public abstract TaskContainer getItem(int position); - public void submitList(List list) { - publishSubject.onNext(list); - } + public abstract void submitList(List list); @Override public void onInserted(int position, int count) { @@ -257,11 +178,4 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter public void onChanged(int position, int count, @Nullable Object payload) { notifyItemRangeChanged(position, count, payload); } - - void moved(int from, int to, int indent) { - adapter.moved(from, to, indent); - TaskContainer task = list.remove(from); - list.add(from < to ? to - 1 : to, task); - taskList.loadTaskListContent(); - } } diff --git a/app/src/main/java/org/tasks/ui/TaskListViewModel.java b/app/src/main/java/org/tasks/ui/TaskListViewModel.java index fe24fd63a..d9a894271 100644 --- a/app/src/main/java/org/tasks/ui/TaskListViewModel.java +++ b/app/src/main/java/org/tasks/ui/TaskListViewModel.java @@ -7,9 +7,13 @@ import static com.todoroo.astrid.activity.TaskListFragment.TAGS_METADATA_JOIN; import androidx.annotation.NonNull; import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModel; +import androidx.paging.DataSource.Factory; +import androidx.paging.LivePagedListBuilder; +import androidx.paging.PagedList; import androidx.sqlite.db.SimpleSQLiteQuery; import com.google.common.collect.Lists; import com.todoroo.andlib.data.Property.StringProperty; @@ -40,7 +44,10 @@ import org.tasks.data.TaskContainer; import org.tasks.preferences.Preferences; import timber.log.Timber; -public class TaskListViewModel extends ViewModel { +public class TaskListViewModel extends ViewModel implements Observer> { + + private static final PagedList.Config PAGED_LIST_CONFIG = + new PagedList.Config.Builder().setPageSize(20).build(); private static final Field TASKS = field("tasks.*"); private static final Field GTASK = field(GTASK_METADATA_JOIN + ".*"); @@ -60,6 +67,7 @@ public class TaskListViewModel extends ViewModel { private Filter filter; private boolean manualSort; private CompositeDisposable disposable = new CompositeDisposable(); + private LiveData> internal; public void setFilter(@NonNull Filter filter, boolean manualSort) { if (!filter.equals(this.filter) @@ -134,22 +142,49 @@ public class TaskListViewModel extends ViewModel { } public void invalidate() { - String query = getQuery(filter); - Timber.v(query); - disposable.add( - Single.fromCallable(() -> taskDao.fetchTasks(new SimpleSQLiteQuery(query))) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(tasks::setValue, Timber::e)); + if (internal != null) { + internal.removeObserver(this); + } + SimpleSQLiteQuery query = new SimpleSQLiteQuery(getQuery(filter)); + Timber.v(query.getSql()); + if (manualSort) { + disposable.add( + Single.fromCallable(() -> taskDao.fetchTasks(query)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(tasks::setValue, Timber::e)); + } else { + Factory factory = taskDao.getTaskFactory(query); + LivePagedListBuilder builder = new LivePagedListBuilder<>( + factory, PAGED_LIST_CONFIG); + List current = tasks.getValue(); + if (current instanceof PagedList) { + Object lastKey = ((PagedList) current).getLastKey(); + if (lastKey instanceof Integer) { + builder.setInitialLoadKey((Integer) lastKey); + } + } + internal = builder.build(); + internal.observeForever(this); + } } @Override protected void onCleared() { disposable.dispose(); + + if (internal != null) { + internal.removeObserver(this); + } } public List getValue() { List value = tasks.getValue(); return value != null ? value : Collections.emptyList(); } + + @Override + public void onChanged(PagedList taskContainers) { + tasks.setValue(taskContainers); + } }