From 7e005549fecd3da18bb8a4f2be6104933865bbd8 Mon Sep 17 00:00:00 2001 From: SpajicM Date: Mon, 30 Oct 2017 11:53:44 +0100 Subject: [PATCH 01/81] Fix history showing even when disabled --- .../main/java/org/schabi/newpipe/history/HistoryFragment.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java index 03657d264..b8641c92b 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -241,6 +241,7 @@ public abstract class HistoryFragment extends BaseFragme if (mHistoryIsEnabled) { mRecyclerView.setVisibility(View.VISIBLE); } else { + mRecyclerView.setVisibility(View.GONE); mDisabledView.setVisibility(View.VISIBLE); } From 65a6488e445e1559c85bb2d26ab962dafe77b635 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 30 Oct 2017 22:04:58 +0100 Subject: [PATCH 02/81] dont show search history in suggestion when disabled --- .../fragments/list/search/SearchFragment.java | 6 ++++++ .../list/search/SuggestionListAdapter.java | 16 +++++++++++++++- app/src/main/res/layout/activity_history.xml | 1 - 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 90d4d9741..fae97bb7b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -113,6 +113,7 @@ public class SearchFragment extends BaseListFragment suggestionPublisher = PublishSubject.create(); private Disposable searchDisposable; @@ -160,7 +161,12 @@ public class SearchFragment extends BaseListFragment items = new ArrayList<>(); private final Context context; private OnSuggestionItemSelected listener; + private boolean showSugestinHistory = true; public interface OnSuggestionItemSelected { void onSuggestionItemSelected(SuggestionItem item); @@ -31,7 +32,16 @@ public class SuggestionListAdapter extends RecyclerView.Adapter items) { this.items.clear(); - this.items.addAll(items); + if (showSugestinHistory) { + this.items.addAll(items); + } else { + // remove history items if history is disabled + for (SuggestionItem item : items) { + if (!item.fromHistory) { + this.items.add(item); + } + } + } notifyDataSetChanged(); } @@ -39,6 +49,10 @@ public class SuggestionListAdapter extends RecyclerView.Adapter From 02d986fc8952b3307a02e57949e7884a5b8f106d Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 30 Oct 2017 22:46:55 +0100 Subject: [PATCH 03/81] fix multidefined swipe problem in history page --- .../org/schabi/newpipe/history/HistoryFragment.java | 12 ++++++------ .../newpipe/history/SearchHistoryFragment.java | 11 +++++++++++ .../newpipe/history/WatchedHistoryFragment.java | 5 ++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java index b8641c92b..c64689775 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -55,7 +55,7 @@ public abstract class HistoryFragment extends BaseFragme private RecyclerView mRecyclerView; private HistoryEntryAdapter mHistoryAdapter; private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback; - private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; + // private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; private HistoryDAO mHistoryDataSource; private PublishSubject> mHistoryEntryDeleteSubject; @@ -99,7 +99,11 @@ public abstract class HistoryFragment extends BaseFragme } }); - mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, allowedSwipeToDeleteDirections) { + + } + + protected void historyItemSwipeCallback(int swipeDirection) { + mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, swipeDirection) { @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return false; @@ -265,10 +269,6 @@ public abstract class HistoryFragment extends BaseFragme mRecyclerViewState = mRecyclerView.getLayoutManager().onSaveInstanceState(); } - public void setAllowedSwipeToDeleteDirections(int allowedSwipeToDeleteDirections) { - this.allowedSwipeToDeleteDirections = allowedSwipeToDeleteDirections; - } - /** * Called when history enabled flag is changed. * diff --git a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java index 888086a83..91e2cecff 100644 --- a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java @@ -1,9 +1,12 @@ package org.schabi.newpipe.history; import android.content.Context; +import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -17,11 +20,19 @@ import org.schabi.newpipe.util.NavigationHelper; public class SearchHistoryFragment extends HistoryFragment { + private static int allowedSwipeToDeleteDirections = ItemTouchHelper.RIGHT; + @NonNull public static SearchHistoryFragment newInstance() { return new SearchHistoryFragment(); } + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + historyItemSwipeCallback(allowedSwipeToDeleteDirections); + } + @NonNull @Override protected SearchHistoryAdapter createAdapter() { diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java index 086528af7..d898bf353 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java @@ -7,6 +7,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -26,6 +27,8 @@ import org.schabi.newpipe.util.NavigationHelper; public class WatchedHistoryFragment extends HistoryFragment { + private static int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT; + @NonNull public static WatchedHistoryFragment newInstance() { return new WatchedHistoryFragment(); @@ -34,7 +37,7 @@ public class WatchedHistoryFragment extends HistoryFragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + historyItemSwipeCallback(allowedSwipeToDeleteDirections); } @StringRes From 391d3e7fc7b13846a49176955dcddc47a788c07c Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 30 Oct 2017 23:03:18 +0100 Subject: [PATCH 04/81] fix back button for feed on main page --- .../org/schabi/newpipe/fragments/list/BaseListFragment.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 48661969f..35f6a08d3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -187,7 +187,11 @@ public abstract class BaseListFragment extends BaseStateFragment implem ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { supportActionBar.setDisplayShowTitleEnabled(true); - supportActionBar.setDisplayHomeAsUpEnabled(true); + if(useAsFrontPage) { + supportActionBar.setDisplayHomeAsUpEnabled(false); + } else { + supportActionBar.setDisplayHomeAsUpEnabled(true); + } } } From e70dcdc6426e8219184d5babc250ef4535db9f2e Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Fri, 25 Aug 2017 15:10:54 -0700 Subject: [PATCH 05/81] -Added rudimentary playlist fragment. -Added schema for stream storage. --- .../schabi/newpipe/database/Converters.java | 17 + .../database/playlist/PlaylistEntity.java | 70 +++ .../newpipe/database/stream/StreamDAO.java | 24 + .../newpipe/database/stream/StreamEntity.java | 207 ++++++++ .../fragments/playlist/PlaylistFragment.java | 445 ++++++++++++++++ .../fragments/search/PlaylistService.java | 4 + .../fragments/subscription/FeedFragment.java | 496 ++++++++++++++++++ .../subscription/SubscriptionEngine.java | 170 ++++++ .../subscription/SubscriptionFragment.java | 244 +++++---- .../info_list/PlaylistInfoItemHolder.java | 49 ++ app/src/main/res/layout/playlist_item.xml | 51 ++ 11 files changed, 1673 insertions(+), 104 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/Converters.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/StreamDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/StreamEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/search/PlaylistService.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/subscription/FeedFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionEngine.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/PlaylistInfoItemHolder.java create mode 100644 app/src/main/res/layout/playlist_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/database/Converters.java b/app/src/main/java/org/schabi/newpipe/database/Converters.java new file mode 100644 index 000000000..3203e2b3c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/Converters.java @@ -0,0 +1,17 @@ +package org.schabi.newpipe.database; + +import android.arch.persistence.room.TypeConverter; + +import java.util.Date; + +public class Converters { + @TypeConverter + public static Date fromTimestamp(Long value) { + return value == null ? null : new Date(value); + } + + @TypeConverter + public static Long dateToTimestamp(Date date) { + return date == null ? null : date.getTime(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistEntity.java new file mode 100644 index 000000000..76cef7ef5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistEntity.java @@ -0,0 +1,70 @@ +package org.schabi.newpipe.database.playlist; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Ignore; +import android.arch.persistence.room.PrimaryKey; + +import static org.schabi.newpipe.database.playlist.PlaylistEntity.PLAYLIST_TABLE; + +@Entity(tableName = PLAYLIST_TABLE) +public class PlaylistEntity { + + final static String PLAYLIST_TABLE = "playlists"; + final static String PLAYLIST_URL = "url"; + final static String PLAYLIST_TITLE = "title"; + final static String PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; + + @PrimaryKey(autoGenerate = true) + private long uid = 0; + + @ColumnInfo(name = PLAYLIST_TITLE) + private String title; + + /* This is used as a reference to the source, should this playlist be dynamic */ + @ColumnInfo(name = PLAYLIST_URL) + private String url; + + @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) + private String thumbnailUrl; + + public long getUid() { + return uid; + } + + /* Keep this package-private since UID should always be auto generated by Room impl */ + void setUid(long uid) { + this.uid = uid; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public void setThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } + + @Ignore + public void setData(final String title, + final String thumbnailUrl) { + this.setTitle(title); + this.setThumbnailUrl(thumbnailUrl); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamDAO.java new file mode 100644 index 000000000..31e156cc8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamDAO.java @@ -0,0 +1,24 @@ +package org.schabi.newpipe.database.stream; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; + +import org.schabi.newpipe.database.BasicDAO; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.stream.StreamEntity.STREAM_SERVICE_ID; +import static org.schabi.newpipe.database.stream.StreamEntity.STREAM_TABLE; + +@Dao +public interface StreamDAO extends BasicDAO { + @Override + @Query("SELECT * FROM " + STREAM_TABLE) + Flowable> findAll(); + + @Override + @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId") + Flowable> listByService(int serviceId); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamEntity.java new file mode 100644 index 000000000..20eddb38a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamEntity.java @@ -0,0 +1,207 @@ +package org.schabi.newpipe.database.stream; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Ignore; +import android.arch.persistence.room.Index; +import android.arch.persistence.room.PrimaryKey; + +import org.schabi.newpipe.extractor.AbstractStreamInfo; +import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; + +import java.util.Date; + +import static org.schabi.newpipe.database.stream.StreamEntity.STREAM_SERVICE_ID; +import static org.schabi.newpipe.database.stream.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipe.database.stream.StreamEntity.STREAM_URL; + +@Entity(tableName = STREAM_TABLE, + indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)}) +public class StreamEntity { + public final static String STREAM_UID = "uid"; + + final static String STREAM_TABLE = "streams"; + final static String STREAM_ID = "id"; + final static String STREAM_TYPE = "type"; + final static String STREAM_SERVICE_ID = "service_id"; + final static String STREAM_URL = "url"; + final static String STREAM_TITLE = "title"; + final static String STREAM_THUMBNAIL_URL = "thumbnail_url"; + final static String STREAM_VIEW_COUNT = "view_count"; + final static String STREAM_UPLOADER = "uploader"; + final static String STREAM_UPLOAD_DATE = "upload_date"; + final static String STREAM_DURATION = "duration"; + + @PrimaryKey(autoGenerate = true) + private long uid = 0; + + @ColumnInfo(name = STREAM_SERVICE_ID) + private int serviceId = -1; + + @ColumnInfo(name = STREAM_ID) + private String id; + + @ColumnInfo(name = STREAM_TYPE) + private String type; + + @ColumnInfo(name = STREAM_URL) + private String url; + + @ColumnInfo(name = STREAM_TITLE) + private String title; + + @ColumnInfo(name = STREAM_THUMBNAIL_URL) + private String thumbnailUrl; + + @ColumnInfo(name = STREAM_VIEW_COUNT) + private Long viewCount; + + @ColumnInfo(name = STREAM_UPLOADER) + private String uploader; + + @ColumnInfo(name = STREAM_UPLOAD_DATE) + private long uploadDate; + + @ColumnInfo(name = STREAM_DURATION) + private int duration; + + @Ignore + public StreamInfoItem toStreamInfoItem() { + StreamInfoItem item = new StreamInfoItem(); + + item.stream_type = AbstractStreamInfo.StreamType.valueOf( this.getType() ); + + item.service_id = this.getServiceId(); + item.id = this.getId(); + item.webpage_url = this.getUrl(); + item.title = this.getTitle(); + item.thumbnail_url = this.getThumbnailUrl(); + item.view_count = this.getViewCount(); + item.uploader = this.getUploader(); + + // TODO: temporary until upload date parsing is fleshed out + item.upload_date = "Unknown"; + item.duration = this.getDuration(); + + return item; + } + + @Ignore + public StreamEntity(final StreamInfoItem item) { + setData(item); + } + + @Ignore + public void setData(final StreamInfoItem item) { + // Do not store ordinals into db since they may change in the future + this.type = item.stream_type.name(); + + this.serviceId = item.service_id; + this.id = item.id; + this.url = item.webpage_url; + this.title = item.title; + this.thumbnailUrl = item.thumbnail_url; + this.viewCount = item.view_count; + this.uploader = item.uploader; + + // TODO: temporary until upload date parsing is fleshed out + this.uploadDate = new Date().getTime(); + this.duration = item.duration; + } + + @Ignore + public boolean is(final StreamInfoItem item) { + return this.type.equals( item.stream_type.name() ) && + this.serviceId == item.service_id && + this.id.equals( item.id ) && + this.url.equals( item.webpage_url ); + } + + public long getUid() { + return uid; + } + + void setUid(long uid) { + this.uid = uid; + } + + public int getServiceId() { + return serviceId; + } + + public void setServiceId(int serviceId) { + this.serviceId = serviceId; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public void setThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } + + public Long getViewCount() { + return viewCount; + } + + public void setViewCount(Long viewCount) { + this.viewCount = viewCount; + } + + public String getUploader() { + return uploader; + } + + public void setUploader(String uploader) { + this.uploader = uploader; + } + + public long getUploadDate() { + return uploadDate; + } + + public void setUploadDate(long uploadDate) { + this.uploadDate = uploadDate; + } + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java new file mode 100644 index 000000000..0493fd525 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java @@ -0,0 +1,445 @@ +package org.schabi.newpipe.fragments.playlist; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.schabi.newpipe.ImageErrorLoadingListener; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.playlist.PlayListExtractor; +import org.schabi.newpipe.extractor.playlist.PlayListInfo; +import org.schabi.newpipe.fragments.BaseFragment; +import org.schabi.newpipe.fragments.search.OnScrollBelowItemsListener; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.NavigationHelper; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.concurrent.Callable; + +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class PlaylistFragment extends BaseFragment { + private final String TAG = "PlaylistFragment@" + Integer.toHexString(hashCode()); + + private static final String INFO_LIST_KEY = "info_list_key"; + private static final String PLAYLIST_INFO_KEY = "playlist_info_key"; + private static final String PAGE_NUMBER_KEY = "page_number_key"; + + private InfoListAdapter infoListAdapter; + + private PlayListInfo currentPlaylistInfo; + private int serviceId = -1; + private String playlistTitle = ""; + private String playlistUrl = ""; + private int pageNumber = 0; + private boolean hasNextPage = true; + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private RecyclerView playlistStreams; + + private View headerRootLayout; + private ImageView headerBannerView; + private ImageView headerAvatarView; + private TextView headerTitleView; + + /*////////////////////////////////////////////////////////////////////////*/ + + public PlaylistFragment() { + } + + public static Fragment getInstance(int serviceId, String playlistUrl, String title) { + PlaylistFragment instance = new PlaylistFragment(); + instance.setPlaylist(serviceId, playlistUrl, title); + return instance; + } + + /*////////////////////////////////////////////////////////////////////////// + // Fragment's LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate(Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + if (savedInstanceState != null) { + playlistUrl = savedInstanceState.getString(Constants.KEY_URL); + playlistTitle = savedInstanceState.getString(Constants.KEY_TITLE); + serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, -1); + + pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0); + Serializable serializable = savedInstanceState.getSerializable(PLAYLIST_INFO_KEY); + if (serializable instanceof PlayListInfo) currentPlaylistInfo = (PlayListInfo) serializable; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); + return inflater.inflate(R.layout.fragment_channel, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (currentPlaylistInfo == null) loadPage(0); + else handlePlayListInfo(currentPlaylistInfo, false, false); + } + + @Override + public void onDestroyView() { + if (DEBUG) Log.d(TAG, "onDestroyView() called"); + headerAvatarView.setImageBitmap(null); + headerBannerView.setImageBitmap(null); + playlistStreams.removeAllViews(); + + playlistStreams = null; + headerRootLayout = null; + headerBannerView = null; + headerAvatarView = null; + headerTitleView = null; + + super.onDestroyView(); + } + + @Override + public void onResume() { + if (DEBUG) Log.d(TAG, "onResume() called"); + super.onResume(); + if (wasLoading.getAndSet(false)) { + loadPage(pageNumber); + } + } + + @Override + public void onStop() { + if (DEBUG) Log.d(TAG, "onStop() called"); + + disposable.dispose(); + disposable = null; + + super.onStop(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]"); + super.onSaveInstanceState(outState); + outState.putString(Constants.KEY_URL, playlistUrl); + outState.putString(Constants.KEY_TITLE, playlistTitle); + outState.putInt(Constants.KEY_SERVICE_ID, serviceId); + + outState.putSerializable(INFO_LIST_KEY, infoListAdapter.getItemsList()); + outState.putSerializable(PLAYLIST_INFO_KEY, currentPlaylistInfo); + outState.putInt(PAGE_NUMBER_KEY, pageNumber); + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.menu_channel, menu); + + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(true); + supportActionBar.setDisplayHomeAsUpEnabled(true); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); + super.onOptionsItemSelected(item); + switch (item.getItemId()) { + case R.id.menu_item_openInBrowser: { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(playlistUrl)); + startActivity(Intent.createChooser(intent, getString(R.string.choose_browser))); + return true; + } + case R.id.menu_item_share: { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, playlistUrl); + intent.setType("text/plain"); + startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); + return true; + } + default: + return super.onOptionsItemSelected(item); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Init's + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + playlistStreams = (RecyclerView) rootView.findViewById(R.id.channel_streams_view); + + playlistStreams.setLayoutManager(new LinearLayoutManager(activity)); + if (infoListAdapter == null) { + infoListAdapter = new InfoListAdapter(activity); + if (savedInstanceState != null) { + //noinspection unchecked + ArrayList serializable = (ArrayList) savedInstanceState.getSerializable(INFO_LIST_KEY); + infoListAdapter.addInfoItemList(serializable); + } + } + + playlistStreams.setAdapter(infoListAdapter); + headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_header, playlistStreams, false); + infoListAdapter.setHeader(headerRootLayout); + infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, playlistStreams, false)); + + headerBannerView = (ImageView) headerRootLayout.findViewById(R.id.playlist_banner_image); + headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.playlist_avatar_view); + headerTitleView = (TextView) headerRootLayout.findViewById(R.id.playlist_title_view); + } + + protected void initListeners() { + super.initListeners(); + + infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(int serviceId, String url, String title) { + if (DEBUG) Log.d(TAG, "selected() called with: serviceId = [" + serviceId + "], url = [" + url + "], title = [" + title + "]"); + NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title); + } + }); + + playlistStreams.clearOnScrollListeners(); + playlistStreams.addOnScrollListener(new OnScrollBelowItemsListener() { + @Override + public void onScrolledDown(RecyclerView recyclerView) { + loadMore(true); + } + }); + } + + + @Override + protected void reloadContent() { + if (DEBUG) Log.d(TAG, "reloadContent() called"); + currentPlaylistInfo = null; + infoListAdapter.clearStreamItemList(); + loadPage(0); + } + + /*////////////////////////////////////////////////////////////////////////// + // Playlist Loader + //////////////////////////////////////////////////////////////////////////*/ + + private StreamingService getService(final int serviceId) throws ExtractionException { + return NewPipe.getService(serviceId); + } + + Disposable disposable; + + private void loadMore(final boolean onlyVideos) { + final Callable task = new Callable() { + @Override + public PlayListInfo call() throws Exception { + final PlayListExtractor extractor = getService(serviceId) + .getPlayListExtractorInstance(playlistUrl, pageNumber); + + return PlayListInfo.getInfo(extractor); + } + }; + + + Observable.fromCallable(task) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + if (disposable == null || disposable.isDisposed()) { + disposable = d; + isLoading.set(true); + } else { + d.dispose(); + } + } + + @Override + public void onNext(@NonNull PlayListInfo playListInfo) { + if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + playListInfo + "]"); + if (playListInfo == null || isRemoving() || !isVisible()) return; + + handlePlayListInfo(playListInfo, onlyVideos, true); + isLoading.set(false); + pageNumber++; + } + + @Override + public void onError(@NonNull Throwable e) { + onRxError(e, "Observer failure"); + } + + @Override + public void onComplete() { + if (disposable != null) { + disposable.dispose(); + disposable = null; + } + } + }); + } + + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void loadPage(int page) { + if (DEBUG) Log.d(TAG, "loadPage() called with: page = [" + page + "]"); + isLoading.set(true); + pageNumber = page; + infoListAdapter.showFooter(false); + + animateView(loadingProgressBar, true, 200); + animateView(errorPanel, false, 200); + + imageLoader.cancelDisplayTask(headerBannerView); + imageLoader.cancelDisplayTask(headerAvatarView); + + headerTitleView.setText(playlistTitle != null ? playlistTitle : ""); + headerBannerView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.channel_banner)); + headerAvatarView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy)); + if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(playlistTitle != null ? playlistTitle : ""); + + loadMore(true); + } + + private void setPlaylist(int serviceId, String playlistUrl, String title) { + this.serviceId = serviceId; + this.playlistUrl = playlistUrl; + this.playlistTitle = title; + } + + private void handlePlayListInfo(PlayListInfo info, boolean onlyVideos, boolean addVideos) { + currentPlaylistInfo = info; + + animateView(errorPanel, false, 300); + animateView(playlistStreams, true, 200); + animateView(loadingProgressBar, false, 200); + + if (!onlyVideos) { + if (activity.getSupportActionBar() != null) activity.getSupportActionBar().invalidateOptionsMenu(); + + headerRootLayout.setVisibility(View.VISIBLE); + //animateView(loadingProgressBar, false, 200, null); + + if (!TextUtils.isEmpty(info.playList_name)) { + if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(info.playList_name); + headerTitleView.setText(info.playList_name); + playlistTitle = info.playList_name; + } else playlistTitle = ""; + + if (!TextUtils.isEmpty(info.banner_url)) { + imageLoader.displayImage(info.banner_url, headerBannerView, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id)); + } + + if (!TextUtils.isEmpty(info.avatar_url)) { + headerAvatarView.setVisibility(View.VISIBLE); + imageLoader.displayImage(info.avatar_url, headerAvatarView, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id)); + } + + infoListAdapter.showFooter(true); + } + + hasNextPage = info.hasNextPage; + if (!hasNextPage) infoListAdapter.showFooter(false); + + //if (!listRestored) { + if (addVideos) infoListAdapter.addInfoItemList(info.related_streams); + //} + } + + @Override + protected void setErrorMessage(String message, boolean showRetryButton) { + super.setErrorMessage(message, showRetryButton); + + animateView(playlistStreams, false, 200); + currentPlaylistInfo = null; + } + + /*////////////////////////////////////////////////////////////////////////// + // Error Handlers + //////////////////////////////////////////////////////////////////////////*/ + + private void onRxError(final Throwable exception, final String tag) { + if (exception instanceof IOException) { + onRecoverableError(R.string.network_error); + } else { + onUnrecoverableError(exception, tag); + } + } + + private void onRecoverableError(int messageId) { + if (!this.isAdded()) return; + + if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); + setErrorMessage(getString(messageId), true); + } + + private void onUnrecoverableError(Throwable exception, final String tag) { + if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); + ErrorActivity.reportError( + getContext(), + exception, + MainActivity.class, + null, + ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_PLAYLIST, "Feed", tag, R.string.general_error) + ); + + activity.finish(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/search/PlaylistService.java b/app/src/main/java/org/schabi/newpipe/fragments/search/PlaylistService.java new file mode 100644 index 000000000..b43c7e356 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/search/PlaylistService.java @@ -0,0 +1,4 @@ +package org.schabi.newpipe.fragments.search; + +public class PlaylistService { +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/FeedFragment.java new file mode 100644 index 000000000..df92449d2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/FeedFragment.java @@ -0,0 +1,496 @@ +package org.schabi.newpipe.fragments.subscription; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.jakewharton.rxbinding2.view.RxView; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.subscription.SubscriptionEntity; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.fragments.BaseFragment; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.util.NavigationHelper; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.Flowable; +import io.reactivex.MaybeObserver; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; + +import static org.schabi.newpipe.report.UserAction.REQUESTED_CHANNEL; +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class FeedFragment extends BaseFragment { + private static final String VIEW_STATE_KEY = "view_state_key"; + private static final String INFO_ITEMS_KEY = "info_items_key"; + + private static final int FEED_LOAD_SIZE = 4; + private static final int LOAD_ITEM_DEBOUNCE_INTERVAL = 500; + + private final String TAG = "FeedFragment@" + Integer.toHexString(hashCode()); + + private View inflatedView; + private View emptyPanel; + private View loadItemFooter; + + private InfoListAdapter infoListAdapter; + private RecyclerView resultRecyclerView; + + private Parcelable viewState; + private AtomicBoolean retainFeedItems; + + private SubscriptionEngine subscriptionEngine; + + private Disposable loadItemObserver; + private Disposable subscriptionObserver; + private Subscription feedSubscriber; + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + /////////////////////////////////////////////////////////////////////////// + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + subscriptionEngine = SubscriptionEngine.getInstance(getContext()); + + retainFeedItems = new AtomicBoolean(false); + + if (infoListAdapter == null) { + infoListAdapter = new InfoListAdapter(getActivity()); + } + + if (savedInstanceState != null) { + // Get recycler view state + viewState = savedInstanceState.getParcelable(VIEW_STATE_KEY); + + // Deserialize and get recycler adapter list + final Object[] serializedInfoItems = (Object[]) savedInstanceState.getSerializable(INFO_ITEMS_KEY); + if (serializedInfoItems != null) { + final InfoItem[] infoItems = Arrays.copyOf( + serializedInfoItems, + serializedInfoItems.length, + InfoItem[].class + ); + final List feedInfos = Arrays.asList(infoItems); + infoListAdapter.addInfoItemList( feedInfos ); + } + + // Already displayed feed items survive configuration changes + retainFeedItems.set(true); + } + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + if (inflatedView == null) { + inflatedView = inflater.inflate(R.layout.fragment_subscription, container, false); + } + return inflatedView; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + if (resultRecyclerView != null) { + outState.putParcelable( + VIEW_STATE_KEY, + resultRecyclerView.getLayoutManager().onSaveInstanceState() + ); + } + + if (infoListAdapter != null) { + outState.putSerializable(INFO_ITEMS_KEY, infoListAdapter.getItemsList().toArray()); + } + } + + @Override + public void onDestroyView() { + // Do not monitor for updates when user is not viewing the feed fragment. + // This is a waste of bandwidth. + if (loadItemObserver != null) loadItemObserver.dispose(); + if (subscriptionObserver != null) subscriptionObserver.dispose(); + if (feedSubscriber != null) feedSubscriber.cancel(); + + loadItemObserver = null; + subscriptionObserver = null; + feedSubscriber = null; + + loadItemFooter = null; + + // Retain the already displayed items for backstack pops + retainFeedItems.set(true); + + super.onDestroyView(); + } + + @Override + public void onDestroy() { + subscriptionEngine = null; + + super.onDestroy(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Views + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + super.onCreateOptionsMenu(menu, inflater); + + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(true); + supportActionBar.setDisplayHomeAsUpEnabled(true); + } + } + + private RecyclerView.OnScrollListener getOnScrollListener() { + return new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + viewState = recyclerView.getLayoutManager().onSaveInstanceState(); + } + } + }; + } + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + if (infoListAdapter == null) return; + + animateView(errorPanel, false, 200); + animateView(loadingProgressBar, true, 200); + + emptyPanel = rootView.findViewById(R.id.empty_panel); + + resultRecyclerView = rootView.findViewById(R.id.result_list_view); + resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); + + loadItemFooter = activity.getLayoutInflater().inflate(R.layout.load_item_footer, resultRecyclerView, false); + infoListAdapter.setFooter(loadItemFooter); + infoListAdapter.showFooter(false); + infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(int serviceId, String url, String title) { + NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title); + } + }); + + resultRecyclerView.setAdapter(infoListAdapter); + resultRecyclerView.addOnScrollListener(getOnScrollListener()); + + if (viewState != null) { + resultRecyclerView.getLayoutManager().onRestoreInstanceState(viewState); + viewState = null; + } + + if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(R.string.fragment_whats_new); + + populateFeed(); + } + + private void resetFragment() { + if (subscriptionObserver != null) subscriptionObserver.dispose(); + if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + } + + @Override + protected void reloadContent() { + resetFragment(); + populateFeed(); + } + + @Override + protected void setErrorMessage(String message, boolean showRetryButton) { + super.setErrorMessage(message, showRetryButton); + + resetFragment(); + } + + /** + * Changes the state of the load item footer. + * + * If the current state of the feed is loaded, this displays the load item button and + * starts its reactor. + * + * Otherwise, show a spinner in place of the loader button. */ + private void setLoader(final boolean isLoaded) { + if (loadItemFooter == null) return; + + if (loadItemObserver != null) loadItemObserver.dispose(); + + if (isLoaded) { + loadItemObserver = getLoadItemObserver(loadItemFooter); + } + + loadItemFooter.findViewById(R.id.paginate_progress_bar).setVisibility(isLoaded ? View.GONE : View.VISIBLE); + loadItemFooter.findViewById(R.id.load_more_text).setVisibility(isLoaded ? View.VISIBLE : View.GONE); + } + + /////////////////////////////////////////////////////////////////////////// + // Feeds Loader + /////////////////////////////////////////////////////////////////////////// + + /** + * Responsible for reacting to subscription database updates and displaying feeds. + * + * Upon each update, the feed info list is cleared unless the fragment is + * recently recovered from a configuration change or backstack. + * + * All existing and pending feed requests are dropped. + * + * The newly received list of subscriptions is then transformed into a + * flowable, reacting to pulling requests. + * + * Pulled requests are transformed first into ChannelInfo, then Stream Info items and + * displayed on the feed fragment. + **/ + private void populateFeed() { + final Consumer> consumer = new Consumer>() { + @Override + public void accept(@NonNull List subscriptionEntities) throws Exception { + animateView(loadingProgressBar, false, 200); + + if (subscriptionEntities.isEmpty()) { + infoListAdapter.clearStreamItemList(); + emptyPanel.setVisibility(View.VISIBLE); + } else { + emptyPanel.setVisibility(View.INVISIBLE); + } + + // show progress bar on receiving a non-empty updated list of subscriptions + if (!retainFeedItems.get() && !subscriptionEntities.isEmpty()) { + infoListAdapter.clearStreamItemList(); + animateView(loadingProgressBar, true, 200); + } + + retainFeedItems.set(false); + Flowable.fromIterable(subscriptionEntities) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getSubscriptionObserver()); + } + }; + + final Consumer onError = new Consumer() { + @Override + public void accept(@NonNull Throwable exception) throws Exception { + onRxError(exception, "Subscription Database Reactor"); + } + }; + + if (subscriptionObserver != null) subscriptionObserver.dispose(); + subscriptionObserver = subscriptionEngine.getSubscription() + .onErrorReturnItem(Collections.emptyList()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(consumer, onError); + } + + /** + * Responsible for reacting to user pulling request and starting a request for new feed stream. + * + * On initialization, it automatically requests the amount of feed needed to display + * a minimum amount required (FEED_LOAD_SIZE). + * + * Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo + * containing the feed streams. + **/ + private Subscriber getSubscriptionObserver() { + return new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + if (feedSubscriber != null) feedSubscriber.cancel(); + feedSubscriber = s; + + final int requestSize = FEED_LOAD_SIZE - infoListAdapter.getItemsList().size(); + if (requestSize > 0) { + requestFeed(requestSize); + } else { + setLoader(true); + } + + animateView(loadingProgressBar, false, 200); + // Footer spinner persists until subscription list is exhausted. + infoListAdapter.showFooter(true); + } + + @Override + public void onNext(SubscriptionEntity subscriptionEntity) { + setLoader(false); + + subscriptionEngine.getChannelInfo(subscriptionEntity) + .observeOn(AndroidSchedulers.mainThread()) + .onErrorComplete() + .subscribe(getChannelInfoObserver()); + } + + @Override + public void onError(Throwable exception) { + onRxError(exception, "Feed Pull Reactor"); + } + + @Override + public void onComplete() { + infoListAdapter.showFooter(false); + } + }; + } + + /** + * On each request, a subscription item from the updated table is transformed + * into a ChannelInfo, containing the latest streams from the channel. + * + * Currently, the feed uses the first into from the list of streams. + * + * If chosen feed already displayed, then we request another feed from another + * subscription, until the subscription table runs out of new items. + * + * This Observer is self-contained and will dispose itself when complete. However, this + * does not obey the fragment lifecycle and may continue running in the background + * until it is complete. This is done due to RxJava2 no longer propagate errors once + * an observer is unsubscribed while the thread process is still running. + * + * To solve the above issue, we can either set a global RxJava Error Handler, or + * manage exceptions case by case. This should be done if the current implementation is + * too costly when dealing with larger subscription sets. + **/ + private MaybeObserver getChannelInfoObserver() { + return new MaybeObserver() { + Disposable observer; + @Override + public void onSubscribe(Disposable d) { + observer = d; + } + + // Called only when response is non-empty + @Override + public void onSuccess(ChannelInfo channelInfo) { + emptyPanel.setVisibility(View.INVISIBLE); + + if (infoListAdapter == null || channelInfo.related_streams.isEmpty()) return; + + final InfoItem item = channelInfo.related_streams.get(0); + // Keep requesting new items if the current one already exists + if (!doesItemExist(infoListAdapter.getItemsList(), item)) { + infoListAdapter.addInfoItem(item); + } else { + requestFeed(1); + } + onDone(); + } + + @Override + public void onError(Throwable exception) { + onRxError(exception, "Feed Display Reactor"); + onDone(); + } + + // Called only when response is empty + @Override + public void onComplete() { + onDone(); + } + + private void onDone() { + setLoader(true); + + observer.dispose(); + observer = null; + } + }; + } + + private boolean doesItemExist(final List items, final InfoItem item) { + for (final InfoItem existingItem: items) { + if (existingItem.infoType() == item.infoType() && + existingItem.getTitle().equals(item.getTitle()) && + existingItem.getLink().equals(item.getLink())) return true; + } + return false; + } + + private void requestFeed(final int count) { + if (feedSubscriber == null) return; + + feedSubscriber.request(count); + } + + private Disposable getLoadItemObserver(@NonNull final View itemLoader) { + final Consumer onNext = new Consumer() { + @Override + public void accept(Object o) throws Exception { + requestFeed(FEED_LOAD_SIZE); + } + }; + + final Consumer onError = new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + onRxError(throwable, "Load Button Reactor"); + } + }; + + return RxView.clicks(itemLoader) + .debounce(LOAD_ITEM_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) + .subscribe(onNext, onError); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + private void onRxError(final Throwable exception, final String tag) { + if (exception instanceof IOException) { + onRecoverableError(R.string.network_error); + } else { + onUnrecoverableError(exception, tag); + } + } + + private void onRecoverableError(int messageId) { + if (!this.isAdded()) return; + + if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); + setErrorMessage(getString(messageId), true); + } + + private void onUnrecoverableError(Throwable exception, final String tag) { + if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); + ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, "Feed", tag, R.string.general_error)); + + activity.finish(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionEngine.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionEngine.java new file mode 100644 index 000000000..25285db41 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionEngine.java @@ -0,0 +1,170 @@ +package org.schabi.newpipe.fragments.subscription; + +import android.content.Context; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.subscription.SubscriptionDAO; +import org.schabi.newpipe.database.subscription.SubscriptionEntity; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.channel.ChannelExtractor; +import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Completable; +import io.reactivex.CompletableSource; +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Scheduler; +import io.reactivex.annotations.NonNull; +import io.reactivex.functions.Function; +import io.reactivex.schedulers.Schedulers; + +/** Subscription Service singleton: + * Provides a basis for channel Subscriptions. + * Provides access to subscription table in database as well as + * up-to-date observations on the subscribed channels + * */ +public class SubscriptionEngine { + + private static SubscriptionEngine sInstance; + private static final Object LOCK = new Object(); + + public static SubscriptionEngine getInstance(Context context) { + if (sInstance == null) { + synchronized (LOCK) { + if (sInstance == null) { + sInstance = new SubscriptionEngine(context); + } + } + } + return sInstance; + } + + protected final String TAG = "SubscriptionEngine@" + Integer.toHexString(hashCode()); + private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500; + private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4; + + private AppDatabase db; + private Flowable> subscription; + + private Scheduler subscriptionScheduler; + + private SubscriptionEngine(Context context) { + db = NewPipeDatabase.getInstance( context ); + subscription = getSubscriptionInfos(); + + final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE); + subscriptionScheduler = Schedulers.from(subscriptionExecutor); + } + + /** Part of subscription observation pipeline + * @see SubscriptionEngine#getSubscription() + */ + private Flowable> getSubscriptionInfos() { + return subscriptionTable().findAll() + // Wait for a period of infrequent updates and return the latest update + .debounce(SUBSCRIPTION_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) + .share() // Share allows multiple subscribers on the same observable + .replay(1) // Replay synchronizes subscribers to the last emitted result + .autoConnect(); + } + + /** + * Provides an observer to the latest update to the subscription table. + * + * This observer may be subscribed multiple times, where each subscriber obtains + * the latest synchronized changes available, effectively share the same data + * across all subscribers. + * + * This observer has a debounce cooldown, meaning if multiple updates are observed + * in the cooldown interval, only the latest changes are emitted to the subscribers. + * This reduces the amount of observations caused by frequent updates to the database. + * */ + @android.support.annotation.NonNull + public Flowable> getSubscription() { + return subscription; + } + + public Maybe getChannelInfo(final SubscriptionEntity subscriptionEntity) { + final StreamingService service = getService(subscriptionEntity.getServiceId()); + if (service == null) return Maybe.empty(); + + final String url = subscriptionEntity.getUrl(); + final Callable callable = new Callable() { + @Override + public ChannelInfo call() throws Exception { + final ChannelExtractor extractor = service.getChannelExtractorInstance(url, 0); + return ChannelInfo.getInfo(extractor); + } + }; + + return Maybe.fromCallable(callable).subscribeOn(subscriptionScheduler); + } + + private StreamingService getService(final int serviceId) { + try { + return NewPipe.getService(serviceId); + } catch (ExtractionException e) { + return null; + } + } + + /** Returns the database access interface for subscription table. */ + public SubscriptionDAO subscriptionTable() { + return db.subscriptionDAO(); + } + + public Completable updateChannelInfo(final int serviceId, + final String channelUrl, + final ChannelInfo info) { + final Function, CompletableSource> update = new Function, CompletableSource>() { + @Override + public CompletableSource apply(@NonNull List subscriptionEntities) throws Exception { + if (subscriptionEntities.size() == 1) { + SubscriptionEntity subscription = subscriptionEntities.get(0); + + // Subscriber count changes very often, making this check almost unnecessary. + // Consider removing it later. + if (isSubscriptionUpToDate(channelUrl, info, subscription)) { + subscription.setData(info.channel_name, info.avatar_url, "", info.subscriberCount); + + return update(subscription); + } + } + + return Completable.complete(); + } + }; + + return subscriptionTable().findAll(serviceId, channelUrl) + .firstOrError() + .flatMapCompletable(update); + } + + private Completable update(final SubscriptionEntity updatedSubscription) { + return Completable.fromRunnable(new Runnable() { + @Override + public void run() { + subscriptionTable().update(updatedSubscription); + } + }); + } + + private boolean isSubscriptionUpToDate(final String channelUrl, + final ChannelInfo info, + final SubscriptionEntity entity) { + return channelUrl.equals( entity.getUrl() ) && + info.service_id == entity.getServiceId() && + info.channel_name.equals( entity.getTitle() ) && + info.avatar_url.equals( entity.getThumbnailUrl() ) && + info.subscriberCount == entity.getSubscriberCount(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java index c4dfbc50d..afcb03d2b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java @@ -1,9 +1,7 @@ package org.schabi.newpipe.fragments.subscription; -import android.content.Context; import android.os.Bundle; import android.os.Parcelable; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -12,43 +10,50 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; -import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.fragments.BaseFragment; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.KioskTranslator; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.NavigationHelper; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -import icepick.State; import io.reactivex.Observer; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import static org.schabi.newpipe.report.UserAction.REQUESTED_CHANNEL; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class SubscriptionFragment extends BaseStateFragment> { +public class SubscriptionFragment extends BaseFragment { + private static final String VIEW_STATE_KEY = "view_state_key"; + private final String TAG = "SubscriptionFragment@" + Integer.toHexString(hashCode()); + + private View inflatedView; + private View emptyPanel; private View headerRootLayout; + private View whatsNewView; private InfoListAdapter infoListAdapter; - private RecyclerView itemsList; - - @State - protected Parcelable itemsListState; + private RecyclerView resultRecyclerView; + private Parcelable viewState; /* Used for independent events */ - private CompositeDisposable disposables = new CompositeDisposable(); - private SubscriptionService subscriptionService; + private CompositeDisposable disposables; + private SubscriptionEngine subscriptionEngine; /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle @@ -66,10 +71,15 @@ public class SubscriptionFragment extends BaseStateFragment() { - @Override - public void selected(ChannelInfoItem selectedItem) { - // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name); - - } - }); - - headerRootLayout.setOnClickListener(new View.OnClickListener() { + private View.OnClickListener getWhatsNewOnClickListener() { + return new View.OnClickListener() { @Override public void onClick(View view) { NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()); } - }); + }; + } + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + emptyPanel = rootView.findViewById(R.id.empty_panel); + + resultRecyclerView = rootView.findViewById(R.id.result_list_view); + resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); + resultRecyclerView.addOnScrollListener(getOnScrollListener()); + + if (infoListAdapter == null) { + infoListAdapter = new InfoListAdapter(getActivity()); + infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, resultRecyclerView, false)); + infoListAdapter.showFooter(false); + infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(int serviceId, String url, String title) { + /* Requires the parent fragment to find holder for fragment replacement */ + NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), serviceId, url, title); + } + }); + } + + headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, resultRecyclerView, false); + infoListAdapter.setHeader(headerRootLayout); + + whatsNewView = headerRootLayout.findViewById(R.id.whatsNew); + whatsNewView.setOnClickListener(getWhatsNewOnClickListener()); + + resultRecyclerView.setAdapter(infoListAdapter); + + populateView(); + } + + @Override + protected void reloadContent() { + populateView(); + } + + @Override + protected void setErrorMessage(String message, boolean showRetryButton) { + super.setErrorMessage(message, showRetryButton); + resetFragment(); } private void resetFragment() { @@ -153,12 +199,13 @@ public class SubscriptionFragment extends BaseStateFragment>() { @Override public void onSubscribe(Disposable d) { - showLoading(); - disposables.add(d); + animateView(loadingProgressBar, true, 200); + + disposables.add( d ); } @Override public void onNext(List subscriptions) { - handleResult(subscriptions); + animateView(loadingProgressBar, true, 200); + + infoListAdapter.clearStreamItemList(); + infoListAdapter.addInfoItemList( getSubscriptionItems(subscriptions) ); + + animateView(loadingProgressBar, false, 200); + + emptyPanel.setVisibility(subscriptions.isEmpty() ? View.VISIBLE : View.INVISIBLE); + + if (viewState != null && resultRecyclerView != null) { + resultRecyclerView.getLayoutManager().onRestoreInstanceState(viewState); + } } @Override public void onError(Throwable exception) { - SubscriptionFragment.this.onError(exception); + if (exception instanceof IOException) { + onRecoverableError(R.string.network_error); + } else { + onUnrecoverableError(exception); + } } @Override public void onComplete() { + } }; } - @Override - public void handleResult(@NonNull List result) { - super.handleResult(result); - - infoListAdapter.clearStreamItemList(); - - if (result.isEmpty()) { - showEmptyState(); - } else { - infoListAdapter.addInfoItemList(getSubscriptionItems(result)); - if (itemsListState != null) { - itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); - itemsListState = null; - } - - hideLoading(); - } - } - - private List getSubscriptionItems(List subscriptions) { List items = new ArrayList<>(); - for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem()); + for (final SubscriptionEntity subscription: subscriptions) { + ChannelInfoItem item = new ChannelInfoItem(); + item.webPageUrl = subscription.getUrl(); + item.serviceId = subscription.getServiceId(); + item.channelName = subscription.getTitle(); + item.thumbnailUrl = subscription.getThumbnailUrl(); + item.subscriberCount = subscription.getSubscriberCount(); + item.description = subscription.getDescription(); + items.add( item ); + } Collections.sort(items, new Comparator() { @Override public int compare(InfoItem o1, InfoItem o2) { - return o1.name.compareToIgnoreCase(o2.name); + return o1.getTitle().compareToIgnoreCase(o2.getTitle()); } }); + return items; } - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void showLoading() { - super.showLoading(); - animateView(itemsList, false, 100); - } - - @Override - public void hideLoading() { - super.hideLoading(); - animateView(itemsList, true, 200); - } - - @Override - public void showEmptyState() { - super.showEmptyState(); - animateView(itemsList, false, 200); - } - /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// - @Override - protected boolean onError(Throwable exception) { - resetFragment(); - if (super.onError(exception)) return true; + private void onRecoverableError(int messageId) { + if (!this.isAdded()) return; - onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error); - return true; + if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); + setErrorMessage(getString(messageId), true); + } + + private void onUnrecoverableError(Throwable exception) { + if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); + ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, "unknown", "unknown", R.string.general_error)); + activity.finish(); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/PlaylistInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/PlaylistInfoItemHolder.java new file mode 100644 index 000000000..ffd3b09e8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/PlaylistInfoItemHolder.java @@ -0,0 +1,49 @@ +package org.schabi.newpipe.info_list; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; + +/** + * Created by Christian Schabesberger on 12.02.17. + * + * Copyright (C) Christian Schabesberger 2016 + * ChannelInfoItemHolder .java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class PlaylistInfoItemHolder extends InfoItemHolder { + public final ImageView itemThumbnailView; + public final TextView itemPlaylistTitleView; + public final TextView itemAdditionalDetailView; + + public final View itemRoot; + + PlaylistInfoItemHolder(View v) { + super(v); + itemRoot = v.findViewById(R.id.itemRoot); + itemThumbnailView = v.findViewById(R.id.itemThumbnailView); + itemPlaylistTitleView = v.findViewById(R.id.itemPlaylistTitleView); + itemAdditionalDetailView = v.findViewById(R.id.itemAdditionalDetails); + } + + @Override + public InfoItem.InfoType infoType() { + return InfoItem.InfoType.PLAYLIST; + } +} diff --git a/app/src/main/res/layout/playlist_item.xml b/app/src/main/res/layout/playlist_item.xml new file mode 100644 index 000000000..cb734ae15 --- /dev/null +++ b/app/src/main/res/layout/playlist_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + \ No newline at end of file From cbcd281784ce07c9def74bf44676f65169ea0d11 Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Mon, 28 Aug 2017 17:38:37 -0700 Subject: [PATCH 06/81] -Added MediaSourceManager and Playlist adapters. --- .../fragments/playlist/PlaylistFragment.java | 93 +++++++++-- .../fragments/search/PlaylistService.java | 4 - .../newpipe/player/BackgroundPlayer.java | 6 +- .../org/schabi/newpipe/player/BasePlayer.java | 9 +- .../newpipe/player/MediaSourceManager.java | 21 +++ .../newpipe/playlist/ExternalPlaylist.java | 99 +++++++++++ .../org/schabi/newpipe/playlist/Playlist.java | 38 +++++ .../newpipe/playlist/PlaylistAdapter.java | 154 ++++++++++++++++++ .../schabi/newpipe/playlist/PlaylistItem.java | 109 +++++++++++++ .../newpipe/playlist/PlaylistItemBuilder.java | 116 +++++++++++++ .../newpipe/playlist/PlaylistItemHolder.java | 43 +++++ .../main/res/layout/activity_main_player.xml | 12 +- .../main/res/layout/playlist_stream_item.xml | 70 ++++++++ 13 files changed, 753 insertions(+), 21 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/search/PlaylistService.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/ExternalPlaylist.java create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/Playlist.java create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/PlaylistAdapter.java create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/PlaylistItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemBuilder.java create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemHolder.java create mode 100644 app/src/main/res/layout/playlist_stream_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java index 0493fd525..032b227e8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java @@ -79,6 +79,11 @@ public class PlaylistFragment extends BaseFragment { private ImageView headerAvatarView; private TextView headerTitleView; + /*////////////////////////////////////////////////////////////////////////*/ + // Reactors + //////////////////////////////////////////////////////////////////////////*/ + private Disposable loadingReactor; + /*////////////////////////////////////////////////////////////////////////*/ public PlaylistFragment() { @@ -153,8 +158,8 @@ public class PlaylistFragment extends BaseFragment { public void onStop() { if (DEBUG) Log.d(TAG, "onStop() called"); - disposable.dispose(); - disposable = null; + if (loadingReactor != null) loadingReactor.dispose(); + loadingReactor = null; super.onStop(); } @@ -221,7 +226,7 @@ public class PlaylistFragment extends BaseFragment { protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - playlistStreams = (RecyclerView) rootView.findViewById(R.id.channel_streams_view); + playlistStreams = rootView.findViewById(R.id.channel_streams_view); playlistStreams.setLayoutManager(new LinearLayoutManager(activity)); if (infoListAdapter == null) { @@ -238,9 +243,9 @@ public class PlaylistFragment extends BaseFragment { infoListAdapter.setHeader(headerRootLayout); infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, playlistStreams, false)); - headerBannerView = (ImageView) headerRootLayout.findViewById(R.id.playlist_banner_image); - headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.playlist_avatar_view); - headerTitleView = (TextView) headerRootLayout.findViewById(R.id.playlist_title_view); + headerBannerView = headerRootLayout.findViewById(R.id.playlist_banner_image); + headerAvatarView = headerRootLayout.findViewById(R.id.playlist_avatar_view); + headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); } protected void initListeners() { @@ -280,7 +285,71 @@ public class PlaylistFragment extends BaseFragment { return NewPipe.getService(serviceId); } - Disposable disposable; + private void loadAll() { + final Callable task = new Callable() { + @Override + public PlayListInfo call() throws Exception { + int pageCount = 0; + + final PlayListExtractor extractor = getService(serviceId) + .getPlayListExtractorInstance(playlistUrl, 0); + + final PlayListInfo info = PlayListInfo.getInfo(extractor); + + boolean hasNext = info.hasNextPage; + while(hasNext) { + pageCount++; + + final PlayListExtractor moreExtractor = getService(serviceId) + .getPlayListExtractorInstance(playlistUrl, pageCount); + + final PlayListInfo moreInfo = PlayListInfo.getInfo(moreExtractor); + + info.related_streams.addAll(moreInfo.related_streams); + hasNext = moreInfo.hasNextPage; + } + return info; + } + }; + + Observable.fromCallable(task) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + if (loadingReactor == null || loadingReactor.isDisposed()) { + loadingReactor = d; + isLoading.set(true); + } else { + d.dispose(); + } + } + + @Override + public void onNext(@NonNull PlayListInfo playListInfo) { + if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + playListInfo + "]"); + if (playListInfo == null || isRemoving() || !isVisible()) return; + + handlePlayListInfo(playListInfo, false, true); + isLoading.set(false); + pageNumber++; + } + + @Override + public void onError(@NonNull Throwable e) { + onRxError(e, "Observer failure"); + } + + @Override + public void onComplete() { + if (loadingReactor != null) { + loadingReactor.dispose(); + loadingReactor = null; + } + } + }); + } private void loadMore(final boolean onlyVideos) { final Callable task = new Callable() { @@ -300,8 +369,8 @@ public class PlaylistFragment extends BaseFragment { .subscribe(new Observer() { @Override public void onSubscribe(@NonNull Disposable d) { - if (disposable == null || disposable.isDisposed()) { - disposable = d; + if (loadingReactor == null || loadingReactor.isDisposed()) { + loadingReactor = d; isLoading.set(true); } else { d.dispose(); @@ -325,9 +394,9 @@ public class PlaylistFragment extends BaseFragment { @Override public void onComplete() { - if (disposable != null) { - disposable.dispose(); - disposable = null; + if (loadingReactor != null) { + loadingReactor.dispose(); + loadingReactor = null; } } }); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/search/PlaylistService.java b/app/src/main/java/org/schabi/newpipe/fragments/search/PlaylistService.java deleted file mode 100644 index b43c7e356..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/search/PlaylistService.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.schabi.newpipe.fragments.search; - -public class PlaylistService { -} diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index be9247569..8659ca645 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -344,13 +344,15 @@ public class BackgroundPlayer extends Service { @Override public void onFastRewind() { - super.onFastRewind(); +// super.onFastRewind(); + simpleExoPlayer.seekTo(0, 0); triggerProgressUpdate(); } @Override public void onFastForward() { - super.onFastForward(); +// super.onFastForward(); + simpleExoPlayer.seekTo(2, 0); triggerProgressUpdate(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index f90352fa1..4d5631bc9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -47,7 +47,9 @@ import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -248,7 +250,12 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O changeState(STATE_LOADING); isPrepared = false; - mediaSource = buildMediaSource(url, format); + + final MediaSource ms = buildMediaSource(url, format); + final DynamicConcatenatingMediaSource dcms = new DynamicConcatenatingMediaSource(); + dcms.addMediaSource(ms); + mediaSource = dcms; + dcms.addMediaSource(new LoopingMediaSource(ms, 2)); if (simpleExoPlayer.getPlaybackState() != Player.STATE_IDLE) simpleExoPlayer.stop(); if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos); diff --git a/app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java new file mode 100644 index 000000000..ee4ef5df4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java @@ -0,0 +1,21 @@ +package org.schabi.newpipe.player; + +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; + +import org.schabi.newpipe.playlist.Playlist; + +import java.util.List; + +public class MediaSourceManager { + + private DynamicConcatenatingMediaSource source; + + private Playlist playlist; + private List sources; + + public MediaSourceManager(Playlist playlist) { + this.source = new DynamicConcatenatingMediaSource(); + this.playlist = playlist; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlaylist.java b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlaylist.java new file mode 100644 index 000000000..9ffc71303 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlaylist.java @@ -0,0 +1,99 @@ +package org.schabi.newpipe.playlist; + +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.playlist.PlayListExtractor; +import org.schabi.newpipe.extractor.playlist.PlayListInfo; +import org.schabi.newpipe.extractor.playlist.PlayListInfoItem; +import org.schabi.newpipe.extractor.stream_info.StreamInfo; +import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; + +public class ExternalPlaylist extends Playlist { + + private AtomicInteger pageNumber; + + private StreamingService service; + + public ExternalPlaylist(final PlayListInfoItem playlist) { + super(); + service = getService(playlist.serviceId); + pageNumber = new AtomicInteger(0); + + load(playlist); + } + + private void load(final PlayListInfoItem playlist) { + final int page = pageNumber.getAndIncrement(); + + final Callable task = new Callable() { + @Override + public PlayListInfo call() throws Exception { + PlayListExtractor extractor = service.getPlayListExtractorInstance(playlist.getLink(), page); + return PlayListInfo.getInfo(extractor); + } + }; + + final Consumer onSuccess = new Consumer() { + @Override + public void accept(PlayListInfo playListInfo) throws Exception { + streams.addAll(extractPlaylistItems(playListInfo)); + changeBroadcast.onNext(streams); + } + }; + + Maybe.fromCallable(task) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .onErrorComplete() + .subscribe(onSuccess); + } + + private List extractPlaylistItems(final PlayListInfo info) { + List result = new ArrayList<>(); + for (final InfoItem stream : info.related_streams) { + if (stream instanceof StreamInfoItem) { + result.add(new PlaylistItem((StreamInfoItem) stream)); + } + } + return result; + } + + @Override + boolean isComplete() { + return false; + } + + @Override + void load(int index) { + while (streams.size() < index) { + pageNumber.incrementAndGet(); + } + } + + @Override + Observable get(int index) { + return null; + } + + private StreamingService getService(final int serviceId) { + try { + return NewPipe.getService(serviceId); + } catch (ExtractionException e) { + return null; + } + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/playlist/Playlist.java b/app/src/main/java/org/schabi/newpipe/playlist/Playlist.java new file mode 100644 index 000000000..02faaa755 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/playlist/Playlist.java @@ -0,0 +1,38 @@ +package org.schabi.newpipe.playlist; + +import android.support.annotation.NonNull; + +import org.schabi.newpipe.extractor.stream_info.StreamInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import io.reactivex.Observable; +import io.reactivex.subjects.PublishSubject; + +public abstract class Playlist { + private final String TAG = "Playlist@" + Integer.toHexString(hashCode()); + + private final int LOAD_BOUND = 2; + + List streams; + PublishSubject> changeBroadcast; + + Playlist() { + streams = Collections.synchronizedList(new ArrayList()); + changeBroadcast = PublishSubject.create(); + } + + @NonNull + public PublishSubject> getChangeBroadcast() { + return changeBroadcast; + } + + abstract boolean isComplete(); + + abstract void load(int index); + + abstract Observable get(int index); +} + diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistAdapter.java b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistAdapter.java new file mode 100644 index 000000000..6f6c924d7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistAdapter.java @@ -0,0 +1,154 @@ +package org.schabi.newpipe.playlist; + +import android.app.Activity; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.info_list.StreamInfoItemHolder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Christian Schabesberger on 01.08.16. + * + * Copyright (C) Christian Schabesberger 2016 + * InfoListAdapter.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class PlaylistAdapter extends RecyclerView.Adapter { + private static final String TAG = PlaylistAdapter.class.toString(); + + private final PlaylistItemBuilder playlistItemBuilder; + private final List playlistItems; + private boolean showFooter = false; + private View header = null; + private View footer = null; + + public class HFHolder extends RecyclerView.ViewHolder { + public HFHolder(View v) { + super(v); + view = v; + } + public View view; + } + + public void showFooter(boolean show) { + showFooter = show; + notifyDataSetChanged(); + } + + public PlaylistAdapter(List data) { + playlistItemBuilder = new PlaylistItemBuilder(); + playlistItems = data; + } + + public void setSelectedListener(PlaylistItemBuilder.OnSelectedListener listener) { + playlistItemBuilder.setOnSelectedListener(listener); + } + + public void addInfoItemList(List data) { + if(data != null) { + playlistItems.addAll(data); + notifyDataSetChanged(); + } + } + + public void addInfoItem(PlaylistItem data) { + if (data != null) { + playlistItems.add(data); + notifyDataSetChanged(); + } + } + + public void clearStreamItemList() { + if(playlistItems.isEmpty()) { + return; + } + playlistItems.clear(); + notifyDataSetChanged(); + } + + public void setHeader(View header) { + this.header = header; + notifyDataSetChanged(); + } + + public void setFooter(View footer) { + this.footer = footer; + notifyDataSetChanged(); + } + + public List getItemsList() { + return playlistItems; + } + + @Override + public int getItemCount() { + int count = playlistItems.size(); + if(header != null) count++; + if(footer != null && showFooter) count++; + return count; + } + + // don't ask why we have to do that this way... it's android accept it -.- + @Override + public int getItemViewType(int position) { + if(header != null && position == 0) { + return 0; + } else if(header != null) { + position--; + } + if(footer != null && position == playlistItems.size() && showFooter) { + return 1; + } + return 2; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) { + switch(type) { + case 0: + return new HFHolder(header); + case 1: + return new HFHolder(footer); + case 2: + return new StreamInfoItemHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.playlist_stream_item, parent, false)); + default: + Log.e(TAG, "Trollolo"); + return null; + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) { + if(holder instanceof PlaylistItemHolder) { + if(header != null) { + i--; + } + playlistItemBuilder.buildStreamInfoItem((PlaylistItemHolder) holder, playlistItems.get(i)); + } else if(holder instanceof HFHolder && i == 0 && header != null) { + ((HFHolder) holder).view = header; + } else if(holder instanceof HFHolder && i == playlistItems.size() && footer != null && showFooter) { + ((HFHolder) holder).view = footer; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItem.java b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItem.java new file mode 100644 index 000000000..cd65b335b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItem.java @@ -0,0 +1,109 @@ +package org.schabi.newpipe.playlist; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.stream_info.StreamExtractor; +import org.schabi.newpipe.extractor.stream_info.StreamInfo; +import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; + +import java.io.Serializable; +import java.util.concurrent.Callable; + +import io.reactivex.Maybe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; + +public class PlaylistItem implements Serializable { + + private String title; + private String url; + private int serviceId; + private int duration; + + private boolean isDone; + private Throwable error; + private Maybe stream; + + public PlaylistItem(final StreamInfoItem streamInfoItem) { + this.title = streamInfoItem.getTitle(); + this.url = streamInfoItem.getLink(); + this.serviceId = streamInfoItem.service_id; + this.duration = streamInfoItem.duration; + + this.isDone = false; + this.stream = getInfo(); + } + + @NonNull + public String getTitle() { + return title; + } + + @NonNull + public String getUrl() { + return url; + } + + public int getServiceId() { + return serviceId; + } + + public int getDuration() { + return duration; + } + + public boolean isDone() { + return isDone; + } + + @Nullable + public Throwable getError() { + return error; + } + + @NonNull + public Maybe getStream() { + return stream; + } + + public void load() { + stream.subscribe(); + } + + @NonNull + private Maybe getInfo() { + final Callable task = new Callable() { + @Override + public StreamInfo call() throws Exception { + final StreamExtractor extractor = NewPipe.getService(serviceId).getExtractorInstance(url); + return StreamInfo.getVideoInfo(extractor); + } + }; + + final Consumer onError = new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + error = throwable; + } + }; + + final Action onComplete = new Action() { + @Override + public void run() throws Exception { + isDone = true; + } + }; + + return Maybe.fromCallable(task) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError(onError) + .onErrorComplete() + .doOnComplete(onComplete) + .cache(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemBuilder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemBuilder.java new file mode 100644 index 000000000..1a74240ce --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemBuilder.java @@ -0,0 +1,116 @@ +package org.schabi.newpipe.playlist; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; + +import org.schabi.newpipe.ImageErrorLoadingListener; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.AbstractStreamInfo; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.playlist.PlayListInfoItem; +import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; +import org.schabi.newpipe.info_list.ChannelInfoItemHolder; +import org.schabi.newpipe.info_list.InfoItemHolder; +import org.schabi.newpipe.info_list.PlaylistInfoItemHolder; +import org.schabi.newpipe.info_list.StreamInfoItemHolder; + +import java.util.Locale; + +/** + * Created by Christian Schabesberger on 26.09.16. + *

+ * Copyright (C) Christian Schabesberger 2016 + * InfoItemBuilder.java is part of NewPipe. + *

+ * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class PlaylistItemBuilder { + + private static final String TAG = PlaylistItemBuilder.class.toString(); + + public interface OnSelectedListener { + void selected(int serviceId, String url, String title); + } + + private OnSelectedListener onStreamInfoItemSelectedListener; + + public PlaylistItemBuilder() {} + + public void setOnSelectedListener(OnSelectedListener listener) { + this.onStreamInfoItemSelectedListener = listener; + } + + public View buildView(ViewGroup parent, final PlaylistItem item) { + final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + final View itemView = inflater.inflate(R.layout.stream_item, parent, false); + final PlaylistItemHolder holder = new PlaylistItemHolder(itemView); + + buildStreamInfoItem(holder, item); + + return itemView; + } + + + public void buildStreamInfoItem(PlaylistItemHolder holder, final PlaylistItem item) { + if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle()); + + if (item.getDuration() > 0) { + holder.itemDurationView.setText(getDurationString(item.getDuration())); + } else { + holder.itemDurationView.setVisibility(View.GONE); + } + + holder.itemRoot.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if(onStreamInfoItemSelectedListener != null) { + onStreamInfoItemSelectedListener.selected(item.getServiceId(), item.getUrl(), item.getTitle()); + } + } + }); + } + + + public static String getDurationString(int duration) { + if(duration < 0) { + duration = 0; + } + String output; + int days = duration / (24 * 60 * 60); /* greater than a day */ + duration %= (24 * 60 * 60); + int hours = duration / (60 * 60); /* greater than an hour */ + duration %= (60 * 60); + int minutes = duration / 60; + int seconds = duration % 60; + + //handle days + if (days > 0) { + output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds); + } else if(hours > 0) { + output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds); + } else { + output = String.format(Locale.US, "%d:%02d", minutes, seconds); + } + return output; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemHolder.java new file mode 100644 index 000000000..d1251c535 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemHolder.java @@ -0,0 +1,43 @@ +package org.schabi.newpipe.playlist; + +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.info_list.InfoItemHolder; + +/** + * Created by Christian Schabesberger on 01.08.16. + *

+ * Copyright (C) Christian Schabesberger 2016 + * StreamInfoItemHolder.java is part of NewPipe. + *

+ * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class PlaylistItemHolder extends RecyclerView.ViewHolder { + + public final TextView itemVideoTitleView, itemDurationView; + public final View itemRoot; + + public PlaylistItemHolder(View v) { + super(v); + itemRoot = v.findViewById(R.id.itemRoot); + itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView); + itemDurationView = (TextView) v.findViewById(R.id.itemDurationView); + } +} diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index e3ef022f9..5b9246f8c 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/playlist_stream_item.xml b/app/src/main/res/layout/playlist_stream_item.xml new file mode 100644 index 000000000..52fac4e31 --- /dev/null +++ b/app/src/main/res/layout/playlist_stream_item.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + \ No newline at end of file From 7c9c3de6449fd00c5fa97e48ac27e2100b1ad64a Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Tue, 29 Aug 2017 08:00:11 -0700 Subject: [PATCH 07/81] -Rename playlist in players to play queue. --- .../org/schabi/newpipe/player/BasePlayer.java | 7 +- ...ourceManager.java => PlaybackManager.java} | 22 ++++-- ...alPlaylist.java => ExternalPlayQueue.java} | 79 ++++++++++++------- .../schabi/newpipe/playlist/PlayQueue.java | 42 ++++++++++ ...listAdapter.java => PlayQueueAdapter.java} | 72 +++++++++++------ .../{PlaylistItem.java => PlayQueueItem.java} | 4 +- ...emHolder.java => PlayQueueItemHolder.java} | 4 +- .../org/schabi/newpipe/playlist/Playlist.java | 38 --------- .../newpipe/playlist/PlaylistItemBuilder.java | 40 +--------- 9 files changed, 164 insertions(+), 144 deletions(-) rename app/src/main/java/org/schabi/newpipe/player/{MediaSourceManager.java => PlaybackManager.java} (50%) rename app/src/main/java/org/schabi/newpipe/playlist/{ExternalPlaylist.java => ExternalPlayQueue.java} (60%) create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java rename app/src/main/java/org/schabi/newpipe/playlist/{PlaylistAdapter.java => PlayQueueAdapter.java} (63%) rename app/src/main/java/org/schabi/newpipe/playlist/{PlaylistItem.java => PlayQueueItem.java} (95%) rename app/src/main/java/org/schabi/newpipe/playlist/{PlaylistItemHolder.java => PlayQueueItemHolder.java} (93%) delete mode 100644 app/src/main/java/org/schabi/newpipe/playlist/Playlist.java diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 4d5631bc9..3746927be 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -250,12 +250,7 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O changeState(STATE_LOADING); isPrepared = false; - - final MediaSource ms = buildMediaSource(url, format); - final DynamicConcatenatingMediaSource dcms = new DynamicConcatenatingMediaSource(); - dcms.addMediaSource(ms); - mediaSource = dcms; - dcms.addMediaSource(new LoopingMediaSource(ms, 2)); + mediaSource = buildMediaSource(url, format); if (simpleExoPlayer.getPlaybackState() != Player.STATE_IDLE) simpleExoPlayer.stop(); if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos); diff --git a/app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java similarity index 50% rename from app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java rename to app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java index ee4ef5df4..8b3973a54 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java @@ -3,19 +3,31 @@ package org.schabi.newpipe.player; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; -import org.schabi.newpipe.playlist.Playlist; +import org.schabi.newpipe.playlist.PlayQueue; import java.util.List; -public class MediaSourceManager { +public class PlaybackManager { private DynamicConcatenatingMediaSource source; - private Playlist playlist; + private PlayQueue playQueue; + private int index; + private List sources; - public MediaSourceManager(Playlist playlist) { + public PlaybackManager(PlayQueue playQueue, int index) { this.source = new DynamicConcatenatingMediaSource(); - this.playlist = playlist; + + this.playQueue = playQueue; + this.index = index; + + + + } + + interface OnChangeListener { + void isLoading(); + void isLoaded(); } } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlaylist.java b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java similarity index 60% rename from app/src/main/java/org/schabi/newpipe/playlist/ExternalPlaylist.java rename to app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java index 9ffc71303..fba48d82d 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlaylist.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java @@ -16,26 +16,61 @@ import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; import io.reactivex.Maybe; -import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; -public class ExternalPlaylist extends Playlist { +public class ExternalPlayQueue extends PlayQueue { + + private final static int LOAD_PROXIMITY = 10; + + private boolean isComplete; private AtomicInteger pageNumber; private StreamingService service; - public ExternalPlaylist(final PlayListInfoItem playlist) { - super(); - service = getService(playlist.serviceId); - pageNumber = new AtomicInteger(0); + private PlayListInfoItem playlist; - load(playlist); + public ExternalPlayQueue(final PlayListInfoItem playlist) { + super(); + this.service = getService(playlist.serviceId); + this.pageNumber = new AtomicInteger(0); + this.playlist = playlist; + + fetch(); } - private void load(final PlayListInfoItem playlist) { + @Override + public boolean isComplete() { + return isComplete; + } + + @Override + public void load(int index, boolean loadNeighbors) { + if (index > streams.size() || streams.get(index) == null) return; + + streams.get(index).load(); + + if (loadNeighbors) { + int leftBound = index - LOAD_BOUND >= 0 ? index - LOAD_BOUND : 0; + int rightBound = index + LOAD_BOUND < streams.size() ? index + LOAD_BOUND : streams.size() - 1; + + for (int i = leftBound; i < rightBound; i++) { + final PlayQueueItem item = streams.get(i); + if (item != null) item.load(); + } + } + } + + @Override + public Maybe get(int index) { + if (index > streams.size() || streams.get(index) == null) return Maybe.empty(); + return streams.get(index).getStream(); + } + + + public synchronized void fetch() { final int page = pageNumber.getAndIncrement(); final Callable task = new Callable() { @@ -49,8 +84,10 @@ public class ExternalPlaylist extends Playlist { final Consumer onSuccess = new Consumer() { @Override public void accept(PlayListInfo playListInfo) throws Exception { + if (!playListInfo.hasNextPage) isComplete = true; + streams.addAll(extractPlaylistItems(playListInfo)); - changeBroadcast.onNext(streams); + notifyChange(); } }; @@ -61,33 +98,16 @@ public class ExternalPlaylist extends Playlist { .subscribe(onSuccess); } - private List extractPlaylistItems(final PlayListInfo info) { - List result = new ArrayList<>(); + private List extractPlaylistItems(final PlayListInfo info) { + List result = new ArrayList<>(); for (final InfoItem stream : info.related_streams) { if (stream instanceof StreamInfoItem) { - result.add(new PlaylistItem((StreamInfoItem) stream)); + result.add(new PlayQueueItem((StreamInfoItem) stream)); } } return result; } - @Override - boolean isComplete() { - return false; - } - - @Override - void load(int index) { - while (streams.size() < index) { - pageNumber.incrementAndGet(); - } - } - - @Override - Observable get(int index) { - return null; - } - private StreamingService getService(final int serviceId) { try { return NewPipe.getService(serviceId); @@ -95,5 +115,4 @@ public class ExternalPlaylist extends Playlist { return null; } } - } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java new file mode 100644 index 000000000..ecf9e578f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -0,0 +1,42 @@ +package org.schabi.newpipe.playlist; + +import android.support.annotation.NonNull; + +import org.schabi.newpipe.extractor.stream_info.StreamInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import io.reactivex.Maybe; +import io.reactivex.subjects.BehaviorSubject; + +public abstract class PlayQueue { + private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode()); + + final int LOAD_BOUND = 2; + + protected List streams; + private BehaviorSubject> changeBroadcast; + + PlayQueue() { + streams = Collections.synchronizedList(new ArrayList()); + changeBroadcast = BehaviorSubject.create(); + } + + @NonNull + public List getStreams() { + return streams; + } + + public void notifyChange() { + changeBroadcast.onNext(streams); + } + + public abstract boolean isComplete(); + + public abstract void load(int index, boolean loadNeighbors); + + public abstract Maybe get(int index); +} + diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistAdapter.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java similarity index 63% rename from app/src/main/java/org/schabi/newpipe/playlist/PlaylistAdapter.java rename to app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java index 6f6c924d7..662f9f2f1 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.playlist; -import android.app.Activity; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; @@ -10,7 +9,6 @@ import android.view.ViewGroup; import org.schabi.newpipe.R; import org.schabi.newpipe.info_list.StreamInfoItemHolder; -import java.util.ArrayList; import java.util.List; /** @@ -33,11 +31,11 @@ import java.util.List; * along with NewPipe. If not, see . */ -public class PlaylistAdapter extends RecyclerView.Adapter { - private static final String TAG = PlaylistAdapter.class.toString(); +public class PlayQueueAdapter extends RecyclerView.Adapter { + private static final String TAG = PlayQueueAdapter.class.toString(); private final PlaylistItemBuilder playlistItemBuilder; - private final List playlistItems; + private final PlayQueue playQueue; private boolean showFooter = false; private View header = null; private View footer = null; @@ -55,34 +53,60 @@ public class PlaylistAdapter extends RecyclerView.Adapter data) { - playlistItemBuilder = new PlaylistItemBuilder(); - playlistItems = data; + public PlayQueueAdapter(PlayQueue playQueue) { + this.playlistItemBuilder = new PlaylistItemBuilder(); + this.playQueue = playQueue; } public void setSelectedListener(PlaylistItemBuilder.OnSelectedListener listener) { playlistItemBuilder.setOnSelectedListener(listener); } - public void addInfoItemList(List data) { + public void addItems(List data) { if(data != null) { - playlistItems.addAll(data); - notifyDataSetChanged(); + playQueue.getStreams().addAll(data); + notifyPlaylistChange(); } } - public void addInfoItem(PlaylistItem data) { + public void addItem(PlayQueueItem data) { if (data != null) { - playlistItems.add(data); - notifyDataSetChanged(); + playQueue.getStreams().add(data); + notifyPlaylistChange(); } } - public void clearStreamItemList() { - if(playlistItems.isEmpty()) { + public void removeItem(int index) { + if (index < playQueue.getStreams().size()) { + playQueue.getStreams().remove(index); + notifyPlaylistChange(); + } + } + + public void swapItems(int source, int target) { + final List items = playQueue.getStreams(); + if (source < items.size() && target < items.size()) { + final PlayQueueItem sourceItem = items.get(source); + final PlayQueueItem targetItem = items.get(target); + + items.set(target, sourceItem); + items.set(source, targetItem); + + notifyPlaylistChange(); + } + } + + public void clear() { + if(playQueue.getStreams().isEmpty()) { return; } - playlistItems.clear(); + playQueue.getStreams().clear(); + + notifyPlaylistChange(); + } + + private void notifyPlaylistChange() { + playQueue.notifyChange(); notifyDataSetChanged(); } @@ -96,13 +120,13 @@ public class PlaylistAdapter extends RecyclerView.Adapter getItemsList() { - return playlistItems; + public List getItems() { + return playQueue.getStreams(); } @Override public int getItemCount() { - int count = playlistItems.size(); + int count = playQueue.getStreams().size(); if(header != null) count++; if(footer != null && showFooter) count++; return count; @@ -116,7 +140,7 @@ public class PlaylistAdapter extends RecyclerView.Adapter stream; - public PlaylistItem(final StreamInfoItem streamInfoItem) { + public PlayQueueItem(final StreamInfoItem streamInfoItem) { this.title = streamInfoItem.getTitle(); this.url = streamInfoItem.getLink(); this.serviceId = streamInfoItem.service_id; diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java similarity index 93% rename from app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemHolder.java rename to app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java index d1251c535..c25210ee2 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java @@ -29,12 +29,12 @@ import org.schabi.newpipe.info_list.InfoItemHolder; * along with NewPipe. If not, see . */ -public class PlaylistItemHolder extends RecyclerView.ViewHolder { +public class PlayQueueItemHolder extends RecyclerView.ViewHolder { public final TextView itemVideoTitleView, itemDurationView; public final View itemRoot; - public PlaylistItemHolder(View v) { + public PlayQueueItemHolder(View v) { super(v); itemRoot = v.findViewById(R.id.itemRoot); itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView); diff --git a/app/src/main/java/org/schabi/newpipe/playlist/Playlist.java b/app/src/main/java/org/schabi/newpipe/playlist/Playlist.java deleted file mode 100644 index 02faaa755..000000000 --- a/app/src/main/java/org/schabi/newpipe/playlist/Playlist.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.schabi.newpipe.playlist; - -import android.support.annotation.NonNull; - -import org.schabi.newpipe.extractor.stream_info.StreamInfo; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import io.reactivex.Observable; -import io.reactivex.subjects.PublishSubject; - -public abstract class Playlist { - private final String TAG = "Playlist@" + Integer.toHexString(hashCode()); - - private final int LOAD_BOUND = 2; - - List streams; - PublishSubject> changeBroadcast; - - Playlist() { - streams = Collections.synchronizedList(new ArrayList()); - changeBroadcast = PublishSubject.create(); - } - - @NonNull - public PublishSubject> getChangeBroadcast() { - return changeBroadcast; - } - - abstract boolean isComplete(); - - abstract void load(int index); - - abstract Observable get(int index); -} - diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemBuilder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemBuilder.java index 1a74240ce..829f502ab 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistItemBuilder.java @@ -1,48 +1,14 @@ package org.schabi.newpipe.playlist; -import android.content.Context; import android.text.TextUtils; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.ImageLoader; - -import org.schabi.newpipe.ImageErrorLoadingListener; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.AbstractStreamInfo; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.channel.ChannelInfoItem; -import org.schabi.newpipe.extractor.playlist.PlayListInfoItem; -import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; -import org.schabi.newpipe.info_list.ChannelInfoItemHolder; -import org.schabi.newpipe.info_list.InfoItemHolder; -import org.schabi.newpipe.info_list.PlaylistInfoItemHolder; -import org.schabi.newpipe.info_list.StreamInfoItemHolder; import java.util.Locale; -/** - * Created by Christian Schabesberger on 26.09.16. - *

- * Copyright (C) Christian Schabesberger 2016 - * InfoItemBuilder.java is part of NewPipe. - *

- * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - *

- * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ public class PlaylistItemBuilder { @@ -60,10 +26,10 @@ public class PlaylistItemBuilder { this.onStreamInfoItemSelectedListener = listener; } - public View buildView(ViewGroup parent, final PlaylistItem item) { + public View buildView(ViewGroup parent, final PlayQueueItem item) { final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); final View itemView = inflater.inflate(R.layout.stream_item, parent, false); - final PlaylistItemHolder holder = new PlaylistItemHolder(itemView); + final PlayQueueItemHolder holder = new PlayQueueItemHolder(itemView); buildStreamInfoItem(holder, item); @@ -71,7 +37,7 @@ public class PlaylistItemBuilder { } - public void buildStreamInfoItem(PlaylistItemHolder holder, final PlaylistItem item) { + public void buildStreamInfoItem(PlayQueueItemHolder holder, final PlayQueueItem item) { if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle()); if (item.getDuration() > 0) { From dcdcf17f5e557149eaddb731e19a717b1624630f Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Thu, 31 Aug 2017 10:07:18 -0700 Subject: [PATCH 08/81] -Added on change event bus to Play Queue. -Added playback manager for player interaction. --- .../org/schabi/newpipe/player/BasePlayer.java | 30 ++- .../newpipe/player/PlaybackManager.java | 195 ++++++++++++++++-- .../newpipe/playlist/ExternalPlayQueue.java | 79 +++---- .../schabi/newpipe/playlist/PlayQueue.java | 117 ++++++++++- .../newpipe/playlist/PlayQueueAdapter.java | 82 ++++---- .../newpipe/playlist/PlayQueueEvent.java | 27 +++ .../newpipe/playlist/PlayQueueItem.java | 3 +- ...Builder.java => PlayQueueItemBuilder.java} | 8 +- ...st_stream_item.xml => play_queue_item.xml} | 0 9 files changed, 419 insertions(+), 122 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/PlayQueueEvent.java rename app/src/main/java/org/schabi/newpipe/playlist/{PlaylistItemBuilder.java => PlayQueueItemBuilder.java} (91%) rename app/src/main/res/layout/{playlist_stream_item.xml => play_queue_item.xml} (100%) diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 3746927be..6e68253f1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -47,9 +47,7 @@ import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -72,6 +70,7 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; +import org.schabi.newpipe.playlist.PlayQueue; import java.io.File; import java.text.DecimalFormat; @@ -86,9 +85,9 @@ import java.util.concurrent.atomic.AtomicBoolean; * @author mauriciocolli */ @SuppressWarnings({"WeakerAccess", "unused"}) -public abstract class BasePlayer implements Player.EventListener, AudioManager.OnAudioFocusChangeListener { +public abstract class BasePlayer implements Player.EventListener, + AudioManager.OnAudioFocusChangeListener, PlaybackManager.PlaybackListener { // TODO: Check api version for deprecated audio manager methods - public static final boolean DEBUG = false; public static final String TAG = "BasePlayer"; @@ -117,6 +116,13 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O protected long videoStartPos = -1; protected String uploaderName = ""; + /*////////////////////////////////////////////////////////////////////////// + // Playlist + //////////////////////////////////////////////////////////////////////////*/ + + protected PlaybackManager playbackManager; + protected PlayQueue playQueue; + /*////////////////////////////////////////////////////////////////////////// // Player //////////////////////////////////////////////////////////////////////////*/ @@ -540,6 +546,22 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O @Override public void onPositionDiscontinuity() { + int newIndex = simpleExoPlayer.getCurrentWindowIndex(); + + } + + /*////////////////////////////////////////////////////////////////////////// + // Playback Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void block() { + if (currentState != STATE_LOADING) changeState(STATE_LOADING); + } + + @Override + public void unblock() { + if (currentState != STATE_PLAYING) changeState(STATE_PLAYING); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java b/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java index 8b3973a54..3873d7c1c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java @@ -3,31 +3,200 @@ package org.schabi.newpipe.player; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlayQueueEvent; +import org.schabi.newpipe.playlist.PlayQueueItem; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import io.reactivex.Maybe; +import io.reactivex.annotations.NonNull; + public class PlaybackManager { - private DynamicConcatenatingMediaSource source; + private DynamicConcatenatingMediaSource mediaSource; + private List queueSource; + private int sourceIndex; + private PlaybackListener listener; private PlayQueue playQueue; - private int index; - - private List sources; - - public PlaybackManager(PlayQueue playQueue, int index) { - this.source = new DynamicConcatenatingMediaSource(); - - this.playQueue = playQueue; - this.index = index; + private Subscription playQueueReactor; + interface PlaybackListener { + void block(); + void unblock(); + void sync(); + MediaSource sourceOf(StreamInfo info); } - interface OnChangeListener { - void isLoading(); - void isLoaded(); + public PlaybackManager(@NonNull final PlaybackListener listener, + @NonNull final PlayQueue playQueue) { + this.mediaSource = new DynamicConcatenatingMediaSource(); + this.queueSource = Collections.synchronizedList(new ArrayList(10)); + this.sourceIndex = 0; + + this.listener = listener; + this.playQueue = playQueue; + + playQueue.getPlayQueueFlowable().subscribe(getReactor()); + } + + @NonNull + public DynamicConcatenatingMediaSource getMediaSource() { + return mediaSource; + } + + private void reload() { + listener.block(); + load(0); + } + + public void refreshMedia(final int newMediaIndex) { + if (newMediaIndex == sourceIndex) return; + + if (newMediaIndex == sourceIndex + 1) { + playQueue.incrementIndex(); + mediaSource.removeMediaSource(0); + queueSource.remove(0); + } else { + //something went wrong + onInit(); + } + } + + private void removeCurrent() { + listener.block(); + mediaSource.removeMediaSource(0); + queueSource.remove(0); + listener.unblock(); + } + + private Subscription loaderReactor; + + private void load() { + if (mediaSource.getSize() < 5 && queueSource.size() < 5) load(mediaSource.getSize()); + } + + private void load(final int from) { + clear(from); + + if (loaderReactor != null) loaderReactor.cancel(); + + List> maybes = new ArrayList<>(); + for (int i = from; i < 5; i++) { + final int index = playQueue.getIndex() + i; + final PlayQueueItem item = playQueue.get(index); + queueSource.set(i, item); + maybes.add(item.getStream()); + } + + Maybe.concat(maybes).subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + loaderReactor = s; + } + + @Override + public void onNext(StreamInfo streamInfo) { + mediaSource.addMediaSource(listener.sourceOf(streamInfo)); + onLoaded(); + } + + @Override + public void onError(Throwable t) { + playQueue.remove(queueSource.size()); + } + + @Override + public void onComplete() { + } + }); + } + + private void onLoaded() { + if (mediaSource.getSize() > 0 && queueSource.size() > 0) listener.unblock(); + } + + private void onInit() { + listener.block(); + load(); + } + + private void clear(int from) { + listener.block(); + while (mediaSource.getSize() > from) { + queueSource.remove(from); + mediaSource.removeMediaSource(from); + } + listener.unblock(); + } + + private Subscriber getReactor() { + return new Subscriber() { + @Override + public void onSubscribe(@NonNull Subscription d) { + if (playQueueReactor != null) playQueueReactor.cancel(); + playQueueReactor = d; + playQueueReactor.request(1); + } + + @Override + public void onNext(@NonNull PlayQueueEvent event) { + if (playQueue.getStreams().size() - playQueue.getIndex() < 10 && !playQueue.isComplete()) { + listener.block(); + playQueue.fetch(); + } + + switch (event) { + case INIT: + onInit(); + break; + case APPEND: + load(); + break; + case REMOVE_CURRENT: + removeCurrent(); + load(); + break; + case SELECT: + reload(); + break; + case REMOVE: + case SWAP: + load(1); + break; + case CLEAR: + clear(0); + break; + case NEXT: + default: + break; + } + + onLoaded(); + if (playQueueReactor != null) playQueueReactor.request(1); + } + + @Override + public void onError(@NonNull Throwable e) { + + } + + @Override + public void onComplete() { + // Never completes, only canceled + } + }; + } + + public void dispose() { + if (playQueueReactor != null) playQueueReactor.cancel(); } } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java index fba48d82d..3d1831a0e 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java @@ -1,44 +1,45 @@ package org.schabi.newpipe.playlist; import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlayListExtractor; import org.schabi.newpipe.extractor.playlist.PlayListInfo; -import org.schabi.newpipe.extractor.playlist.PlayListInfoItem; import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; public class ExternalPlayQueue extends PlayQueue { - - private final static int LOAD_PROXIMITY = 10; + private final String TAG = "ExternalPlayQueue@" + Integer.toHexString(hashCode()); private boolean isComplete; - private AtomicInteger pageNumber; - private StreamingService service; + private String playlistUrl; - private PlayListInfoItem playlist; + private AtomicInteger pageNumber; + private Disposable fetchReactor; - public ExternalPlayQueue(final PlayListInfoItem playlist) { - super(); - this.service = getService(playlist.serviceId); - this.pageNumber = new AtomicInteger(0); - this.playlist = playlist; + public ExternalPlayQueue(final String playlistUrl, + final PlayListInfo info, + final int nextPage, + final int index) { + super(index); + this.service = getService(info.service_id); + this.pageNumber = new AtomicInteger(nextPage); + this.playlistUrl = playlistUrl; - fetch(); + getStreams().addAll(extractPlaylistItems(info)); } @Override @@ -47,36 +48,25 @@ public class ExternalPlayQueue extends PlayQueue { } @Override - public void load(int index, boolean loadNeighbors) { - if (index > streams.size() || streams.get(index) == null) return; - - streams.get(index).load(); - - if (loadNeighbors) { - int leftBound = index - LOAD_BOUND >= 0 ? index - LOAD_BOUND : 0; - int rightBound = index + LOAD_BOUND < streams.size() ? index + LOAD_BOUND : streams.size() - 1; - - for (int i = leftBound; i < rightBound; i++) { - final PlayQueueItem item = streams.get(i); - if (item != null) item.load(); - } - } + public void load(int index) { + if (index > getStreams().size() || getStreams().get(index) == null) return; + getStreams().get(index).load(); } @Override - public Maybe get(int index) { - if (index > streams.size() || streams.get(index) == null) return Maybe.empty(); - return streams.get(index).getStream(); + public PlayQueueItem get(int index) { + if (index > getStreams().size() || getStreams().get(index) == null) return null; + return getStreams().get(index); } - - public synchronized void fetch() { - final int page = pageNumber.getAndIncrement(); + @Override + public void fetch() { + if (fetchReactor != null && !fetchReactor.isDisposed()) return; final Callable task = new Callable() { @Override public PlayListInfo call() throws Exception { - PlayListExtractor extractor = service.getPlayListExtractorInstance(playlist.getLink(), page); + PlayListExtractor extractor = service.getPlayListExtractorInstance(playlistUrl, pageNumber.get()); return PlayListInfo.getInfo(extractor); } }; @@ -86,18 +76,23 @@ public class ExternalPlayQueue extends PlayQueue { public void accept(PlayListInfo playListInfo) throws Exception { if (!playListInfo.hasNextPage) isComplete = true; - streams.addAll(extractPlaylistItems(playListInfo)); - notifyChange(); + append(extractPlaylistItems(playListInfo)); + pageNumber.incrementAndGet(); } }; - Maybe.fromCallable(task) + fetchReactor = Maybe.fromCallable(task) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .onErrorComplete() .subscribe(onSuccess); } + @Override + public void dispose() { + if (fetchReactor != null) fetchReactor.dispose(); + } + private List extractPlaylistItems(final PlayListInfo info) { List result = new ArrayList<>(); for (final InfoItem stream : info.related_streams) { @@ -107,12 +102,4 @@ public class ExternalPlayQueue extends PlayQueue { } return result; } - - private StreamingService getService(final int serviceId) { - try { - return NewPipe.getService(serviceId); - } catch (ExtractionException e) { - return null; - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java index ecf9e578f..87e21cfee 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -2,41 +2,138 @@ package org.schabi.newpipe.playlist; import android.support.annotation.NonNull; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream_info.StreamInfo; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import io.reactivex.BackpressureStrategy; +import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.subjects.BehaviorSubject; public abstract class PlayQueue { private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode()); - final int LOAD_BOUND = 2; + private List streams; + private AtomicInteger queueIndex; - protected List streams; - private BehaviorSubject> changeBroadcast; + private BehaviorSubject changeBroadcast; + private Flowable playQueueFlowable; - PlayQueue() { + PlayQueue(final int index) { streams = Collections.synchronizedList(new ArrayList()); + queueIndex = new AtomicInteger(index); + changeBroadcast = BehaviorSubject.create(); + playQueueFlowable = changeBroadcast.startWith(PlayQueueEvent.INIT).toFlowable(BackpressureStrategy.BUFFER); + } + + // a queue is complete if it has loaded all items in an external playlist + // single stream or local queues are always complete + public abstract boolean isComplete(); + + // load in the background the item at index, may do nothing if the queue is incomplete + public abstract void load(int index); + + // load partial queue in the background, does nothing if the queue is complete + public abstract void fetch(); + + // returns a Rx Future to the stream info of the play queue item at index + // may return an empty of the queue is incomplete + public abstract PlayQueueItem get(int index); + + public abstract void dispose(); + + public int size() { + return streams.size(); } @NonNull public List getStreams() { - return streams; + return Collections.unmodifiableList(streams); } - public void notifyChange() { - changeBroadcast.onNext(streams); + @NonNull + public Flowable getPlayQueueFlowable() { + return playQueueFlowable; } - public abstract boolean isComplete(); + private void broadcast(final PlayQueueEvent event) { + changeBroadcast.onNext(event); + } - public abstract void load(int index, boolean loadNeighbors); + public int getIndex() { + return queueIndex.get(); + } - public abstract Maybe get(int index); + public void setIndex(final int index) { + queueIndex.set(index); + broadcast(PlayQueueEvent.SELECT); + } + + public void incrementIndex() { + queueIndex.incrementAndGet(); + broadcast(PlayQueueEvent.NEXT); + } + + protected void append(final PlayQueueItem item) { + streams.add(item); + broadcast(PlayQueueEvent.APPEND); + } + + protected void append(final Collection items) { + streams.addAll(items); + broadcast(PlayQueueEvent.APPEND); + } + + public void remove(final int index) { + if (index < streams.size()) { + streams.remove(index); + broadcast(PlayQueueEvent.REMOVE); + } + } + + protected void clear() { + if (!streams.isEmpty()) { + streams.clear(); + broadcast(PlayQueueEvent.CLEAR); + } + } + + protected void swap(final int source, final int target) { + final List items = streams; + if (source < items.size() && target < items.size()) { + // Swap two items + final PlayQueueItem sourceItem = items.get(source); + final PlayQueueItem targetItem = items.get(target); + + items.set(target, sourceItem); + items.set(source, targetItem); + + // If the current playing index is one of the swapped indices, change that as well + final int index = queueIndex.get(); + if (index == source || index == target) { + final int newIndex = index == source ? target : source; + queueIndex.set(newIndex); + } + + broadcast(PlayQueueEvent.SWAP); + } + } + + protected StreamingService getService(final int serviceId) { + try { + return NewPipe.getService(serviceId); + } catch (ExtractionException e) { + return null; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java index 662f9f2f1..170311f7d 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java @@ -11,6 +11,9 @@ import org.schabi.newpipe.info_list.StreamInfoItemHolder; import java.util.List; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; + /** * Created by Christian Schabesberger on 01.08.16. * @@ -34,12 +37,14 @@ import java.util.List; public class PlayQueueAdapter extends RecyclerView.Adapter { private static final String TAG = PlayQueueAdapter.class.toString(); - private final PlaylistItemBuilder playlistItemBuilder; + private final PlayQueueItemBuilder playQueueItemBuilder; private final PlayQueue playQueue; private boolean showFooter = false; private View header = null; private View footer = null; + private Disposable playQueueReactor; + public class HFHolder extends RecyclerView.ViewHolder { public HFHolder(View v) { super(v); @@ -48,66 +53,57 @@ public class PlayQueueAdapter extends RecyclerView.Adapter data) { - if(data != null) { - playQueue.getStreams().addAll(data); - notifyPlaylistChange(); - } + public void add(final List data) { + playQueue.append(data); } - public void addItem(PlayQueueItem data) { - if (data != null) { - playQueue.getStreams().add(data); - notifyPlaylistChange(); - } + public void add(final PlayQueueItem data) { + playQueue.append(data); } - public void removeItem(int index) { - if (index < playQueue.getStreams().size()) { - playQueue.getStreams().remove(index); - notifyPlaylistChange(); - } + public void remove(final int index) { + playQueue.remove(index); } - public void swapItems(int source, int target) { - final List items = playQueue.getStreams(); - if (source < items.size() && target < items.size()) { - final PlayQueueItem sourceItem = items.get(source); - final PlayQueueItem targetItem = items.get(target); - - items.set(target, sourceItem); - items.set(source, targetItem); - - notifyPlaylistChange(); - } + public void swap(final int source, final int target) { + playQueue.swap(source, target); } public void clear() { - if(playQueue.getStreams().isEmpty()) { - return; - } - playQueue.getStreams().clear(); - - notifyPlaylistChange(); + playQueue.clear(); } - private void notifyPlaylistChange() { - playQueue.notifyChange(); - notifyDataSetChanged(); + private Disposable getReactor() { + final Consumer onNext = new Consumer() { + @Override + public void accept(PlayQueueEvent playQueueEvent) throws Exception { + notifyDataSetChanged(); + } + }; + + return playQueue.getPlayQueueFlowable() + .toObservable() + .subscribe(onNext); + } + + public void dispose() { + if (playQueueReactor != null) playQueueReactor.dispose(); } public void setHeader(View header) { @@ -155,7 +151,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter Date: Thu, 31 Aug 2017 17:47:56 -0700 Subject: [PATCH 09/81] -Added separate events for play queue index removal. --- .../java/org/schabi/newpipe/player/BasePlayer.java | 12 ++++++++++++ .../org/schabi/newpipe/player/PlaybackManager.java | 14 +++++++++----- .../org/schabi/newpipe/playlist/PlayQueue.java | 12 ++++++++---- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 6e68253f1..df588df5d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -70,6 +70,7 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.playlist.PlayQueue; import java.io.File; @@ -564,6 +565,17 @@ public abstract class BasePlayer implements Player.EventListener, if (currentState != STATE_PLAYING) changeState(STATE_PLAYING); } + @Override + public void sync(final StreamInfo info) { + + } + + @Override + public MediaSource sourceOf(final StreamInfo info) { + + return null; + } + /*////////////////////////////////////////////////////////////////////////// // General Player //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java b/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java index 3873d7c1c..72f1daede 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java @@ -31,9 +31,9 @@ public class PlaybackManager { interface PlaybackListener { void block(); void unblock(); - void sync(); - MediaSource sourceOf(StreamInfo info); + void sync(final StreamInfo info); + MediaSource sourceOf(final StreamInfo info); } public PlaybackManager(@NonNull final PlaybackListener listener, @@ -58,6 +58,10 @@ public class PlaybackManager { load(0); } + public void changeSource(final int index) { + + } + public void refreshMedia(final int newMediaIndex) { if (newMediaIndex == sourceIndex) return; @@ -67,7 +71,7 @@ public class PlaybackManager { queueSource.remove(0); } else { //something went wrong - onInit(); + init(); } } @@ -124,7 +128,7 @@ public class PlaybackManager { if (mediaSource.getSize() > 0 && queueSource.size() > 0) listener.unblock(); } - private void onInit() { + private void init() { listener.block(); load(); } @@ -156,7 +160,7 @@ public class PlaybackManager { switch (event) { case INIT: - onInit(); + init(); break; case APPEND: load(); diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java index 87e21cfee..67adc0cf2 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -5,7 +5,6 @@ import android.support.annotation.NonNull; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; import java.util.ArrayList; import java.util.Collection; @@ -15,7 +14,6 @@ import java.util.concurrent.atomic.AtomicInteger; import io.reactivex.BackpressureStrategy; import io.reactivex.Flowable; -import io.reactivex.Maybe; import io.reactivex.subjects.BehaviorSubject; public abstract class PlayQueue { @@ -94,8 +92,14 @@ public abstract class PlayQueue { } public void remove(final int index) { - if (index < streams.size()) { - streams.remove(index); + if (index >= streams.size()) return; + final boolean isCurrent = index == queueIndex.get(); + + streams.remove(index); + + if (isCurrent) { + broadcast(PlayQueueEvent.REMOVE_CURRENT); + } else { broadcast(PlayQueueEvent.REMOVE); } } From b85982301163f9e389ae56c371f60b1d2201e1a2 Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Fri, 1 Sep 2017 12:10:36 -0700 Subject: [PATCH 10/81] -Hooking playback manager and play queue into main video player. --- .../fragments/playlist/PlaylistFragment.java | 34 +++++++++++- .../org/schabi/newpipe/player/BasePlayer.java | 8 +-- .../newpipe/player/MainVideoPlayer.java | 12 +++- .../newpipe/player/PlaybackManager.java | 55 ++++++++++++++----- .../schabi/newpipe/player/VideoPlayer.java | 38 ++++++++++++- .../newpipe/playlist/ExternalPlayQueue.java | 13 +---- .../schabi/newpipe/playlist/PlayQueue.java | 11 ++-- .../newpipe/playlist/PlayQueueItem.java | 5 -- app/src/main/res/layout/playlist_header.xml | 15 +++++ 9 files changed, 147 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java index 032b227e8..dd58b6567 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.fragments.playlist; +import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -17,6 +18,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; @@ -29,14 +31,19 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlayListExtractor; import org.schabi.newpipe.extractor.playlist.PlayListInfo; +import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.fragments.BaseFragment; import org.schabi.newpipe.fragments.search.OnScrollBelowItemsListener; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.MainVideoPlayer; +import org.schabi.newpipe.player.VideoPlayer; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.Utils; import java.io.IOException; import java.io.Serializable; @@ -78,6 +85,7 @@ public class PlaylistFragment extends BaseFragment { private ImageView headerBannerView; private ImageView headerAvatarView; private TextView headerTitleView; + private Button headerPlayAllButton; /*////////////////////////////////////////////////////////////////////////*/ // Reactors @@ -95,6 +103,15 @@ public class PlaylistFragment extends BaseFragment { return instance; } + public void play(Context context, Class targetClazz) { + Intent mIntent = new Intent(context, targetClazz) + .putExtra("url", playlistUrl) + .putExtra("nextPage", 1) + .putExtra("index", 0) + .putExtra("stream", currentPlaylistInfo); + startActivity(mIntent); + } + /*////////////////////////////////////////////////////////////////////////// // Fragment's LifeCycle //////////////////////////////////////////////////////////////////////////*/ @@ -246,6 +263,9 @@ public class PlaylistFragment extends BaseFragment { headerBannerView = headerRootLayout.findViewById(R.id.playlist_banner_image); headerAvatarView = headerRootLayout.findViewById(R.id.playlist_avatar_view); headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); + + headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_play_all_button); + headerPlayAllButton.setVisibility(View.VISIBLE); } protected void initListeners() { @@ -266,6 +286,13 @@ public class PlaylistFragment extends BaseFragment { loadMore(true); } }); + + headerPlayAllButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + play(activity, MainVideoPlayer.class); + } + }); } @@ -434,7 +461,7 @@ public class PlaylistFragment extends BaseFragment { } private void handlePlayListInfo(PlayListInfo info, boolean onlyVideos, boolean addVideos) { - currentPlaylistInfo = info; + if (currentPlaylistInfo == null) currentPlaylistInfo = info; animateView(errorPanel, false, 300); animateView(playlistStreams, true, 200); @@ -468,7 +495,10 @@ public class PlaylistFragment extends BaseFragment { if (!hasNextPage) infoListAdapter.showFooter(false); //if (!listRestored) { - if (addVideos) infoListAdapter.addInfoItemList(info.related_streams); + if (addVideos) { + infoListAdapter.addInfoItemList(info.related_streams); + currentPlaylistInfo.related_streams.addAll(info.related_streams); + } //} } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index df588df5d..17c2f0b5a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -72,6 +72,7 @@ import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.util.Utils; import java.io.File; import java.text.DecimalFormat; @@ -257,7 +258,6 @@ public abstract class BasePlayer implements Player.EventListener, changeState(STATE_LOADING); isPrepared = false; - mediaSource = buildMediaSource(url, format); if (simpleExoPlayer.getPlaybackState() != Player.STATE_IDLE) simpleExoPlayer.stop(); if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos); @@ -548,7 +548,7 @@ public abstract class BasePlayer implements Player.EventListener, @Override public void onPositionDiscontinuity() { int newIndex = simpleExoPlayer.getCurrentWindowIndex(); - + playbackManager.refreshMedia(newIndex); } /*////////////////////////////////////////////////////////////////////////// @@ -567,12 +567,12 @@ public abstract class BasePlayer implements Player.EventListener, @Override public void sync(final StreamInfo info) { - + videoTitle = info.title; + channelName = info.uploader; } @Override public MediaSource sourceOf(final StreamInfo info) { - return null; } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 301200dfc..ebb5b52aa 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -454,9 +454,15 @@ public class MainVideoPlayer extends Activity { @Override public boolean onDoubleTap(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); - if (!playerImpl.isPlaying()) return false; - if (e.getX() > playerImpl.getRootView().getWidth() / 2) playerImpl.onFastForward(); - else playerImpl.onFastRewind(); + //if (!playerImpl.isPlaying()) return false; + + if (e.getX() > playerImpl.getRootView().getWidth() / 2) + playerImpl.playQueue.setIndex(playerImpl.playQueue.getIndex() + 1); + //playerImpl.onFastForward(); + else + playerImpl.playQueue.setIndex(playerImpl.playQueue.getIndex() - 1); + //playerImpl.onFastRewind(); + return true; } diff --git a/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java b/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java index 72f1daede..a98d9d3a1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java @@ -15,10 +15,14 @@ import java.util.Collections; import java.util.List; import io.reactivex.Maybe; +import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; +import io.reactivex.schedulers.Schedulers; public class PlaybackManager { + private static final int WINDOW_SIZE = 5; + private DynamicConcatenatingMediaSource mediaSource; private List queueSource; private int sourceIndex; @@ -58,8 +62,11 @@ public class PlaybackManager { load(0); } - public void changeSource(final int index) { - + public void changeSource(final MediaSource newSource) { + listener.block(); + this.mediaSource.removeMediaSource(0); + this.mediaSource.addMediaSource(0, newSource); + listener.unblock(); } public void refreshMedia(final int newMediaIndex) { @@ -71,7 +78,7 @@ public class PlaybackManager { queueSource.remove(0); } else { //something went wrong - init(); + reload(); } } @@ -85,7 +92,8 @@ public class PlaybackManager { private Subscription loaderReactor; private void load() { - if (mediaSource.getSize() < 5 && queueSource.size() < 5) load(mediaSource.getSize()); + if (mediaSource.getSize() < WINDOW_SIZE && queueSource.size() < WINDOW_SIZE) + load(mediaSource.getSize()); } private void load(final int from) { @@ -94,23 +102,33 @@ public class PlaybackManager { if (loaderReactor != null) loaderReactor.cancel(); List> maybes = new ArrayList<>(); - for (int i = from; i < 5; i++) { + for (int i = from; i < WINDOW_SIZE; i++) { final int index = playQueue.getIndex() + i; final PlayQueueItem item = playQueue.get(index); - queueSource.set(i, item); + + if (queueSource.size() > i) queueSource.set(i, item); + else queueSource.add(item); + maybes.add(item.getStream()); } - Maybe.concat(maybes).subscribe(new Subscriber() { + Maybe.concat(maybes).subscribe(getSubscriber()); + } + + private Subscriber getSubscriber() { + return new Subscriber() { @Override public void onSubscribe(Subscription s) { + if (loaderReactor != null) loaderReactor.cancel(); loaderReactor = s; + s.request(1); } @Override public void onNext(StreamInfo streamInfo) { mediaSource.addMediaSource(listener.sourceOf(streamInfo)); - onLoaded(); + tryUnblock(); + loaderReactor.request(1); } @Override @@ -120,11 +138,13 @@ public class PlaybackManager { @Override public void onComplete() { + if (loaderReactor != null) loaderReactor.cancel(); + loaderReactor = null; } - }); + }; } - private void onLoaded() { + private void tryUnblock() { if (mediaSource.getSize() > 0 && queueSource.size() > 0) listener.unblock(); } @@ -134,11 +154,15 @@ public class PlaybackManager { } private void clear(int from) { - listener.block(); while (mediaSource.getSize() > from) { queueSource.remove(from); mediaSource.removeMediaSource(from); } + } + + private void clear() { + listener.block(); + clear(0); listener.unblock(); } @@ -153,7 +177,7 @@ public class PlaybackManager { @Override public void onNext(@NonNull PlayQueueEvent event) { - if (playQueue.getStreams().size() - playQueue.getIndex() < 10 && !playQueue.isComplete()) { + if (playQueue.getStreams().size() - playQueue.getIndex() < WINDOW_SIZE && !playQueue.isComplete()) { listener.block(); playQueue.fetch(); } @@ -177,14 +201,14 @@ public class PlaybackManager { load(1); break; case CLEAR: - clear(0); + clear(); break; case NEXT: default: break; } - onLoaded(); + tryUnblock(); if (playQueueReactor != null) playQueueReactor.request(1); } @@ -195,12 +219,13 @@ public class PlaybackManager { @Override public void onComplete() { - // Never completes, only canceled + dispose(); } }; } public void dispose() { if (playQueueReactor != null) playQueueReactor.cancel(); + playQueueReactor = null; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index fa25cc957..644deaf32 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -56,7 +56,11 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.playlist.PlayListInfo; +import org.schabi.newpipe.playlist.ExternalPlayQueue; import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.Utils; import java.io.Serializable; import java.util.ArrayList; @@ -198,7 +202,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. } @SuppressWarnings("unchecked") - public void handleIntent(Intent intent) { + public void handleIntent2(Intent intent) { super.handleIntent(intent); if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); if (intent == null) return; @@ -217,6 +221,38 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. play(true); } + @Override + public MediaSource sourceOf(final StreamInfo info) { + videoStreamsList = Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); + videoOnlyAudioStream = Utils.getHighestQualityAudio(info.audio_streams); + + return buildMediaSource(getSelectedVideoStream().url, MediaFormat.getSuffixById(getSelectedVideoStream().format)); + } + + @Override + public void unblock() { + play(true); + super.unblock(); + } + + public void handleIntent(Intent intent) { + if (intent == null) return; + + selectedIndexStream = 0; + + String url = intent.getStringExtra("url"); + int nextPage = intent.getIntExtra("nextPage", 0); + int index = intent.getIntExtra("index", 0); + + PlayListInfo info; + Serializable serializable = intent.getSerializableExtra("stream"); + if (serializable instanceof PlayListInfo) info = (PlayListInfo) serializable; + else return; + + playQueue = new ExternalPlayQueue(url, info, nextPage, index); + playbackManager = new PlaybackManager(this, playQueue); + mediaSource = playbackManager.getMediaSource(); + } public void play(boolean autoPlay) { playUrl(getSelectedVideoStream().url, MediaFormat.getSuffixById(getSelectedVideoStream().format), autoPlay); diff --git a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java index 3d1831a0e..4fab68a1b 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java @@ -34,12 +34,11 @@ public class ExternalPlayQueue extends PlayQueue { final PlayListInfo info, final int nextPage, final int index) { - super(index); + super(index, extractPlaylistItems(info)); + this.service = getService(info.service_id); this.pageNumber = new AtomicInteger(nextPage); this.playlistUrl = playlistUrl; - - getStreams().addAll(extractPlaylistItems(info)); } @Override @@ -47,12 +46,6 @@ public class ExternalPlayQueue extends PlayQueue { return isComplete; } - @Override - public void load(int index) { - if (index > getStreams().size() || getStreams().get(index) == null) return; - getStreams().get(index).load(); - } - @Override public PlayQueueItem get(int index) { if (index > getStreams().size() || getStreams().get(index) == null) return null; @@ -93,7 +86,7 @@ public class ExternalPlayQueue extends PlayQueue { if (fetchReactor != null) fetchReactor.dispose(); } - private List extractPlaylistItems(final PlayListInfo info) { + private static List extractPlaylistItems(final PlayListInfo info) { List result = new ArrayList<>(); for (final InfoItem stream : info.related_streams) { if (stream instanceof StreamInfoItem) { diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java index 67adc0cf2..99f46261e 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -25,8 +25,14 @@ public abstract class PlayQueue { private BehaviorSubject changeBroadcast; private Flowable playQueueFlowable; - PlayQueue(final int index) { + PlayQueue() { + this(0, Collections.emptyList()); + } + + PlayQueue(final int index, final List startWith) { streams = Collections.synchronizedList(new ArrayList()); + streams.addAll(startWith); + queueIndex = new AtomicInteger(index); changeBroadcast = BehaviorSubject.create(); @@ -37,9 +43,6 @@ public abstract class PlayQueue { // single stream or local queues are always complete public abstract boolean isComplete(); - // load in the background the item at index, may do nothing if the queue is incomplete - public abstract void load(int index); - // load partial queue in the background, does nothing if the queue is complete public abstract void fetch(); diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java index 0f2cb3202..a028d33e1 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java @@ -69,10 +69,6 @@ public class PlayQueueItem { return stream; } - public void load() { - stream.subscribe(); - } - @NonNull private Maybe getInfo() { final Callable task = new Callable() { @@ -101,7 +97,6 @@ public class PlayQueueItem { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(onError) - .onErrorComplete() .doOnComplete(onComplete) .cache(); } diff --git a/app/src/main/res/layout/playlist_header.xml b/app/src/main/res/layout/playlist_header.xml index 192363359..7de6f79a9 100644 --- a/app/src/main/res/layout/playlist_header.xml +++ b/app/src/main/res/layout/playlist_header.xml @@ -78,4 +78,19 @@ tools:ignore="RtlHardcoded" tools:text="234 videos"/> +