Reuse DebounceSaver

This commit is contained in:
GGAutomaton 2022-04-17 14:53:02 +08:00
parent bd1aae8d66
commit bb5390d63a
5 changed files with 142 additions and 103 deletions

View file

@ -17,15 +17,15 @@ public interface PlaylistLocalItem extends LocalItem {
final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> 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<PlaylistLocalItem> result = new ArrayList<>(
localPlaylists.size() + remotePlaylists.size());
final List<PlaylistLocalItem> 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<PlaylistLocalItem> 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();

View file

@ -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<List<PlaylistLocalItem>, Void> {
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, 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<List<PlaylistL
private RemotePlaylistManager remotePlaylistManager;
private ItemTouchHelper itemTouchHelper;
private PublishSubject<Long> 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<Pair<Long, LocalItem.LocalItemType>, Long> displayIndexInDatabase;
@ -91,9 +88,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable();
debouncedSaveSignal = PublishSubject.create();
isLoadingComplete = new AtomicBoolean();
isModified = new AtomicBoolean();
debounceSaver = new DebounceSaver(10000, this);
displayIndexInDatabase = new HashMap<>();
}
@ -183,9 +179,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
disposables.add(getDebouncedSaver());
if (debounceSaver != null) {
disposables.add(debounceSaver.getDebouncedSaver());
debounceSaver.setIsModified(false);
}
isLoadingComplete.set(false);
isModified.set(false);
Flowable.combineLatest(localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
@ -225,21 +223,20 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override
public void onDestroy() {
super.onDestroy();
if (debouncedSaveSignal != null) {
debouncedSaveSignal.onComplete();
if (debounceSaver != null) {
debounceSaver.getDebouncedSaveSignal().onComplete();
}
if (disposables != null) {
disposables.dispose();
}
debouncedSaveSignal = null;
debounceSaver = null;
disposables = null;
localPlaylistManager = null;
remotePlaylistManager = null;
itemsListState = null;
isLoadingComplete = null;
isModified = null;
displayIndexInDatabase = null;
}
@ -263,7 +260,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override
public void onNext(final List<PlaylistLocalItem> 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<List<PlaylistL
}
itemListAdapter.removeItem(item);
saveChanges();
debounceSaver.saveChanges();
}
private void checkDisplayIndexModified(@NonNull final List<PlaylistLocalItem> result) {
if (isModified != null && isModified.get()) {
if (debounceSaver != null && debounceSaver.getIsModified()) {
return;
}
@ -358,8 +355,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
// If the display index does not match actual index in the list, update the display index.
// This may happen when a new list is created
// or on the first run after database update
// or displayIndex is not continuous for some reason.
// or on the first run after database migration
// or display index is not continuous for some reason
// or the user changes the display index.
boolean isDisplayIndexModified = false;
for (int i = 0; i < result.size(); i++) {
final PlaylistLocalItem item = result.get(i);
@ -388,40 +386,19 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
}
if (isDisplayIndexModified) {
saveChanges();
debounceSaver.saveChanges();
}
}
private void saveChanges() {
if (isModified == null || debouncedSaveSignal == null) {
return;
}
isModified.set(true);
debouncedSaveSignal.onNext(System.currentTimeMillis());
}
private Disposable getDebouncedSaver() {
if (debouncedSaveSignal == null) {
return Disposable.empty();
}
return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> 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<List<PlaylistL
remoteItemsUpdate, remoteItemsDeleteUid)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
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<List<PlaylistL
final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped) {
saveChanges();
debounceSaver.saveChanges();
}
return isSwapped;
}

View file

@ -46,6 +46,8 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.DebounceSavable;
import org.schabi.newpipe.util.DebounceSaver;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@ -55,7 +57,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
@ -64,11 +65,9 @@ import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
// Save the list 10 seconds after the last change occurred
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, 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<List<PlaylistSt
private LocalPlaylistManager playlistManager;
private Subscription databaseSubscription;
private PublishSubject<Long> 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<List<PlaylistSt
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
debouncedSaveSignal = PublishSubject.create();
disposables = new CompositeDisposable();
isLoadingComplete = new AtomicBoolean();
isModified = new AtomicBoolean();
debounceSaver = new DebounceSaver(10000, this);
}
@Override
@ -220,10 +218,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (disposables != null) {
disposables.clear();
}
disposables.add(getDebouncedSaver());
if (debounceSaver != null) {
disposables.add(debounceSaver.getDebouncedSaver());
debounceSaver.setIsModified(false);
}
isLoadingComplete.set(false);
isModified.set(false);
playlistManager.getPlaylistStreams(playlistId)
.onBackpressureLatest()
@ -285,19 +286,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void onDestroy() {
super.onDestroy();
if (debouncedSaveSignal != null) {
debouncedSaveSignal.onComplete();
if (debounceSaver != null) {
debounceSaver.getDebouncedSaveSignal().onComplete();
}
if (disposables != null) {
disposables.dispose();
}
debouncedSaveSignal = null;
debounceSaver = null;
playlistManager = null;
disposables = null;
isLoadingComplete = null;
isModified = null;
}
///////////////////////////////////////////////////////////////////////////
@ -321,7 +321,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void onNext(final List<PlaylistStreamEntry> 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<List<PlaylistSt
itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(notWatchedItems);
saveChanges();
debounceSaver.saveChanges();
if (thumbnailVideoRemoved) {
@ -609,39 +609,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
setVideoCount(itemListAdapter.getItemsList().size());
saveChanges();
debounceSaver.saveChanges();
}
private void saveChanges() {
if (isModified == null || debouncedSaveSignal == null) {
return;
}
isModified.set(true);
debouncedSaveSignal.onNext(System.currentTimeMillis());
}
private Disposable getDebouncedSaver() {
if (debouncedSaveSignal == null) {
return Disposable.empty();
}
return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> 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<List<PlaylistSt
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
() -> {
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<List<PlaylistSt
final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped) {
saveChanges();
debounceSaver.saveChanges();
}
return isSwapped;
}

View file

@ -0,0 +1,15 @@
package org.schabi.newpipe.util;
import org.schabi.newpipe.error.ErrorInfo;
public interface DebounceSavable {
/**
* Execute operations to save the data. <br>
* Must set {@link DebounceSaver#setIsModified(boolean)} false in this method manually
* after the data has been saved.
*/
void saveImmediate();
void showError(ErrorInfo errorInfo);
}

View file

@ -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<Long> 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<Long> 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());
}
}