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<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) { final List<PlaylistRemoteEntity> remotePlaylists) {
// Merge localPlaylists and remotePlaylists by displayIndex. // Merge localPlaylists and remotePlaylists by display index.
// If two items have the same displayIndex, sort them in CASE_INSENSITIVE_ORDER. // 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. // This algorithm is similar to the merge operation in merge sort.
final List<PlaylistLocalItem> result = new ArrayList<>( final List<PlaylistLocalItem> result = new ArrayList<>(
localPlaylists.size() + remotePlaylists.size()); localPlaylists.size() + remotePlaylists.size());
final List<PlaylistLocalItem> itemsWithSameIndex = new ArrayList<>(); 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, Collections.sort(localPlaylists,
Comparator.comparingLong(PlaylistMetadataEntry::getDisplayIndex)); Comparator.comparingLong(PlaylistMetadataEntry::getDisplayIndex));
Collections.sort(remotePlaylists, Collections.sort(remotePlaylists,
@ -58,7 +58,7 @@ public interface PlaylistLocalItem extends LocalItem {
final List<PlaylistLocalItem> itemsWithSameIndex) { final List<PlaylistLocalItem> itemsWithSameIndex) {
if (!itemsWithSameIndex.isEmpty() if (!itemsWithSameIndex.isEmpty()
&& itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) { && 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. // index to the result.
addItemsWithSameIndex(result, itemsWithSameIndex); addItemsWithSameIndex(result, itemsWithSameIndex);
itemsWithSameIndex.clear(); 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.holder.RemoteBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; 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.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
@ -42,7 +44,6 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State; import icepick.State;
@ -50,12 +51,10 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; 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; private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
@State @State
protected Parcelable itemsListState; protected Parcelable itemsListState;
@ -66,12 +65,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
private RemotePlaylistManager remotePlaylistManager; private RemotePlaylistManager remotePlaylistManager;
private ItemTouchHelper itemTouchHelper; private ItemTouchHelper itemTouchHelper;
private PublishSubject<Long> debouncedSaveSignal; /* Have the bookmarked playlists been fully loaded from db */
/* Has the playlist been fully loaded from db */
private AtomicBoolean isLoadingComplete; 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. // Map from (uid, local/remote item) to the saved display index in the database.
private Map<Pair<Long, LocalItem.LocalItemType>, Long> displayIndexInDatabase; private Map<Pair<Long, LocalItem.LocalItemType>, Long> displayIndexInDatabase;
@ -91,9 +88,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
remotePlaylistManager = new RemotePlaylistManager(database); remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable(); disposables = new CompositeDisposable();
debouncedSaveSignal = PublishSubject.create();
isLoadingComplete = new AtomicBoolean(); isLoadingComplete = new AtomicBoolean();
isModified = new AtomicBoolean(); debounceSaver = new DebounceSaver(10000, this);
displayIndexInDatabase = new HashMap<>(); displayIndexInDatabase = new HashMap<>();
} }
@ -183,9 +179,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
public void startLoading(final boolean forceLoad) { public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad); super.startLoading(forceLoad);
disposables.add(getDebouncedSaver()); if (debounceSaver != null) {
disposables.add(debounceSaver.getDebouncedSaver());
debounceSaver.setIsModified(false);
}
isLoadingComplete.set(false); isLoadingComplete.set(false);
isModified.set(false);
Flowable.combineLatest(localPlaylistManager.getPlaylists(), Flowable.combineLatest(localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
@ -225,21 +223,20 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (debouncedSaveSignal != null) { if (debounceSaver != null) {
debouncedSaveSignal.onComplete(); debounceSaver.getDebouncedSaveSignal().onComplete();
} }
if (disposables != null) { if (disposables != null) {
disposables.dispose(); disposables.dispose();
} }
debouncedSaveSignal = null; debounceSaver = null;
disposables = null; disposables = null;
localPlaylistManager = null; localPlaylistManager = null;
remotePlaylistManager = null; remotePlaylistManager = null;
itemsListState = null; itemsListState = null;
isLoadingComplete = null; isLoadingComplete = null;
isModified = null;
displayIndexInDatabase = null; displayIndexInDatabase = null;
} }
@ -263,7 +260,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override @Override
public void onNext(final List<PlaylistLocalItem> subscriptions) { public void onNext(final List<PlaylistLocalItem> subscriptions) {
if (isModified == null || !isModified.get()) { if (debounceSaver == null || !debounceSaver.getIsModified()) {
checkDisplayIndexModified(subscriptions); checkDisplayIndexModified(subscriptions);
handleResult(subscriptions); handleResult(subscriptions);
isLoadingComplete.set(true); isLoadingComplete.set(true);
@ -346,11 +343,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
} }
itemListAdapter.removeItem(item); itemListAdapter.removeItem(item);
saveChanges(); debounceSaver.saveChanges();
} }
private void checkDisplayIndexModified(@NonNull final List<PlaylistLocalItem> result) { private void checkDisplayIndexModified(@NonNull final List<PlaylistLocalItem> result) {
if (isModified != null && isModified.get()) { if (debounceSaver != null && debounceSaver.getIsModified()) {
return; 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. // 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 // This may happen when a new list is created
// or on the first run after database update // or on the first run after database migration
// or displayIndex is not continuous for some reason. // or display index is not continuous for some reason
// or the user changes the display index.
boolean isDisplayIndexModified = false; boolean isDisplayIndexModified = false;
for (int i = 0; i < result.size(); i++) { for (int i = 0; i < result.size(); i++) {
final PlaylistLocalItem item = result.get(i); final PlaylistLocalItem item = result.get(i);
@ -388,40 +386,19 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
} }
if (isDisplayIndexModified) { if (isDisplayIndexModified) {
saveChanges(); debounceSaver.saveChanges();
} }
} }
private void saveChanges() { @Override
if (isModified == null || debouncedSaveSignal == null) { public void saveImmediate() {
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() {
if (itemListAdapter == null) { if (itemListAdapter == null) {
return; return;
} }
// List must be loaded and modified in order to save // List must be loaded and modified in order to save
if (isLoadingComplete == null || isModified == null if (isLoadingComplete == null || debounceSaver == null
|| !isLoadingComplete.get() || !isModified.get()) { || !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
Log.w(TAG, "Attempting to save playlists in bookmark when bookmark " Log.w(TAG, "Attempting to save playlists in bookmark when bookmark "
+ "is not loaded or playlists not modified"); + "is not loaded or playlists not modified");
return; return;
@ -485,8 +462,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
remoteItemsUpdate, remoteItemsDeleteUid) remoteItemsUpdate, remoteItemsDeleteUid)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> { .subscribe(() -> {
if (isModified != null) { if (debounceSaver != null) {
isModified.set(false); debounceSaver.setIsModified(false);
} }
}, },
throwable -> showError(new ErrorInfo(throwable, throwable -> showError(new ErrorInfo(throwable,
@ -544,7 +521,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
final int targetIndex = target.getBindingAdapterPosition(); final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped) { if (isSwapped) {
saveChanges(); debounceSaver.saveChanges();
} }
return isSwapped; 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.MainPlayer.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; 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.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
@ -55,7 +57,6 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State; import icepick.State;
@ -64,11 +65,9 @@ import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> { public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
// Save the list 10 seconds after the last change occurred implements DebounceSavable {
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
@State @State
protected Long playlistId; protected Long playlistId;
@ -85,13 +84,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private LocalPlaylistManager playlistManager; private LocalPlaylistManager playlistManager;
private Subscription databaseSubscription; private Subscription databaseSubscription;
private PublishSubject<Long> debouncedSaveSignal;
private CompositeDisposable disposables; private CompositeDisposable disposables;
/* Has the playlist been fully loaded from db */ /* Has the playlist been fully loaded from db */
private AtomicBoolean isLoadingComplete; 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 */ /* Is the playlist currently being processed to remove watched videos */
private boolean isRemovingWatched = false; private boolean isRemovingWatched = false;
@ -109,12 +108,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
public void onCreate(final Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext())); playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
debouncedSaveSignal = PublishSubject.create();
disposables = new CompositeDisposable(); disposables = new CompositeDisposable();
isLoadingComplete = new AtomicBoolean(); isLoadingComplete = new AtomicBoolean();
isModified = new AtomicBoolean(); debounceSaver = new DebounceSaver(10000, this);
} }
@Override @Override
@ -220,10 +218,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (disposables != null) { if (disposables != null) {
disposables.clear(); disposables.clear();
} }
disposables.add(getDebouncedSaver());
if (debounceSaver != null) {
disposables.add(debounceSaver.getDebouncedSaver());
debounceSaver.setIsModified(false);
}
isLoadingComplete.set(false); isLoadingComplete.set(false);
isModified.set(false);
playlistManager.getPlaylistStreams(playlistId) playlistManager.getPlaylistStreams(playlistId)
.onBackpressureLatest() .onBackpressureLatest()
@ -285,19 +286,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (debouncedSaveSignal != null) { if (debounceSaver != null) {
debouncedSaveSignal.onComplete(); debounceSaver.getDebouncedSaveSignal().onComplete();
} }
if (disposables != null) { if (disposables != null) {
disposables.dispose(); disposables.dispose();
} }
debouncedSaveSignal = null; debounceSaver = null;
playlistManager = null; playlistManager = null;
disposables = null; disposables = null;
isLoadingComplete = null; isLoadingComplete = null;
isModified = null;
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -321,7 +321,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override @Override
public void onNext(final List<PlaylistStreamEntry> streams) { public void onNext(final List<PlaylistStreamEntry> streams) {
// Skip handling the result after it has been modified // Skip handling the result after it has been modified
if (isModified == null || !isModified.get()) { if (debounceSaver == null || !debounceSaver.getIsModified()) {
handleResult(streams); handleResult(streams);
isLoadingComplete.set(true); isLoadingComplete.set(true);
} }
@ -441,7 +441,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
itemListAdapter.clearStreamItemList(); itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(notWatchedItems); itemListAdapter.addItems(notWatchedItems);
saveChanges(); debounceSaver.saveChanges();
if (thumbnailVideoRemoved) { if (thumbnailVideoRemoved) {
@ -609,39 +609,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
} }
setVideoCount(itemListAdapter.getItemsList().size()); setVideoCount(itemListAdapter.getItemsList().size());
saveChanges(); debounceSaver.saveChanges();
} }
private void saveChanges() { @Override
if (isModified == null || debouncedSaveSignal == null) { public void saveImmediate() {
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() {
if (playlistManager == null || itemListAdapter == null) { if (playlistManager == null || itemListAdapter == null) {
return; return;
} }
// List must be loaded and modified in order to save // List must be loaded and modified in order to save
if (isLoadingComplete == null || isModified == null if (isLoadingComplete == null || debounceSaver == null
|| !isLoadingComplete.get() || !isModified.get()) { || !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
Log.w(TAG, "Attempting to save playlist when local playlist " Log.w(TAG, "Attempting to save playlist when local playlist "
+ "is not loaded or not modified: playlist id=[" + playlistId + "]"); + "is not loaded or not modified: playlist id=[" + playlistId + "]");
return; return;
@ -664,8 +643,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
() -> { () -> {
if (isModified != null) { if (debounceSaver != null) {
isModified.set(false); debounceSaver.setIsModified(false);
} }
}, },
throwable -> showError(new ErrorInfo(throwable, throwable -> showError(new ErrorInfo(throwable,
@ -708,7 +687,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final int targetIndex = target.getBindingAdapterPosition(); final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped) { if (isSwapped) {
saveChanges(); debounceSaver.saveChanges();
} }
return isSwapped; 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());
}
}