diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index e74a6f2ba..3cd06f3d6 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -1,16 +1,14 @@ package org.schabi.newpipe.info_list; import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.ViewGroup; -import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; @@ -29,16 +27,11 @@ import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; -import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.util.FallbackViewHolder; import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; -import java.util.Objects; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; /* * Created by Christian Schabesberger on 01.08.16. @@ -60,7 +53,7 @@ import io.reactivex.disposables.CompositeDisposable; * along with NewPipe. If not, see . */ -public class InfoListAdapter extends RecyclerView.Adapter { +public class InfoListAdapter extends StateObjectsListAdapter { private static final String TAG = InfoListAdapter.class.getSimpleName(); private static final boolean DEBUG = false; @@ -80,10 +73,7 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; - private final ArrayList states; - private final CompositeDisposable stateLoaders; private boolean useMiniVariant = false; private boolean useGridVariant = false; private boolean showFooter = false; @@ -100,11 +90,9 @@ public class InfoListAdapter extends RecyclerView.Adapter(); - states = new ArrayList<>(); - stateLoaders = new CompositeDisposable(); } public void setOnStreamSelectedListener(OnClickGesture listener) { @@ -131,107 +119,64 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadStreamStateBatch(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntities -> { - addInfoItemList(data, streamStateEntities); - }) - ); - } else { - final ArrayList states = new ArrayList<>(data.size()); - for (int i = data.size(); i > 0; i--) states.add(null); - addInfoItemList(data, states); - } - } - - private void addInfoItemList(List data, List statesEntities) { + public void addInfoItemList(@Nullable final List data) { if (data != null) { - if (DEBUG) { - Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size()); - } - - int offsetStart = sizeConsideringHeaderOffset(); - infoItemList.addAll(data); - states.addAll(statesEntities); - - if (DEBUG) { - Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); - } - - notifyItemRangeInserted(offsetStart, data.size()); - - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeaderOffset(); - notifyItemMoved(offsetStart, footerNow); - - if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow); - } + loadStates(data, infoItemList.size(), () -> addInfoItemListImpl(data)); } } - public void addInfoItem(InfoItem data) { - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadStreamState(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntity -> { - addInfoItem(data, streamStateEntity[0]); - }) - ); - } else { - addInfoItem(data, null); + private void addInfoItemListImpl(@NonNull List data) { + if (DEBUG) { + Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size()); + } + + int offsetStart = sizeConsideringHeaderOffset(); + infoItemList.addAll(data); + + if (DEBUG) { + Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); + } + + notifyItemRangeInserted(offsetStart, data.size()); + + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeaderOffset(); + notifyItemMoved(offsetStart, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow); } } - private void addInfoItem(InfoItem data, StreamStateEntity state) { + public void addInfoItem(@Nullable InfoItem data) { if (data != null) { - if (DEBUG) { - Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); - } + loadState(data, infoItemList.size(), () -> addInfoItemImpl(data)); + } + } - int positionInserted = sizeConsideringHeaderOffset(); - infoItemList.add(data); - states.add(state); + private void addInfoItemImpl(@NonNull InfoItem data) { + if (DEBUG) { + Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); + } - if (DEBUG) { - Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); - } - notifyItemInserted(positionInserted); + int positionInserted = sizeConsideringHeaderOffset(); + infoItemList.add(data); - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeaderOffset(); - notifyItemMoved(positionInserted, footerNow); + if (DEBUG) { + Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); + } + notifyItemInserted(positionInserted); - if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow); - } + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeaderOffset(); + notifyItemMoved(positionInserted, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow); } } public void updateStates() { - if (infoItemList.isEmpty()) { - return; - } - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadStreamStateBatch(infoItemList) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((streamStateEntities) -> { - if (streamStateEntities.size() == states.size()) { - for (int i = 0; i < states.size(); i++) { - final StreamStateEntity newState = streamStateEntities.get(i); - if (!Objects.equals(states.get(i), newState)) { - states.set(i, newState); - notifyItemChanged(header == null ? i : i + 1); - } - } - } else { - //oops, something is wrong - } - }) - ); + if (!infoItemList.isEmpty()) { + updateAllStates(infoItemList); } } @@ -240,7 +185,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { + + private final SparseArray states; + private final HistoryRecordManager recordManager; + private final CompositeDisposable stateLoaders; + private final Context context; + + public StateObjectsListAdapter(Context context) { + this.states = new SparseArray<>(); + this.recordManager = new HistoryRecordManager(context); + this.context = context; + this.stateLoaders = new CompositeDisposable(); + } + + @Nullable + public StreamStateEntity getState(int position) { + return states.get(position); + } + + protected void clearStates() { + states.clear(); + } + + private void appendStates(List statesEntities, int offset) { + for (int i = 0; i < statesEntities.size(); i++) { + final StreamStateEntity state = statesEntities.get(i); + if (state != null) { + states.append(offset + i, state); + } + } + } + + private void appendState(StreamStateEntity statesEntity, int offset) { + if (statesEntity != null) { + states.append(offset, statesEntity); + } + } + + protected void removeState(int index) { + states.remove(index); + } + + protected void moveState(int from, int to) { + final StreamStateEntity item = states.get(from); + if (from < to) { + SparseArrayUtils.shiftItemsDown(states, from, to); + } else { + SparseArrayUtils.shiftItemsUp(states, to, from); + } + states.put(to, item); + } + + protected void loadStates(List list, int offset, Runnable callback) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + appendStates(streamStateEntities, offset); + callback.run(); + }, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + callback.run(); + }) + ); + } else { + callback.run(); + } + } + + protected void loadState(InfoItem item, int offset, Runnable callback) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadStreamState(item) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + appendState(streamStateEntities[0], offset); + callback.run(); + }, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + callback.run(); + }) + ); + } else { + callback.run(); + } + } + + protected void loadStatesForLocal(List list, int offset, Runnable callback) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadLocalStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + appendStates(streamStateEntities, offset); + callback.run(); + }, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + callback.run(); + }) + ); + } else { + callback.run(); + } + } + + private void processStatesUpdates(List streamStateEntities) { + for (int i = 0; i < streamStateEntities.size(); i++) { + final StreamStateEntity newState = streamStateEntities.get(i); + if (!Objects.equals(states.get(i), newState)) { + if (newState == null) { + states.remove(i); + } else { + states.put(i, newState); + } + onItemStateChanged(i, newState); + } + } + } + + protected void updateAllStates(List list) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::processStatesUpdates, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + }) + ); + } else { + final int[] positions = SparseArrayUtils.getKeys(states); + states.clear(); + for (int pos : positions) onItemStateChanged(pos, null); + } + } + + protected void updateAllLocalStates(List list) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadLocalStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::processStatesUpdates, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + }) + ); + } else { + final int[] positions = SparseArrayUtils.getKeys(states); + states.clear(); + for (int pos : positions) onItemStateChanged(pos, null); + } + } + + public void dispose() { + stateLoaders.dispose(); + } + + protected boolean isPlaybackStatesVisible() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) + && prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true) + && prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true); + } + + protected abstract void onItemStateChanged(int position, @Nullable StreamStateEntity state); + +} diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 6dddcb714..903712af2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -1,19 +1,17 @@ package org.schabi.newpipe.local; import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.ViewGroup; -import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.model.StreamStateEntity; -import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.info_list.StateObjectsListAdapter; import org.schabi.newpipe.local.holder.LocalItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder; @@ -30,10 +28,6 @@ import org.schabi.newpipe.util.OnClickGesture; import java.text.DateFormat; import java.util.ArrayList; import java.util.List; -import java.util.Objects; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; /* * Created by Christian Schabesberger on 01.08.16. @@ -55,7 +49,7 @@ import io.reactivex.disposables.CompositeDisposable; * along with NewPipe. If not, see . */ -public class LocalItemListAdapter extends RecyclerView.Adapter { +public class LocalItemListAdapter extends StateObjectsListAdapter { private static final String TAG = LocalItemListAdapter.class.getSimpleName(); private static final boolean DEBUG = false; @@ -73,10 +67,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter localItems; - private final ArrayList states; - private final CompositeDisposable stateLoaders; private final DateFormat dateFormat; private boolean showFooter = false; @@ -85,13 +76,11 @@ public class LocalItemListAdapter extends RecyclerView.Adapter(); dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Localization.getPreferredLocale(activity)); - states = new ArrayList<>(); - stateLoaders = new CompositeDisposable(); } public void setSelectedListener(OnClickGesture listener) { @@ -102,76 +91,49 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadLocalStreamStateBatch(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntities -> - addItems(data, streamStateEntities)) - ); - } else { - final ArrayList states = new ArrayList<>(data.size()); - for (int i = data.size(); i > 0; i--) states.add(null); - addItems(data, states); + public void addItems(@Nullable List data) { + if (data != null) { + loadStatesForLocal(data, localItems.size(), () -> addItemsImpl(data)); } } - private void addItems(List data, List streamStates) { - if (data != null) { - if (DEBUG) { - Log.d(TAG, "addItems() before > localItems.size() = " + - localItems.size() + ", data.size() = " + data.size()); - } + private void addItemsImpl(@NonNull List data) { + if (DEBUG) { + Log.d(TAG, "addItems() before > localItems.size() = " + + localItems.size() + ", data.size() = " + data.size()); + } - int offsetStart = sizeConsideringHeader(); - localItems.addAll(data); - states.addAll(streamStates); + int offsetStart = sizeConsideringHeader(); + localItems.addAll(data); - if (DEBUG) { - Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + - ", localItems.size() = " + localItems.size() + - ", header = " + header + ", footer = " + footer + - ", showFooter = " + showFooter); - } + if (DEBUG) { + Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + + ", localItems.size() = " + localItems.size() + + ", header = " + header + ", footer = " + footer + + ", showFooter = " + showFooter); + } - notifyItemRangeInserted(offsetStart, data.size()); + notifyItemRangeInserted(offsetStart, data.size()); - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeader(); - notifyItemMoved(offsetStart, footerNow); + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeader(); + notifyItemMoved(offsetStart, footerNow); - if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart + - " to " + footerNow); - } + if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart + + " to " + footerNow); } } public void updateStates() { - if (localItems.isEmpty() || !isPlaybackStatesVisible()) return; - stateLoaders.add( - historyRecordManager.loadLocalStreamStateBatch(localItems) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((streamStateEntities) -> { - if (streamStateEntities.size() == states.size()) { - for (int i = 0; i < states.size(); i++) { - final StreamStateEntity newState = streamStateEntities.get(i); - if (!Objects.equals(states.get(i), newState)) { - states.set(i, newState); - notifyItemChanged(header == null ? i : i + 1); - } - } - } else { - //oops, something is wrong - } - }) - ); + if (!localItems.isEmpty()) { + updateAllLocalStates(localItems); + } } public void removeItem(final LocalItem data) { final int index = localItems.indexOf(data); - localItems.remove(index); + removeState(index); notifyItemRemoved(index + (header != null ? 1 : 0)); } @@ -183,7 +145,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter= localItems.size() || actualTo >= localItems.size()) return false; localItems.add(actualTo, localItems.remove(actualFrom)); - states.add(actualTo, states.remove(actualFrom)); + moveState(actualFrom, actualTo); notifyItemMoved(fromAdapterPosition, toAdapterPosition); return true; } @@ -193,6 +155,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter void shiftItemsDown(SparseArray sparseArray, int lower, int upper) { + for (int i = lower + 1; i <= upper; i++) { + final T o = sparseArray.get(i); + sparseArray.put(i - 1, o); + sparseArray.remove(i); + } + } + + public static void shiftItemsUp(SparseArray sparseArray, int lower, int upper) { + for (int i = upper - 1; i >= lower; i--) { + final T o = sparseArray.get(i); + sparseArray.put(i + 1, o); + sparseArray.remove(i); + } + } + + public static int[] getKeys(SparseArray sparseArray) { + final int[] result = new int[sparseArray.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = sparseArray.keyAt(i); + } + return result; + } +}