diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/TaskRepository.java b/Habitica/src/main/java/com/habitrpg/android/habitica/data/TaskRepository.java index a96ad378e..e4ad2abda 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/TaskRepository.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/TaskRepository.java @@ -47,7 +47,7 @@ public interface TaskRepository extends BaseRepository { void swapTaskPosition(int firstPosition, int secondPosition); - Observable> updateTaskPosition(int currentPosition); + Observable> updateTaskPosition(int oldPosition, int newPosition); Observable getUnmanagedTask(String taskid); diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImpl.java b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImpl.java index f78b2fabc..b17311abb 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImpl.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImpl.java @@ -219,14 +219,16 @@ public class TaskRepositoryImpl extends BaseRepositoryImpl localRepository.swapTaskPosition(firstPosition, secondPosition); } - public Observable> updateTaskPosition(int currentPosition) { - return localRepository.getTaskAtPosition(currentPosition).first() + public Observable> updateTaskPosition(int oldPosition, int newPosition) { + return localRepository.getTaskAtPosition(oldPosition) + .first() .flatMap(task -> { if (task.isValid()) { - return apiClient.postTaskNewPosition(task.getId(), currentPosition); + return apiClient.postTaskNewPosition(task.getId(), newPosition); } return Observable.just(null); - }); + }) + .doOnNext(localRepository::updateTaskPositions); } @Override diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/TaskLocalRepository.java b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/TaskLocalRepository.java index 28423ff5b..ebd036853 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/TaskLocalRepository.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/TaskLocalRepository.java @@ -35,4 +35,6 @@ public interface TaskLocalRepository extends BaseLocalRepository { Observable getTaskAtPosition(int currentPosition); Observable updateIsdue(TaskList daily); + + void updateTaskPositions(List taskOrder); } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTaskLocalRepository.java b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTaskLocalRepository.java index 78a39c04d..fe5cf12fd 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTaskLocalRepository.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTaskLocalRepository.java @@ -218,4 +218,18 @@ public class RealmTaskLocalRepository extends RealmBaseLocalRepository implement return dailies; }); } + + @Override + public void updateTaskPositions(List taskOrder) { + if (taskOrder.size() > 0) { + RealmResults tasks = realm.where(Task.class).in("id", taskOrder.toArray(new String[0])).findAll(); + realm.executeTransaction(realm1 -> { + for (Task task : tasks) { + if (taskOrder.contains(task.getId())) { + task.position = taskOrder.indexOf(task.getId()); + } + } + }); + } + } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.java index 79d514917..edfbc1910 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.java @@ -1,6 +1,8 @@ package com.habitrpg.android.habitica.ui.adapter.tasks; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -11,23 +13,192 @@ import com.habitrpg.android.habitica.helpers.TaskFilterHelper; import com.habitrpg.android.habitica.models.tasks.Task; import com.habitrpg.android.habitica.ui.viewHolders.tasks.BaseTaskViewHolder; +import io.realm.OrderedCollectionChangeSet; import io.realm.OrderedRealmCollection; +import io.realm.OrderedRealmCollectionChangeListener; +import io.realm.RealmList; +import io.realm.RealmModel; import io.realm.RealmQuery; import io.realm.RealmRecyclerViewAdapter; +import io.realm.RealmResults; + +public abstract class RealmBaseTasksRecyclerViewAdapter extends RecyclerView.Adapter implements TaskRecyclerViewAdapter { + + private final boolean hasAutoUpdates; + private final boolean updateOnModification; + private boolean ignoreUpdates; + private final OrderedRealmCollectionChangeListener listener; + @Nullable + private OrderedRealmCollection adapterData; + + private OrderedRealmCollectionChangeListener createListener() { + return new OrderedRealmCollectionChangeListener() { + @Override + public void onChange(Object collection, OrderedCollectionChangeSet changeSet) { + if (ignoreUpdates) { + return; + } + // null Changes means the async query returns the first time. + if (changeSet == null) { + notifyDataSetChanged(); + return; + } + // For deletions, the adapter has to be notified in reverse order. + OrderedCollectionChangeSet.Range[] deletions = changeSet.getDeletionRanges(); + for (int i = deletions.length - 1; i >= 0; i--) { + OrderedCollectionChangeSet.Range range = deletions[i]; + notifyItemRangeRemoved(range.startIndex, range.length); + } + + OrderedCollectionChangeSet.Range[] insertions = changeSet.getInsertionRanges(); + for (OrderedCollectionChangeSet.Range range : insertions) { + notifyItemRangeInserted(range.startIndex, range.length); + } + + if (!updateOnModification) { + return; + } + + OrderedCollectionChangeSet.Range[] modifications = changeSet.getChangeRanges(); + for (OrderedCollectionChangeSet.Range range : modifications) { + notifyItemRangeChanged(range.startIndex, range.length); + } + } + }; + } + + public RealmBaseTasksRecyclerViewAdapter(@Nullable OrderedRealmCollection data, boolean autoUpdate, int layoutResource, @Nullable TaskFilterHelper taskFilterHelper) { + this.unfilteredData = data; + this.layoutResource = layoutResource; + this.taskFilterHelper = taskFilterHelper; + if (data != null && !data.isManaged()) + throw new IllegalStateException("Only use this adapter with managed RealmCollection, " + + "for un-managed lists you can just use the BaseRecyclerViewAdapter"); + this.adapterData = data; + this.hasAutoUpdates = autoUpdate; + this.listener = hasAutoUpdates ? createListener() : null; + this.updateOnModification = true; + } + + @Override + public void onAttachedToRecyclerView(final RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + if (hasAutoUpdates && isDataValid()) { + //noinspection ConstantConditions + addListener(adapterData); + } + } + + @Override + public void onDetachedFromRecyclerView(final RecyclerView recyclerView) { + super.onDetachedFromRecyclerView(recyclerView); + if (hasAutoUpdates && isDataValid()) { + //noinspection ConstantConditions + removeListener(adapterData); + } + } + + /** + * Returns the current ID for an item. Note that item IDs are not stable so you cannot rely on the item ID being the + * same after notifyDataSetChanged() or {@link #updateData(OrderedRealmCollection)} has been called. + * + * @param index position of item in the adapter. + * @return current item ID. + */ + @Override + public long getItemId(final int index) { + return index; + } + + @Override + public int getItemCount() { + //noinspection ConstantConditions + return isDataValid() ? adapterData.size() : 0; + } + + /** + * Returns the item associated with the specified position. + * Can return {@code null} if provided Realm instance by {@link OrderedRealmCollection} is closed. + * + * @param index index of the item. + * @return the item at the specified position, {@code null} if adapter data is not valid. + */ + @SuppressWarnings("WeakerAccess") + @Nullable + public Task getItem(int index) { + //noinspection ConstantConditions + return isDataValid() ? adapterData.get(index) : null; + } + + /** + * Returns data associated with this adapter. + * + * @return adapter data. + */ + @Nullable + public OrderedRealmCollection getData() { + return adapterData; + } + + /** + * Updates the data associated to the Adapter. Useful when the query has been changed. + * If the query does not change you might consider using the automaticUpdate feature. + * + * @param data the new {@link OrderedRealmCollection} to display. + */ + @SuppressWarnings("WeakerAccess") + public void updateData(@Nullable OrderedRealmCollection data) { + if (hasAutoUpdates) { + if (isDataValid()) { + //noinspection ConstantConditions + removeListener(adapterData); + } + if (data != null) { + addListener(data); + } + } + + this.adapterData = data; + notifyDataSetChanged(); + } + + private void addListener(@NonNull OrderedRealmCollection data) { + if (data instanceof RealmResults) { + RealmResults results = (RealmResults) data; + //noinspection unchecked + results.addChangeListener(listener); + } else if (data instanceof RealmList) { + RealmList list = (RealmList) data; + //noinspection unchecked + list.addChangeListener(listener); + } else { + throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass()); + } + } + + private void removeListener(@NonNull OrderedRealmCollection data) { + if (data instanceof RealmResults) { + RealmResults results = (RealmResults) data; + //noinspection unchecked + results.removeChangeListener(listener); + } else if (data instanceof RealmList) { + RealmList list = (RealmList) data; + //noinspection unchecked + list.removeChangeListener(listener); + } else { + throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass()); + } + } + + private boolean isDataValid() { + return adapterData != null && adapterData.isValid(); + } -public abstract class RealmBaseTasksRecyclerViewAdapter extends RealmRecyclerViewAdapter implements TaskRecyclerViewAdapter { private final int layoutResource; private final TaskFilterHelper taskFilterHelper; private final OrderedRealmCollection unfilteredData; - public RealmBaseTasksRecyclerViewAdapter(@Nullable OrderedRealmCollection data, boolean autoUpdate, int layoutResource, @Nullable TaskFilterHelper taskFilterHelper) { - super(data, autoUpdate); - this.unfilteredData = data; - this.layoutResource = layoutResource; - this.taskFilterHelper = taskFilterHelper; - } - @Override public void onBindViewHolder(VH holder, int position) { Task item = getItem(position); @@ -36,6 +207,7 @@ public abstract class RealmBaseTasksRecyclerViewAdapter {}, RxErrorHandler.handleEmptyError()); + if (fromPosition != null) { + recyclerAdapter.setIgnoreUpdates(true); + taskRepository.updateTaskPosition(fromPosition, viewHolder.getAdapterPosition()) + .delay(2, TimeUnit.SECONDS) + .subscribe(taskPositions -> recyclerAdapter.setIgnoreUpdates(false), RxErrorHandler.handleEmptyError()); + fromPosition = null; } } };