mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +00:00 
			
		
		
		
	-Fixed database updates cause outdated record to overwrite reordered local playlist when fragment is active.
-Fixed save on exit causes empty list being saved after orientation changes on older devices. -Fixed NPE on animating garbage collected views on local item fragments. -Reduced drag speed from 15 to 12 items per second.
This commit is contained in:
		| @@ -47,8 +47,8 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity | |||||||
|     @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " + |     @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " + | ||||||
|             // get ids of streams of the given playlist |             // get ids of streams of the given playlist | ||||||
|             "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX + |             "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX + | ||||||
|             " FROM " + PLAYLIST_STREAM_JOIN_TABLE + " WHERE " |             " FROM " + PLAYLIST_STREAM_JOIN_TABLE + | ||||||
|             + JOIN_PLAYLIST_ID + " = :playlistId)" + |             " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" + | ||||||
|  |  | ||||||
|             // then merge with the stream metadata |             // then merge with the stream metadata | ||||||
|             " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + |             " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + | ||||||
|   | |||||||
| @@ -126,14 +126,14 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I> | |||||||
|     @Override |     @Override | ||||||
|     public void showLoading() { |     public void showLoading() { | ||||||
|         super.showLoading(); |         super.showLoading(); | ||||||
|         animateView(itemsList, false, 200); |         if (itemsList != null) animateView(itemsList, false, 200); | ||||||
|         if (headerRootView != null) animateView(headerRootView, false, 200); |         if (headerRootView != null) animateView(headerRootView, false, 200); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void hideLoading() { |     public void hideLoading() { | ||||||
|         super.hideLoading(); |         super.hideLoading(); | ||||||
|         animateView(itemsList, true, 200); |         if (itemsList != null) animateView(itemsList, true, 200); | ||||||
|         if (headerRootView != null) animateView(headerRootView, true, 200); |         if (headerRootView != null) animateView(headerRootView, true, 200); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -142,7 +142,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I> | |||||||
|         super.showError(message, showRetryButton); |         super.showError(message, showRetryButton); | ||||||
|         showListFooter(false); |         showListFooter(false); | ||||||
|  |  | ||||||
|         animateView(itemsList, false, 200); |         if (itemsList != null) animateView(itemsList, false, 200); | ||||||
|         if (headerRootView != null) animateView(headerRootView, false, 200); |         if (headerRootView != null) animateView(headerRootView, false, 200); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -37,6 +37,7 @@ import org.schabi.newpipe.util.OnClickGesture; | |||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  |  | ||||||
| import icepick.State; | import icepick.State; | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
| @@ -49,7 +50,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|  |  | ||||||
|     // Save the list 10 seconds after the last change occurred |     // Save the list 10 seconds after the last change occurred | ||||||
|     private static final long SAVE_DEBOUNCE_MILLIS = 10000; |     private static final long SAVE_DEBOUNCE_MILLIS = 10000; | ||||||
|     private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 15; |     private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; | ||||||
|  |  | ||||||
|     private View headerRootLayout; |     private View headerRootLayout; | ||||||
|     private TextView headerTitleView; |     private TextView headerTitleView; | ||||||
| @@ -75,6 +76,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|     private PublishSubject<Long> debouncedSaveSignal; |     private PublishSubject<Long> debouncedSaveSignal; | ||||||
|     private Disposable debouncedSaver; |     private Disposable debouncedSaver; | ||||||
|  |  | ||||||
|  |     /* 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; | ||||||
|  |  | ||||||
|     public static LocalPlaylistFragment getInstance(long playlistId, String name) { |     public static LocalPlaylistFragment getInstance(long playlistId, String name) { | ||||||
|         LocalPlaylistFragment instance = new LocalPlaylistFragment(); |         LocalPlaylistFragment instance = new LocalPlaylistFragment(); | ||||||
|         instance.setInitialData(playlistId, name); |         instance.setInitialData(playlistId, name); | ||||||
| @@ -90,6 +96,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); |         playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); | ||||||
|         debouncedSaveSignal = PublishSubject.create(); |         debouncedSaveSignal = PublishSubject.create(); | ||||||
|  |  | ||||||
|  |         isLoadingComplete = new AtomicBoolean(); | ||||||
|  |         isModified = new AtomicBoolean(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -176,15 +185,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|     @Override |     @Override | ||||||
|     public void showLoading() { |     public void showLoading() { | ||||||
|         super.showLoading(); |         super.showLoading(); | ||||||
|         animateView(headerRootLayout, false, 200); |         if (headerRootLayout != null) animateView(headerRootLayout, false, 200); | ||||||
|         animateView(playlistControl, false, 200); |         if (playlistControl != null) animateView(playlistControl, false, 200); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void hideLoading() { |     public void hideLoading() { | ||||||
|         super.hideLoading(); |         super.hideLoading(); | ||||||
|         animateView(headerRootLayout, true, 200); |         if (headerRootLayout != null) animateView(headerRootLayout, true, 200); | ||||||
|         animateView(playlistControl, true, 200); |         if (playlistControl != null) animateView(playlistControl, true, 200); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -194,7 +203,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|         if (debouncedSaver != null) debouncedSaver.dispose(); |         if (debouncedSaver != null) debouncedSaver.dispose(); | ||||||
|         debouncedSaver = getDebouncedSaver(); |         debouncedSaver = getDebouncedSaver(); | ||||||
|  |  | ||||||
|  |         isLoadingComplete.set(false); | ||||||
|  |         isModified.set(false); | ||||||
|  |  | ||||||
|         playlistManager.getPlaylistStreams(playlistId) |         playlistManager.getPlaylistStreams(playlistId) | ||||||
|  |                 .onBackpressureLatest() | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(getPlaylistObserver()); |                 .subscribe(getPlaylistObserver()); | ||||||
|     } |     } | ||||||
| @@ -207,12 +220,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|     public void onPause() { |     public void onPause() { | ||||||
|         super.onPause(); |         super.onPause(); | ||||||
|         itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); |         itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); | ||||||
|         saveImmediate(); // Save on exit |  | ||||||
|  |         // Save on exit | ||||||
|  |         saveImmediate(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onDestroyView() { |     public void onDestroyView() { | ||||||
|         super.onDestroyView(); |         super.onDestroyView(); | ||||||
|  |  | ||||||
|         if (databaseSubscription != null) databaseSubscription.cancel(); |         if (databaseSubscription != null) databaseSubscription.cancel(); | ||||||
|         if (debouncedSaver != null) debouncedSaver.dispose(); |         if (debouncedSaver != null) debouncedSaver.dispose(); | ||||||
|  |  | ||||||
| @@ -228,6 +244,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|  |  | ||||||
|         debouncedSaveSignal = null; |         debouncedSaveSignal = null; | ||||||
|         playlistManager = null; |         playlistManager = null; | ||||||
|  |  | ||||||
|  |         isLoadingComplete = null; | ||||||
|  |         isModified = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /////////////////////////////////////////////////////////////////////////// |     /////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -239,6 +258,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|             @Override |             @Override | ||||||
|             public void onSubscribe(Subscription s) { |             public void onSubscribe(Subscription s) { | ||||||
|                 showLoading(); |                 showLoading(); | ||||||
|  |                 isLoadingComplete.set(false); | ||||||
|  |  | ||||||
|                 if (databaseSubscription != null) databaseSubscription.cancel(); |                 if (databaseSubscription != null) databaseSubscription.cancel(); | ||||||
|                 databaseSubscription = s; |                 databaseSubscription = s; | ||||||
| @@ -247,10 +267,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onNext(List<PlaylistStreamEntry> streams) { |             public void onNext(List<PlaylistStreamEntry> streams) { | ||||||
|                 // Do not allow saving while the result is being updated |                 // Skip handling the result after it has been modified | ||||||
|                 if (debouncedSaver != null) debouncedSaver.dispose(); |                 if (isModified == null || !isModified.get()) { | ||||||
|                 handleResult(streams); |                     handleResult(streams); | ||||||
|                 debouncedSaver = getDebouncedSaver(); |                     isLoadingComplete.set(true); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 if (databaseSubscription != null) databaseSubscription.request(1); |                 if (databaseSubscription != null) databaseSubscription.request(1); | ||||||
|             } |             } | ||||||
| @@ -339,7 +360,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|         this.name = name; |         this.name = name; | ||||||
|         setTitle(name); |         setTitle(name); | ||||||
|  |  | ||||||
|         Log.e(TAG, "Updating playlist id=[" + playlistId + |         Log.d(TAG, "Updating playlist id=[" + playlistId + | ||||||
|                 "] with new name=[" + name + "] items"); |                 "] with new name=[" + name + "] items"); | ||||||
|  |  | ||||||
|         playlistManager.renamePlaylist(playlistId, name) |         playlistManager.renamePlaylist(playlistId, name) | ||||||
| @@ -352,7 +373,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|                 R.string.playlist_thumbnail_change_success, |                 R.string.playlist_thumbnail_change_success, | ||||||
|                 Toast.LENGTH_SHORT); |                 Toast.LENGTH_SHORT); | ||||||
|  |  | ||||||
|         Log.e(TAG, "Updating playlist id=[" + playlistId + |         Log.d(TAG, "Updating playlist id=[" + playlistId + | ||||||
|                 "] with new thumbnail url=[" + thumbnailUrl + "]"); |                 "] with new thumbnail url=[" + thumbnailUrl + "]"); | ||||||
|  |  | ||||||
|         playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl) |         playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl) | ||||||
| @@ -363,10 +384,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|     private void deleteItem(final PlaylistStreamEntry item) { |     private void deleteItem(final PlaylistStreamEntry item) { | ||||||
|         itemListAdapter.removeItem(item); |         itemListAdapter.removeItem(item); | ||||||
|         setVideoCount(itemListAdapter.getItemsList().size()); |         setVideoCount(itemListAdapter.getItemsList().size()); | ||||||
|         saveDebounced(); |         saveChanges(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void saveDebounced() { |     private void saveChanges() { | ||||||
|  |         isModified.set(true); | ||||||
|         debouncedSaveSignal.onNext(System.currentTimeMillis()); |         debouncedSaveSignal.onNext(System.currentTimeMillis()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -374,10 +396,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|         return debouncedSaveSignal |         return debouncedSaveSignal | ||||||
|                 .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) |                 .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(ignored -> saveImmediate()); |                 .subscribe(ignored -> saveImmediate(), this::onError); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void saveImmediate() { |     private void saveImmediate() { | ||||||
|  |         // List must be loaded and modified in order to save | ||||||
|  |         if (isLoadingComplete == null || isModified == null || | ||||||
|  |                 !isLoadingComplete.get() || !isModified.get()) { | ||||||
|  |             Log.w(TAG, "Attempting to save playlist when local playlist " + | ||||||
|  |                     "is not loaded or not modified: playlist id=[" + playlistId + "]"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         final List<LocalItem> items = itemListAdapter.getItemsList(); |         final List<LocalItem> items = itemListAdapter.getItemsList(); | ||||||
|         List<Long> streamIds = new ArrayList<>(items.size()); |         List<Long> streamIds = new ArrayList<>(items.size()); | ||||||
|         for (final LocalItem item : items) { |         for (final LocalItem item : items) { | ||||||
| @@ -386,12 +416,60 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Log.e(TAG, "Updating playlist id=[" + playlistId + |         Log.d(TAG, "Updating playlist id=[" + playlistId + | ||||||
|                 "] with [" + streamIds.size() + "] items"); |                 "] with [" + streamIds.size() + "] items"); | ||||||
|  |  | ||||||
|         playlistManager.updateJoin(playlistId, streamIds) |         playlistManager.updateJoin(playlistId, streamIds) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(() -> {/*Do nothing on success*/}, this::onError); |                 .subscribe( | ||||||
|  |                         () -> { if (isModified != null) isModified.set(false); }, | ||||||
|  |                         this::onError | ||||||
|  |                 ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private ItemTouchHelper.SimpleCallback getItemTouchCallback() { | ||||||
|  |         return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, | ||||||
|  |                 ItemTouchHelper.ACTION_STATE_IDLE) { | ||||||
|  |             @Override | ||||||
|  |             public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, | ||||||
|  |                                                     int viewSizeOutOfBounds, int totalSize, | ||||||
|  |                                                     long msSinceStartScroll) { | ||||||
|  |                 final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, | ||||||
|  |                         viewSizeOutOfBounds, totalSize, msSinceStartScroll); | ||||||
|  |                 final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, | ||||||
|  |                         Math.abs(standardSpeed)); | ||||||
|  |                 return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, | ||||||
|  |                                   RecyclerView.ViewHolder target) { | ||||||
|  |                 if (source.getItemViewType() != target.getItemViewType() || | ||||||
|  |                         itemListAdapter == null) { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 final int sourceIndex = source.getAdapterPosition(); | ||||||
|  |                 final int targetIndex = target.getAdapterPosition(); | ||||||
|  |                 final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); | ||||||
|  |                 if (isSwapped) saveChanges(); | ||||||
|  |                 return isSwapped; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public boolean isLongPressDragEnabled() { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public boolean isItemViewSwipeEnabled() { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} | ||||||
|  |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -449,50 +527,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|         new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); |         new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private ItemTouchHelper.SimpleCallback getItemTouchCallback() { |  | ||||||
|         return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, |  | ||||||
|                 ItemTouchHelper.ACTION_STATE_IDLE) { |  | ||||||
|             @Override |  | ||||||
|             public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, |  | ||||||
|                                                     int viewSizeOutOfBounds, int totalSize, |  | ||||||
|                                                     long msSinceStartScroll) { |  | ||||||
|                 final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, |  | ||||||
|                         viewSizeOutOfBounds, totalSize, msSinceStartScroll); |  | ||||||
|                 final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, |  | ||||||
|                         Math.abs(standardSpeed)); |  | ||||||
|                 return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, |  | ||||||
|                                   RecyclerView.ViewHolder target) { |  | ||||||
|                 if (source.getItemViewType() != target.getItemViewType() || |  | ||||||
|                         itemListAdapter == null) { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 final int sourceIndex = source.getAdapterPosition(); |  | ||||||
|                 final int targetIndex = target.getAdapterPosition(); |  | ||||||
|                 final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); |  | ||||||
|                 if (isSwapped) saveDebounced(); |  | ||||||
|                 return isSwapped; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             public boolean isLongPressDragEnabled() { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             public boolean isItemViewSwipeEnabled() { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void setInitialData(long playlistId, String name) { |     private void setInitialData(long playlistId, String name) { | ||||||
|         this.playlistId = playlistId; |         this.playlistId = playlistId; | ||||||
|         this.name = !TextUtils.isEmpty(name) ? name : ""; |         this.name = !TextUtils.isEmpty(name) ? name : ""; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 John Zhen Mo
					John Zhen Mo