Update index modification logic & redo sorting in the merge algorithm

This commit is contained in:
GGAutomaton 2022-06-23 23:19:59 +08:00
parent 4e401bc059
commit 898a936064
5 changed files with 39 additions and 101 deletions

View File

@ -21,29 +21,18 @@ public interface PlaylistLocalItem extends LocalItem {
* Merge localPlaylists and remotePlaylists by the display index. * Merge localPlaylists and remotePlaylists by the display index.
* If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}. * If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}.
* *
* @param localPlaylists local playlists in the display index order * @param localPlaylists local playlists
* @param remotePlaylists remote playlists in the display index order * @param remotePlaylists remote playlists
* @return merged playlists * @return merged playlists
*/ */
static List<PlaylistLocalItem> merge( static List<PlaylistLocalItem> merge(
final List<PlaylistMetadataEntry> localPlaylists, final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) { final List<PlaylistRemoteEntity> remotePlaylists) {
for (int i = 1; i < localPlaylists.size(); i++) { Collections.sort(localPlaylists,
if (localPlaylists.get(i).getDisplayIndex() Comparator.comparingLong(PlaylistMetadataEntry::getDisplayIndex));
< localPlaylists.get(i - 1).getDisplayIndex()) { Collections.sort(remotePlaylists,
throw new IllegalArgumentException( Comparator.comparingLong(PlaylistRemoteEntity::getDisplayIndex));
"localPlaylists is not in the display index order");
}
}
for (int i = 1; i < remotePlaylists.size(); i++) {
if (remotePlaylists.get(i).getDisplayIndex()
< remotePlaylists.get(i - 1).getDisplayIndex()) {
throw new IllegalArgumentException(
"remotePlaylists is not in the display index order");
}
}
// This algorithm is similar to the merge operation in merge sort. // This algorithm is similar to the merge operation in merge sort.

View File

@ -41,9 +41,7 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State; import icepick.State;
@ -70,8 +68,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
private DebounceSaver debounceSaver; private DebounceSaver debounceSaver;
// Map from (uid, local/remote item) to the saved display index in the database. private List<Pair<Long, LocalItem.LocalItemType>> deletedItems;
private Map<Pair<Long, LocalItem.LocalItemType>, Long> displayIndexInDatabase;
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Creation // Fragment LifeCycle - Creation
@ -89,9 +86,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
disposables = new CompositeDisposable(); disposables = new CompositeDisposable();
isLoadingComplete = new AtomicBoolean(); isLoadingComplete = new AtomicBoolean();
debounceSaver = new DebounceSaver(this); debounceSaver = new DebounceSaver(3000, this);
displayIndexInDatabase = new HashMap<>(); deletedItems = new ArrayList<>();
} }
@Nullable @Nullable
@ -186,7 +183,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
isLoadingComplete.set(false); isLoadingComplete.set(false);
Flowable.combineLatest(localPlaylistManager.getDisplayIndexOrderedPlaylists(), Flowable.combineLatest(localPlaylistManager.getDisplayIndexOrderedPlaylists(),
remotePlaylistManager.getDisplayIndexOrderedPlaylists(), PlaylistLocalItem::merge) remotePlaylistManager.getDisplayIndexOrderedPlaylists(),
PlaylistLocalItem::merge)
.onBackpressureLatest() .onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistsSubscriber()); .subscribe(getPlaylistsSubscriber());
@ -237,7 +235,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
itemsListState = null; itemsListState = null;
isLoadingComplete = null; isLoadingComplete = null;
displayIndexInDatabase = null; deletedItems = null;
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -343,7 +341,15 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
} }
itemListAdapter.removeItem(item); itemListAdapter.removeItem(item);
debounceSaver.saveChanges(); if (item instanceof PlaylistMetadataEntry) {
deletedItems.add(new Pair<>(item.getUid(),
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM));
} else if (item instanceof PlaylistRemoteEntity) {
deletedItems.add(new Pair<>(item.getUid(),
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM));
}
debounceSaver.setHasChangesToSave();
} }
private void checkDisplayIndexModified(@NonNull final List<PlaylistLocalItem> result) { private void checkDisplayIndexModified(@NonNull final List<PlaylistLocalItem> result) {
@ -351,9 +357,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
return; return;
} }
displayIndexInDatabase.clear(); // Check if the display index does not match the actual index in the list.
// 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 migration // or on the first run after database migration
// or display index is not continuous for some reason // or display index is not continuous for some reason
@ -363,29 +367,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
final PlaylistLocalItem item = result.get(i); final PlaylistLocalItem item = result.get(i);
if (item.getDisplayIndex() != i) { if (item.getDisplayIndex() != i) {
isDisplayIndexModified = true; isDisplayIndexModified = true;
} break;
// Updating display index in the item does not affect the value inserts into
// database, which will be recalculated during the database update. Updating
// display index in the item here is to determine whether it is recently modified.
// Save the index read from the database.
if (item instanceof PlaylistMetadataEntry) {
displayIndexInDatabase.put(new Pair<>(item.getUid(),
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM), item.getDisplayIndex());
item.setDisplayIndex(i);
} else if (item instanceof PlaylistRemoteEntity) {
displayIndexInDatabase.put(new Pair<>(item.getUid(),
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM), item.getDisplayIndex());
item.setDisplayIndex(i);
} }
} }
if (debounceSaver != null && isDisplayIndexModified) { if (debounceSaver != null && isDisplayIndexModified) {
debounceSaver.saveChanges(); debounceSaver.setHasChangesToSave();
} }
} }
@ -414,43 +401,28 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
final LocalItem item = items.get(i); final LocalItem item = items.get(i);
if (item instanceof PlaylistMetadataEntry) { if (item instanceof PlaylistMetadataEntry) {
((PlaylistMetadataEntry) item).setDisplayIndex(i); if (((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
((PlaylistMetadataEntry) item).setDisplayIndex(i);
final Long uid = ((PlaylistMetadataEntry) item).getUid();
final Pair<Long, LocalItem.LocalItemType> key = new Pair<>(uid,
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM);
final Long databaseIndex = displayIndexInDatabase.remove(key);
// The database index should not be null because inserting new item into database
// is not handled here. NullPointerException has occurred once, but I can't
// reproduce it. Enhance robustness here.
if (databaseIndex != null && databaseIndex != i) {
localItemsUpdate.add((PlaylistMetadataEntry) item); localItemsUpdate.add((PlaylistMetadataEntry) item);
} }
} else if (item instanceof PlaylistRemoteEntity) { } else if (item instanceof PlaylistRemoteEntity) {
((PlaylistRemoteEntity) item).setDisplayIndex(i); if (((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
((PlaylistRemoteEntity) item).setDisplayIndex(i);
final Long uid = ((PlaylistRemoteEntity) item).getUid();
final Pair<Long, LocalItem.LocalItemType> key = new Pair<>(uid,
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM);
final Long databaseIndex = displayIndexInDatabase.remove(key);
if (databaseIndex != null && databaseIndex != i) {
remoteItemsUpdate.add((PlaylistRemoteEntity) item); remoteItemsUpdate.add((PlaylistRemoteEntity) item);
} }
} }
} }
// Find deleted items // Find deleted items
for (final Pair<Long, LocalItem.LocalItemType> key : displayIndexInDatabase.keySet()) { for (final Pair<Long, LocalItem.LocalItemType> item : deletedItems) {
if (key.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) { if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) {
localItemsDeleteUid.add(key.first); localItemsDeleteUid.add(item.first);
} else if (key.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) { } else if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) {
remoteItemsDeleteUid.add(key.first); remoteItemsDeleteUid.add(item.first);
} }
} }
displayIndexInDatabase.clear(); deletedItems.clear();
// 1. Update local playlists // 1. Update local playlists
// 2. Update remote playlists // 2. Update remote playlists
@ -515,7 +487,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) {
debounceSaver.saveChanges(); debounceSaver.setHasChangesToSave();
} }
return isSwapped; return isSwapped;
} }

View File

@ -441,7 +441,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
itemListAdapter.clearStreamItemList(); itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(notWatchedItems); itemListAdapter.addItems(notWatchedItems);
debounceSaver.saveChanges(); debounceSaver.setHasChangesToSave();
if (thumbnailVideoRemoved) { if (thumbnailVideoRemoved) {
@ -609,7 +609,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
} }
setVideoCount(itemListAdapter.getItemsList().size()); setVideoCount(itemListAdapter.getItemsList().size());
debounceSaver.saveChanges(); debounceSaver.setHasChangesToSave();
} }
@Override @Override
@ -687,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) {
debounceSaver.saveChanges(); debounceSaver.setHasChangesToSave();
} }
return isSwapped; return isSwapped;
} }

View File

@ -70,7 +70,7 @@ public class DebounceSaver {
UserAction.SOMETHING_ELSE, "Debounced saver"))); UserAction.SOMETHING_ELSE, "Debounced saver")));
} }
public void saveChanges() { public void setHasChangesToSave() {
if (isModified == null || debouncedSaveSignal == null) { if (isModified == null || debouncedSaveSignal == null) {
return; return;
} }

View File

@ -36,16 +36,6 @@ public class PlaylistLocalItemTest {
assertEquals(3, mergedPlaylists.get(2).getDisplayIndex()); assertEquals(3, mergedPlaylists.get(2).getDisplayIndex());
} }
@Test(expected = IllegalArgumentException.class)
public void invalidLocalPlaylists() {
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", 2, 1));
localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", 1, 1));
localPlaylists.add(new PlaylistMetadataEntry(3, "name3", "", 0, 1));
PlaylistLocalItem.merge(localPlaylists, remotePlaylists);
}
@Test @Test
public void onlyRemotePlaylists() { public void onlyRemotePlaylists() {
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>(); final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
@ -65,19 +55,6 @@ public class PlaylistLocalItemTest {
assertEquals(4, mergedPlaylists.get(2).getDisplayIndex()); assertEquals(4, mergedPlaylists.get(2).getDisplayIndex());
} }
@Test(expected = IllegalArgumentException.class)
public void invalidRemotePlaylists() {
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
remotePlaylists.add(new PlaylistRemoteEntity(
1, "name1", "url1", "", "", 1, 1L));
remotePlaylists.add(new PlaylistRemoteEntity(
2, "name2", "url2", "", "", 3, 1L));
remotePlaylists.add(new PlaylistRemoteEntity(
3, "name3", "url3", "", "", 0, 1L));
PlaylistLocalItem.merge(localPlaylists, remotePlaylists);
}
@Test @Test
public void sameIndexWithDifferentName() { public void sameIndexWithDifferentName() {
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>(); final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();