diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index 47c6dd617..0e7beba41 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -17,15 +17,15 @@ public interface PlaylistLocalItem extends LocalItem { final List localPlaylists, final List remotePlaylists) { - // Merge localPlaylists and remotePlaylists by displayIndex. - // If two items have the same displayIndex, sort them in CASE_INSENSITIVE_ORDER. + // Merge localPlaylists and remotePlaylists by display index. + // If two items have the same display index, sort them in CASE_INSENSITIVE_ORDER. // This algorithm is similar to the merge operation in merge sort. final List result = new ArrayList<>( localPlaylists.size() + remotePlaylists.size()); final List itemsWithSameIndex = new ArrayList<>(); - // The data from database may not be in the displayIndex order + // The data from database may not be in the display index order Collections.sort(localPlaylists, Comparator.comparingLong(PlaylistMetadataEntry::getDisplayIndex)); Collections.sort(remotePlaylists, @@ -58,7 +58,7 @@ public interface PlaylistLocalItem extends LocalItem { final List itemsWithSameIndex) { if (!itemsWithSameIndex.isEmpty() && itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) { - // The new item has a different displayIndex, add previous items with same + // The new item has a different display index, add previous items with same // index to the result. addItemsWithSameIndex(result, itemsWithSameIndex); itemsWithSameIndex.clear(); diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index b19107817..ceeb980c7 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -35,6 +35,8 @@ import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder; import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; +import org.schabi.newpipe.util.DebounceSavable; +import org.schabi.newpipe.util.DebounceSaver; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; @@ -42,7 +44,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import icepick.State; @@ -50,12 +51,10 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; -import io.reactivex.rxjava3.subjects.PublishSubject; -public final class BookmarkFragment extends BaseLocalListFragment, Void> { +public final class BookmarkFragment extends BaseLocalListFragment, Void> + implements DebounceSavable { - // Save the list 10 seconds after the last change occurred - private static final long SAVE_DEBOUNCE_MILLIS = 10000; private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; @State protected Parcelable itemsListState; @@ -66,12 +65,10 @@ public final class BookmarkFragment extends BaseLocalListFragment debouncedSaveSignal; - - /* Has the playlist been fully loaded from db */ + /* Have the bookmarked playlists been fully loaded from db */ private AtomicBoolean isLoadingComplete; - /* Has the playlist been modified (e.g. items reordered or deleted) */ - private AtomicBoolean isModified; + + private DebounceSaver debounceSaver; // Map from (uid, local/remote item) to the saved display index in the database. private Map, Long> displayIndexInDatabase; @@ -91,9 +88,8 @@ public final class BookmarkFragment extends BaseLocalListFragment(); } @@ -183,9 +179,11 @@ public final class BookmarkFragment extends BaseLocalListFragment subscriptions) { - if (isModified == null || !isModified.get()) { + if (debounceSaver == null || !debounceSaver.getIsModified()) { checkDisplayIndexModified(subscriptions); handleResult(subscriptions); isLoadingComplete.set(true); @@ -346,11 +343,11 @@ public final class BookmarkFragment extends BaseLocalListFragment result) { - if (isModified != null && isModified.get()) { + if (debounceSaver != null && debounceSaver.getIsModified()) { return; } @@ -358,8 +355,9 @@ public final class BookmarkFragment extends BaseLocalListFragment saveImmediate(), throwable -> - showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE, - "Debounced saver"))); - } - - private void saveImmediate() { + @Override + public void saveImmediate() { if (itemListAdapter == null) { return; } // List must be loaded and modified in order to save - if (isLoadingComplete == null || isModified == null - || !isLoadingComplete.get() || !isModified.get()) { + if (isLoadingComplete == null || debounceSaver == null + || !isLoadingComplete.get() || !debounceSaver.getIsModified()) { Log.w(TAG, "Attempting to save playlists in bookmark when bookmark " + "is not loaded or playlists not modified"); return; @@ -485,8 +462,8 @@ public final class BookmarkFragment extends BaseLocalListFragment { - if (isModified != null) { - isModified.set(false); + if (debounceSaver != null) { + debounceSaver.setIsModified(false); } }, throwable -> showError(new ErrorInfo(throwable, @@ -544,7 +521,7 @@ public final class BookmarkFragment extends BaseLocalListFragment, Void> { - // Save the list 10 seconds after the last change occurred - private static final long SAVE_DEBOUNCE_MILLIS = 10000; +public class LocalPlaylistFragment extends BaseLocalListFragment, Void> + implements DebounceSavable { private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; @State protected Long playlistId; @@ -85,13 +84,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment debouncedSaveSignal; private CompositeDisposable disposables; /* Has the playlist been fully loaded from db */ private AtomicBoolean isLoadingComplete; - /* Has the playlist been modified (e.g. items reordered or deleted) */ - private AtomicBoolean isModified; + + private DebounceSaver debounceSaver; + /* Is the playlist currently being processed to remove watched videos */ private boolean isRemovingWatched = false; @@ -109,12 +108,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment streams) { // Skip handling the result after it has been modified - if (isModified == null || !isModified.get()) { + if (debounceSaver == null || !debounceSaver.getIsModified()) { handleResult(streams); isLoadingComplete.set(true); } @@ -441,7 +441,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment saveImmediate(), throwable -> - showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE, - "Debounced saver"))); - } - - private void saveImmediate() { + @Override + public void saveImmediate() { if (playlistManager == null || itemListAdapter == null) { return; } // List must be loaded and modified in order to save - if (isLoadingComplete == null || isModified == null - || !isLoadingComplete.get() || !isModified.get()) { + if (isLoadingComplete == null || debounceSaver == null + || !isLoadingComplete.get() || !debounceSaver.getIsModified()) { Log.w(TAG, "Attempting to save playlist when local playlist " + "is not loaded or not modified: playlist id=[" + playlistId + "]"); return; @@ -664,8 +643,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { - if (isModified != null) { - isModified.set(false); + if (debounceSaver != null) { + debounceSaver.setIsModified(false); } }, throwable -> showError(new ErrorInfo(throwable, @@ -708,7 +687,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment + * Must set {@link DebounceSaver#setIsModified(boolean)} false in this method manually + * after the data has been saved. + */ + void saveImmediate(); + + void showError(ErrorInfo errorInfo); +} diff --git a/app/src/main/java/org/schabi/newpipe/util/DebounceSaver.java b/app/src/main/java/org/schabi/newpipe/util/DebounceSaver.java new file mode 100644 index 000000000..b17d7a29c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/DebounceSaver.java @@ -0,0 +1,68 @@ +package org.schabi.newpipe.util; + +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.UserAction; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.subjects.PublishSubject; + +public class DebounceSaver { + + private final long saveDebounceMillis; + + private final PublishSubject debouncedSaveSignal; + + private final DebounceSavable debounceSavable; + + // Has the object been modified + private final AtomicBoolean isModified; + + + /** + * Creates a new {@code DebounceSaver}. + * + * @param saveDebounceMillis Save the object milliseconds later after the last change + * occurred. + * @param debounceSavable The object containing data to be saved. + */ + public DebounceSaver(final long saveDebounceMillis, final DebounceSavable debounceSavable) { + this.saveDebounceMillis = saveDebounceMillis; + debouncedSaveSignal = PublishSubject.create(); + this.debounceSavable = debounceSavable; + this.isModified = new AtomicBoolean(); + } + + public boolean getIsModified() { + return isModified.get(); + } + + public void setIsModified(final boolean isModified) { + this.isModified.set(isModified); + } + + public PublishSubject getDebouncedSaveSignal() { + return debouncedSaveSignal; + } + + public Disposable getDebouncedSaver() { + return debouncedSaveSignal + .debounce(saveDebounceMillis, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> debounceSavable.saveImmediate(), throwable -> + debounceSavable.showError(new ErrorInfo(throwable, + UserAction.SOMETHING_ELSE, "Debounced saver"))); + } + + public void saveChanges() { + if (isModified == null || debouncedSaveSignal == null) { + return; + } + + isModified.set(true); + debouncedSaveSignal.onNext(System.currentTimeMillis()); + } +}