From f71242a0362bec9d23260e43c2f25eea817f24c2 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 15 Jan 2018 12:30:52 -0800 Subject: [PATCH 01/52] -Added schema for local playlist and stream statistics. -Added normalized schema for stream history. -Added managers for specialized database access for stream and local playlist. --- .../org/schabi/newpipe/NewPipeDatabase.java | 6 + .../schabi/newpipe/database/AppDatabase.java | 27 ++- .../org/schabi/newpipe/database/BasicDAO.java | 3 - .../database/{history => }/Converters.java | 14 +- .../playlist/PlaylistMetadataEntry.java | 36 ++++ .../database/playlist/dao/PlaylistDAO.java | 35 ++++ .../playlist/dao/PlaylistStreamDAO.java | 69 ++++++++ .../playlist/model/PlaylistEntity.java | 59 +++++++ .../playlist/model/PlaylistStreamEntity.java | 77 +++++++++ .../stream/StreamStatisticsEntry.java | 54 ++++++ .../database/stream/dao/StreamDAO.java | 57 +++++++ .../database/stream/dao/StreamHistoryDAO.java | 54 ++++++ .../database/stream/model/StreamEntity.java | 154 ++++++++++++++++++ .../stream/model/StreamHistoryEntity.java | 58 +++++++ .../subscription/SubscriptionEntity.java | 3 +- .../playlist/LocalPlaylistManager.java | 78 +++++++++ .../playlist/StreamRecordManager.java | 47 ++++++ .../stored/LocalPlaylistInfoItem.java | 30 ++++ .../stored/StreamStatisticsInfoItem.java | 38 +++++ .../org/schabi/newpipe/player/BasePlayer.java | 36 ++-- .../org/schabi/newpipe/util/Constants.java | 1 + 21 files changed, 913 insertions(+), 23 deletions(-) rename app/src/main/java/org/schabi/newpipe/database/{history => }/Converters.java (63%) create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 7111abcf7..4da1c63f2 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -28,4 +28,10 @@ public final class NewPipeDatabase { return databaseInstance; } + + @NonNull + public static AppDatabase getInstance(Context context) { + if (databaseInstance == null) init(context); + return databaseInstance; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index 21868e3c2..e09687ce4 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -4,16 +4,31 @@ import android.arch.persistence.room.Database; import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.TypeConverters; -import org.schabi.newpipe.database.history.Converters; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.WatchHistoryEntry; +import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistEntity; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; @TypeConverters({Converters.class}) -@Database(entities = {SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class}, version = 1, exportSchema = false) +@Database( + entities = { + SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class, + StreamEntity.class, StreamHistoryEntity.class, PlaylistEntity.class, + PlaylistStreamEntity.class + }, + version = 1, + exportSchema = false +) public abstract class AppDatabase extends RoomDatabase { public static final String DATABASE_NAME = "newpipe.db"; @@ -23,4 +38,12 @@ public abstract class AppDatabase extends RoomDatabase { public abstract WatchHistoryDAO watchHistoryDAO(); public abstract SearchHistoryDAO searchHistoryDAO(); + + public abstract StreamDAO streamDAO(); + + public abstract StreamHistoryDAO streamHistoryDAO(); + + public abstract PlaylistDAO playlistDAO(); + + public abstract PlaylistStreamDAO playlistStreamDAO(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java index 03a94508b..425c122ca 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java @@ -23,9 +23,6 @@ public interface BasicDAO { @Insert(onConflict = OnConflictStrategy.FAIL) List insertAll(final Collection entities); - @Insert(onConflict = OnConflictStrategy.REPLACE) - long upsert(final Entity entity); - /* Searches */ Flowable> getAll(); diff --git a/app/src/main/java/org/schabi/newpipe/database/history/Converters.java b/app/src/main/java/org/schabi/newpipe/database/Converters.java similarity index 63% rename from app/src/main/java/org/schabi/newpipe/database/history/Converters.java rename to app/src/main/java/org/schabi/newpipe/database/Converters.java index 093c741f1..d48fbfaf1 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/Converters.java +++ b/app/src/main/java/org/schabi/newpipe/database/Converters.java @@ -1,7 +1,9 @@ -package org.schabi.newpipe.database.history; +package org.schabi.newpipe.database; import android.arch.persistence.room.TypeConverter; +import org.schabi.newpipe.extractor.stream.StreamType; + import java.util.Date; public class Converters { @@ -25,4 +27,14 @@ public class Converters { public static Long dateToTimestamp(Date date) { return date == null ? null : date.getTime(); } + + @TypeConverter + public static StreamType streamTypeOf(String value) { + return StreamType.valueOf(value); + } + + @TypeConverter + public static String stringOf(StreamType streamType) { + return streamType.name(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java new file mode 100644 index 000000000..53ae3d48a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -0,0 +1,36 @@ +package org.schabi.newpipe.database.playlist; + +import android.arch.persistence.room.ColumnInfo; + +import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; + +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; + +public class PlaylistMetadataEntry { + final public static String PLAYLIST_STREAM_COUNT = "streamCount"; + + @ColumnInfo(name = PLAYLIST_ID) + final public long uid; + @ColumnInfo(name = PLAYLIST_NAME) + final public String name; + @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) + final public String thumbnailUrl; + @ColumnInfo(name = PLAYLIST_STREAM_COUNT) + final public long streamCount; + + public PlaylistMetadataEntry(long uid, String name, String thumbnailUrl, long streamCount) { + this.uid = uid; + this.name = name; + this.thumbnailUrl = thumbnailUrl; + this.streamCount = streamCount; + } + + public LocalPlaylistInfoItem toStoredPlaylistInfoItem() { + LocalPlaylistInfoItem storedPlaylistInfoItem = new LocalPlaylistInfoItem(uid, name); + storedPlaylistInfoItem.setThumbnailUrl(thumbnailUrl); + storedPlaylistInfoItem.setStreamCount(streamCount); + return storedPlaylistInfoItem; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java new file mode 100644 index 000000000..b337769bc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java @@ -0,0 +1,35 @@ +package org.schabi.newpipe.database.playlist.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistEntity; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; + +@Dao +public abstract class PlaylistDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + PLAYLIST_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + PLAYLIST_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") + public abstract Flowable> getPlaylist(final long playlistId); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java new file mode 100644 index 000000000..b9f325aa2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -0,0 +1,69 @@ +package org.schabi.newpipe.database.playlist.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.*; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.*; +import static org.schabi.newpipe.database.stream.model.StreamEntity.*; + +@Dao +public abstract class PlaylistStreamDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE + + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") + public abstract void deleteBatch(final long playlistId); + + @Query("SELECT MAX(" + JOIN_INDEX + ")" + + " FROM " + PLAYLIST_STREAM_JOIN_TABLE + + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") + public abstract Flowable getMaximumIndexOf(final long playlistId); + + @Transaction + @Query("SELECT " + STREAM_ID + ", " + STREAM_SERVICE_ID + ", " + STREAM_URL + ", " + + STREAM_TITLE + ", " + STREAM_TYPE + ", " + STREAM_UPLOADER + ", " + + STREAM_DURATION + ", " + STREAM_THUMBNAIL_URL + + + " FROM " + STREAM_TABLE + " INNER JOIN " + + // get ids of streams of the given playlist + "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX + + " FROM " + PLAYLIST_STREAM_JOIN_TABLE + " WHERE " + + JOIN_PLAYLIST_ID + " = :playlistId)" + + + // then merge with the stream metadata + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " ORDER BY " + JOIN_INDEX + " ASC") + public abstract Flowable> getOrderedStreamsOf(long playlistId); + + @Transaction + @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + + PLAYLIST_THUMBNAIL_URL + ", COUNT(*) AS " + PLAYLIST_STREAM_COUNT + + + " FROM " + PLAYLIST_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + PLAYLIST_STREAM_JOIN_TABLE + "." + JOIN_PLAYLIST_ID + + " GROUP BY " + JOIN_PLAYLIST_ID) + public abstract Flowable> getPlaylistMetadata(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java new file mode 100644 index 000000000..a3ec1b5f2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java @@ -0,0 +1,59 @@ +package org.schabi.newpipe.database.playlist.model; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Index; +import android.arch.persistence.room.PrimaryKey; + +import java.util.Date; + +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; + +@Entity(tableName = PLAYLIST_TABLE, + indices = {@Index(value = {PLAYLIST_NAME})}) +public class PlaylistEntity { + final public static String PLAYLIST_TABLE = "playlists"; + final public static String PLAYLIST_ID = "uid"; + final public static String PLAYLIST_NAME = "name"; + final public static String PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = PLAYLIST_ID) + private long uid = 0; + + @ColumnInfo(name = PLAYLIST_NAME) + private String name; + + @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) + private String thumbnailUrl; + + public PlaylistEntity(String name, String thumbnailUrl) { + this.name = name; + this.thumbnailUrl = thumbnailUrl; + } + + public long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public void setThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java new file mode 100644 index 000000000..3d71f7e70 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java @@ -0,0 +1,77 @@ +package org.schabi.newpipe.database.playlist.model; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.ForeignKey; +import android.arch.persistence.room.Index; + +import org.schabi.newpipe.database.stream.model.StreamEntity; + +import static android.arch.persistence.room.ForeignKey.CASCADE; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; + +@Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE, + primaryKeys = {JOIN_PLAYLIST_ID, JOIN_STREAM_ID, JOIN_INDEX}, + indices = { + @Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true), + @Index(value = {JOIN_STREAM_ID}) + }, + foreignKeys = { + @ForeignKey(entity = PlaylistEntity.class, + parentColumns = PlaylistEntity.PLAYLIST_ID, + childColumns = JOIN_PLAYLIST_ID, + onDelete = CASCADE, onUpdate = CASCADE, deferred = true), + @ForeignKey(entity = StreamEntity.class, + parentColumns = StreamEntity.STREAM_ID, + childColumns = JOIN_STREAM_ID, + onDelete = CASCADE, onUpdate = CASCADE, deferred = true) + }) +public class PlaylistStreamEntity { + + final public static String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join"; + final public static String JOIN_PLAYLIST_ID = "playlist_id"; + final public static String JOIN_STREAM_ID = "stream_id"; + final public static String JOIN_INDEX = "join_index"; + + @ColumnInfo(name = JOIN_PLAYLIST_ID) + private long playlistUid; + + @ColumnInfo(name = JOIN_STREAM_ID) + private long streamUid; + + @ColumnInfo(name = JOIN_INDEX) + private int index; + + public PlaylistStreamEntity(final long playlistUid, final long streamUid, final int index) { + this.playlistUid = playlistUid; + this.streamUid = streamUid; + this.index = index; + } + + public long getPlaylistUid() { + return playlistUid; + } + + public long getStreamUid() { + return streamUid; + } + + public int getIndex() { + return index; + } + + public void setPlaylistUid(long playlistUid) { + this.playlistUid = playlistUid; + } + + public void setStreamUid(long streamUid) { + this.streamUid = streamUid; + } + + public void setIndex(int index) { + this.index = index; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java new file mode 100644 index 000000000..5893394c5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -0,0 +1,54 @@ +package org.schabi.newpipe.database.stream; + +import android.arch.persistence.room.ColumnInfo; + +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamType; + +import java.util.Date; + +public class StreamStatisticsEntry { + final public static String STREAM_LATEST_DATE = "latestAccess"; + final public static String STREAM_WATCH_COUNT = "watchCount"; + + @ColumnInfo(name = StreamEntity.STREAM_ID) + final public long uid; + @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) + final public int serviceId; + @ColumnInfo(name = StreamEntity.STREAM_URL) + final public String url; + @ColumnInfo(name = StreamEntity.STREAM_TITLE) + final public String title; + @ColumnInfo(name = StreamEntity.STREAM_TYPE) + final public StreamType streamType; + @ColumnInfo(name = StreamEntity.STREAM_DURATION) + final public long duration; + @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) + final public String uploader; + @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) + final public String thumbnailUrl; + @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) + final public long streamId; + @ColumnInfo(name = StreamStatisticsEntry.STREAM_LATEST_DATE) + final public Date latestAccessDate; + @ColumnInfo(name = StreamStatisticsEntry.STREAM_WATCH_COUNT) + final public long watchCount; + + public StreamStatisticsEntry(long uid, int serviceId, String url, String title, + StreamType streamType, long duration, String uploader, + String thumbnailUrl, long streamId, Date latestAccessDate, + long watchCount) { + this.uid = uid; + this.serviceId = serviceId; + this.url = url; + this.title = title; + this.streamType = streamType; + this.duration = duration; + this.uploader = uploader; + this.thumbnailUrl = thumbnailUrl; + this.streamId = streamId; + this.latestAccessDate = latestAccessDate; + this.watchCount = watchCount; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java new file mode 100644 index 000000000..f7807ef42 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -0,0 +1,57 @@ +package org.schabi.newpipe.database.stream.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.stream.model.StreamEntity; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; + +@Dao +public abstract class StreamDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + STREAM_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + STREAM_TABLE) + public abstract int deleteAll(); + + @Override + @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId") + public abstract Flowable> listByService(int serviceId); + + @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + + STREAM_URL + " LIKE :url AND " + + STREAM_SERVICE_ID + " = :serviceId") + public abstract Flowable> getStream(long serviceId, String url); + + @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + + STREAM_URL + " LIKE :url AND " + + STREAM_SERVICE_ID + " = :serviceId") + abstract List getStreamInternal(long serviceId, String url); + + @Transaction + public long upsert(StreamEntity stream) { + final List streams = getStreamInternal(stream.getServiceId(), stream.getUrl()); + + final long uid; + if (streams.isEmpty()) { + uid = insert(stream); + } else { + uid = streams.get(0).getUid(); + stream.setUid(uid); + update(stream); + } + return uid; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java new file mode 100644 index 000000000..19c7b9e90 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java @@ -0,0 +1,54 @@ +package org.schabi.newpipe.database.stream.dao; + + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; +import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; + +@Dao +public abstract class StreamHistoryDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + STREAM_HISTORY_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + STREAM_HISTORY_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") + public abstract int deleteHistory(final long streamId); + + @Query("SELECT * FROM " + STREAM_TABLE + + + // Select the latest entry and watch count for each stream id on history table + " INNER JOIN " + + "(SELECT " + JOIN_STREAM_ID + ", " + + " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " + + " COUNT(*) AS " + STREAM_WATCH_COUNT + + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" + + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " ORDER BY " + STREAM_ACCESS_DATE + " DESC") + public abstract Flowable> getStatistics(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java new file mode 100644 index 000000000..27d0aa7e1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -0,0 +1,154 @@ +package org.schabi.newpipe.database.stream.model; + +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.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.util.Constants; + +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; + +@Entity(tableName = STREAM_TABLE, + indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)}) +public class StreamEntity { + + final public static String STREAM_TABLE = "streams"; + final public static String STREAM_ID = "uid"; + final public static String STREAM_SERVICE_ID = "service_id"; + final public static String STREAM_URL = "url"; + final public static String STREAM_TITLE = "title"; + final public static String STREAM_TYPE = "streamType"; + final public static String STREAM_UPLOADER = "uploader"; + final public static String STREAM_DURATION = "duration"; + final public static String STREAM_THUMBNAIL_URL = "thumbnail_url"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = STREAM_ID) + private long uid = 0; + + @ColumnInfo(name = STREAM_SERVICE_ID) + private int serviceId = Constants.NO_SERVICE_ID; + + @ColumnInfo(name = STREAM_URL) + private String url; + + @ColumnInfo(name = STREAM_TITLE) + private String title; + + @ColumnInfo(name = STREAM_TYPE) + private StreamType streamType; + + @ColumnInfo(name = STREAM_DURATION) + private Long duration; + + @ColumnInfo(name = STREAM_UPLOADER) + private String uploader; + + @ColumnInfo(name = STREAM_THUMBNAIL_URL) + private String thumbnailUrl; + + public StreamEntity(final int serviceId, final String title, final String url, + final StreamType streamType, final String thumbnailUrl, final String uploader, + final long duration) { + this.serviceId = serviceId; + this.title = title; + this.url = url; + this.streamType = streamType; + this.thumbnailUrl = thumbnailUrl; + this.uploader = uploader; + this.duration = duration; + } + + @Ignore + public StreamEntity(final StreamInfoItem item) { + this(item.service_id, item.name, item.url, item.stream_type, item.thumbnail_url, + item.uploader_name, item.duration); + } + + @Ignore + public StreamEntity(final StreamInfo info) { + this(info.service_id, info.name, info.url, info.stream_type, info.thumbnail_url, + info.uploader_name, info.duration); + } + + @Ignore + public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { + StreamInfoItem item = new StreamInfoItem( + getServiceId(), getUrl(), getTitle(), getStreamType()); + item.setThumbnailUrl(getThumbnailUrl()); + item.setUploaderName(getUploader()); + item.setDuration(getDuration()); + return item; + } + + public long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + + public int getServiceId() { + return serviceId; + } + + public void setServiceId(int serviceId) { + this.serviceId = serviceId; + } + + 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 StreamType getStreamType() { + return streamType; + } + + public void setStreamType(StreamType type) { + this.streamType = type; + } + + public Long getDuration() { + return duration; + } + + public void setDuration(Long duration) { + this.duration = duration; + } + + public String getUploader() { + return uploader; + } + + public void setUploader(String uploader) { + this.uploader = uploader; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public void setThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java new file mode 100644 index 000000000..d937a29ed --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java @@ -0,0 +1,58 @@ +package org.schabi.newpipe.database.stream.model; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.ForeignKey; +import android.arch.persistence.room.Index; +import android.support.annotation.NonNull; + +import java.util.Date; + +import static android.arch.persistence.room.ForeignKey.CASCADE; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE; + +@Entity(tableName = STREAM_HISTORY_TABLE, + primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, + // No need to index for timestamp as they will almost always be unique + indices = {@Index(value = {JOIN_STREAM_ID})}, + foreignKeys = { + @ForeignKey(entity = StreamEntity.class, + parentColumns = StreamEntity.STREAM_ID, + childColumns = JOIN_STREAM_ID, + onDelete = CASCADE, onUpdate = CASCADE) + }) +public class StreamHistoryEntity { + final public static String STREAM_HISTORY_TABLE = "stream_history"; + final public static String JOIN_STREAM_ID = "stream_id"; + final public static String STREAM_ACCESS_DATE = "access_date"; + + @ColumnInfo(name = JOIN_STREAM_ID) + private long streamUid; + + @NonNull + @ColumnInfo(name = STREAM_ACCESS_DATE) + private Date accessDate; + + public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) { + this.streamUid = streamUid; + this.accessDate = accessDate; + } + + public long getStreamUid() { + return streamUid; + } + + public void setStreamUid(long streamUid) { + this.streamUid = streamUid; + } + + public Date getAccessDate() { + return accessDate; + } + + public void setAccessDate(@NonNull Date accessDate) { + this.accessDate = accessDate; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index e71088ac9..60eb0c3d3 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java @@ -50,8 +50,7 @@ public class SubscriptionEntity { return uid; } - /* Keep this package-private since UID should always be auto generated by Room impl */ - void setUid(long uid) { + public void setUid(long uid) { this.uid = uid; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java new file mode 100644 index 000000000..db32a392e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java @@ -0,0 +1,78 @@ +package org.schabi.newpipe.fragments.playlist; + +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistEntity; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.stream.model.StreamEntity; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.Maybe; + +public class LocalPlaylistManager { + + private final AppDatabase database; + private final StreamDAO streamTable; + private final PlaylistDAO playlistTable; + private final PlaylistStreamDAO playlistStreamTable; + + public LocalPlaylistManager(final AppDatabase db) { + database = db; + streamTable = db.streamDAO(); + playlistTable = db.playlistDAO(); + playlistStreamTable = db.playlistStreamDAO(); + } + + public Maybe> createPlaylist(final String name, final List streams) { + // Disallow creation of empty playlists until user is able to select thumbnail + if (streams.isEmpty()) return Maybe.empty(); + final StreamEntity defaultStream = streams.get(0); + final PlaylistEntity newPlaylist = new PlaylistEntity(name, defaultStream.getThumbnailUrl()); + + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + final long playlistId = playlistTable.insert(newPlaylist); + + List joinEntities = new ArrayList<>(streams.size()); + for (int index = 0; index < streams.size(); index++) { + // Upsert streams and get their ids + final long streamId = streamTable.upsert(streams.get(index)); + joinEntities.add(new PlaylistStreamEntity(playlistId, streamId, index)); + } + + return playlistStreamTable.insertAll(joinEntities); + })); + } + + public Maybe appendToPlaylist(final long playlistId, final StreamEntity stream) { + final Maybe streamIdFuture = Maybe.fromCallable(() -> streamTable.upsert(stream)); + final Maybe joinIndexFuture = + playlistStreamTable.getMaximumIndexOf(playlistId).firstElement(); + + return Maybe.zip(streamIdFuture, joinIndexFuture, (streamId, currentMaxJoinIndex) -> + playlistStreamTable.insert(new PlaylistStreamEntity(playlistId, + streamId, currentMaxJoinIndex + 1)) + ); + } + + public Completable updateJoin(final long playlistId, final List streamIds) { + List joinEntities = new ArrayList<>(streamIds.size()); + for (int i = 0; i < streamIds.size(); i++) { + joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(i), i)); + } + + return Completable.fromRunnable(() -> database.runInTransaction(() -> { + playlistStreamTable.deleteBatch(playlistId); + playlistStreamTable.insertAll(joinEntities); + })); + } + + public Maybe> getPlaylists() { + return playlistStreamTable.getPlaylistMetadata().firstElement(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java new file mode 100644 index 000000000..bd5bd36a2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.fragments.playlist; + +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; + +import java.util.Date; +import java.util.List; + +import io.reactivex.MaybeObserver; +import io.reactivex.Single; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +public class StreamRecordManager { + + private final AppDatabase database; + private final StreamDAO streamTable; + private final StreamHistoryDAO historyTable; + + public StreamRecordManager(final AppDatabase db) { + database = db; + streamTable = db.streamDAO(); + historyTable = db.streamHistoryDAO(); + } + + public int onChanged(final StreamInfoItem infoItem) { + // Only existing streams are updated + return streamTable.update(new StreamEntity(infoItem)); + } + + public Single onViewed(final StreamInfo info) { + return Single.fromCallable(() -> database.runInTransaction(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return historyTable.insert(new StreamHistoryEntity(streamId, new Date())); + })).subscribeOn(Schedulers.io()); + } + + public int removeHistory(final long streamId) { + return historyTable.deleteHistory(streamId); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java new file mode 100644 index 000000000..63f61cc43 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java @@ -0,0 +1,30 @@ +package org.schabi.newpipe.info_list.stored; + +import org.schabi.newpipe.extractor.InfoItem; + +import static org.schabi.newpipe.util.Constants.NO_SERVICE_ID; +import static org.schabi.newpipe.util.Constants.NO_URL; + +public class LocalPlaylistInfoItem extends InfoItem { + private final long playlistId; + private long streamCount; + + public LocalPlaylistInfoItem(final long playlistId, final String name) { + super(InfoType.PLAYLIST, NO_SERVICE_ID, NO_URL, name); + + this.playlistId = playlistId; + this.streamCount = streamCount; + } + + public long getPlaylistId() { + return playlistId; + } + + public long getStreamCount() { + return streamCount; + } + + public void setStreamCount(long streamCount) { + this.streamCount = streamCount; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java new file mode 100644 index 000000000..ef82826ba --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java @@ -0,0 +1,38 @@ +package org.schabi.newpipe.info_list.stored; + +import org.schabi.newpipe.extractor.InfoItem; + +import java.util.Date; + +public class StreamStatisticsInfoItem extends InfoItem { + private final long streamId; + + private Date latestAccessDate; + private long watchCount; + + public StreamStatisticsInfoItem(final long streamId, final int serviceId, + final String url, final String name) { + super(InfoType.STREAM, serviceId, url, name); + this.streamId = streamId; + } + + public long getStreamId() { + return streamId; + } + + public Date getLatestAccessDate() { + return latestAccessDate; + } + + public void setLatestAccessDate(Date latestAccessDate) { + this.latestAccessDate = latestAccessDate; + } + + public long getWatchCount() { + return watchCount; + } + + public void setWatchCount(long watchCount) { + this.watchCount = watchCount; + } +} 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 55a73d484..ad2200bfc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -61,8 +61,10 @@ import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.fragments.playlist.StreamRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; @@ -77,9 +79,9 @@ import java.util.concurrent.TimeUnit; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.functions.Predicate; +import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; @@ -147,6 +149,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected DefaultExtractorsFactory extractorsFactory; protected Disposable progressUpdateReactor; + protected CompositeDisposable databaseUpdateReactor; + + protected StreamRecordManager recordManager; //////////////////////////////////////////////////////////////////////////*/ @@ -172,6 +177,12 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void initPlayer() { if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); + if (recordManager == null) { + recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); + } + if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); + databaseUpdateReactor = new CompositeDisposable(); + final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); final LoadControl loadControl = new LoadController(context); @@ -193,18 +204,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen private Disposable getProgressReactor() { return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .filter(new Predicate() { - @Override - public boolean test(Long aLong) throws Exception { - return isProgressLoopRunning(); - } - }) - .subscribe(new Consumer() { - @Override - public void accept(Long aLong) throws Exception { - triggerProgressUpdate(); - } - }); + .filter(ignored -> isProgressLoopRunning()) + .subscribe(ignored -> triggerProgressUpdate()); } public void handleIntent(Intent intent) { @@ -281,6 +282,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (playQueue != null) playQueue.dispose(); if (playbackManager != null) playbackManager.dispose(); if (audioReactor != null) audioReactor.abandonAudioFocus(); + if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); } public void destroy() { @@ -291,6 +293,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen trackSelector = null; simpleExoPlayer = null; + recordManager = null; } public MediaSource buildMediaSource(String url, String overrideExtension) { @@ -668,10 +671,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen "], queue index=[" + playQueue.getIndex() + "]"); } else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) { final long startPos = info != null ? info.start_position : 0; - if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos)); + if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + + " at: " + getTimeString((int)startPos)); simpleExoPlayer.seekTo(currentSourceIndex, startPos); } + databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe()); + recordManager.removeRecord(); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java index a6aec96e2..4238424d9 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Constants.java +++ b/app/src/main/java/org/schabi/newpipe/util/Constants.java @@ -12,4 +12,5 @@ public class Constants { public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; public static final int NO_SERVICE_ID = -1; + public static final String NO_URL = ""; } From 38946e4b0f28501fd357b3d4f894646aa7d49146 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 16 Jan 2018 11:48:52 -0800 Subject: [PATCH 02/52] -Added UI for creating playlist. -Added UI for appending item to playlists. -Added mini variant of playlist info item. --- .../database/stream/dao/StreamHistoryDAO.java | 3 +- .../playlist/LocalPlaylistManager.java | 10 +- .../playlist/PlaylistAppendDialog.java | 147 ++++++++++++++++++ .../playlist/PlaylistCreationDialog.java | 91 +++++++++++ .../playlist/StreamRecordManager.java | 27 ++++ .../newpipe/info_list/InfoItemBuilder.java | 3 +- .../newpipe/info_list/InfoItemDialog.java | 5 +- .../newpipe/info_list/InfoListAdapter.java | 6 +- .../holder/PlaylistInfoItemHolder.java | 51 +----- .../holder/PlaylistMiniInfoItemHolder.java | 62 ++++++++ .../stored/LocalPlaylistInfoItem.java | 16 +- .../stored/StreamStatisticsInfoItem.java | 9 +- .../org/schabi/newpipe/player/BasePlayer.java | 1 - .../res/layout/dialog_create_playlist.xml | 22 +++ app/src/main/res/layout/dialog_playlists.xml | 57 +++++++ .../res/layout/list_playlist_mini_item.xml | 70 +++++++++ app/src/main/res/values/strings.xml | 6 + 17 files changed, 508 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java create mode 100644 app/src/main/res/layout/dialog_create_playlist.xml create mode 100644 app/src/main/res/layout/dialog_playlists.xml create mode 100644 app/src/main/res/layout/list_playlist_mini_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java index 19c7b9e90..522c03522 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java @@ -48,7 +48,6 @@ public abstract class StreamHistoryDAO implements BasicDAO " COUNT(*) AS " + STREAM_WATCH_COUNT + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" + - " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + - " ORDER BY " + STREAM_ACCESS_DATE + " DESC") + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID) public abstract Flowable> getStatistics(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java index db32a392e..911b3c7fd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java @@ -14,6 +14,8 @@ import java.util.List; import io.reactivex.Completable; import io.reactivex.Maybe; +import io.reactivex.Scheduler; +import io.reactivex.schedulers.Schedulers; public class LocalPlaylistManager { @@ -46,7 +48,7 @@ public class LocalPlaylistManager { } return playlistStreamTable.insertAll(joinEntities); - })); + })).subscribeOn(Schedulers.io()); } public Maybe appendToPlaylist(final long playlistId, final StreamEntity stream) { @@ -57,7 +59,7 @@ public class LocalPlaylistManager { return Maybe.zip(streamIdFuture, joinIndexFuture, (streamId, currentMaxJoinIndex) -> playlistStreamTable.insert(new PlaylistStreamEntity(playlistId, streamId, currentMaxJoinIndex + 1)) - ); + ).subscribeOn(Schedulers.io()); } public Completable updateJoin(final long playlistId, final List streamIds) { @@ -73,6 +75,8 @@ public class LocalPlaylistManager { } public Maybe> getPlaylists() { - return playlistStreamTable.getPlaylistMetadata().firstElement(); + return playlistStreamTable.getPlaylistMetadata() + .firstElement() + .subscribeOn(Schedulers.io()); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java new file mode 100644 index 000000000..bee3b347e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java @@ -0,0 +1,147 @@ +package org.schabi.newpipe.fragments.playlist; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.android.schedulers.AndroidSchedulers; + +public class PlaylistAppendDialog extends DialogFragment { + private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); + private static final String INFO_KEY = "info_key"; + + private StreamInfo streamInfo; + + private View newPlaylistButton; + private RecyclerView playlistRecyclerView; + private InfoListAdapter playlistAdapter; + + public static PlaylistAppendDialog newInstance(final StreamInfo info) { + PlaylistAppendDialog dialog = new PlaylistAppendDialog(); + dialog.setInfo(info); + return dialog; + } + + private void setInfo(StreamInfo info) { + this.streamInfo = info; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + playlistAdapter = new InfoListAdapter(getActivity()); + playlistAdapter.useMiniItemVariants(true); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + Serializable serial = savedInstanceState.getSerializable(INFO_KEY); + if (serial instanceof StreamInfo) streamInfo = (StreamInfo) serial; + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.dialog_playlists, container); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + newPlaylistButton = view.findViewById(R.id.newPlaylist); + playlistRecyclerView = view.findViewById(R.id.playlist_list); + playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + playlistRecyclerView.setAdapter(playlistAdapter); + + final LocalPlaylistManager playlistManager = + new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + + newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); + + playlistAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(PlaylistInfoItem selectedItem) { + if (!(selectedItem instanceof LocalPlaylistInfoItem)) return; + final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); + final Toast successToast = + Toast.makeText(getContext(), "Added", Toast.LENGTH_SHORT); + + playlistManager.appendToPlaylist(playlistId, new StreamEntity(streamInfo)) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> successToast.show()); + + getDialog().dismiss(); + } + + @Override + public void held(PlaylistInfoItem selectedItem) {} + }); + + playlistManager.getPlaylists() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(metadataEntries -> { + if (metadataEntries.isEmpty()) { + openCreatePlaylistDialog(); + } + + List playlistInfoItems = new ArrayList<>(metadataEntries.size()); + for (final PlaylistMetadataEntry metadataEntry : metadataEntries) { + playlistInfoItems.add(metadataEntry.toStoredPlaylistInfoItem()); + } + + playlistAdapter.clearStreamItemList(); + playlistAdapter.addInfoItemList(playlistInfoItems); + playlistRecyclerView.setVisibility(View.VISIBLE); + + getDialog().setCanceledOnTouchOutside(true); + }); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(INFO_KEY, streamInfo); + } + + /*////////////////////////////////////////////////////////////////////////// + // Helper + //////////////////////////////////////////////////////////////////////////*/ + + public void openCreatePlaylistDialog() { + if (streamInfo == null || getFragmentManager() == null) return; + + getDialog().dismiss(); + PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java new file mode 100644 index 000000000..15e787e2a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java @@ -0,0 +1,91 @@ +package org.schabi.newpipe.fragments.playlist; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.view.View; +import android.widget.EditText; +import android.widget.Toast; + +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfo; + +import java.util.Collections; +import java.util.List; + +import io.reactivex.android.schedulers.AndroidSchedulers; + +public class PlaylistCreationDialog extends DialogFragment { + private static final String TAG = PlaylistCreationDialog.class.getCanonicalName(); + private static final boolean DEBUG = MainActivity.DEBUG; + + private static final String INFO_KEY = "info_key"; + + private StreamInfo streamInfo; + + public static PlaylistCreationDialog newInstance(final StreamInfo info) { + PlaylistCreationDialog dialog = new PlaylistCreationDialog(); + dialog.setInfo(info); + return dialog; + } + + private void setInfo(final StreamInfo info) { + this.streamInfo = info; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (streamInfo != null) { + outState.putSerializable(INFO_KEY, streamInfo); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + if (savedInstanceState != null && streamInfo == null) { + final Object infoCandidate = savedInstanceState.getSerializable(INFO_KEY); + if (infoCandidate != null && infoCandidate instanceof StreamInfo) { + streamInfo = (StreamInfo) infoCandidate; + } + } + + if (streamInfo == null) return super.onCreateDialog(savedInstanceState); + + View dialogView = View.inflate(getContext(), + R.layout.dialog_create_playlist, null); + EditText nameInput = dialogView.findViewById(R.id.playlist_name); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) + .setTitle(R.string.create_playlist) + .setView(dialogView) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.create, (dialogInterface, i) -> { + final String name = nameInput.getText().toString(); + final LocalPlaylistManager playlistManager = + new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + final List streams = + Collections.singletonList(new StreamEntity(streamInfo)); + + playlistManager.createPlaylist(name, streams) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> Toast.makeText(getActivity(), + "Playlist " + name + " successfully created", + Toast.LENGTH_SHORT).show()); + }); + + return dialogBuilder.create(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java index bd5bd36a2..31f6284eb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java @@ -44,4 +44,31 @@ public class StreamRecordManager { public int removeHistory(final long streamId) { return historyTable.deleteHistory(streamId); } + + public void removeRecord() { + historyTable.getStatistics().firstElement().subscribe( + new MaybeObserver>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(List streamStatisticsEntries) { + hashCode(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + } + ); + } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index ab3d73149..c81235623 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -16,6 +16,7 @@ import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; +import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; @@ -75,7 +76,7 @@ public class InfoItemBuilder { case CHANNEL: return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent); case PLAYLIST: - return new PlaylistInfoItemHolder(this, parent); + return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent); default: Log.e(TAG, "Trollolo"); throw new RuntimeException("InfoType not expected = " + infoType.name()); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java index cdb2191e5..88aa76887 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java @@ -19,7 +19,7 @@ public class InfoItemDialog { @NonNull final StreamInfoItem info, @NonNull final String[] commands, @NonNull final DialogInterface.OnClickListener actions) { - this(activity, commands, actions, info.getName(), info.uploader_name); + this(activity, commands, actions, info.getName(), info.getUploaderName()); } public InfoItemDialog(@NonNull final Activity activity, @@ -28,8 +28,7 @@ public class InfoItemDialog { @NonNull final String title, @Nullable final String additionalDetail) { - final LayoutInflater inflater = activity.getLayoutInflater(); - final View bannerView = inflater.inflate(R.layout.dialog_title, null); + final View bannerView = View.inflate(activity, R.layout.dialog_title, null); bannerView.setSelected(true); TextView titleView = bannerView.findViewById(R.id.itemTitleView); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 806b348d7..5494eae23 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -15,6 +15,7 @@ import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; +import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; @@ -52,6 +53,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { + if (itemBuilder.getOnPlaylistSelectedListener() != null) { + itemBuilder.getOnPlaylistSelectedListener().selected(item); + } + }); + } + + /** + * Display options for playlist thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java index 63f61cc43..3ac5fabb7 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java @@ -1,30 +1,20 @@ package org.schabi.newpipe.info_list.stored; -import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import static org.schabi.newpipe.util.Constants.NO_SERVICE_ID; import static org.schabi.newpipe.util.Constants.NO_URL; -public class LocalPlaylistInfoItem extends InfoItem { +public class LocalPlaylistInfoItem extends PlaylistInfoItem { private final long playlistId; - private long streamCount; public LocalPlaylistInfoItem(final long playlistId, final String name) { - super(InfoType.PLAYLIST, NO_SERVICE_ID, NO_URL, name); + super(NO_SERVICE_ID, NO_URL, name); this.playlistId = playlistId; - this.streamCount = streamCount; } public long getPlaylistId() { return playlistId; } - - public long getStreamCount() { - return streamCount; - } - - public void setStreamCount(long streamCount) { - this.streamCount = streamCount; - } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java index ef82826ba..76984d363 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java @@ -1,18 +1,19 @@ package org.schabi.newpipe.info_list.stored; -import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Date; -public class StreamStatisticsInfoItem extends InfoItem { +public class StreamStatisticsInfoItem extends StreamInfoItem { private final long streamId; private Date latestAccessDate; private long watchCount; public StreamStatisticsInfoItem(final long streamId, final int serviceId, - final String url, final String name) { - super(InfoType.STREAM, serviceId, url, name); + final String url, final String name, final StreamType type) { + super(serviceId, url, name, type); this.streamId = streamId; } 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 ad2200bfc..ca863fc8a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -81,7 +81,6 @@ import io.reactivex.Observable; 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.player.helper.PlayerHelper.getTimeString; diff --git a/app/src/main/res/layout/dialog_create_playlist.xml b/app/src/main/res/layout/dialog_create_playlist.xml new file mode 100644 index 000000000..b42d3101f --- /dev/null +++ b/app/src/main/res/layout/dialog_create_playlist.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_playlists.xml b/app/src/main/res/layout/dialog_playlists.xml new file mode 100644 index 000000000..5abe91a8e --- /dev/null +++ b/app/src/main/res/layout/dialog_playlists.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_playlist_mini_item.xml b/app/src/main/res/layout/list_playlist_mini_item.xml new file mode 100644 index 000000000..3e854bb8e --- /dev/null +++ b/app/src/main/res/layout/list_playlist_mini_item.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d05d088d..c94453570 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -224,6 +224,7 @@ Start Pause Play + Create Delete Checksum @@ -353,6 +354,7 @@ YouTube SoundCloud + @string/preferred_player_settings_title Open with preferred player @@ -365,4 +367,8 @@ Getting info… "The requested content is loading" + + + Create New Playlist + Name From ba9d0d77075b32a8a8e7f7600e0bedf1b777c3f3 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 16 Jan 2018 21:12:03 -0800 Subject: [PATCH 03/52] -Added basic UI for local playlists. -Added UI for watch history and most played fragments. -Added stream state table for storing playback timestamp and future usage. -Enabled playlist deletion. --- .../schabi/newpipe/database/AppDatabase.java | 8 +- .../database/playlist/dao/PlaylistDAO.java | 3 + .../stream/StreamStatisticsEntry.java | 13 + .../database/stream/dao/StreamStateDAO.java | 33 ++ .../stream/model/StreamStateEntity.java | 51 +++ .../newpipe/fragments/MainFragment.java | 22 +- .../fragments/local/BookmarkFragment.java | 318 ++++++++++++++++ .../local/HistoryPlaylistFragment.java | 323 ++++++++++++++++ .../local/LocalPlaylistFragment.java | 356 ++++++++++++++++++ .../LocalPlaylistManager.java | 18 +- .../fragments/local/MostPlayedFragment.java | 35 ++ .../PlaylistAppendDialog.java | 7 +- .../PlaylistCreationDialog.java | 2 +- .../StreamRecordManager.java | 38 +- .../fragments/local/WatchHistoryFragment.java | 36 ++ .../holder/PlaylistMiniInfoItemHolder.java | 8 + .../org/schabi/newpipe/player/BasePlayer.java | 3 +- .../newpipe/playlist/SinglePlayQueue.java | 18 +- .../schabi/newpipe/util/NavigationHelper.java | 27 ++ app/src/main/res/layout/bookmark_header.xml | 81 ++++ .../main/res/layout/fragment_bookmarks.xml | 44 +++ app/src/main/res/layout/fragment_feed.xml | 2 +- .../main/res/layout/fragment_subscription.xml | 2 +- ...eed_empty_view.xml => list_empty_view.xml} | 0 .../res/layout/list_playlist_mini_item.xml | 2 +- .../main/res/layout/local_playlist_header.xml | 48 +++ app/src/main/res/values/settings_keys.xml | 2 + app/src/main/res/values/strings.xml | 4 + 28 files changed, 1446 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java rename app/src/main/java/org/schabi/newpipe/fragments/{playlist => local}/LocalPlaylistManager.java (84%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java rename app/src/main/java/org/schabi/newpipe/fragments/{playlist => local}/PlaylistAppendDialog.java (98%) rename app/src/main/java/org/schabi/newpipe/fragments/{playlist => local}/PlaylistCreationDialog.java (98%) rename app/src/main/java/org/schabi/newpipe/fragments/{playlist => local}/StreamRecordManager.java (56%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java create mode 100644 app/src/main/res/layout/bookmark_header.xml create mode 100644 app/src/main/res/layout/fragment_bookmarks.xml rename app/src/main/res/layout/{subscription_feed_empty_view.xml => list_empty_view.xml} (100%) create mode 100644 app/src/main/res/layout/local_playlist_header.xml diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index e09687ce4..dedbfbf68 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -14,8 +14,10 @@ import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.dao.StreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; @@ -23,8 +25,8 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; @Database( entities = { SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class, - StreamEntity.class, StreamHistoryEntity.class, PlaylistEntity.class, - PlaylistStreamEntity.class + StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, + PlaylistEntity.class, PlaylistStreamEntity.class }, version = 1, exportSchema = false @@ -43,6 +45,8 @@ public abstract class AppDatabase extends RoomDatabase { public abstract StreamHistoryDAO streamHistoryDAO(); + public abstract StreamStateDAO streamStateDAO(); + public abstract PlaylistDAO playlistDAO(); public abstract PlaylistStreamDAO playlistStreamDAO(); diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java index b337769bc..88d5645af 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java @@ -32,4 +32,7 @@ public abstract class PlaylistDAO implements BasicDAO { @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") public abstract Flowable> getPlaylist(final long playlistId); + + @Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") + public abstract int deletePlaylist(final long playlistId); } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java index 5893394c5..722cff5cd 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -4,7 +4,9 @@ import android.arch.persistence.room.ColumnInfo; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import java.util.Date; @@ -51,4 +53,15 @@ public class StreamStatisticsEntry { this.latestAccessDate = latestAccessDate; this.watchCount = watchCount; } + + public StreamStatisticsInfoItem toStreamStatisticsInfoItem() { + StreamStatisticsInfoItem item = + new StreamStatisticsInfoItem(uid, serviceId, url, title, streamType); + item.setDuration(duration); + item.setUploaderName(uploader); + item.setThumbnailUrl(thumbnailUrl); + item.setLatestAccessDate(latestAccessDate); + item.setWatchCount(watchCount); + return item; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java new file mode 100644 index 000000000..f89f2f7ef --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java @@ -0,0 +1,33 @@ +package org.schabi.newpipe.database.stream.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; + +@Dao +public abstract class StreamStateDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + STREAM_STATE_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + STREAM_STATE_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") + public abstract int deleteState(final long streamId); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java new file mode 100644 index 000000000..15940a964 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -0,0 +1,51 @@ +package org.schabi.newpipe.database.stream.model; + + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.ForeignKey; + +import static android.arch.persistence.room.ForeignKey.CASCADE; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; + +@Entity(tableName = STREAM_STATE_TABLE, + primaryKeys = {JOIN_STREAM_ID}, + foreignKeys = { + @ForeignKey(entity = StreamEntity.class, + parentColumns = StreamEntity.STREAM_ID, + childColumns = JOIN_STREAM_ID, + onDelete = CASCADE, onUpdate = CASCADE) + }) +public class StreamStateEntity { + final public static String STREAM_STATE_TABLE = "stream_state"; + final public static String JOIN_STREAM_ID = "stream_id"; + final public static String STREAM_PROGRESS_TIME = "progress_time"; + + @ColumnInfo(name = JOIN_STREAM_ID) + private long streamUid; + + @ColumnInfo(name = STREAM_PROGRESS_TIME) + private long progressTime; + + public StreamStateEntity(long streamUid, long progressTime) { + this.streamUid = streamUid; + this.progressTime = progressTime; + } + + public long getStreamUid() { + return streamUid; + } + + public void setStreamUid(long streamUid) { + this.streamUid = streamUid; + } + + public long getProgressTime() { + return progressTime; + } + + public void setProgressTime(long progressTime) { + this.progressTime = progressTime; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 3a8c7569c..e76b97086 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskList; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; +import org.schabi.newpipe.fragments.local.BookmarkFragment; import org.schabi.newpipe.fragments.subscription.SubscriptionFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -87,9 +88,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte if (isSubscriptionsPageOnlySelected()) { tabLayout.getTabAt(0).setIcon(channelIcon); + tabLayout.getTabAt(1).setText(R.string.tab_bookmarks); } else { tabLayout.getTabAt(0).setIcon(whatsHotIcon); tabLayout.getTabAt(1).setIcon(channelIcon); + tabLayout.getTabAt(2).setText(R.string.tab_bookmarks); } } @@ -147,7 +150,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte } private class PagerAdapter extends FragmentPagerAdapter { - PagerAdapter(FragmentManager fm) { super(fm); } @@ -158,7 +160,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte case 0: return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment(); case 1: - return new SubscriptionFragment(); + if(PreferenceManager.getDefaultSharedPreferences(getActivity()) + .getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key)) + .equals(getString(R.string.subscription_page_key))) { + return new BookmarkFragment(); + } else { + return new SubscriptionFragment(); + } + case 2: + return new BookmarkFragment(); default: return new BlankFragment(); } @@ -172,7 +182,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte @Override public int getCount() { - return isSubscriptionsPageOnlySelected() ? 1 : 2; + return isSubscriptionsPageOnlySelected() ? 2 : 3; } } @@ -187,6 +197,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte } private Fragment getMainPageFragment() { + if (getActivity() == null) return new BlankFragment(); + try { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); @@ -216,6 +228,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name); fragment.useAsFrontPage(true); return fragment; + } else if (setMainPage.equals(getString(R.string.bookmark_page_key))) { + final BookmarkFragment fragment = new BookmarkFragment(); + fragment.useAsFrontPage(true); + return fragment; } else { return new BlankFragment(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java new file mode 100644 index 000000000..ecbd416ee --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java @@ -0,0 +1,318 @@ +package org.schabi.newpipe.fragments.local; + +import android.content.Context; +import android.content.DialogInterface; +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; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; +import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.NavigationHelper; + +import java.util.ArrayList; +import java.util.Collections; +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 static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class BookmarkFragment extends BaseStateFragment> { + private View watchHistoryButton; + private View mostWatchedButton; + + private InfoListAdapter infoListAdapter; + private RecyclerView itemsList; + + @State + protected Parcelable itemsListState; + + private Subscription databaseSubscription; + private CompositeDisposable disposables = new CompositeDisposable(); + private LocalPlaylistManager localPlaylistManager; + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + /////////////////////////////////////////////////////////////////////////// + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if(isVisibleToUser && activity != null && activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setTitle(R.string.tab_bookmarks); + } + } + + + @Override + public void onAttach(Context context) { + super.onAttach(context); + infoListAdapter = new InfoListAdapter(activity); + localPlaylistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(context)); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + Bundle savedInstanceState) { + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayShowTitleEnabled(true); + } + + activity.setTitle(R.string.tab_bookmarks); + if(useAsFrontPage) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(false); + } + return inflater.inflate(R.layout.fragment_bookmarks, container, false); + } + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + if (disposables != null) disposables.clear(); + if (databaseSubscription != null) databaseSubscription.cancel(); + + super.onDestroyView(); + } + + @Override + public void onDestroy() { + if (disposables != null) disposables.dispose(); + if (databaseSubscription != null) databaseSubscription.cancel(); + + disposables = null; + databaseSubscription = null; + localPlaylistManager = null; + + super.onDestroy(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Views + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + infoListAdapter = new InfoListAdapter(getActivity()); + itemsList = rootView.findViewById(R.id.items_list); + itemsList.setLayoutManager(new LinearLayoutManager(activity)); + + final View headerRootLayout = activity.getLayoutInflater() + .inflate(R.layout.bookmark_header, itemsList, false); + watchHistoryButton = headerRootLayout.findViewById(R.id.watchHistory); + mostWatchedButton = headerRootLayout.findViewById(R.id.mostWatched); + + infoListAdapter.setHeader(headerRootLayout); + infoListAdapter.useMiniItemVariants(true); + + itemsList.setAdapter(infoListAdapter); + } + + @Override + protected void initListeners() { + super.initListeners(); + + infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(PlaylistInfoItem selectedItem) { + // Requires the parent fragment to find holder for fragment replacement + if (selectedItem instanceof LocalPlaylistInfoItem && getParentFragment() != null) { + final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); + + NavigationHelper.openLocalPlaylistFragment( + getParentFragment().getFragmentManager(), + playlistId, + selectedItem.getName() + ); + } + } + + @Override + public void held(PlaylistInfoItem selectedItem) { + if (selectedItem instanceof LocalPlaylistInfoItem) { + showPlaylistDialog((LocalPlaylistInfoItem) selectedItem); + } + } + }); + + watchHistoryButton.setOnClickListener(view -> { + if (getParentFragment() != null) { + NavigationHelper.openWatchHistoryFragment(getParentFragment().getFragmentManager()); + } + }); + + mostWatchedButton.setOnClickListener(view -> { + if (getParentFragment() != null) { + NavigationHelper.openMostPlayedFragment(getParentFragment().getFragmentManager()); + } + }); + } + + private void showPlaylistDialog(final LocalPlaylistInfoItem item) { + final Context context = getContext(); + if (context == null || context.getResources() == null || getActivity() == null) return; + + final String[] commands = new String[]{ + context.getResources().getString(R.string.delete_playlist) + }; + + final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { + switch (i) { + case 0: + final Toast deleteSuccessful = + Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT); + disposables.add(localPlaylistManager.deletePlaylist(item.getPlaylistId()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> deleteSuccessful.show())); + break; + default: + break; + } + }; + + final String videoCount = getResources().getQuantityString(R.plurals.videos, + (int) item.getStreamCount(), (int) item.getStreamCount()); + new InfoItemDialog(getActivity(), commands, actions, item.getName(), videoCount).show(); + } + + private void resetFragment() { + if (disposables != null) disposables.clear(); + if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + } + + /////////////////////////////////////////////////////////////////////////// + // Subscriptions Loader + /////////////////////////////////////////////////////////////////////////// + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + resetFragment(); + + localPlaylistManager.getPlaylists() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getSubscriptionSubscriber()); + } + + private Subscriber> getSubscriptionSubscriber() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List subscriptions) { + handleResult(subscriptions); + if (databaseSubscription != null) databaseSubscription.request(1); + } + + @Override + public void onError(Throwable exception) { + BookmarkFragment.this.onError(exception); + } + + @Override + public void onComplete() { + } + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + + infoListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + } else { + infoListAdapter.addInfoItemList(infoItemsOf(result)); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + hideLoading(); + } + } + + + private List infoItemsOf(List playlists) { + List playlistInfoItems = new ArrayList<>(playlists.size()); + for (final PlaylistMetadataEntry playlist : playlists) { + playlistInfoItems.add(playlist.toStoredPlaylistInfoItem()); + } + Collections.sort(playlistInfoItems, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); + return playlistInfoItems; + } + + /*////////////////////////////////////////////////////////////////////////// + // 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(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected boolean onError(Throwable exception) { + resetFragment(); + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, + "none", "Bookmark", R.string.general_error); + return true; + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java new file mode 100644 index 000000000..3941df6c0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java @@ -0,0 +1,323 @@ +package org.schabi.newpipe.fragments.local; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.SinglePlayQueue; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.NavigationHelper; + +import java.util.ArrayList; +import java.util.List; + +import icepick.State; +import io.reactivex.android.schedulers.AndroidSchedulers; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public abstract class HistoryPlaylistFragment + extends BaseListFragment, Void> { + + private View headerRootLayout; + private View playlistControl; + private View headerPlayAllButton; + private View headerPopupButton; + private View headerBackgroundButton; + + @State + protected Parcelable itemsListState; + + /* Used for independent events */ + private Subscription databaseSubscription; + private StreamRecordManager recordManager; + + /////////////////////////////////////////////////////////////////////////// + // Abstracts + /////////////////////////////////////////////////////////////////////////// + + protected abstract String getName(); + + protected abstract List processResult(final List results); + + protected abstract String getAdditionalDetail(final StreamStatisticsInfoItem infoItem); + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onAttach(Context context) { + super.onAttach(context); + recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_playlist, container, false); + } + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + if (databaseSubscription != null) databaseSubscription.cancel(); + super.onDestroyView(); + } + + @Override + public void onDestroy() { + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = null; + recordManager = null; + + super.onDestroy(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Views + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + infoListAdapter.useMiniItemVariants(true); + + setFragmentTitle(getName()); + } + + @Override + protected View getListHeader() { + headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control, + itemsList, false); + playlistControl = headerRootLayout.findViewById(R.id.playlist_control); + headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); + headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); + headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); + + return headerRootLayout; + } + + @Override + protected void initListeners() { + super.initListeners(); + + infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(StreamInfoItem selectedItem) { + if (getParentFragment() == null) return; + // Requires the parent fragment to find holder for fragment replacement + NavigationHelper.openVideoDetailFragment(getParentFragment().getFragmentManager(), + selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); + } + + @Override + public void held(StreamInfoItem selectedItem) { + showStreamDialog(selectedItem); + } + }); + + } + + @Override + protected void showStreamDialog(final StreamInfoItem item) { + final Context context = getContext(); + final Activity activity = getActivity(); + if (context == null || context.getResources() == null + || getActivity() == null || !(item instanceof StreamStatisticsInfoItem)) return; + + final String[] commands = new String[]{ + context.getResources().getString(R.string.enqueue_on_background), + context.getResources().getString(R.string.enqueue_on_popup), + context.getResources().getString(R.string.start_here_on_main), + context.getResources().getString(R.string.start_here_on_background), + context.getResources().getString(R.string.start_here_on_popup), + }; + + final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { + final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + break; + case 2: + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + break; + case 3: + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + break; + case 4: + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + break; + default: + break; + } + }; + + final String detail = getAdditionalDetail((StreamStatisticsInfoItem) item); + new InfoItemDialog(getActivity(), commands, actions, item.getName(), detail).show(); + } + + private void resetFragment() { + if (databaseSubscription != null) databaseSubscription.cancel(); + if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + } + + /////////////////////////////////////////////////////////////////////////// + // Loader + /////////////////////////////////////////////////////////////////////////// + + @Override + public void showLoading() { + super.showLoading(); + animateView(headerRootLayout, false, 200); + animateView(itemsList, false, 100); + } + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + resetFragment(); + + recordManager.getStatistics() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getHistoryObserver()); + } + + private Subscriber> getHistoryObserver() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List streams) { + handleResult(streams); + if (databaseSubscription != null) databaseSubscription.request(1); + } + + @Override + public void onError(Throwable exception) { + HistoryPlaylistFragment.this.onError(exception); + } + + @Override + public void onComplete() { + } + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + infoListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + return; + } + + animateView(headerRootLayout, true, 100); + animateView(itemsList, true, 300); + + infoListAdapter.addInfoItemList(processResult(result)); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + + playlistControl.setVisibility(View.VISIBLE); + headerPlayAllButton.setOnClickListener(view -> + NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener(view -> + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener(view -> + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + hideLoading(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void loadMoreItems() { + // Do nothing + } + + @Override + protected boolean hasMoreItems() { + return false; + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected boolean onError(Throwable exception) { + resetFragment(); + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, + "none", "History", R.string.general_error); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + protected void setFragmentTitle(final String title) { + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setTitle(title); + } + } + + private PlayQueue getPlayQueue() { + return getPlayQueue(0); + } + + private PlayQueue getPlayQueue(final int index) { + final List infoItems = infoListAdapter.getItemsList(); + List streamInfoItems = new ArrayList<>(infoItems.size()); + for (final InfoItem item : infoItems) { + if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + } + return new SinglePlayQueue(streamInfoItems, index); + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java new file mode 100644 index 000000000..6709b1bad --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -0,0 +1,356 @@ +package org.schabi.newpipe.fragments.local; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.SinglePlayQueue; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.NavigationHelper; + +import java.util.ArrayList; +import java.util.List; + +import icepick.State; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class LocalPlaylistFragment extends BaseListFragment, Void> { + + private View headerRootLayout; + private TextView headerTitleView; + private TextView headerStreamCount; + private View playlistControl; + + private View headerPlayAllButton; + private View headerPopupButton; + private View headerBackgroundButton; + + @State + protected long playlistId; + @State + protected String name; + @State + protected Parcelable itemsListState; + + /* Used for independent events */ + private CompositeDisposable disposables = new CompositeDisposable(); + private Subscription databaseSubscription; + private LocalPlaylistManager playlistManager; + + public static LocalPlaylistFragment getInstance(long playlistId, String name) { + LocalPlaylistFragment instance = new LocalPlaylistFragment(); + instance.setInitialData(playlistId, name); + return instance; + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onAttach(Context context) { + super.onAttach(context); + playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(context)); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_playlist, container, false); + } + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + if (disposables != null) disposables.clear(); + + super.onDestroyView(); + } + + @Override + public void onDestroy() { + if (disposables != null) disposables.dispose(); + if (databaseSubscription != null) databaseSubscription.cancel(); + + disposables = null; + databaseSubscription = null; + playlistManager = null; + + super.onDestroy(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Views + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + infoListAdapter.useMiniItemVariants(true); + + setFragmentTitle(name); + } + + @Override + protected View getListHeader() { + headerRootLayout = activity.getLayoutInflater().inflate(R.layout.local_playlist_header, + itemsList, false); + + headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); + headerTitleView.setSelected(true); + + headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count); + playlistControl = headerRootLayout.findViewById(R.id.playlist_control); + headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); + headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); + headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); + + return headerRootLayout; + } + + @Override + protected void initListeners() { + super.initListeners(); + + infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(StreamInfoItem selectedItem) { + if (getParentFragment() == null) return; + // Requires the parent fragment to find holder for fragment replacement + NavigationHelper.openVideoDetailFragment(getParentFragment().getFragmentManager(), + selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); + } + + @Override + public void held(StreamInfoItem selectedItem) { + showStreamDialog(selectedItem); + } + }); + + } + + @Override + protected void showStreamDialog(final StreamInfoItem item) { + final Context context = getContext(); + final Activity activity = getActivity(); + if (context == null || context.getResources() == null || getActivity() == null) return; + + final String[] commands = new String[]{ + context.getResources().getString(R.string.enqueue_on_background), + context.getResources().getString(R.string.enqueue_on_popup), + context.getResources().getString(R.string.start_here_on_main), + context.getResources().getString(R.string.start_here_on_background), + context.getResources().getString(R.string.start_here_on_popup), + }; + + final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { + final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + break; + case 2: + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + break; + case 3: + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + break; + case 4: + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + break; + default: + break; + } + }; + + new InfoItemDialog(getActivity(), item, commands, actions).show(); + } + + private void resetFragment() { + if (disposables != null) disposables.clear(); + if (databaseSubscription != null) databaseSubscription.cancel(); + if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + } + + /////////////////////////////////////////////////////////////////////////// + // Loader + /////////////////////////////////////////////////////////////////////////// + + @Override + public void showLoading() { + super.showLoading(); + animateView(headerRootLayout, false, 200); + animateView(itemsList, false, 100); + } + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + resetFragment(); + + playlistManager.getPlaylist(playlistId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistObserver()); + } + + private Subscriber> getPlaylistObserver() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List streams) { + handleResult(streams); + if (databaseSubscription != null) databaseSubscription.request(1); + } + + @Override + public void onError(Throwable exception) { + LocalPlaylistFragment.this.onError(exception); + } + + @Override + public void onComplete() { + } + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + infoListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + return; + } + + animateView(headerRootLayout, true, 100); + animateView(itemsList, true, 300); + + infoListAdapter.addInfoItemList(getStreamItems(result)); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + + playlistControl.setVisibility(View.VISIBLE); + headerStreamCount.setText( + getResources().getQuantityString(R.plurals.videos, result.size(), result.size())); + + headerPlayAllButton.setOnClickListener(view -> + NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener(view -> + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener(view -> + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + hideLoading(); + } + + + private List getStreamItems(final List streams) { + List items = new ArrayList<>(streams.size()); + for (final StreamEntity stream : streams) { + items.add(stream.toStreamInfoItem()); + } + return items; + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void loadMoreItems() { + // Do nothing + } + + @Override + protected boolean hasMoreItems() { + return false; + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected boolean onError(Throwable exception) { + resetFragment(); + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, + "none", "Subscriptions", R.string.general_error); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + protected void setInitialData(long playlistId, String name) { + this.playlistId = playlistId; + this.name = !TextUtils.isEmpty(name) ? name : ""; + } + + protected void setFragmentTitle(final String title) { + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setTitle(title); + } + if (headerTitleView != null) { + headerTitleView.setText(title); + } + } + + private PlayQueue getPlayQueue() { + return getPlayQueue(0); + } + + private PlayQueue getPlayQueue(final int index) { + final List infoItems = infoListAdapter.getItemsList(); + List streamInfoItems = new ArrayList<>(infoItems.size()); + for (final InfoItem item : infoItems) { + if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + } + return new SinglePlayQueue(streamInfoItems, index); + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java similarity index 84% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index 911b3c7fd..bf7bc14c8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.playlist; +package org.schabi.newpipe.fragments.local; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; @@ -13,8 +13,9 @@ import java.util.ArrayList; import java.util.List; import io.reactivex.Completable; +import io.reactivex.Flowable; import io.reactivex.Maybe; -import io.reactivex.Scheduler; +import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; public class LocalPlaylistManager { @@ -74,9 +75,16 @@ public class LocalPlaylistManager { })); } - public Maybe> getPlaylists() { - return playlistStreamTable.getPlaylistMetadata() - .firstElement() + public Flowable> getPlaylists() { + return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); + } + + public Flowable> getPlaylist(final long playlistId) { + return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); + } + + public Single deletePlaylist(final long playlistId) { + return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId)) .subscribeOn(Schedulers.io()); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java new file mode 100644 index 000000000..466b1d569 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java @@ -0,0 +1,35 @@ +package org.schabi.newpipe.fragments.local; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MostPlayedFragment extends HistoryPlaylistFragment { + @Override + protected String getName() { + return getString(R.string.title_most_played); + } + + @Override + protected List processResult(List results) { + Collections.sort(results, (left, right) -> + ((Long) right.watchCount).compareTo(left.watchCount)); + + List items = new ArrayList<>(results.size()); + for (final StreamStatisticsEntry stream : results) { + items.add(stream.toStreamStatisticsInfoItem()); + } + return items; + } + + @Override + protected String getAdditionalDetail(StreamStatisticsInfoItem infoItem) { + final int watchCount = (int) infoItem.getWatchCount(); + return getResources().getQuantityString(R.plurals.views, watchCount, watchCount); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index bee3b347e..6fad839f1 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.playlist; +package org.schabi.newpipe.fragments.local; import android.content.Context; import android.os.Bundle; @@ -113,6 +113,7 @@ public class PlaylistAppendDialog extends DialogFragment { .subscribe(metadataEntries -> { if (metadataEntries.isEmpty()) { openCreatePlaylistDialog(); + return; } List playlistInfoItems = new ArrayList<>(metadataEntries.size()); @@ -123,8 +124,6 @@ public class PlaylistAppendDialog extends DialogFragment { playlistAdapter.clearStreamItemList(); playlistAdapter.addInfoItemList(playlistInfoItems); playlistRecyclerView.setVisibility(View.VISIBLE); - - getDialog().setCanceledOnTouchOutside(true); }); } @@ -141,7 +140,7 @@ public class PlaylistAppendDialog extends DialogFragment { public void openCreatePlaylistDialog() { if (streamInfo == null || getFragmentManager() == null) return; - getDialog().dismiss(); PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG); + getDialog().dismiss(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index 15e787e2a..843b84de6 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.playlist; +package org.schabi.newpipe.fragments.local; import android.app.AlertDialog; import android.app.Dialog; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java similarity index 56% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java index 31f6284eb..458ec4da2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.playlist; +package org.schabi.newpipe.fragments.local; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; @@ -7,14 +7,12 @@ import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; import java.util.Date; import java.util.List; -import io.reactivex.MaybeObserver; +import io.reactivex.Flowable; import io.reactivex.Single; -import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; public class StreamRecordManager { @@ -29,11 +27,6 @@ public class StreamRecordManager { historyTable = db.streamHistoryDAO(); } - public int onChanged(final StreamInfoItem infoItem) { - // Only existing streams are updated - return streamTable.update(new StreamEntity(infoItem)); - } - public Single onViewed(final StreamInfo info) { return Single.fromCallable(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); @@ -45,30 +38,7 @@ public class StreamRecordManager { return historyTable.deleteHistory(streamId); } - public void removeRecord() { - historyTable.getStatistics().firstElement().subscribe( - new MaybeObserver>() { - - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onSuccess(List streamStatisticsEntries) { - hashCode(); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - } - ); + public Flowable> getStatistics() { + return historyTable.getStatistics(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java new file mode 100644 index 000000000..794872954 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java @@ -0,0 +1,36 @@ +package org.schabi.newpipe.fragments.local; + +import android.text.format.DateFormat; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class WatchHistoryFragment extends HistoryPlaylistFragment { + @Override + protected String getName() { + return getString(R.string.title_watch_history); + } + + @Override + protected List processResult(List results) { + Collections.sort(results, (left, right) -> + right.latestAccessDate.compareTo(left.latestAccessDate)); + + List items = new ArrayList<>(results.size()); + for (final StreamStatisticsEntry stream : results) { + items.add(stream.toStreamStatisticsInfoItem()); + } + return items; + } + + @Override + protected String getAdditionalDetail(StreamStatisticsInfoItem infoItem) { + return DateFormat.getLongDateFormat(getContext()).format(infoItem.getLatestAccessDate()); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java index 2e8919575..50b551c61 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java @@ -47,6 +47,14 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder { itemBuilder.getOnPlaylistSelectedListener().selected(item); } }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnPlaylistSelectedListener() != null) { + itemBuilder.getOnPlaylistSelectedListener().held(item); + } + return true; + }); } /** 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 ca863fc8a..3cf169ecd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -64,7 +64,7 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.fragments.playlist.StreamRecordManager; +import org.schabi.newpipe.fragments.local.StreamRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; @@ -676,7 +676,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe()); - recordManager.removeRecord(); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java index ae74528eb..9c4d2fb39 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java @@ -3,19 +3,29 @@ package org.schabi.newpipe.playlist; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; public final class SinglePlayQueue extends PlayQueue { public SinglePlayQueue(final StreamInfoItem item) { - this(new PlayQueueItem(item)); + super(0, Collections.singletonList(new PlayQueueItem(item))); } public SinglePlayQueue(final StreamInfo info) { - this(new PlayQueueItem(info)); + super(0, Collections.singletonList(new PlayQueueItem(info))); } - private SinglePlayQueue(final PlayQueueItem playQueueItem) { - super(0, Collections.singletonList(playQueueItem)); + public SinglePlayQueue(final List items, final int index) { + super(index, playQueueItemsOf(items)); + } + + private static List playQueueItemsOf(List items) { + List playQueueItems = new ArrayList<>(items.size()); + for (final StreamInfoItem item : items) { + playQueueItems.add(new PlayQueueItem(item)); + } + return playQueueItems; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 8894af9df..7ffbf07ed 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -34,6 +34,9 @@ import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; +import org.schabi.newpipe.fragments.local.LocalPlaylistFragment; +import org.schabi.newpipe.fragments.local.MostPlayedFragment; +import org.schabi.newpipe.fragments.local.WatchHistoryFragment; import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; @@ -323,6 +326,30 @@ public class NavigationHelper { .commit(); } + public static void openLocalPlaylistFragment(FragmentManager fragmentManager, long playlistId, String name) { + if (name == null) name = ""; + fragmentManager.beginTransaction() + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + .replace(R.id.fragment_holder, LocalPlaylistFragment.getInstance(playlistId, name)) + .addToBackStack(null) + .commit(); + } + + public static void openWatchHistoryFragment(FragmentManager fragmentManager) { + fragmentManager.beginTransaction() + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + .replace(R.id.fragment_holder, new WatchHistoryFragment()) + .addToBackStack(null) + .commit(); + } + + public static void openMostPlayedFragment(FragmentManager fragmentManager) { + fragmentManager.beginTransaction() + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + .replace(R.id.fragment_holder, new MostPlayedFragment()) + .addToBackStack(null) + .commit(); + } /*////////////////////////////////////////////////////////////////////////// // Through Intents //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/res/layout/bookmark_header.xml b/app/src/main/res/layout/bookmark_header.xml new file mode 100644 index 000000000..b087a5157 --- /dev/null +++ b/app/src/main/res/layout/bookmark_header.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bookmarks.xml b/app/src/main/res/layout/fragment_bookmarks.xml new file mode 100644 index 000000000..56e13225f --- /dev/null +++ b/app/src/main/res/layout/fragment_bookmarks.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml index 0868d8233..d45060440 100644 --- a/app/src/main/res/layout/fragment_feed.xml +++ b/app/src/main/res/layout/fragment_feed.xml @@ -26,7 +26,7 @@ diff --git a/app/src/main/res/layout/local_playlist_header.xml b/app/src/main/res/layout/local_playlist_header.xml new file mode 100644 index 000000000..0ceee5d9a --- /dev/null +++ b/app/src/main/res/layout/local_playlist_header.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..14216dd88 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -119,12 +119,14 @@ subscription_page_key kiosk_page channel_page + bookmark_page @string/blank_page_key @string/kiosk_page_key @string/feed_page_key @string/subscription_page_key @string/channel_page_key + @string/bookmark_page_key main_page_selected_service main_page_selected_channel_name diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c94453570..df5b15c19 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ Main Subscriptions + Bookmarks What\'s New @@ -304,6 +305,8 @@ History cleared Item deleted Do you want to delete this item from search history? + Watch History + Most Played Content of main page @@ -370,5 +373,6 @@ Create New Playlist + Delete Playlist Name From 3c314ced0ae0896e0dbc3c1a0246f311da867045 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 17 Jan 2018 13:24:59 -0800 Subject: [PATCH 04/52] -Bump database version to 2. -Added migration script for upgrading database from version 1 to 2. -Fixed database name of stream type in stream entity. --- .../org/schabi/newpipe/NewPipeDatabase.java | 8 ++-- .../schabi/newpipe/database/AppDatabase.java | 10 ++-- .../schabi/newpipe/database/Migrations.java | 47 +++++++++++++++++++ .../database/stream/dao/StreamHistoryDAO.java | 2 +- .../database/stream/model/StreamEntity.java | 4 +- .../fragments/local/StreamRecordManager.java | 2 +- 6 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/Migrations.java diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 4da1c63f2..15d9cf389 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -7,6 +7,7 @@ import android.support.annotation.NonNull; import org.schabi.newpipe.database.AppDatabase; import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; +import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12; public final class NewPipeDatabase { @@ -17,9 +18,10 @@ public final class NewPipeDatabase { } public static void init(Context context) { - databaseInstance = Room.databaseBuilder(context.getApplicationContext(), - AppDatabase.class, DATABASE_NAME - ).build(); + databaseInstance = Room + .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) + .addMigrations(MIGRATION_11_12) + .build(); } @NonNull diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index dedbfbf68..d5a9164dc 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -10,17 +10,19 @@ import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.WatchHistoryEntry; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; -import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; -import org.schabi.newpipe.database.stream.dao.StreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.dao.StreamStateDAO; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; +import static org.schabi.newpipe.database.Migrations.DB_VER_12_0; + @TypeConverters({Converters.class}) @Database( entities = { @@ -28,7 +30,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, PlaylistEntity.class, PlaylistStreamEntity.class }, - version = 1, + version = DB_VER_12_0, exportSchema = false ) public abstract class AppDatabase extends RoomDatabase { diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java new file mode 100644 index 000000000..f1aa52392 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.database; + +import android.arch.persistence.db.SupportSQLiteDatabase; +import android.arch.persistence.room.migration.Migration; +import android.support.annotation.NonNull; + +public class Migrations { + + public static final int DB_VER_11_0 = 1; + public static final int DB_VER_12_0 = 2; + + public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); + database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); + database.execSQL("CREATE INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)"); + database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `stream_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); + database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)"); + database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)"); + + // Populate streams table with existing entries in watch history + // Latest data first, thus ignoring older entries with the same indices + database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " + + "stream_type, duration, uploader, thumbnail_url) " + + + "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + + "uploader, thumbnail_url " + + + "FROM watch_history " + + "ORDER BY creation_date DESC"); + + // Once the streams have PKs, join them with the normalized history table + // and populate it with the remaining data from watch history + database.execSQL("INSERT INTO stream_history (stream_id, access_date)" + + "SELECT uid, creation_date " + + "FROM watch_history INNER JOIN streams " + + "ON watch_history.service_id == streams.service_id " + + "AND watch_history.url == streams.url " + + "ORDER BY creation_date DESC"); + } + }; +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java index 522c03522..527d151ea 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java @@ -37,7 +37,7 @@ public abstract class StreamHistoryDAO implements BasicDAO } @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - public abstract int deleteHistory(final long streamId); + public abstract int deleteStreamHistory(final long streamId); @Query("SELECT * FROM " + STREAM_TABLE + diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java index 27d0aa7e1..c7ef889b9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -24,9 +24,9 @@ public class StreamEntity { final public static String STREAM_SERVICE_ID = "service_id"; final public static String STREAM_URL = "url"; final public static String STREAM_TITLE = "title"; - final public static String STREAM_TYPE = "streamType"; - final public static String STREAM_UPLOADER = "uploader"; + final public static String STREAM_TYPE = "stream_type"; final public static String STREAM_DURATION = "duration"; + final public static String STREAM_UPLOADER = "uploader"; final public static String STREAM_THUMBNAIL_URL = "thumbnail_url"; @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java index 458ec4da2..993ed58da 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java @@ -35,7 +35,7 @@ public class StreamRecordManager { } public int removeHistory(final long streamId) { - return historyTable.deleteHistory(streamId); + return historyTable.deleteStreamHistory(streamId); } public Flowable> getStatistics() { From 4ae81a2de428aca9a518341b5ee66066e26296a0 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 17 Jan 2018 13:53:32 -0800 Subject: [PATCH 05/52] -Deprecating database get instance without context. -Added comments to migrations. --- app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java | 2 ++ .../main/java/org/schabi/newpipe/database/Migrations.java | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 15d9cf389..9cd56fca4 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -21,10 +21,12 @@ public final class NewPipeDatabase { databaseInstance = Room .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) .addMigrations(MIGRATION_11_12) + .fallbackToDestructiveMigration() .build(); } @NonNull + @Deprecated public static AppDatabase getInstance() { if (databaseInstance == null) throw new RuntimeException("Database not initialized"); diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index f1aa52392..72b0d2126 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -12,6 +12,14 @@ public class Migrations { public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { + /* + * Unfortunately these queries must be hardcoded due to the possibility of + * schema and names changing at a later date, thus invalidating the older migration + * scripts if names are not hardcoded. + * */ + + // Not much we can do about this, since room doesn't create tables before migration. + // It's either this or blasting the entire database anew. database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); From 9bd26798b669296acbcd3021cee2427e4bcf8bb8 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 17 Jan 2018 14:32:09 -0800 Subject: [PATCH 06/52] -Added icon for adding stream to playlist. -Renamed HistoryPlaylistFragment to StatisticsPlaylistFragment. --- .../fragments/detail/VideoDetailFragment.java | 9 ++++++++ .../local/LocalPlaylistFragment.java | 3 +-- .../fragments/local/MostPlayedFragment.java | 2 +- ...t.java => StatisticsPlaylistFragment.java} | 8 +++---- .../fragments/local/WatchHistoryFragment.java | 2 +- .../ic_playlist_add_black_24dp.png | Bin 0 -> 106 bytes .../ic_playlist_add_white_24dp.png | Bin 0 -> 107 bytes .../ic_playlist_add_black_24dp.png | Bin 0 -> 100 bytes .../ic_playlist_add_white_24dp.png | Bin 0 -> 101 bytes .../ic_playlist_add_black_24dp.png | Bin 0 -> 113 bytes .../ic_playlist_add_white_24dp.png | Bin 0 -> 109 bytes .../ic_playlist_add_black_24dp.png | Bin 0 -> 129 bytes .../ic_playlist_add_white_24dp.png | Bin 0 -> 113 bytes .../ic_playlist_add_black_24dp.png | Bin 0 -> 128 bytes .../ic_playlist_add_white_24dp.png | Bin 0 -> 111 bytes .../main/res/layout/fragment_video_detail.xml | 21 +++++++++++++++++- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/values/styles.xml | 2 ++ 19 files changed, 40 insertions(+), 10 deletions(-) rename app/src/main/java/org/schabi/newpipe/fragments/local/{HistoryPlaylistFragment.java => StatisticsPlaylistFragment.java} (96%) create mode 100644 app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_playlist_add_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_playlist_add_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_playlist_add_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_playlist_add_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_playlist_add_white_24dp.png diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index c7b61eceb..7f8afdbe8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -58,6 +58,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.history.HistoryListener; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; @@ -145,6 +146,7 @@ public class VideoDetailFragment extends BaseStateFragment implement private TextView detailControlsBackground; private TextView detailControlsPopup; + private TextView detailControlsAddToPlaylist; private TextView appendControlsDetail; private LinearLayout videoDescriptionRootLayout; @@ -327,6 +329,11 @@ public class VideoDetailFragment extends BaseStateFragment implement case R.id.detail_controls_popup: openPopupPlayer(false); break; + case R.id.detail_controls_playlist_append: + if (getFragmentManager() != null && currentInfo != null) { + PlaylistAppendDialog.newInstance(currentInfo).show(getFragmentManager(), TAG); + } + break; case R.id.detail_uploader_root_layout: if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { Log.w(TAG, "Can't open channel because we got no channel URL"); @@ -429,6 +436,7 @@ public class VideoDetailFragment extends BaseStateFragment implement detailControlsBackground = rootView.findViewById(R.id.detail_controls_background); detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup); + detailControlsAddToPlaylist = rootView.findViewById(R.id.detail_controls_playlist_append); appendControlsDetail = rootView.findViewById(R.id.touch_append_detail); videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); @@ -479,6 +487,7 @@ public class VideoDetailFragment extends BaseStateFragment implement thumbnailBackgroundButton.setOnClickListener(this); detailControlsBackground.setOnClickListener(this); detailControlsPopup.setOnClickListener(this); + detailControlsAddToPlaylist.setOnClickListener(this); relatedStreamExpandButton.setOnClickListener(this); detailControlsBackground.setLongClickable(true); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 6709b1bad..44ecfb924 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -144,9 +144,8 @@ public class LocalPlaylistFragment extends BaseListFragment, infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override public void selected(StreamInfoItem selectedItem) { - if (getParentFragment() == null) return; // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openVideoDetailFragment(getParentFragment().getFragmentManager(), + NavigationHelper.openVideoDetailFragment(getFragmentManager(), selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java index 466b1d569..7862cf2f4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java @@ -9,7 +9,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class MostPlayedFragment extends HistoryPlaylistFragment { +public class MostPlayedFragment extends StatisticsPlaylistFragment { @Override protected String getName() { return getString(R.string.title_most_played); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java similarity index 96% rename from app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java index 3941df6c0..8db1f8780 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java @@ -35,7 +35,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public abstract class HistoryPlaylistFragment +public abstract class StatisticsPlaylistFragment extends BaseListFragment, Void> { private View headerRootLayout; @@ -130,9 +130,7 @@ public abstract class HistoryPlaylistFragment infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override public void selected(StreamInfoItem selectedItem) { - if (getParentFragment() == null) return; - // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openVideoDetailFragment(getParentFragment().getFragmentManager(), + NavigationHelper.openVideoDetailFragment(getFragmentManager(), selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); } @@ -231,7 +229,7 @@ public abstract class HistoryPlaylistFragment @Override public void onError(Throwable exception) { - HistoryPlaylistFragment.this.onError(exception); + StatisticsPlaylistFragment.this.onError(exception); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java index 794872954..2a4b8cfb0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java @@ -11,7 +11,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class WatchHistoryFragment extends HistoryPlaylistFragment { +public class WatchHistoryFragment extends StatisticsPlaylistFragment { @Override protected String getName() { return getString(R.string.title_watch_history); diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..731b42590633cb2654a0c553b297f87e209cdf09 GIT binary patch literal 106 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;p{I*uNCjiEL{EZ9Pt*UFhZjsT zOs)xNu`hI-(x8|nu(04DJNtY`XSHi?yi#HbA`A=@6CWrnHJ!i))Xd=N>gTe~DWM4f D-jN$L literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4fb76e1784aa8d42add6d5dcfc91c304c6c201d8 GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>`VBp6OsFEs^HOeH~n!3+##lh0a!q&!_5Ln;`P z75Gp5|KI5IDCjdoD+{AZ(7*YA|FfkCDfxA%@G#8Nlb=@fz2!d06i-(_mvv4FO#njp B9#a4S literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d7a7514a84072ca393b32a30aa1427ab8ed37bb5 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1ZBG}+kP60R1*x8<|2Gs*O0lh! xcQDy0#lie4^T0b1ACG-(r~W$B^B(45U@*U(JU7SI#s;X3!PC{xWt~$(69A%_8>#>R literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..73c981285837f550453b25eda148c90f1b1c7a47 GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^5+KY7Bp6QcFoXgrrjj7PUx9{KW6@ll{Pre}Cj9mdKI;Vst09JY%g#Z8m literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..dc4ebe9f39a7bc76959e00f975a36880980654b4 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}t3r`ovkP61PmoIWMC3FG+?2Zo5J^ZOdkg}ZJ7n!(`d L>gTe~DWM4fRg5BO literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..52ccba0b2f500aab5cc7f89184de089425638fe7 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PUsElAj0bSzCLqp zSfb;PGdFyqYu2$dFl?Bu>@h(h;YxhjeQqHY4ONc`3m)8bpKNFb6tz|X5{^wQf7uz> YgL@iQo&P4j3uq>Tr>mdKI;Vst06PUJ-T(jq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3f652366df1d17852a763aa0634361a066ac41da GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^9w5vJBp7O^^}Pa8OeH~n!3+##lh0ZJd0L(>jv*C{ z$r4=+KkWr%6#89v?yWzOaFF4p&I--?AC7Dj&pFL-3cuFC$iUQ8>8mdK II;Vst04|sy&Hw-a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..46020a7e04f2cd8f9945abd5eb75e7917c2e22d4 GIT binary patch literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeK3?y%aJ*@^(YymzYu0R?HmZtAK52P4Ng8YIR z9G=}s19CJxT^vIy7?Tx*dz$_yI3)A^{rh?TG#Qo!w(Lo&92cfFZzy?D+$PV!;Kh90 TQvZJM5s*Qiu6{1-oD!MT$m`Z~Df*BafCZDwc@-#eM978G? zlNE$}n*Jv^B=i0K`+5E}8I}dM>`AH|7p66DD0xxbCeOg&#eCdS|9 + android:textSize="12sp"/> + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 61bc5e520..46676e200 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -26,6 +26,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df5b15c19..361f453c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ Background Popup + Add To Video download path Path to store downloaded videos in @@ -375,4 +376,5 @@ Create New Playlist Delete Playlist Name + Add To Playlist diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ee526ca41..1f79bbf3d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -41,6 +41,7 @@ @drawable/ic_play_arrow_black_24dp @drawable/ic_whatshot_black_24dp @drawable/ic_channel_black_24dp + @drawable/ic_playlist_add_black_24dp @color/light_separator_color @color/light_contrast_background_color @@ -88,6 +89,7 @@ @drawable/ic_play_arrow_white_24dp @drawable/ic_whatshot_white_24dp @drawable/ic_channel_white_24dp + @drawable/ic_playlist_add_white_24dp @color/dark_separator_color @color/dark_contrast_background_color From 168ac91ab8c25d5b47b4229aeffa6a3ef3e3c4dc Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 18 Jan 2018 11:02:06 -0800 Subject: [PATCH 07/52] -Fixed toast exception on playlist creation. --- .../newpipe/fragments/local/PlaylistCreationDialog.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index 843b84de6..c43ba25b8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -78,12 +78,13 @@ public class PlaylistCreationDialog extends DialogFragment { new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); final List streams = Collections.singletonList(new StreamEntity(streamInfo)); + final Toast successToast = Toast.makeText(getActivity(), + "Playlist " + name + " successfully created", + Toast.LENGTH_SHORT); playlistManager.createPlaylist(name, streams) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(longs -> Toast.makeText(getActivity(), - "Playlist " + name + " successfully created", - Toast.LENGTH_SHORT).show()); + .subscribe(longs -> successToast.show()); }); return dialogBuilder.create(); From 776dbc34f78ccab76979f3beffd4399c39cc6b4c Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 21 Jan 2018 19:32:49 -0800 Subject: [PATCH 08/52] -Added bulk playlist creation and append. -Added UI to create playlist from service player activity. -Added state saving to playlist dialogs. -Removed access to history activity on service player activity. -Made StreamEntity serializable. --- .../org/schabi/newpipe/NewPipeDatabase.java | 2 +- .../schabi/newpipe/database/Migrations.java | 2 +- .../database/stream/model/StreamEntity.java | 11 ++- .../fragments/detail/VideoDetailFragment.java | 3 +- .../fragments/local/LocalPlaylistManager.java | 48 ++++++------ .../fragments/local/PlaylistAppendDialog.java | 64 ++++++++-------- .../local/PlaylistCreationDialog.java | 44 ++--------- .../fragments/local/PlaylistDialog.java | 73 +++++++++++++++++++ .../org/schabi/newpipe/player/BasePlayer.java | 1 + .../newpipe/player/ServicePlayerActivity.java | 13 +++- .../newpipe/playlist/PlayQueueItem.java | 16 +++- app/src/main/res/menu/menu_play_queue.xml | 6 +- 12 files changed, 182 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 9cd56fca4..7b33d0c10 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -19,7 +19,7 @@ public final class NewPipeDatabase { public static void init(Context context) { databaseInstance = Room - .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) + .databaseBuilder(context, AppDatabase.class, DATABASE_NAME) .addMigrations(MIGRATION_11_12) .fallbackToDestructiveMigration() .build(); diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 72b0d2126..9200a64b0 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -15,7 +15,7 @@ public class Migrations { /* * Unfortunately these queries must be hardcoded due to the possibility of * schema and names changing at a later date, thus invalidating the older migration - * scripts if names are not hardcoded. + * scripts if they are not hardcoded. * */ // Not much we can do about this, since room doesn't create tables before migration. diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java index c7ef889b9..0b73e81e9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -9,15 +9,18 @@ import android.arch.persistence.room.PrimaryKey; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.Constants; +import java.io.Serializable; + import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; @Entity(tableName = STREAM_TABLE, indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)}) -public class StreamEntity { +public class StreamEntity implements Serializable { final public static String STREAM_TABLE = "streams"; final public static String STREAM_ID = "uid"; @@ -78,6 +81,12 @@ public class StreamEntity { info.uploader_name, info.duration); } + @Ignore + public StreamEntity(final PlayQueueItem item) { + this(item.getServiceId(), item.getTitle(), item.getUrl(), item.getStreamType(), + item.getThumbnailUrl(), item.getUploader(), item.getDuration()); + } + @Ignore public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { StreamInfoItem item = new StreamInfoItem( diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 7f8afdbe8..91299ac14 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -331,7 +331,8 @@ public class VideoDetailFragment extends BaseStateFragment implement break; case R.id.detail_controls_playlist_append: if (getFragmentManager() != null && currentInfo != null) { - PlaylistAppendDialog.newInstance(currentInfo).show(getFragmentManager(), TAG); + PlaylistAppendDialog.fromStreamInfo(currentInfo) + .show(getFragmentManager(), TAG); } break; case R.id.detail_uploader_root_layout: diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index bf7bc14c8..89d69d4b4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -33,34 +33,38 @@ public class LocalPlaylistManager { } public Maybe> createPlaylist(final String name, final List streams) { - // Disallow creation of empty playlists until user is able to select thumbnail + // Disallow creation of empty playlists if (streams.isEmpty()) return Maybe.empty(); final StreamEntity defaultStream = streams.get(0); - final PlaylistEntity newPlaylist = new PlaylistEntity(name, defaultStream.getThumbnailUrl()); + final PlaylistEntity newPlaylist = + new PlaylistEntity(name, defaultStream.getThumbnailUrl()); - return Maybe.fromCallable(() -> database.runInTransaction(() -> { - final long playlistId = playlistTable.insert(newPlaylist); - - List joinEntities = new ArrayList<>(streams.size()); - for (int index = 0; index < streams.size(); index++) { - // Upsert streams and get their ids - final long streamId = streamTable.upsert(streams.get(index)); - joinEntities.add(new PlaylistStreamEntity(playlistId, streamId, index)); - } - - return playlistStreamTable.insertAll(joinEntities); - })).subscribeOn(Schedulers.io()); + return Maybe.fromCallable(() -> database.runInTransaction(() -> + upsertStreams(playlistTable.insert(newPlaylist), streams, 0)) + ).subscribeOn(Schedulers.io()); } - public Maybe appendToPlaylist(final long playlistId, final StreamEntity stream) { - final Maybe streamIdFuture = Maybe.fromCallable(() -> streamTable.upsert(stream)); - final Maybe joinIndexFuture = - playlistStreamTable.getMaximumIndexOf(playlistId).firstElement(); + public Maybe> appendToPlaylist(final long playlistId, + final List streams) { + return playlistStreamTable.getMaximumIndexOf(playlistId) + .firstElement() + .map(maxJoinIndex -> database.runInTransaction(() -> + upsertStreams(playlistId, streams, maxJoinIndex + 1)) + ).subscribeOn(Schedulers.io()); + } - return Maybe.zip(streamIdFuture, joinIndexFuture, (streamId, currentMaxJoinIndex) -> - playlistStreamTable.insert(new PlaylistStreamEntity(playlistId, - streamId, currentMaxJoinIndex + 1)) - ).subscribeOn(Schedulers.io()); + private List upsertStreams(final long playlistId, + final List streams, + final int indexOffset) { + + List joinEntities = new ArrayList<>(streams.size()); + for (int index = 0; index < streams.size(); index++) { + // Upsert streams and get their ids + final long streamId = streamTable.upsert(streams.get(index)); + joinEntities.add(new PlaylistStreamEntity(playlistId, streamId, + index + indexOffset)); + } + return playlistStreamTable.insertAll(joinEntities); } public Completable updateJoin(final long playlistId, final List streamIds) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index 6fad839f1..de854ae0c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -4,7 +4,6 @@ import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -19,34 +18,48 @@ import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; +import org.schabi.newpipe.playlist.PlayQueueItem; -import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import io.reactivex.android.schedulers.AndroidSchedulers; -public class PlaylistAppendDialog extends DialogFragment { +public final class PlaylistAppendDialog extends PlaylistDialog { private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); - private static final String INFO_KEY = "info_key"; - private StreamInfo streamInfo; - - private View newPlaylistButton; private RecyclerView playlistRecyclerView; private InfoListAdapter playlistAdapter; - public static PlaylistAppendDialog newInstance(final StreamInfo info) { + public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) { PlaylistAppendDialog dialog = new PlaylistAppendDialog(); - dialog.setInfo(info); + dialog.setInfo(Collections.singletonList(new StreamEntity(info))); return dialog; } - private void setInfo(StreamInfo info) { - this.streamInfo = info; + public static PlaylistAppendDialog fromStreamInfoItems(final List items) { + PlaylistAppendDialog dialog = new PlaylistAppendDialog(); + List entities = new ArrayList<>(items.size()); + for (final StreamInfoItem item : items) { + entities.add(new StreamEntity(item)); + } + dialog.setInfo(entities); + return dialog; + } + + public static PlaylistAppendDialog fromPlayQueueItems(final List items) { + PlaylistAppendDialog dialog = new PlaylistAppendDialog(); + List entities = new ArrayList<>(items.size()); + for (final PlayQueueItem item : items) { + entities.add(new StreamEntity(item)); + } + dialog.setInfo(entities); + return dialog; } /*////////////////////////////////////////////////////////////////////////// @@ -60,14 +73,9 @@ public class PlaylistAppendDialog extends DialogFragment { playlistAdapter.useMiniItemVariants(true); } - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - Serializable serial = savedInstanceState.getSerializable(INFO_KEY); - if (serial instanceof StreamInfo) streamInfo = (StreamInfo) serial; - } - } + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -79,7 +87,7 @@ public class PlaylistAppendDialog extends DialogFragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - newPlaylistButton = view.findViewById(R.id.newPlaylist); + final View newPlaylistButton = view.findViewById(R.id.newPlaylist); playlistRecyclerView = view.findViewById(R.id.playlist_list); playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); playlistRecyclerView.setAdapter(playlistAdapter); @@ -92,12 +100,14 @@ public class PlaylistAppendDialog extends DialogFragment { playlistAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override public void selected(PlaylistInfoItem selectedItem) { - if (!(selectedItem instanceof LocalPlaylistInfoItem)) return; + if (!(selectedItem instanceof LocalPlaylistInfoItem) || getStreams() == null) + return; + final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); final Toast successToast = Toast.makeText(getContext(), "Added", Toast.LENGTH_SHORT); - playlistManager.appendToPlaylist(playlistId, new StreamEntity(streamInfo)) + playlistManager.appendToPlaylist(playlistId, getStreams()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> successToast.show()); @@ -127,20 +137,14 @@ public class PlaylistAppendDialog extends DialogFragment { }); } - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putSerializable(INFO_KEY, streamInfo); - } - /*////////////////////////////////////////////////////////////////////////// // Helper //////////////////////////////////////////////////////////////////////////*/ public void openCreatePlaylistDialog() { - if (streamInfo == null || getFragmentManager() == null) return; + if (getStreams() == null || getFragmentManager() == null) return; - PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG); + PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG); getDialog().dismiss(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index c43ba25b8..386ac1819 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -5,63 +5,35 @@ import android.app.Dialog; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; import android.view.View; import android.widget.EditText; import android.widget.Toast; -import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import java.util.Collections; import java.util.List; import io.reactivex.android.schedulers.AndroidSchedulers; -public class PlaylistCreationDialog extends DialogFragment { +public final class PlaylistCreationDialog extends PlaylistDialog { private static final String TAG = PlaylistCreationDialog.class.getCanonicalName(); - private static final boolean DEBUG = MainActivity.DEBUG; - private static final String INFO_KEY = "info_key"; - - private StreamInfo streamInfo; - - public static PlaylistCreationDialog newInstance(final StreamInfo info) { + public static PlaylistCreationDialog newInstance(final List streams) { PlaylistCreationDialog dialog = new PlaylistCreationDialog(); - dialog.setInfo(info); + dialog.setInfo(streams); return dialog; } - private void setInfo(final StreamInfo info) { - this.streamInfo = info; - } - /*////////////////////////////////////////////////////////////////////////// - // LifeCycle + // Dialog //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (streamInfo != null) { - outState.putSerializable(INFO_KEY, streamInfo); - } - } - @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - if (savedInstanceState != null && streamInfo == null) { - final Object infoCandidate = savedInstanceState.getSerializable(INFO_KEY); - if (infoCandidate != null && infoCandidate instanceof StreamInfo) { - streamInfo = (StreamInfo) infoCandidate; - } - } - - if (streamInfo == null) return super.onCreateDialog(savedInstanceState); + if (getStreams() == null) return super.onCreateDialog(savedInstanceState); View dialogView = View.inflate(getContext(), R.layout.dialog_create_playlist, null); @@ -76,13 +48,11 @@ public class PlaylistCreationDialog extends DialogFragment { final String name = nameInput.getText().toString(); final LocalPlaylistManager playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); - final List streams = - Collections.singletonList(new StreamEntity(streamInfo)); final Toast successToast = Toast.makeText(getActivity(), - "Playlist " + name + " successfully created", + "Playlist successfully created", Toast.LENGTH_SHORT); - playlistManager.createPlaylist(name, streams) + playlistManager.createPlaylist(name, getStreams()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(longs -> successToast.show()); }); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java new file mode 100644 index 000000000..010ba0181 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java @@ -0,0 +1,73 @@ +package org.schabi.newpipe.fragments.local; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; + +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.util.StateSaver; + +import java.util.List; +import java.util.Queue; + +public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead { + + private List streamEntities; + + private StateSaver.SavedState savedState; + + protected void setInfo(final List entities) { + this.streamEntities = entities; + } + + protected List getStreams() { + return streamEntities; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + savedState = StateSaver.tryToRestore(savedInstanceState, this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + StateSaver.onDestroy(savedState); + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public String generateSuffix() { + final int size = streamEntities == null ? 0 : streamEntities.size(); + return "." + size + ".list"; + } + + @Override + public void writeTo(Queue objectsToSave) { + objectsToSave.add(streamEntities); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull Queue savedObjects) throws Exception { + streamEntities = (List) savedObjects.poll(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (getActivity() != null) { + savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(), + savedState, outState, this); + } + } +} 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 3cf169ecd..a481b3335 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -675,6 +675,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen simpleExoPlayer.seekTo(currentSourceIndex, startPos); } + // TODO: update exoplayer to 2.6.x in order to register view count on repeated streams databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe()); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 4165dc087..6e0f5c1d7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.Player; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; @@ -149,8 +150,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity case android.R.id.home: finish(); return true; - case R.id.action_history: - NavigationHelper.openHistory(this); + case R.id.action_append_playlist: + appendToPlaylist(); return true; case R.id.action_settings: NavigationHelper.openSettings(this); @@ -185,6 +186,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity null ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } + + private void appendToPlaylist() { + if (this.player != null && this.player.getPlayQueue() != null) { + PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams()) + .show(getSupportFragmentManager(), getTag()); + } + } + //////////////////////////////////////////////////////////////////////////// // Service Connection //////////////////////////////////////////////////////////////////////////// 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 9b14e8f03..f8e7b8655 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java @@ -5,6 +5,7 @@ import android.support.annotation.Nullable; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.util.ExtractorHelper; import java.io.Serializable; @@ -23,6 +24,7 @@ public class PlayQueueItem implements Serializable { final private long duration; final private String thumbnailUrl; final private String uploader; + final private StreamType streamType; private long recoveryPosition; private Throwable error; @@ -30,22 +32,26 @@ public class PlayQueueItem implements Serializable { private transient Single stream; PlayQueueItem(@NonNull final StreamInfo info) { - this(info.getName(), info.getUrl(), info.getServiceId(), info.duration, info.thumbnail_url, info.uploader_name); + this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(), + info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType()); this.stream = Single.just(info); } PlayQueueItem(@NonNull final StreamInfoItem item) { - this(item.getName(), item.getUrl(), item.getServiceId(), item.duration, item.thumbnail_url, item.uploader_name); + this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(), + item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType()); } private PlayQueueItem(final String name, final String url, final int serviceId, - final long duration, final String thumbnailUrl, final String uploader) { + final long duration, final String thumbnailUrl, final String uploader, + final StreamType streamType) { this.title = name; this.url = url; this.serviceId = serviceId; this.duration = duration; this.thumbnailUrl = thumbnailUrl; this.uploader = uploader; + this.streamType = streamType; this.recoveryPosition = RECOVERY_UNSET; } @@ -78,6 +84,10 @@ public class PlayQueueItem implements Serializable { return uploader; } + public StreamType getStreamType() { + return streamType; + } + public long getRecoveryPosition() { return recoveryPosition; } diff --git a/app/src/main/res/menu/menu_play_queue.xml b/app/src/main/res/menu/menu_play_queue.xml index 671d46329..fb64cb9fa 100644 --- a/app/src/main/res/menu/menu_play_queue.xml +++ b/app/src/main/res/menu/menu_play_queue.xml @@ -1,11 +1,11 @@ + tools:context=".player.BackgroundPlayerActivity"> - Date: Mon, 22 Jan 2018 14:13:11 -0800 Subject: [PATCH 09/52] -Improved bulk stream upsert into playlist performance by 5x. -Added custom info item type for plain stream entity. --- .../database/stream/dao/StreamDAO.java | 45 ++++++++++++++----- .../database/stream/model/StreamEntity.java | 7 +-- .../local/LocalPlaylistFragment.java | 2 +- .../fragments/local/LocalPlaylistManager.java | 9 ++-- .../stored/LocalPlaylistInfoItem.java | 2 +- .../stored/StreamEntityInfoItem.java | 18 ++++++++ .../stored/StreamStatisticsInfoItem.java | 12 +---- 7 files changed, 64 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java index f7807ef42..ee246db1a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.database.stream.dao; import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; import android.arch.persistence.room.Query; import android.arch.persistence.room.Transaction; @@ -12,6 +14,7 @@ import java.util.List; import io.reactivex.Flowable; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; @@ -31,27 +34,47 @@ public abstract class StreamDAO implements BasicDAO { public abstract Flowable> listByService(int serviceId); @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + - STREAM_URL + " LIKE :url AND " + + STREAM_URL + " = :url AND " + STREAM_SERVICE_ID + " = :serviceId") public abstract Flowable> getStream(long serviceId, String url); - @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + - STREAM_URL + " LIKE :url AND " + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract void silentInsertAllInternal(final List streams); + + @Query("SELECT " + STREAM_ID + " FROM " + STREAM_TABLE + " WHERE " + + STREAM_URL + " = :url AND " + STREAM_SERVICE_ID + " = :serviceId") - abstract List getStreamInternal(long serviceId, String url); + abstract Long getStreamIdInternal(long serviceId, String url); @Transaction public long upsert(StreamEntity stream) { - final List streams = getStreamInternal(stream.getServiceId(), stream.getUrl()); + final Long streamIdCandidate = getStreamIdInternal(stream.getServiceId(), stream.getUrl()); - final long uid; - if (streams.isEmpty()) { - uid = insert(stream); + if (streamIdCandidate == null) { + return insert(stream); } else { - uid = streams.get(0).getUid(); - stream.setUid(uid); + stream.setUid(streamIdCandidate); update(stream); + return streamIdCandidate; } - return uid; + } + + @Transaction + public List upsertAll(List streams) { + silentInsertAllInternal(streams); + + final List streamIds = new ArrayList<>(streams.size()); + for (StreamEntity stream : streams) { + final Long streamId = getStreamIdInternal(stream.getServiceId(), stream.getUrl()); + if (streamId == null) { + throw new IllegalStateException("StreamID cannot be null just after insertion."); + } + + streamIds.add(streamId); + stream.setUid(streamId); + } + + update(streams); + return streamIds; } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java index 0b73e81e9..eb078a03c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -9,6 +9,7 @@ import android.arch.persistence.room.PrimaryKey; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.info_list.stored.StreamEntityInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.Constants; @@ -88,9 +89,9 @@ public class StreamEntity implements Serializable { } @Ignore - public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { - StreamInfoItem item = new StreamInfoItem( - getServiceId(), getUrl(), getTitle(), getStreamType()); + public StreamEntityInfoItem toStreamEntityInfoItem() throws IllegalArgumentException { + StreamEntityInfoItem item = new StreamEntityInfoItem(getUid(), getServiceId(), + getUrl(), getTitle(), getStreamType()); item.setThumbnailUrl(getThumbnailUrl()); item.setUploaderName(getUploader()); item.setDuration(getDuration()); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 44ecfb924..802532272 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -288,7 +288,7 @@ public class LocalPlaylistFragment extends BaseListFragment, private List getStreamItems(final List streams) { List items = new ArrayList<>(streams.size()); for (final StreamEntity stream : streams) { - items.add(stream.toStreamInfoItem()); + items.add(stream.toStreamEntityInfoItem()); } return items; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index 89d69d4b4..5633e104d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -58,10 +58,9 @@ public class LocalPlaylistManager { final int indexOffset) { List joinEntities = new ArrayList<>(streams.size()); - for (int index = 0; index < streams.size(); index++) { - // Upsert streams and get their ids - final long streamId = streamTable.upsert(streams.get(index)); - joinEntities.add(new PlaylistStreamEntity(playlistId, streamId, + final List streamIds = streamTable.upsertAll(streams); + for (int index = 0; index < streamIds.size(); index++) { + joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(index), index + indexOffset)); } return playlistStreamTable.insertAll(joinEntities); @@ -76,7 +75,7 @@ public class LocalPlaylistManager { return Completable.fromRunnable(() -> database.runInTransaction(() -> { playlistStreamTable.deleteBatch(playlistId); playlistStreamTable.insertAll(joinEntities); - })); + })).subscribeOn(Schedulers.io()); } public Flowable> getPlaylists() { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java index 3ac5fabb7..b0afe1948 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java @@ -5,7 +5,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import static org.schabi.newpipe.util.Constants.NO_SERVICE_ID; import static org.schabi.newpipe.util.Constants.NO_URL; -public class LocalPlaylistInfoItem extends PlaylistInfoItem { +public final class LocalPlaylistInfoItem extends PlaylistInfoItem { private final long playlistId; public LocalPlaylistInfoItem(final long playlistId, final String name) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java new file mode 100644 index 000000000..a54135211 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java @@ -0,0 +1,18 @@ +package org.schabi.newpipe.info_list.stored; + +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; + +public class StreamEntityInfoItem extends StreamInfoItem { + protected final long streamId; + + public StreamEntityInfoItem(final long streamId, final int serviceId, + final String url, final String name, final StreamType type) { + super(serviceId, url, name, type); + this.streamId = streamId; + } + + public long getStreamId() { + return streamId; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java index 76984d363..6659b551a 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java @@ -1,24 +1,16 @@ package org.schabi.newpipe.info_list.stored; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Date; -public class StreamStatisticsInfoItem extends StreamInfoItem { - private final long streamId; - +public final class StreamStatisticsInfoItem extends StreamEntityInfoItem { private Date latestAccessDate; private long watchCount; public StreamStatisticsInfoItem(final long streamId, final int serviceId, final String url, final String name, final StreamType type) { - super(serviceId, url, name, type); - this.streamId = streamId; - } - - public long getStreamId() { - return streamId; + super(streamId, serviceId, url, name, type); } public Date getLatestAccessDate() { From 81f481833c89ad2e86b95a139bc77007a9ecb9c7 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 22 Jan 2018 14:21:00 -0800 Subject: [PATCH 10/52] -Added icon for bookmark pager. --- .../schabi/newpipe/fragments/MainFragment.java | 5 +++-- .../res/drawable-hdpi/ic_bookmark_black_24dp.png | Bin 0 -> 180 bytes .../res/drawable-hdpi/ic_bookmark_white_24dp.png | Bin 0 -> 185 bytes .../res/drawable-mdpi/ic_bookmark_black_24dp.png | Bin 0 -> 137 bytes .../res/drawable-mdpi/ic_bookmark_white_24dp.png | Bin 0 -> 139 bytes .../res/drawable-xhdpi/ic_bookmark_black_24dp.png | Bin 0 -> 204 bytes .../res/drawable-xhdpi/ic_bookmark_white_24dp.png | Bin 0 -> 213 bytes .../drawable-xxhdpi/ic_bookmark_black_24dp.png | Bin 0 -> 261 bytes .../drawable-xxhdpi/ic_bookmark_white_24dp.png | Bin 0 -> 273 bytes .../drawable-xxxhdpi/ic_bookmark_black_24dp.png | Bin 0 -> 332 bytes .../drawable-xxxhdpi/ic_bookmark_white_24dp.png | Bin 0 -> 351 bytes app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/styles.xml | 2 ++ 13 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_bookmark_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_bookmark_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_bookmark_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_bookmark_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_bookmark_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_bookmark_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_bookmark_white_24dp.png diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index e76b97086..fc4f9a323 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -85,14 +85,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte int channelIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_channel); int whatsHotIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_hot); + int bookmarkIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_bookmark); if (isSubscriptionsPageOnlySelected()) { tabLayout.getTabAt(0).setIcon(channelIcon); - tabLayout.getTabAt(1).setText(R.string.tab_bookmarks); + tabLayout.getTabAt(1).setIcon(bookmarkIcon); } else { tabLayout.getTabAt(0).setIcon(whatsHotIcon); tabLayout.getTabAt(1).setIcon(channelIcon); - tabLayout.getTabAt(2).setText(R.string.tab_bookmarks); + tabLayout.getTabAt(2).setIcon(bookmarkIcon); } } diff --git a/app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad39da3adb4e7edea43312e46bac72af4a97395 GIT binary patch literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8wWo_?NCo5D^BV;Z83?#se9sW& zP-?M&`$|K$V2t6p*mkWz-Y%9#4-P+d{)d0h-ak{Ml&?5RAk|;#^bE_4Hd$q@OFh*K zrkx3J@;x)b;*0zV#devZ6`P9x=PaMs&ZVDM!_AQ@cPc05$Jzopr0DpEqQUCw| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..9de15c51a92bbc29536fc7a8e34da51f1b6961de GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8gQtsQh{y4_*EaGtCXxw1 zJlv$4!kF$T**4E|&JoEK`&v7DIc1K0vU?gE=foPj+|<4QurOEp;fvR2wk!!*Sl|(T zYJ#})J|0#>5s@Gd#a5AD?5A9Grg{A1oBPJ+{1E|*+Y6hgJ~Wk$v|JLPmeg(i-BtTS ikD%?Fw#fbFYh(9%S-#0z*fJ0376wmOKbLh*2~7a_ZbD%I literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0a10c249467ff61ec41efe8951720d964cf9b8e1 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+i08bakkP60Ri8~4t9;zO$*ZCmY zY#;Wu;OUGHs%5)Iaz?{r|?s z@c(}|W*=B5!xEQty@9*PJwv*lZbnu4RXIwb+Xpg(_C> zuBdKQG}`7|c7iKr!rhKFC*N5-Xkg}-DLBwraDb8hpJ;|gt(q#vkvvDKNDNO*IwoXI*-BA)z4*}Q$iB} Dg$qwX literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_bookmark_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..872349cca6ffc150af47fddd05a5cd9648619c1f GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DIi4<#ArXh)Uf9Tc$Uww3&^)Ep zfYG3F`bPtU9&fG$ZvRPo3JW%}#O(4AJh91FX;%5|n&%$pC6=ajnX<2YQuh4FvM1lQ z&rK=QRJaj**--w)>V{~$4=b2Xu+L*+;dr3x(7+&|;Ba7nL&x!?k4IiM_UXR;PzWCs&hKm{}Ung#;dGK1}q&RP1!jxG{&BN`jM9sN0LV@mN N@O1TaS?83{1OP}SQ~Ce^ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bookmark_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2189be346cf4bb7b0335b4e58e1c140790d1bd2d GIT binary patch literal 261 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw&Um^whEy=Vy?BuGkb_9e!~c(> znikbAnK?yak!*(HN>`4qMZQn8IFcM<{o`zG%=Szbn2|5_BT+zb;yWhqtUQE?^-f{N*#pR)!`v7Kfji}^^59SkVhFjUHx3vIVCg!0OO@*WB>pF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bookmark_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3faff90bb2b05245359ab9bc9ac54d7e7838f369 GIT binary patch literal 273 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawZg{#lhGg7(d(n}rDL|w(QT?V+ z*RA&1g`9$0o1=QNIhSZ;3HvoG2(8*5DR47*=X4i~|2KaB_~0@z$ZO>`L1XpjO-rS% zr}5ZY2=i5xCT0D%uC}ojy6s@RC;EniboqsAY*xFQ*O^_oE8D{RMZiO$kwwU3!Mx^q zOq?nc9GEz-6bnFEK<<pF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_bookmark_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2b90acd7446b6737f60905b2f51679e8626ababe GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z$onL;uuoF_~!iDz@rW_3?EGm zUNTfUu(%p)vPD%e9a<1CpO9?mqt$V2@8idOJZ!ByejDfKm23Y3g+IWy{qyf+mo zSWw08_2Q~`EHev-fP%w;=<5uOOhD#`d!`NzAZASoFHjuFoDWsv@JnA9J9H`$wr0 zZy3HcXmp(|G2Fn)!sA#jpO9_1M0>@t(jPOJxtvd=PO>&Pc|RxnzVQAh`+roY0l_g# z+i12;(l;(`PBDDD%=Z0`^ptaM?_NA(DzOQ;sC?I^A$rxT=L~ZUgRY)1k}vR0HUHV5 zu2p@GeNVtmJNXUEx6~ZCVB*lgz{te1$Bb7%0mRJrWoF?3G7nT=28nYBe7Fcz(gT%o zC}muWsi>^BNep8S44c-;+IzJ$YFi{=0KM{fM7-jHC@eP3T9gi}Gnx8UFc zErx>`P26mu{2lJX{Hx>)Doq|Pc*S_xU6^kbmq5Q-dcec(kJQ4;w e1!%j*4u4672{qE|*LeX0j=|H_&t;ucLK6V3Jc9`U literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 46676e200..e770cf102 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -26,6 +26,7 @@ + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 1f79bbf3d..bcbc759d2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -41,6 +41,7 @@ @drawable/ic_play_arrow_black_24dp @drawable/ic_whatshot_black_24dp @drawable/ic_channel_black_24dp + @drawable/ic_bookmark_black_24dp @drawable/ic_playlist_add_black_24dp @color/light_separator_color @@ -89,6 +90,7 @@ @drawable/ic_play_arrow_white_24dp @drawable/ic_whatshot_white_24dp @drawable/ic_channel_white_24dp + @drawable/ic_bookmark_white_24dp @drawable/ic_playlist_add_white_24dp @color/dark_separator_color From f0829f9ef37e61385d84c69c377a5d93bded0683 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 25 Jan 2018 22:24:59 -0800 Subject: [PATCH 11/52] -Added support for changing local playlist name and thumbnail url. -Added query to remove stream table orphans. -Added query for retrieving flattened watch history records. -Added holder for local playlist stream info items. -Refactored info item on select listener as on touch gesture. --- .../database/stream/StreamHistoryEntry.java | 47 ++++++++ .../database/stream/dao/StreamDAO.java | 24 +++++ .../database/stream/dao/StreamHistoryDAO.java | 7 ++ .../fragments/detail/VideoDetailFragment.java | 3 +- .../fragments/list/BaseListFragment.java | 19 +--- .../fragments/local/BookmarkFragment.java | 6 +- .../local/LocalPlaylistFragment.java | 8 +- .../fragments/local/LocalPlaylistManager.java | 28 ++++- .../fragments/local/PlaylistAppendDialog.java | 7 +- .../local/PlaylistCreationDialog.java | 3 +- .../local/StatisticsPlaylistFragment.java | 4 +- .../subscription/SubscriptionFragment.java | 21 ++-- .../newpipe/info_list/InfoItemBuilder.java | 23 ++-- .../newpipe/info_list/InfoListAdapter.java | 13 ++- .../newpipe/info_list/OnInfoItemGesture.java | 18 ++++ .../holder/StreamPlaylistInfoItemHolder.java | 102 ++++++++++++++++++ ..._playlist.xml => dialog_playlist_name.xml} | 1 - .../res/layout/list_stream_playlist_item.xml | 86 +++++++++++++++ app/src/main/res/values/strings.xml | 1 + 19 files changed, 353 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java rename app/src/main/res/layout/{dialog_create_playlist.xml => dialog_playlist_name.xml} (96%) create mode 100644 app/src/main/res/layout/list_stream_playlist_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java new file mode 100644 index 000000000..3df641372 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.database.stream; + +import android.arch.persistence.room.ColumnInfo; + +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.extractor.stream.StreamType; + +import java.util.Date; + +public class StreamHistoryEntry { + @ColumnInfo(name = StreamEntity.STREAM_ID) + final public long uid; + @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) + final public int serviceId; + @ColumnInfo(name = StreamEntity.STREAM_URL) + final public String url; + @ColumnInfo(name = StreamEntity.STREAM_TITLE) + final public String title; + @ColumnInfo(name = StreamEntity.STREAM_TYPE) + final public StreamType streamType; + @ColumnInfo(name = StreamEntity.STREAM_DURATION) + final public long duration; + @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) + final public String uploader; + @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) + final public String thumbnailUrl; + @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) + final public long streamId; + @ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE) + final public Date accessDate; + + public StreamHistoryEntry(long uid, int serviceId, String url, String title, + StreamType streamType, long duration, String uploader, + String thumbnailUrl, long streamId, Date accessDate) { + this.uid = uid; + this.serviceId = serviceId; + this.url = url; + this.title = title; + this.streamType = streamType; + this.duration = duration; + this.uploader = uploader; + this.thumbnailUrl = thumbnailUrl; + this.streamId = streamId; + this.accessDate = accessDate; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java index ee246db1a..a4955d835 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -7,17 +7,23 @@ import android.arch.persistence.room.Query; import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import java.util.ArrayList; import java.util.List; import io.reactivex.Flowable; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @Dao public abstract class StreamDAO implements BasicDAO { @@ -77,4 +83,22 @@ public abstract class StreamDAO implements BasicDAO { update(streams); return streamIds; } + + @Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID + + " NOT IN " + + "(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE + + + " LEFT JOIN " + STREAM_HISTORY_TABLE + + " ON " + STREAM_ID + " = " + + StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID + + + " LEFT JOIN " + STREAM_STATE_TABLE + + " ON " + STREAM_ID + " = " + + StreamStateEntity.STREAM_STATE_TABLE + "." + StreamStateEntity.JOIN_STREAM_ID + + + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + + " ON " + STREAM_ID + " = " + + PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID + + ")") + public abstract int deleteOrphans(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java index 527d151ea..81ee9d912 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java @@ -6,6 +6,7 @@ import android.arch.persistence.room.Query; import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.stream.StreamHistoryEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; @@ -36,6 +37,12 @@ public abstract class StreamHistoryDAO implements BasicDAO throw new UnsupportedOperationException(); } + @Query("SELECT * FROM " + STREAM_TABLE + + " INNER JOIN " + STREAM_HISTORY_TABLE + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " ORDER BY " + STREAM_ACCESS_DATE + " DESC") + public abstract Flowable> getHistory(); + @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") public abstract int deleteStreamHistory(final long streamId); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 91299ac14..05550a0a5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -62,6 +62,7 @@ import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.history.HistoryListener; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.player.helper.PlayerHelper; @@ -471,7 +472,7 @@ public class VideoDetailFragment extends BaseStateFragment implement @Override protected void initListeners() { super.initListeners(); - infoItemBuilder.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoItemBuilder.setOnStreamSelectedListener(new OnInfoItemGesture() { @Override public void selected(StreamInfoItem selectedItem) { selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); 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 a09a472a5..9e4fe89ab 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 @@ -3,19 +3,15 @@ package org.schabi.newpipe.fragments.list; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; -import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; 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.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.View; -import android.widget.TextView; -import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; @@ -24,12 +20,11 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.StateSaver; import java.util.List; @@ -140,7 +135,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { @Override public void selected(StreamInfoItem selectedItem) { onItemSelected(selectedItem); @@ -155,7 +150,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem } }); - infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnChannelSelectedListener(new OnInfoItemGesture() { @Override public void selected(ChannelInfoItem selectedItem) { onItemSelected(selectedItem); @@ -163,12 +158,9 @@ public abstract class BaseListFragment extends BaseStateFragment implem useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } - - @Override - public void held(ChannelInfoItem selectedItem) {} }); - infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture() { @Override public void selected(PlaylistInfoItem selectedItem) { onItemSelected(selectedItem); @@ -176,9 +168,6 @@ public abstract class BaseListFragment extends BaseStateFragment implem useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } - - @Override - public void held(PlaylistInfoItem selectedItem) {} }); itemsList.clearOnScrollListeners(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java index ecbd416ee..769365dd8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java @@ -21,9 +21,9 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; @@ -33,10 +33,8 @@ import java.util.Collections; 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 static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -143,7 +141,7 @@ public class BookmarkFragment extends BaseStateFragment() { + infoListAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture() { @Override public void selected(PlaylistInfoItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 802532272..7ba5db7e1 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -21,8 +21,8 @@ import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; @@ -141,7 +141,7 @@ public class LocalPlaylistFragment extends BaseListFragment, protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { @Override public void selected(StreamInfoItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement @@ -219,7 +219,7 @@ public class LocalPlaylistFragment extends BaseListFragment, super.startLoading(forceLoad); resetFragment(); - playlistManager.getPlaylist(playlistId) + playlistManager.getPlaylistStreams(playlistId) .observeOn(AndroidSchedulers.mainThread()) .subscribe(getPlaylistObserver()); } @@ -317,7 +317,7 @@ public class LocalPlaylistFragment extends BaseListFragment, if (super.onError(exception)) return true; onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, - "none", "Subscriptions", R.string.general_error); + "none", "Local Playlist", R.string.general_error); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index 5633e104d..4bc161c04 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.fragments.local; +import android.support.annotation.Nullable; + import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; @@ -82,7 +84,7 @@ public class LocalPlaylistManager { return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); } - public Flowable> getPlaylist(final long playlistId) { + public Flowable> getPlaylistStreams(final long playlistId) { return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); } @@ -90,4 +92,28 @@ public class LocalPlaylistManager { return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId)) .subscribeOn(Schedulers.io()); } + + public Maybe renamePlaylist(final long playlistId, final String name) { + return modifyPlaylist(playlistId, name, null); + } + + public Maybe changePlaylistThumbnail(final long playlistId, + final String thumbnailUrl) { + return modifyPlaylist(playlistId, null, thumbnailUrl); + } + + private Maybe modifyPlaylist(final long playlistId, + @Nullable final String name, + @Nullable final String thumbnailUrl) { + return playlistTable.getPlaylist(playlistId) + .firstElement() + .filter(playlistEntities -> !playlistEntities.isEmpty()) + .map(playlistEntities -> { + PlaylistEntity playlist = playlistEntities.get(0); + if (name != null) playlist.setName(name); + if (thumbnailUrl != null) playlist.setThumbnailUrl(thumbnailUrl); + return playlistTable.update(playlist); + }).subscribeOn(Schedulers.io()); + } + } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index de854ae0c..6ed357e36 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -19,8 +19,8 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; @@ -97,7 +97,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog { newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); - playlistAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + playlistAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture() { @Override public void selected(PlaylistInfoItem selectedItem) { if (!(selectedItem instanceof LocalPlaylistInfoItem) || getStreams() == null) @@ -113,9 +113,6 @@ public final class PlaylistAppendDialog extends PlaylistDialog { getDialog().dismiss(); } - - @Override - public void held(PlaylistInfoItem selectedItem) {} }); playlistManager.getPlaylists() diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index 386ac1819..791e90fa2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -35,8 +35,7 @@ public final class PlaylistCreationDialog extends PlaylistDialog { public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { if (getStreams() == null) return super.onCreateDialog(savedInstanceState); - View dialogView = View.inflate(getContext(), - R.layout.dialog_create_playlist, null); + View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); EditText nameInput = dialogView.findViewById(R.id.playlist_name); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java index 8db1f8780..c2181ca8d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java @@ -19,8 +19,8 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; @@ -127,7 +127,7 @@ public abstract class StatisticsPlaylistFragment protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { @Override public void selected(StreamInfoItem selectedItem) { NavigationHelper.openVideoDetailFragment(getFragmentManager(), 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 662f617bb..8db5d5f00 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 @@ -16,8 +16,8 @@ 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.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; @@ -125,24 +125,17 @@ public class SubscriptionFragment extends BaseStateFragment() { + infoListAdapter.setOnChannelSelectedListener(new OnInfoItemGesture() { @Override public void selected(ChannelInfoItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); - - } - - @Override - public void held(ChannelInfoItem selectedItem) {} - }); - - headerRootLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()); + NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), + selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); } }); + + headerRootLayout.setOnClickListener(view -> + NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager())); } private void resetFragment() { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index c81235623..cdad31674 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -43,17 +43,12 @@ import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; public class InfoItemBuilder { private static final String TAG = InfoItemBuilder.class.toString(); - public interface OnInfoItemSelectedListener { - void selected(T selectedItem); - void held(T selectedItem); - } - private final Context context; private ImageLoader imageLoader = ImageLoader.getInstance(); - private OnInfoItemSelectedListener onStreamSelectedListener; - private OnInfoItemSelectedListener onChannelSelectedListener; - private OnInfoItemSelectedListener onPlaylistSelectedListener; + private OnInfoItemGesture onStreamSelectedListener; + private OnInfoItemGesture onChannelSelectedListener; + private OnInfoItemGesture onPlaylistSelectedListener; public InfoItemBuilder(Context context) { this.context = context; @@ -91,27 +86,27 @@ public class InfoItemBuilder { return imageLoader; } - public OnInfoItemSelectedListener getOnStreamSelectedListener() { + public OnInfoItemGesture getOnStreamSelectedListener() { return onStreamSelectedListener; } - public void setOnStreamSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnStreamSelectedListener(OnInfoItemGesture listener) { this.onStreamSelectedListener = listener; } - public OnInfoItemSelectedListener getOnChannelSelectedListener() { + public OnInfoItemGesture getOnChannelSelectedListener() { return onChannelSelectedListener; } - public void setOnChannelSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnChannelSelectedListener(OnInfoItemGesture listener) { this.onChannelSelectedListener = listener; } - public OnInfoItemSelectedListener getOnPlaylistSelectedListener() { + public OnInfoItemGesture getOnPlaylistSelectedListener() { return onPlaylistSelectedListener; } - public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnPlaylistSelectedListener(OnInfoItemGesture listener) { this.onPlaylistSelectedListener = listener; } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 5494eae23..1dc4442c7 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -10,7 +10,6 @@ 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.StreamInfoItem; -import org.schabi.newpipe.info_list.InfoItemBuilder.OnInfoItemSelectedListener; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; @@ -56,6 +55,10 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; private boolean useMiniVariant = false; @@ -77,15 +80,15 @@ public class InfoListAdapter extends RecyclerView.Adapter(); } - public void setOnStreamSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnStreamSelectedListener(OnInfoItemGesture listener) { infoItemBuilder.setOnStreamSelectedListener(listener); } - public void setOnChannelSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnChannelSelectedListener(OnInfoItemGesture listener) { infoItemBuilder.setOnChannelSelectedListener(listener); } - public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnPlaylistSelectedListener(OnInfoItemGesture listener) { infoItemBuilder.setOnPlaylistSelectedListener(listener); } @@ -202,7 +205,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { + + public abstract void selected(T selectedItem); + + public void held(T selectedItem) { + // Optional gesture + } + + public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) { + // Optional gesture + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java new file mode 100644 index 000000000..8261d4760 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java @@ -0,0 +1,102 @@ +package org.schabi.newpipe.info_list.holder; + +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.Localization; + +public class StreamPlaylistInfoItemHolder extends InfoItemHolder { + + public final ImageView itemThumbnailView; + public final TextView itemVideoTitleView; + public final TextView itemUploaderView; + public final TextView itemDurationView; + public final View itemHandleView; + + StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + itemDurationView = itemView.findViewById(R.id.itemDurationView); + itemHandleView = itemView.findViewById(R.id.itemHandle); + } + + public StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + } + + @Override + public void updateFromItem(final InfoItem infoItem) { + if (!(infoItem instanceof StreamInfoItem)) return; + final StreamInfoItem item = (StreamInfoItem) infoItem; + + itemVideoTitleView.setText(item.getName()); + itemUploaderView.setText(item.uploader_name); + + if (item.duration > 0) { + itemDurationView.setText(Localization.getDurationString(item.duration)); + itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), + R.color.duration_background_color)); + itemDurationView.setVisibility(View.VISIBLE); + } else { + itemDurationView.setVisibility(View.GONE); + } + + // Default thumbnail is shown on error, while loading and if the url is empty + itemBuilder.getImageLoader().displayImage(item.thumbnail_url, itemThumbnailView, + StreamPlaylistInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnStreamSelectedListener() != null) { + itemBuilder.getOnStreamSelectedListener().selected(item); + } + }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnStreamSelectedListener() != null) { + itemBuilder.getOnStreamSelectedListener().held(item); + } + return true; + }); + + itemThumbnailView.setOnTouchListener(getOnTouchListener(item)); + itemHandleView.setOnTouchListener(getOnTouchListener(item)); + } + + private View.OnTouchListener getOnTouchListener(final StreamInfoItem item) { + return (view, motionEvent) -> { + view.performClick(); + if (itemBuilder != null && + motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + itemBuilder.getOnStreamSelectedListener() + .drag(item, StreamPlaylistInfoItemHolder.this); + } + return false; + }; + } + + /** + * Display options for stream thumbnails + */ + private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnFail(R.drawable.dummy_thumbnail) + .showImageForEmptyUri(R.drawable.dummy_thumbnail) + .showImageOnLoading(R.drawable.dummy_thumbnail) + .build(); +} diff --git a/app/src/main/res/layout/dialog_create_playlist.xml b/app/src/main/res/layout/dialog_playlist_name.xml similarity index 96% rename from app/src/main/res/layout/dialog_create_playlist.xml rename to app/src/main/res/layout/dialog_playlist_name.xml index b42d3101f..2dfab228b 100644 --- a/app/src/main/res/layout/dialog_create_playlist.xml +++ b/app/src/main/res/layout/dialog_playlist_name.xml @@ -1,6 +1,5 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 361f453c4..161cf1735 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -188,6 +188,7 @@ No results @string/no_videos Nothing Here But Crickets + Drag to reorder Cannot create download directory \'%1$s\' Created download directory \'%1$s\' From 388ec3e3d3ac9754acae04271cd39f91223387a3 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Fri, 26 Jan 2018 21:34:17 -0800 Subject: [PATCH 12/52] -Added history record manager as single entry for all database history transactions. -Merged stream record manager into history record manager. -Removed subject-based history database actions. -Merged normalized history table into watch history fragment. -Modified history fragments to use long click for delete actions. -Refactored DAO operations from search fragment to record manager. -Added index to search history table on search string. -Fix baseplayer round repeat not detected by discontinuity. --- .../java/org/schabi/newpipe/MainActivity.java | 98 +-------- .../schabi/newpipe/database/AppDatabase.java | 4 +- .../schabi/newpipe/database/Migrations.java | 1 + .../history/dao/SearchHistoryDAO.java | 2 + .../dao/StreamHistoryDAO.java | 13 +- .../history/model/SearchHistoryEntry.java | 6 +- .../model/StreamHistoryEntity.java | 10 +- .../model}/StreamHistoryEntry.java | 7 +- .../stream/StreamStatisticsEntry.java | 3 +- .../database/stream/dao/StreamDAO.java | 4 +- .../fragments/detail/VideoDetailFragment.java | 17 +- .../fragments/list/search/SearchFragment.java | 189 ++++++++---------- .../local/StatisticsPlaylistFragment.java | 8 +- .../fragments/local/StreamRecordManager.java | 44 ---- .../newpipe/history/HistoryActivity.java | 23 +-- .../newpipe/history/HistoryEntryAdapter.java | 45 ++--- .../newpipe/history/HistoryFragment.java | 176 ++++++---------- .../newpipe/history/HistoryRecordManager.java | 147 ++++++++++++++ .../history/SearchHistoryFragment.java | 63 +++++- .../history/WatchedHistoryFragment.java | 85 +++++--- .../org/schabi/newpipe/player/BasePlayer.java | 12 +- app/src/main/res/values/strings.xml | 4 + 22 files changed, 476 insertions(+), 485 deletions(-) rename app/src/main/java/org/schabi/newpipe/database/{stream => history}/dao/StreamHistoryDAO.java (80%) rename app/src/main/java/org/schabi/newpipe/database/{stream => history}/model/StreamHistoryEntity.java (81%) rename app/src/main/java/org/schabi/newpipe/database/{stream => history/model}/StreamHistoryEntry.java (90%) delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 9e8f3fa76..9a1ecd07a 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -26,8 +26,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; @@ -42,40 +40,21 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.history.dao.HistoryDAO; -import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; -import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; -import org.schabi.newpipe.database.history.model.HistoryEntry; -import org.schabi.newpipe.database.history.model.SearchHistoryEntry; -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; -import org.schabi.newpipe.history.HistoryListener; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; -import java.util.Date; - -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; -import io.reactivex.subjects.PublishSubject; - -public class MainActivity extends AppCompatActivity implements HistoryListener { +public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); - private SharedPreferences sharedPreferences; private ActionBarDrawerToggle toggle = null; /*////////////////////////////////////////////////////////////////////////// @@ -86,7 +65,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { protected void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); super.onCreate(savedInstanceState); @@ -98,7 +76,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { setSupportActionBar(findViewById(R.id.toolbar)); setupDrawer(); - initHistory(); } private void setupDrawer() { @@ -149,8 +126,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { if (!isChangingConfigurations()) { StateSaver.clearStateFiles(); } - - disposeHistory(); } @Override @@ -357,75 +332,4 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { NavigationHelper.gotoMainFragment(getSupportFragmentManager()); } } - - /*////////////////////////////////////////////////////////////////////////// - // History - //////////////////////////////////////////////////////////////////////////*/ - - private WatchHistoryDAO watchHistoryDAO; - private SearchHistoryDAO searchHistoryDAO; - private PublishSubject historyEntrySubject; - private Disposable disposable; - - private void initHistory() { - final AppDatabase database = NewPipeDatabase.getInstance(); - watchHistoryDAO = database.watchHistoryDAO(); - searchHistoryDAO = database.searchHistoryDAO(); - historyEntrySubject = PublishSubject.create(); - disposable = historyEntrySubject - .observeOn(Schedulers.io()) - .subscribe(getHistoryEntryConsumer()); - } - - private void disposeHistory() { - if (disposable != null) disposable.dispose(); - watchHistoryDAO = null; - searchHistoryDAO = null; - } - - @NonNull - private Consumer getHistoryEntryConsumer() { - return new Consumer() { - @Override - public void accept(HistoryEntry historyEntry) throws Exception { - //noinspection unchecked - HistoryDAO historyDAO = (HistoryDAO) - (historyEntry instanceof SearchHistoryEntry ? searchHistoryDAO : watchHistoryDAO); - - HistoryEntry latestEntry = historyDAO.getLatestEntry(); - if (historyEntry.hasEqualValues(latestEntry)) { - latestEntry.setCreationDate(historyEntry.getCreationDate()); - historyDAO.update(latestEntry); - } else { - historyDAO.insert(historyEntry); - } - } - }; - } - - private void addWatchHistoryEntry(StreamInfo streamInfo) { - if (sharedPreferences.getBoolean(getString(R.string.enable_watch_history_key), true)) { - WatchHistoryEntry entry = new WatchHistoryEntry(streamInfo); - historyEntrySubject.onNext(entry); - } - } - - @Override - public void onVideoPlayed(StreamInfo streamInfo, @Nullable VideoStream videoStream) { - addWatchHistoryEntry(streamInfo); - } - - @Override - public void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream) { - addWatchHistoryEntry(streamInfo); - } - - @Override - public void onSearch(int serviceId, String query) { - // Add search history entry - if (sharedPreferences.getBoolean(getString(R.string.enable_search_history_key), true)) { - SearchHistoryEntry searchHistoryEntry = new SearchHistoryEntry(new Date(), serviceId, query); - historyEntrySubject.onNext(searchHistoryEntry); - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index d5a9164dc..7097dd4a7 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -13,10 +13,10 @@ import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 9200a64b0..b977e43e9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -20,6 +20,7 @@ public class Migrations { // Not much we can do about this, since room doesn't create tables before migration. // It's either this or blasting the entire database anew. + database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java index 70799d971..257c1ec3d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.database.history.dao; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; +import android.support.annotation.Nullable; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; @@ -22,6 +23,7 @@ public interface SearchHistoryDAO extends HistoryDAO { @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") @Override + @Nullable SearchHistoryEntry getLatestEntry(); @Query("DELETE FROM " + TABLE_NAME) diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java similarity index 80% rename from app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java rename to app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 81ee9d912..64003910e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -1,14 +1,13 @@ -package org.schabi.newpipe.database.stream.dao; +package org.schabi.newpipe.database.history.dao; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; -import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.stream.StreamHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import java.util.List; @@ -18,9 +17,9 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LA import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @Dao public abstract class StreamHistoryDAO implements BasicDAO { diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java index d18974089..bba3f4295 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java @@ -3,10 +3,14 @@ package org.schabi.newpipe.database.history.model; 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 java.util.Date; -@Entity(tableName = SearchHistoryEntry.TABLE_NAME) +import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH; + +@Entity(tableName = SearchHistoryEntry.TABLE_NAME, + indices = {@Index(value = SEARCH)}) public class SearchHistoryEntry extends HistoryEntry { public static final String TABLE_NAME = "search_history"; diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java similarity index 81% rename from app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java rename to app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java index d937a29ed..b238af0a9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.database.stream.model; +package org.schabi.newpipe.database.history.model; import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; @@ -6,12 +6,14 @@ import android.arch.persistence.room.ForeignKey; import android.arch.persistence.room.Index; import android.support.annotation.NonNull; +import org.schabi.newpipe.database.stream.model.StreamEntity; + import java.util.Date; import static android.arch.persistence.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; @Entity(tableName = STREAM_HISTORY_TABLE, primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java similarity index 90% rename from app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java rename to app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java index 3df641372..cdc9cc40a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java @@ -1,9 +1,8 @@ -package org.schabi.newpipe.database.stream; +package org.schabi.newpipe.database.history.model; import android.arch.persistence.room.ColumnInfo; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Date; @@ -44,4 +43,8 @@ public class StreamHistoryEntry { this.streamId = streamId; this.accessDate = accessDate; } + + public StreamHistoryEntity toStreamHistoryEntity() { + return new StreamHistoryEntity(streamId, accessDate); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java index 722cff5cd..1c2a7028e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -2,9 +2,8 @@ package org.schabi.newpipe.database.stream; import android.arch.persistence.room.ColumnInfo; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java index a4955d835..b699e0b6b 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -9,7 +9,7 @@ import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import java.util.ArrayList; @@ -22,7 +22,7 @@ import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @Dao diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 05550a0a5..b134bc98d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -44,6 +44,7 @@ import com.nirhart.parallaxscroll.views.ParallaxScrollView; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; @@ -60,6 +61,7 @@ import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.history.HistoryListener; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.OnInfoItemGesture; @@ -649,9 +651,6 @@ public class VideoDetailFragment extends BaseStateFragment implement public void onActionSelected(int selectedStreamId) { try { NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http"))); - if(activity instanceof HistoryListener) { - ((HistoryListener) activity).onVideoPlayed(info, null); - } } catch (Exception e) { if(DEBUG) Log.i(TAG, "Failed to start kore", e); showInstallKoreDialog(activity); @@ -805,10 +804,6 @@ public class VideoDetailFragment extends BaseStateFragment implement private void openBackgroundPlayer(final boolean append) { AudioStream audioStream = currentInfo.getAudioStreams().get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); - if (activity instanceof HistoryListener) { - ((HistoryListener) activity).onAudioPlayed(currentInfo, audioStream); - } - boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); @@ -825,10 +820,6 @@ public class VideoDetailFragment extends BaseStateFragment implement return; } - if (activity instanceof HistoryListener) { - ((HistoryListener) activity).onVideoPlayed(currentInfo, getSelectedVideoStream()); - } - final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); if (append) { NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue); @@ -844,10 +835,6 @@ public class VideoDetailFragment extends BaseStateFragment implement private void openVideoPlayer() { VideoStream selectedVideoStream = getSelectedVideoStream(); - if (activity instanceof HistoryListener) { - ((HistoryListener) activity).onVideoPlayed(currentInfo, selectedVideoStream); - } - if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream); } else { 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 d6ed2a313..0638c06e7 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 @@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.list.search; import android.app.Activity; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -30,10 +29,8 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; -import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; @@ -44,7 +41,7 @@ import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchResult; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; -import org.schabi.newpipe.history.HistoryListener; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.AnimationUtils; @@ -64,16 +61,11 @@ import java.util.concurrent.TimeUnit; import icepick.State; import io.reactivex.Flowable; -import io.reactivex.Notification; import io.reactivex.Observable; -import io.reactivex.ObservableSource; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.BiFunction; import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; -import io.reactivex.functions.Predicate; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; @@ -121,7 +113,7 @@ public class SearchFragment extends BaseListFragment suggestionPublisher + .onNext(searchEditText.getText().toString()), + + throwable -> showSnackBarError(throwable, + UserAction.SOMETHING_ELSE, "none", + "Deleting item failed", R.string.general_error) + ); + new AlertDialog.Builder(activity) .setTitle(item.query) .setMessage(R.string.delete_item_search_history) .setCancelable(true) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - disposables.add(Observable - .fromCallable(new Callable() { - @Override - public Integer call() throws Exception { - return searchHistoryDAO.deleteAllWhereQuery(item.query); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Integer howManyDeleted) throws Exception { - suggestionPublisher.onNext(searchEditText.getText().toString()); - } - }, new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - showSnackBarError(throwable, UserAction.SOMETHING_ELSE, "none", "Deleting item failed", R.string.general_error); - } - })); - } - }).show(); + .setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete)) + .show(); } @Override @@ -589,83 +569,67 @@ public class SearchFragment extends BaseListFragment observable = suggestionPublisher .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) .startWith(searchQuery != null ? searchQuery : "") - .filter(new Predicate() { - @Override - public boolean test(@io.reactivex.annotations.NonNull String query) throws Exception { - return isSuggestionsEnabled; - } - }); + .filter(query -> isSuggestionsEnabled); suggestionDisposable = observable - .switchMap(new Function>>>() { - @Override - public ObservableSource>> apply(@io.reactivex.annotations.NonNull final String query) throws Exception { - final Flowable> flowable = query.length() > 0 - ? searchHistoryDAO.getSimilarEntries(query, 3) - : searchHistoryDAO.getUniqueEntries(25); - final Observable> local = flowable.toObservable() - .map(new Function, List>() { - @Override - public List apply(@io.reactivex.annotations.NonNull List searchHistoryEntries) throws Exception { - List result = new ArrayList<>(); - for (SearchHistoryEntry entry : searchHistoryEntries) - result.add(new SuggestionItem(true, entry.getSearch())); - return result; - } - }); + .switchMap(query -> { + final Flowable> flowable = historyRecordManager + .getRelatedSearches(query, 3, 25); + final Observable> local = flowable.toObservable() + .map(searchHistoryEntries -> { + List result = new ArrayList<>(); + for (SearchHistoryEntry entry : searchHistoryEntries) + result.add(new SuggestionItem(true, entry.getSearch())); + return result; + }); - if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { - // Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION - return local.materialize(); + if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { + // Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION + return local.materialize(); + } + + final Observable> network = ExtractorHelper + .suggestionsFor(serviceId, query, contentCountry) + .toObservable() + .map(strings -> { + List result = new ArrayList<>(); + for (String entry : strings) { + result.add(new SuggestionItem(false, entry)); + } + return result; + }); + + return Observable.zip(local, network, (localResult, networkResult) -> { + List result = new ArrayList<>(); + if (localResult.size() > 0) result.addAll(localResult); + + // Remove duplicates + final Iterator iterator = networkResult.iterator(); + while (iterator.hasNext() && localResult.size() > 0) { + final SuggestionItem next = iterator.next(); + for (SuggestionItem item : localResult) { + if (item.query.equals(next.query)) { + iterator.remove(); + break; + } + } } - final Observable> network = ExtractorHelper.suggestionsFor(serviceId, query, contentCountry).toObservable() - .map(new Function, List>() { - @Override - public List apply(@io.reactivex.annotations.NonNull List strings) throws Exception { - List result = new ArrayList<>(); - for (String entry : strings) result.add(new SuggestionItem(false, entry)); - return result; - } - }); - - return Observable.zip(local, network, new BiFunction, List, List>() { - @Override - public List apply(@io.reactivex.annotations.NonNull List localResult, @io.reactivex.annotations.NonNull List networkResult) throws Exception { - List result = new ArrayList<>(); - if (localResult.size() > 0) result.addAll(localResult); - - // Remove duplicates - final Iterator iterator = networkResult.iterator(); - while (iterator.hasNext() && localResult.size() > 0) { - final SuggestionItem next = iterator.next(); - for (SuggestionItem item : localResult) { - if (item.query.equals(next.query)) { - iterator.remove(); - break; - } - } - } - - if (networkResult.size() > 0) result.addAll(networkResult); - return result; - } - }).materialize(); - } + if (networkResult.size() > 0) result.addAll(networkResult); + return result; + }).materialize(); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer>>() { - @Override - public void accept(@io.reactivex.annotations.NonNull Notification> listNotification) throws Exception { - if (listNotification.isOnNext()) { - handleSuggestions(listNotification.getValue()); - } else if (listNotification.isOnError()) { - Throwable error = listNotification.getError(); - if (!ExtractorHelper.hasAssignableCauseThrowable(error, - IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class)) { - onSuggestionError(error); - } + .subscribe(listNotification -> { + if (listNotification.isOnNext()) { + handleSuggestions(listNotification.getValue()); + } else if (listNotification.isOnError()) { + Throwable error = listNotification.getError(); + if (!ExtractorHelper.hasAssignableCauseThrowable(error, + IOException.class, SocketException.class, + InterruptedException.class, InterruptedIOException.class)) { + onSuggestionError(error); } } }); @@ -718,11 +682,14 @@ public class SearchFragment extends BaseListFragment {}, + error -> showSnackBarError(error, UserAction.SEARCHED, + NewPipe.getNameOfService(serviceId), query, 0) + ); + suggestionPublisher.onNext(query); startLoading(false); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java index c2181ca8d..6eddc3a5c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java @@ -13,12 +13,12 @@ import android.view.ViewGroup; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; @@ -49,7 +49,7 @@ public abstract class StatisticsPlaylistFragment /* Used for independent events */ private Subscription databaseSubscription; - private StreamRecordManager recordManager; + private HistoryRecordManager recordManager; /////////////////////////////////////////////////////////////////////////// // Abstracts @@ -68,7 +68,7 @@ public abstract class StatisticsPlaylistFragment @Override public void onAttach(Context context) { super.onAttach(context); - recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); + recordManager = new HistoryRecordManager(context); } @Override @@ -205,7 +205,7 @@ public abstract class StatisticsPlaylistFragment super.startLoading(forceLoad); resetFragment(); - recordManager.getStatistics() + recordManager.getStreamStatistics() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getHistoryObserver()); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java deleted file mode 100644 index 993ed58da..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.schabi.newpipe.fragments.local; - -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; -import org.schabi.newpipe.extractor.stream.StreamInfo; - -import java.util.Date; -import java.util.List; - -import io.reactivex.Flowable; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; - -public class StreamRecordManager { - - private final AppDatabase database; - private final StreamDAO streamTable; - private final StreamHistoryDAO historyTable; - - public StreamRecordManager(final AppDatabase db) { - database = db; - streamTable = db.streamDAO(); - historyTable = db.streamHistoryDAO(); - } - - public Single onViewed(final StreamInfo info) { - return Single.fromCallable(() -> database.runInTransaction(() -> { - final long streamId = streamTable.upsert(new StreamEntity(info)); - return historyTable.insert(new StreamHistoryEntity(streamId, new Date())); - })).subscribeOn(Schedulers.io()); - } - - public int removeHistory(final long streamId) { - return historyTable.deleteStreamHistory(streamId); - } - - public Flowable> getStatistics() { - return historyTable.getStatistics(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java index 8d8e4ef16..30589a22c 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java @@ -9,6 +9,7 @@ import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; @@ -50,8 +51,10 @@ public class HistoryActivity extends AppCompatActivity { Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setTitle(R.string.title_activity_history); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setTitle(R.string.title_activity_history); + } // Create the adapter that will return a fragment for each of the three // primary sections of the activity. mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); @@ -66,17 +69,11 @@ public class HistoryActivity extends AppCompatActivity { final FloatingActionButton fab = findViewById(R.id.fab); RxView.clicks(fab) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Object o) { - int currentItem = mViewPager.getCurrentItem(); - HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.instantiateItem(mViewPager, currentItem); - if(fragment != null) { - fragment.onHistoryCleared(); - } else { - Log.w(TAG, "Couldn't find current fragment"); - } - } + .subscribe(ignored -> { + int currentItem = mViewPager.getCurrentItem(); + HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter + .instantiateItem(mViewPager, currentItem); + fragment.onHistoryCleared(); }); } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java index d56469a7e..f61e8eb7d 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java @@ -4,9 +4,8 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; -import android.view.View; -import org.schabi.newpipe.database.history.model.HistoryEntry; +import org.schabi.newpipe.util.Localization; import java.text.DateFormat; import java.util.ArrayList; @@ -19,7 +18,7 @@ import java.util.Date; * @param the type of the entries * @param the type of the view holder */ -public abstract class HistoryEntryAdapter extends RecyclerView.Adapter { +public abstract class HistoryEntryAdapter extends RecyclerView.Adapter { private final ArrayList mEntries; private final DateFormat mDateFormat; @@ -29,9 +28,8 @@ public abstract class HistoryEntryAdapter(); - mDateFormat = android.text.format.DateFormat.getDateFormat(context.getApplicationContext()); - - setHasStableIds(true); + mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, + Localization.getPreferredLocale(context)); } public void setEntries(@NonNull Collection historyEntries) { @@ -53,11 +51,6 @@ public abstract class HistoryEntryAdapter historyItemClickListener = onHistoryItemClickListener; - if(historyItemClickListener != null) { - historyItemClickListener.onHistoryItemClick(entry); - } + holder.itemView.setOnClickListener(v -> { + if(onHistoryItemClickListener != null) { + onHistoryItemClickListener.onHistoryItemClick(entry); } }); + + holder.itemView.setOnLongClickListener(view -> { + if (onHistoryItemClickListener != null) { + onHistoryItemClickListener.onHistoryItemLongClick(entry); + return true; + } + return false; + }); + onBindViewHolder(holder, entry, position); } @@ -94,13 +92,8 @@ public abstract class HistoryEntryAdapter { - void onHistoryItemClick(E historyItem); + public interface OnHistoryItemClickListener { + void onHistoryItemClick(E item); + void onHistoryItemLongClick(E item); } } 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 c64689775..462c12e61 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -2,7 +2,6 @@ package org.schabi.newpipe.history; import android.content.SharedPreferences; -import android.graphics.Color; import android.os.Bundle; import android.os.Parcelable; import android.preference.PreferenceManager; @@ -12,34 +11,31 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; 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; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.history.dao.HistoryDAO; -import org.schabi.newpipe.database.history.model.HistoryEntry; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import icepick.State; -import io.reactivex.Observer; +import io.reactivex.Flowable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; -import io.reactivex.subjects.PublishSubject; +import io.reactivex.disposables.CompositeDisposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public abstract class HistoryFragment extends BaseFragment +public abstract class HistoryFragment extends BaseFragment implements HistoryEntryAdapter.OnHistoryItemClickListener { private SharedPreferences mSharedPreferences; @@ -54,12 +50,11 @@ public abstract class HistoryFragment extends BaseFragme Parcelable mRecyclerViewState; private RecyclerView mRecyclerView; private HistoryEntryAdapter mHistoryAdapter; - private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback; - // private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; - private HistoryDAO mHistoryDataSource; - private PublishSubject> mHistoryEntryDeleteSubject; - private PublishSubject> mHistoryEntryInsertSubject; + private Subscription historySubscription; + + protected HistoryRecordManager historyRecordManager; + protected CompositeDisposable disposables; @StringRes abstract int getEnabledConfigKey(); @@ -77,88 +72,47 @@ public abstract class HistoryFragment extends BaseFragme // Register history enabled listener mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); - mHistoryDataSource = createHistoryDAO(); - - mHistoryEntryDeleteSubject = PublishSubject.create(); - mHistoryEntryDeleteSubject - .observeOn(Schedulers.io()) - .subscribe(new Consumer>() { - @Override - public void accept(Collection historyEntries) throws Exception { - mHistoryDataSource.delete(historyEntries); - } - }); - - mHistoryEntryInsertSubject = PublishSubject.create(); - mHistoryEntryInsertSubject - .observeOn(Schedulers.io()) - .subscribe(new Consumer>() { - @Override - public void accept(Collection historyEntries) throws Exception { - mHistoryDataSource.insertAll(historyEntries); - } - }); - - - } - - 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; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { - if (mHistoryAdapter != null) { - final E historyEntry = mHistoryAdapter.removeItemAt(viewHolder.getAdapterPosition()); - mHistoryEntryDeleteSubject.onNext(Collections.singletonList(historyEntry)); - - View view = getActivity().findViewById(R.id.main_content); - if (view == null) view = mRecyclerView.getRootView(); - - Snackbar.make(view, R.string.item_deleted, 5 * 1000) - .setActionTextColor(Color.WHITE) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - mHistoryEntryInsertSubject.onNext(Collections.singletonList(historyEntry)); - } - }).show(); - } - } - }; + historyRecordManager = new HistoryRecordManager(getContext()); + disposables = new CompositeDisposable(); } @NonNull protected abstract HistoryEntryAdapter createAdapter(); + protected abstract Single> insert(final Collection entries); + + protected abstract Single delete(final Collection entries); + + @NonNull + protected abstract Flowable> getAll(); + @Override public void onResume() { super.onResume(); - mHistoryDataSource.getAll() - .toObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getHistoryListConsumer()); - boolean newEnabled = isHistoryEnabled(); + + getAll().observeOn(AndroidSchedulers.mainThread()).subscribe(getHistorySubscriber()); + + final boolean newEnabled = isHistoryEnabled(); if (newEnabled != mHistoryIsEnabled) { onHistoryIsEnabledChanged(newEnabled); } } @NonNull - private Observer> getHistoryListConsumer() { - return new Observer>() { + private Subscriber> getHistorySubscriber() { + return new Subscriber>() { @Override - public void onSubscribe(@NonNull Disposable d) { + public void onSubscribe(Subscription s) { + if (historySubscription != null) historySubscription.cancel(); + historySubscription = s; + historySubscription.request(1); } @Override - public void onNext(@NonNull List historyEntries) { - if (!historyEntries.isEmpty()) { - mHistoryAdapter.setEntries(historyEntries); + public void onNext(List entries) { + if (!entries.isEmpty()) { + mHistoryAdapter.setEntries(entries); animateView(mEmptyHistoryView, false, 200); if (mRecyclerViewState != null) { @@ -169,11 +123,13 @@ public abstract class HistoryFragment extends BaseFragme mHistoryAdapter.clear(); showEmptyHistory(); } + + if (historySubscription != null) historySubscription.request(1); } @Override - public void onError(@NonNull Throwable e) { - // TODO: error handling like in (see e.g. subscription fragment) + public void onError(Throwable t) { + } @Override @@ -192,30 +148,33 @@ public abstract class HistoryFragment extends BaseFragme */ @MainThread public void onHistoryCleared() { - final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState(); - final Collection itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); - mHistoryEntryDeleteSubject.onNext(itemsToDelete); + if (getContext() == null) return; + + new AlertDialog.Builder(getContext()) + .setTitle(R.string.delete_all) + .setMessage(R.string.delete_all_history_prompt) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.delete, (dialog, i) -> clearHistory()) + .show(); + } + + protected void makeSnackbar(@StringRes final int text) { + if (getActivity() == null) return; View view = getActivity().findViewById(R.id.main_content); if (view == null) view = mRecyclerView.getRootView(); + Snackbar.make(view, text, Snackbar.LENGTH_LONG).show(); + } - if (!itemsToDelete.isEmpty()) { - Snackbar.make(view, R.string.history_cleared, 5 * 1000) - .setActionTextColor(Color.WHITE) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - mRecyclerViewState = stateBeforeClear; - mHistoryEntryInsertSubject.onNext(itemsToDelete); - } - }).show(); - } else { - Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show(); - } + private void clearHistory() { + final Collection itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); + disposables.add(delete(itemsToDelete).observeOn(AndroidSchedulers.mainThread()) + .subscribe()); + makeSnackbar(R.string.history_cleared); mHistoryAdapter.clear(); showEmptyHistory(); - } private void showEmptyHistory() { @@ -227,18 +186,18 @@ public abstract class HistoryFragment extends BaseFragme @Nullable @CallSuper @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_history, container, false); mRecyclerView = rootView.findViewById(R.id.history_view); - RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), + LinearLayoutManager.VERTICAL, false); mRecyclerView.setLayoutManager(layoutManager); mHistoryAdapter = createAdapter(); mHistoryAdapter.setOnHistoryItemClickListener(this); mRecyclerView.setAdapter(mHistoryAdapter); - ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mHistoryItemSwipeCallback); - itemTouchHelper.attachToRecyclerView(mRecyclerView); mDisabledView = rootView.findViewById(R.id.history_disabled_view); mEmptyHistoryView = rootView.findViewById(R.id.history_empty); @@ -260,7 +219,7 @@ public abstract class HistoryFragment extends BaseFragme mSharedPreferences = null; mHistoryIsEnabledChangeListener = null; mHistoryIsEnabledKey = null; - mHistoryDataSource = null; + if (disposables != null) disposables.dispose(); } @Override @@ -290,15 +249,8 @@ public abstract class HistoryFragment extends BaseFragme } } - /** - * Creates a new history DAO - * - * @return the history DAO - */ - @NonNull - protected abstract HistoryDAO createHistoryDAO(); - - private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener { + private class HistoryIsEnabledChangeListener + implements SharedPreferences.OnSharedPreferenceChangeListener { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(mHistoryIsEnabledKey)) { diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java new file mode 100644 index 000000000..1a5fe0525 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java @@ -0,0 +1,147 @@ +package org.schabi.newpipe.history; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; +import org.schabi.newpipe.database.history.model.SearchHistoryEntry; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.extractor.stream.StreamInfo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class HistoryRecordManager { + + private final AppDatabase database; + private final StreamDAO streamTable; + private final StreamHistoryDAO streamHistoryTable; + private final SearchHistoryDAO searchHistoryTable; + private final SharedPreferences sharedPreferences; + private final String searchHistoryKey; + private final String streamHistoryKey; + + public HistoryRecordManager(final Context context) { + database = NewPipeDatabase.getInstance(context); + streamTable = database.streamDAO(); + streamHistoryTable = database.streamHistoryDAO(); + searchHistoryTable = database.searchHistoryDAO(); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + searchHistoryKey = context.getString(R.string.enable_search_history_key); + streamHistoryKey = context.getString(R.string.enable_watch_history_key); + } + + public Maybe onViewed(final StreamInfo info) { + if (!isStreamHistoryEnabled()) return Maybe.empty(); + + final Date currentTime = new Date(); + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); + })).subscribeOn(Schedulers.io()); + } + + public Single deleteStreamHistory(final long streamId) { + return Single.fromCallable(() -> streamHistoryTable.deleteStreamHistory(streamId)) + .subscribeOn(Schedulers.io()); + } + + public Flowable> getStreamHistory() { + return streamHistoryTable.getHistory().subscribeOn(Schedulers.io()); + } + + public Flowable> getStreamStatistics() { + return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); + } + + public Single> insertStreamHistory(final Collection entries) { + List entities = new ArrayList<>(entries.size()); + for (final StreamHistoryEntry entry : entries) { + entities.add(entry.toStreamHistoryEntity()); + } + return Single.fromCallable(() -> streamHistoryTable.insertAll(entities)) + .subscribeOn(Schedulers.io()); + } + + public Single deleteStreamHistory(final Collection entries) { + List entities = new ArrayList<>(entries.size()); + for (final StreamHistoryEntry entry : entries) { + entities.add(entry.toStreamHistoryEntity()); + } + return Single.fromCallable(() -> streamHistoryTable.delete(entities)) + .subscribeOn(Schedulers.io()); + } + + private boolean isStreamHistoryEnabled() { + return sharedPreferences.getBoolean(streamHistoryKey, false); + } + + /////////////////////////////////////////////////////// + // Search History + /////////////////////////////////////////////////////// + + public Single> insertSearches(final Collection entries) { + return Single.fromCallable(() -> searchHistoryTable.insertAll(entries)) + .subscribeOn(Schedulers.io()); + } + + public Single deleteSearches(final Collection entries) { + return Single.fromCallable(() -> searchHistoryTable.delete(entries)) + .subscribeOn(Schedulers.io()); + } + + public Flowable> getSearchHistory() { + return searchHistoryTable.getAll(); + } + + public Maybe onSearched(final int serviceId, final String search) { + if (!isSearchHistoryEnabled()) return Maybe.empty(); + + final Date currentTime = new Date(); + final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search); + + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry(); + if (latestEntry != null && latestEntry.hasEqualValues(newEntry)) { + latestEntry.setCreationDate(currentTime); + return (long) searchHistoryTable.update(latestEntry); + } else { + return searchHistoryTable.insert(newEntry); + } + })).subscribeOn(Schedulers.io()); + } + + public Single deleteSearchHistory(final String search) { + return Single.fromCallable(() -> searchHistoryTable.deleteAllWhereQuery(search)) + .subscribeOn(Schedulers.io()); + } + + public Flowable> getRelatedSearches(final String query, + final int similarQueryLimit, + final int uniqueQueryLimit) { + return query.length() > 0 + ? searchHistoryTable.getSimilarEntries(query, similarQueryLimit) + : searchHistoryTable.getUniqueEntries(uniqueQueryLimit); + } + + private boolean isSearchHistoryEnabled() { + return sharedPreferences.getBoolean(searchHistoryKey, false); + } +} 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 91e2cecff..a8bba0573 100644 --- a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java @@ -5,22 +5,27 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; +import android.support.v7.app.AlertDialog; 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; import android.widget.TextView; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.history.dao.HistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.util.NavigationHelper; -public class SearchHistoryFragment extends HistoryFragment { +import java.util.Collection; +import java.util.Collections; +import java.util.List; - private static int allowedSwipeToDeleteDirections = ItemTouchHelper.RIGHT; +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; + +public class SearchHistoryFragment extends HistoryFragment { @NonNull public static SearchHistoryFragment newInstance() { @@ -30,7 +35,6 @@ public class SearchHistoryFragment extends HistoryFragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - historyItemSwipeCallback(allowedSwipeToDeleteDirections); } @NonNull @@ -39,21 +43,58 @@ public class SearchHistoryFragment extends HistoryFragment { return new SearchHistoryAdapter(getContext()); } + @Override + protected Single> insert(Collection entries) { + return historyRecordManager.insertSearches(entries); + } + + @Override + protected Single delete(Collection entries) { + return historyRecordManager.deleteSearches(entries); + } + + @NonNull + @Override + protected Flowable> getAll() { + return historyRecordManager.getSearchHistory(); + } + @StringRes @Override int getEnabledConfigKey() { return R.string.enable_search_history_key; } - @NonNull @Override - protected HistoryDAO createHistoryDAO() { - return NewPipeDatabase.getInstance().searchHistoryDAO(); + public void onHistoryItemClick(final SearchHistoryEntry historyItem) { + NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), + historyItem.getSearch()); } @Override - public void onHistoryItemClick(SearchHistoryEntry historyItem) { - NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), historyItem.getSearch()); + public void onHistoryItemLongClick(final SearchHistoryEntry item) { + if (activity == null) return; + + new AlertDialog.Builder(activity) + .setTitle(item.getSearch()) + .setMessage(R.string.delete_item_search_history) + .setCancelable(true) + .setNeutralButton(R.string.cancel, null) + .setPositiveButton(R.string.delete_one, (dialog, i) -> { + final Single onDelete = historyRecordManager + .deleteSearches(Collections.singleton(item)) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDelete.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .setNegativeButton(R.string.delete_all, (dialog, i) -> { + final Single onDeleteAll = historyRecordManager + .deleteSearchHistory(item.getSearch()) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDeleteAll.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .show(); } private static class ViewHolder extends RecyclerView.ViewHolder { 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 d898bf353..026d5ee16 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java @@ -6,8 +6,8 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; +import android.support.v7.app.AlertDialog; 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; @@ -16,18 +16,22 @@ import android.widget.TextView; import com.nostra13.universalimageloader.core.ImageLoader; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.history.dao.HistoryDAO; -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import java.util.Collection; +import java.util.Collections; +import java.util.List; -public class WatchedHistoryFragment extends HistoryFragment { +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; - private static int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT; + +public class WatchedHistoryFragment extends HistoryFragment { @NonNull public static WatchedHistoryFragment newInstance() { @@ -37,7 +41,6 @@ public class WatchedHistoryFragment extends HistoryFragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - historyItemSwipeCallback(allowedSwipeToDeleteDirections); } @StringRes @@ -48,27 +51,59 @@ public class WatchedHistoryFragment extends HistoryFragment { @NonNull @Override - protected WatchedHistoryAdapter createAdapter() { - return new WatchedHistoryAdapter(getContext()); + protected StreamHistoryAdapter createAdapter() { + return new StreamHistoryAdapter(getContext()); + } + + @Override + protected Single> insert(Collection entries) { + return historyRecordManager.insertStreamHistory(entries); + } + + @Override + protected Single delete(Collection entries) { + return historyRecordManager.deleteStreamHistory(entries); } @NonNull @Override - protected HistoryDAO createHistoryDAO() { - return NewPipeDatabase.getInstance().watchHistoryDAO(); + protected Flowable> getAll() { + return historyRecordManager.getStreamHistory(); } @Override - public void onHistoryItemClick(WatchHistoryEntry historyItem) { - NavigationHelper.openVideoDetail(getContext(), - historyItem.getServiceId(), - historyItem.getUrl(), - historyItem.getTitle()); + public void onHistoryItemClick(StreamHistoryEntry historyItem) { + NavigationHelper.openVideoDetail(getContext(), historyItem.serviceId, historyItem.url, + historyItem.title); } - private static class WatchedHistoryAdapter extends HistoryEntryAdapter { + @Override + public void onHistoryItemLongClick(StreamHistoryEntry item) { + new AlertDialog.Builder(activity) + .setTitle(item.title) + .setMessage(R.string.delete_stream_history_prompt) + .setCancelable(true) + .setNeutralButton(R.string.cancel, null) + .setPositiveButton(R.string.delete_one, (dialog, i) -> { + final Single onDelete = historyRecordManager + .deleteStreamHistory(Collections.singleton(item)) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDelete.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .setNegativeButton(R.string.delete_all, (dialog, i) -> { + final Single onDeleteAll = historyRecordManager + .deleteStreamHistory(item.streamId) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDeleteAll.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .show(); + } - public WatchedHistoryAdapter(Context context) { + private static class StreamHistoryAdapter extends HistoryEntryAdapter { + + StreamHistoryAdapter(Context context) { super(context); } @@ -87,13 +122,13 @@ public class WatchedHistoryFragment extends HistoryFragment { } @Override - void onBindViewHolder(ViewHolder holder, WatchHistoryEntry entry, int position) { - holder.date.setText(getFormattedDate(entry.getCreationDate())); - holder.streamTitle.setText(entry.getTitle()); - holder.uploader.setText(entry.getUploader()); - holder.duration.setText(Localization.getDurationString(entry.getDuration())); - ImageLoader.getInstance() - .displayImage(entry.getThumbnailURL(), holder.thumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + void onBindViewHolder(ViewHolder holder, StreamHistoryEntry entry, int position) { + holder.date.setText(getFormattedDate(entry.accessDate)); + holder.streamTitle.setText(entry.title); + holder.uploader.setText(entry.uploader); + holder.duration.setText(Localization.getDurationString(entry.duration)); + ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView, + StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); } } 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 a481b3335..8260adc6e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -61,10 +61,9 @@ import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.fragments.local.StreamRecordManager; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; @@ -150,7 +149,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected Disposable progressUpdateReactor; protected CompositeDisposable databaseUpdateReactor; - protected StreamRecordManager recordManager; + protected HistoryRecordManager recordManager; //////////////////////////////////////////////////////////////////////////*/ @@ -176,9 +175,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void initPlayer() { if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); - if (recordManager == null) { - recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); - } + if (recordManager == null) recordManager = new HistoryRecordManager(context); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); databaseUpdateReactor = new CompositeDisposable(); @@ -614,7 +611,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen // If the user selects a new track, then the discontinuity occurs after the index is changed. // Therefore, the only source that causes a discrepancy would be gapless transition, // which can only offset the current track by +1. - if (newWindowIndex == playQueue.getIndex() + 1) { + if (newWindowIndex == playQueue.getIndex() + 1 || + (newWindowIndex == 0 && playQueue.getIndex() == playQueue.size() - 1)) { playQueue.offsetIndex(+1); } playbackManager.load(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 161cf1735..fcfe5c4a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -229,6 +229,8 @@ Play Create Delete + Delete One + Delete All Checksum @@ -307,6 +309,8 @@ History cleared Item deleted Do you want to delete this item from search history? + Do you want to delete this item from watch history? + Are you sure you want to delete all items from history? Watch History Most Played From 17d77aa31ffd3be0d9a6d31f66d8a47d42180f13 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Fri, 26 Jan 2018 21:45:48 -0800 Subject: [PATCH 13/52] -Removed watch history table. -Added migration for dropping watch history table. --- .../java/org/schabi/newpipe/database/AppDatabase.java | 10 +++------- .../java/org/schabi/newpipe/database/Migrations.java | 2 ++ .../newpipe/database/history/dao/StreamHistoryDAO.java | 10 +++++++++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index 7097dd4a7..086e1bed0 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -5,18 +5,16 @@ import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.TypeConverters; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; -import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; @@ -26,7 +24,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_12_0; @TypeConverters({Converters.class}) @Database( entities = { - SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class, + SubscriptionEntity.class, SearchHistoryEntry.class, StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, PlaylistEntity.class, PlaylistStreamEntity.class }, @@ -39,8 +37,6 @@ public abstract class AppDatabase extends RoomDatabase { public abstract SubscriptionDAO subscriptionDAO(); - public abstract WatchHistoryDAO watchHistoryDAO(); - public abstract SearchHistoryDAO searchHistoryDAO(); public abstract StreamDAO streamDAO(); diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index b977e43e9..825ec5fd5 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -51,6 +51,8 @@ public class Migrations { "ON watch_history.service_id == streams.service_id " + "AND watch_history.url == streams.url " + "ORDER BY creation_date DESC"); + + database.execSQL("DROP TABLE IF EXISTS watch_history"); } }; } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 64003910e..fe19d362e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.database.history.dao; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; +import android.support.annotation.Nullable; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; @@ -22,7 +23,14 @@ import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STRE import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @Dao -public abstract class StreamHistoryDAO implements BasicDAO { +public abstract class StreamHistoryDAO implements HistoryDAO { + @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + + " WHERE " + STREAM_ACCESS_DATE + " = " + + "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")") + @Override + @Nullable + public abstract StreamHistoryEntity getLatestEntry(); + @Override @Query("SELECT * FROM " + STREAM_HISTORY_TABLE) public abstract Flowable> getAll(); From 84c5d274169b853f821c152ce93b41ef64b53b07 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 27 Jan 2018 22:14:38 -0800 Subject: [PATCH 14/52] -Revamped local items to display more information such as service name, etc. -Enabled reordering, renaming, removing of items on playlist fragment. -Enabled removal of dangling streams entries when history is cleared. -Changed playlist append menu item to icon on service player activity. -Added adapter and builder for local items, removed dependency on infoitem and existing infolist for database entry items. -Removed watch history entity and DAO. -Extracted info item selected listener to remove adding boilerplate code when long click functionality is optional. -Fixed query returning no record on left join when right table is empty. --- .../schabi/newpipe/database/LocalItem.java | 11 + .../database/history/dao/WatchHistoryDAO.java | 37 --- .../history/model/WatchHistoryEntry.java | 109 -------- .../playlist/PlaylistMetadataEntry.java | 8 +- .../playlist/PlaylistStreamEntry.java | 60 +++++ .../playlist/dao/PlaylistStreamDAO.java | 19 +- .../stream/StreamStatisticsEntry.java | 17 +- .../database/stream/model/StreamEntity.java | 11 - .../newpipe/fragments/MainFragment.java | 2 +- .../local/BaseLocalListFragment.java | 184 +++++++++++++ .../fragments/local/HeaderFooterHolder.java | 13 + .../fragments/local/LocalItemBuilder.java | 56 ++++ .../fragments/local/LocalItemListAdapter.java | 243 ++++++++++++++++++ .../local/LocalPlaylistFragment.java | 221 +++++++++++----- .../fragments/local/LocalPlaylistManager.java | 3 +- .../fragments/local/MostPlayedFragment.java | 35 --- .../fragments/local/OnCustomItemGesture.java | 19 ++ .../fragments/local/WatchHistoryFragment.java | 36 --- .../{ => bookmark}/BookmarkFragment.java | 92 +++---- .../local/bookmark/MostPlayedFragment.java | 22 ++ .../StatisticsPlaylistFragment.java | 78 +++--- .../local/bookmark/WatchHistoryFragment.java | 21 ++ .../local/holder/LocalItemHolder.java | 56 ++++ .../local/holder/LocalPlaylistItemHolder.java | 74 ++++++ .../LocalPlaylistStreamItemHolder.java} | 53 ++-- .../LocalStatisticStreamItemHolder.java | 119 +++++++++ .../newpipe/history/HistoryFragment.java | 11 +- .../newpipe/history/HistoryRecordManager.java | 8 + .../newpipe/info_list/InfoListAdapter.java | 4 - .../newpipe/info_list/OnInfoItemGesture.java | 6 - .../stored/StreamEntityInfoItem.java | 18 -- .../stored/StreamStatisticsInfoItem.java | 31 --- .../org/schabi/newpipe/util/Localization.java | 27 ++ .../schabi/newpipe/util/NavigationHelper.java | 4 +- .../res/layout/list_stream_playlist_item.xml | 2 +- .../main/res/layout/local_playlist_header.xml | 9 +- app/src/main/res/menu/menu_play_queue.xml | 8 +- app/src/main/res/values/strings.xml | 3 + 38 files changed, 1224 insertions(+), 506 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/LocalItem.java delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => bookmark}/BookmarkFragment.java (74%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => bookmark}/StatisticsPlaylistFragment.java (80%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java rename app/src/main/java/org/schabi/newpipe/{info_list/holder/StreamPlaylistInfoItemHolder.java => fragments/local/holder/LocalPlaylistStreamItemHolder.java} (58%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java new file mode 100644 index 000000000..95d0d9213 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java @@ -0,0 +1,11 @@ +package org.schabi.newpipe.database; + +public interface LocalItem { + enum LocalItemType { + PLAYLIST_ITEM, + PLAYLIST_STREAM_ITEM, + STATISTIC_STREAM_ITEM + } + + LocalItemType getLocalItemType(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java deleted file mode 100644 index a01d8e46d..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.schabi.newpipe.database.history.dao; - -import android.arch.persistence.room.Dao; -import android.arch.persistence.room.Query; - -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; - -import java.util.List; - -import io.reactivex.Flowable; - -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.CREATION_DATE; -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.ID; -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.SERVICE_ID; -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.TABLE_NAME; - -@Dao -public interface WatchHistoryDAO extends HistoryDAO { - - String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; - - @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") - @Override - WatchHistoryEntry getLatestEntry(); - - @Query("DELETE FROM " + TABLE_NAME) - @Override - int deleteAll(); - - @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) - @Override - Flowable> getAll(); - - @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) - @Override - Flowable> listByService(int serviceId); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java deleted file mode 100644 index bfd84d377..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.schabi.newpipe.database.history.model; - -import android.arch.persistence.room.ColumnInfo; -import android.arch.persistence.room.Entity; -import android.arch.persistence.room.Ignore; - -import org.schabi.newpipe.extractor.stream.StreamInfo; - -import java.util.Date; - -@Entity(tableName = WatchHistoryEntry.TABLE_NAME) -public class WatchHistoryEntry extends HistoryEntry { - - public static final String TABLE_NAME = "watch_history"; - public static final String TITLE = "title"; - public static final String URL = "url"; - public static final String STREAM_ID = "stream_id"; - public static final String THUMBNAIL_URL = "thumbnail_url"; - public static final String UPLOADER = "uploader"; - public static final String DURATION = "duration"; - - @ColumnInfo(name = TITLE) - private String title; - - @ColumnInfo(name = URL) - private String url; - - @ColumnInfo(name = STREAM_ID) - private String streamId; - - @ColumnInfo(name = THUMBNAIL_URL) - private String thumbnailURL; - - @ColumnInfo(name = UPLOADER) - private String uploader; - - @ColumnInfo(name = DURATION) - private long duration; - - public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, long duration) { - super(creationDate, serviceId); - this.title = title; - this.url = url; - this.streamId = streamId; - this.thumbnailURL = thumbnailURL; - this.uploader = uploader; - this.duration = duration; - } - - public WatchHistoryEntry(StreamInfo streamInfo) { - this(new Date(), streamInfo.getServiceId(), streamInfo.getName(), streamInfo.getUrl(), - streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader_name, streamInfo.duration); - } - - 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 getStreamId() { - return streamId; - } - - public void setStreamId(String streamId) { - this.streamId = streamId; - } - - public String getThumbnailURL() { - return thumbnailURL; - } - - public void setThumbnailURL(String thumbnailURL) { - this.thumbnailURL = thumbnailURL; - } - - public String getUploader() { - return uploader; - } - - public void setUploader(String uploader) { - this.uploader = uploader; - } - - public long getDuration() { - return duration; - } - - public void setDuration(int duration) { - this.duration = duration; - } - - @Ignore - @Override - public boolean hasEqualValues(HistoryEntry otherEntry) { - return otherEntry instanceof WatchHistoryEntry && super.hasEqualValues(otherEntry) - && getUrl().equals(((WatchHistoryEntry) otherEntry).getUrl()); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java index 53ae3d48a..2daea298b 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -2,13 +2,14 @@ package org.schabi.newpipe.database.playlist; import android.arch.persistence.room.ColumnInfo; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; -public class PlaylistMetadataEntry { +public class PlaylistMetadataEntry implements LocalItem { final public static String PLAYLIST_STREAM_COUNT = "streamCount"; @ColumnInfo(name = PLAYLIST_ID) @@ -33,4 +34,9 @@ public class PlaylistMetadataEntry { storedPlaylistInfoItem.setStreamCount(streamCount); return storedPlaylistInfoItem; } + + @Override + public LocalItemType getLocalItemType() { + return LocalItemType.PLAYLIST_ITEM; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java new file mode 100644 index 000000000..b6ecfe1f0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java @@ -0,0 +1,60 @@ +package org.schabi.newpipe.database.playlist; + +import android.arch.persistence.room.ColumnInfo; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; + +public class PlaylistStreamEntry implements LocalItem { + @ColumnInfo(name = StreamEntity.STREAM_ID) + final public long uid; + @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) + final public int serviceId; + @ColumnInfo(name = StreamEntity.STREAM_URL) + final public String url; + @ColumnInfo(name = StreamEntity.STREAM_TITLE) + final public String title; + @ColumnInfo(name = StreamEntity.STREAM_TYPE) + final public StreamType streamType; + @ColumnInfo(name = StreamEntity.STREAM_DURATION) + final public long duration; + @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) + final public String uploader; + @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) + final public String thumbnailUrl; + @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) + final public long streamId; + @ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX) + final public int joinIndex; + + public PlaylistStreamEntry(long uid, int serviceId, String url, String title, + StreamType streamType, long duration, String uploader, + String thumbnailUrl, long streamId, int joinIndex) { + this.uid = uid; + this.serviceId = serviceId; + this.url = url; + this.title = title; + this.streamType = streamType; + this.duration = duration; + this.uploader = uploader; + this.thumbnailUrl = thumbnailUrl; + this.streamId = streamId; + this.joinIndex = joinIndex; + } + + public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { + StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); + item.setThumbnailUrl(thumbnailUrl); + item.setUploaderName(uploader); + item.setDuration(duration); + return item; + } + + @Override + public LocalItemType getLocalItemType() { + return LocalItemType.PLAYLIST_STREAM_ITEM; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java index b9f325aa2..2d645e793 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -6,6 +6,7 @@ import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; @@ -37,17 +38,13 @@ public abstract class PlaylistStreamDAO implements BasicDAO getMaximumIndexOf(final long playlistId); @Transaction - @Query("SELECT " + STREAM_ID + ", " + STREAM_SERVICE_ID + ", " + STREAM_URL + ", " + - STREAM_TITLE + ", " + STREAM_TYPE + ", " + STREAM_UPLOADER + ", " + - STREAM_DURATION + ", " + STREAM_THUMBNAIL_URL + - - " FROM " + STREAM_TABLE + " INNER JOIN " + + @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " + // get ids of streams of the given playlist "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX + " FROM " + PLAYLIST_STREAM_JOIN_TABLE + " WHERE " @@ -56,14 +53,16 @@ public abstract class PlaylistStreamDAO implements BasicDAO> getOrderedStreamsOf(long playlistId); + public abstract Flowable> getOrderedStreamsOf(long playlistId); @Transaction @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + - PLAYLIST_THUMBNAIL_URL + ", COUNT(*) AS " + PLAYLIST_STREAM_COUNT + + PLAYLIST_THUMBNAIL_URL + ", " + + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + - " FROM " + PLAYLIST_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + - " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + PLAYLIST_STREAM_JOIN_TABLE + "." + JOIN_PLAYLIST_ID + + " FROM " + PLAYLIST_TABLE + + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + + " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + " GROUP BY " + JOIN_PLAYLIST_ID) public abstract Flowable> getPlaylistMetadata(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java index 1c2a7028e..6909f3397 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -2,14 +2,15 @@ package org.schabi.newpipe.database.stream; import android.arch.persistence.room.ColumnInfo; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import java.util.Date; -public class StreamStatisticsEntry { +public class StreamStatisticsEntry implements LocalItem { final public static String STREAM_LATEST_DATE = "latestAccess"; final public static String STREAM_WATCH_COUNT = "watchCount"; @@ -53,14 +54,16 @@ public class StreamStatisticsEntry { this.watchCount = watchCount; } - public StreamStatisticsInfoItem toStreamStatisticsInfoItem() { - StreamStatisticsInfoItem item = - new StreamStatisticsInfoItem(uid, serviceId, url, title, streamType); + public StreamInfoItem toStreamInfoItem() { + StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); item.setDuration(duration); item.setUploaderName(uploader); item.setThumbnailUrl(thumbnailUrl); - item.setLatestAccessDate(latestAccessDate); - item.setWatchCount(watchCount); return item; } + + @Override + public LocalItemType getLocalItemType() { + return LocalItemType.STATISTIC_STREAM_ITEM; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java index eb078a03c..2fddaa1bb 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -9,7 +9,6 @@ import android.arch.persistence.room.PrimaryKey; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.info_list.stored.StreamEntityInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.Constants; @@ -88,16 +87,6 @@ public class StreamEntity implements Serializable { item.getThumbnailUrl(), item.getUploader(), item.getDuration()); } - @Ignore - public StreamEntityInfoItem toStreamEntityInfoItem() throws IllegalArgumentException { - StreamEntityInfoItem item = new StreamEntityInfoItem(getUid(), getServiceId(), - getUrl(), getTitle(), getStreamType()); - item.setThumbnailUrl(getThumbnailUrl()); - item.setUploaderName(getUploader()); - item.setDuration(getDuration()); - return item; - } - public long getUid() { return uid; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index fc4f9a323..fc4913114 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -29,7 +29,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskList; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; -import org.schabi.newpipe.fragments.local.BookmarkFragment; +import org.schabi.newpipe.fragments.local.bookmark.BookmarkFragment; import org.schabi.newpipe.fragments.subscription.SubscriptionFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java new file mode 100644 index 000000000..afc67aa68 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java @@ -0,0 +1,184 @@ +package org.schabi.newpipe.fragments.local; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +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.Menu; +import android.view.MenuInflater; +import android.view.View; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.fragments.list.ListViewContract; +import org.schabi.newpipe.util.StateSaver; + +import java.util.List; +import java.util.Queue; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public abstract class BaseLocalListFragment extends BaseStateFragment + implements ListViewContract, StateSaver.WriteRead { + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + protected LocalItemListAdapter itemListAdapter; + protected RecyclerView itemsList; + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + itemListAdapter = new LocalItemListAdapter(activity); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onDestroy() { + super.onDestroy(); + StateSaver.onDestroy(savedState); + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + protected StateSaver.SavedState savedState; + + @Override + public String generateSuffix() { + // Naive solution, but it's good for now (the items don't change) + return "." + itemListAdapter.getItemsList().size() + ".list"; + } + + @Override + public void writeTo(Queue objectsToSave) { + objectsToSave.add(itemListAdapter.getItemsList()); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull Queue savedObjects) throws Exception { + itemListAdapter.getItemsList().clear(); + itemListAdapter.getItemsList().addAll((List) savedObjects.poll()); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle bundle) { + super.onRestoreInstanceState(bundle); + savedState = StateSaver.tryToRestore(bundle, this); + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + protected View getListHeader() { + return null; + } + + protected View getListFooter() { + return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false); + } + + protected RecyclerView.LayoutManager getListLayoutManager() { + return new LinearLayoutManager(activity); + } + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + itemsList = rootView.findViewById(R.id.items_list); + itemsList.setLayoutManager(getListLayoutManager()); + + itemListAdapter.setFooter(getListFooter()); + itemListAdapter.setHeader(getListHeader()); + + itemsList.setAdapter(itemListAdapter); + } + + @Override + protected void initListeners() { + super.initListeners(); + } + + /*////////////////////////////////////////////////////////////////////////// + // 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); + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(true); + if(useAsFrontPage) { + supportActionBar.setDisplayHomeAsUpEnabled(false); + } else { + supportActionBar.setDisplayHomeAsUpEnabled(true); + } + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + // animateView(itemsList, false, 400); + } + + @Override + public void hideLoading() { + super.hideLoading(); + animateView(itemsList, true, 300); + } + + @Override + public void showError(String message, boolean showRetryButton) { + super.showError(message, showRetryButton); + showListFooter(false); + animateView(itemsList, false, 200); + } + + @Override + public void showEmptyState() { + super.showEmptyState(); + showListFooter(false); + } + + @Override + public void showListFooter(final boolean show) { + itemsList.post(() -> itemListAdapter.showFooter(show)); + } + + @Override + public void handleNextItems(N result) { + isLoading.set(false); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java new file mode 100644 index 000000000..3c0830751 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java @@ -0,0 +1,13 @@ +package org.schabi.newpipe.fragments.local; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class HeaderFooterHolder extends RecyclerView.ViewHolder { + public View view; + + public HeaderFooterHolder(View v) { + super(v); + view = v; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java new file mode 100644 index 000000000..d31c85712 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java @@ -0,0 +1,56 @@ +package org.schabi.newpipe.fragments.local; + +import android.content.Context; + +import com.nostra13.universalimageloader.core.ImageLoader; + +import org.schabi.newpipe.database.LocalItem; + +/* + * 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 LocalItemBuilder { + private static final String TAG = LocalItemBuilder.class.toString(); + + private final Context context; + private ImageLoader imageLoader = ImageLoader.getInstance(); + + private OnCustomItemGesture onSelectedListener; + + public LocalItemBuilder(Context context) { + this.context = context; + } + + public Context getContext() { + return context; + } + + public ImageLoader getImageLoader() { + return imageLoader; + } + + public OnCustomItemGesture getOnItemSelectedListener() { + return onSelectedListener; + } + + public void setOnItemSelectedListener(OnCustomItemGesture listener) { + this.onSelectedListener = listener; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java new file mode 100644 index 000000000..9c621a55c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -0,0 +1,243 @@ +package org.schabi.newpipe.fragments.local; + +import android.app.Activity; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.local.holder.LocalItemHolder; +import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder; +import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder; +import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder; +import org.schabi.newpipe.util.Localization; + +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collections; +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 LocalItemListAdapter extends RecyclerView.Adapter { + + private static final String TAG = LocalItemListAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final int HEADER_TYPE = 0; + private static final int FOOTER_TYPE = 1; + + private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000; + private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001; + private static final int PLAYLIST_HOLDER_TYPE = 0x2000; + + private final LocalItemBuilder localItemBuilder; + private final ArrayList localItems; + private final DateFormat dateFormat; + + private boolean showFooter = false; + private View header = null; + private View footer = null; + + + public LocalItemListAdapter(Activity activity) { + localItemBuilder = new LocalItemBuilder(activity); + localItems = new ArrayList<>(); + dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, + Localization.getPreferredLocale(activity)); + } + + public void setSelectedListener(OnCustomItemGesture listener) { + localItemBuilder.setOnItemSelectedListener(listener); + } + + public void addInfoItemList(List data) { + if (data != null) { + if (DEBUG) { + Log.d(TAG, "addInfoItemList() before > localItems.size() = " + + localItems.size() + ", data.size() = " + data.size()); + } + + int offsetStart = sizeConsideringHeader(); + localItems.addAll(data); + + if (DEBUG) { + Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + + ", localItems.size() = " + localItems.size() + + ", header = " + header + ", footer = " + footer + + ", showFooter = " + showFooter); + } + + notifyItemRangeInserted(offsetStart, data.size()); + + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeader(); + notifyItemMoved(offsetStart, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + + " to " + footerNow); + } + } + } + + public void addInfoItem(LocalItem data) { + addInfoItemList(Collections.singletonList(data)); + } + + public void removeItemAt(final int infoListPosition) { + if (infoListPosition < 0 || infoListPosition >= localItems.size()) return; + + localItems.remove(infoListPosition); + + notifyItemRemoved(infoListPosition + (header != null ? 1 : 0)); + } + + public boolean swapItems(int fromAdapterPosition, int toAdapterPosition) { + final int actualFrom = offsetWithoutHeader(fromAdapterPosition); + final int actualTo = offsetWithoutHeader(toAdapterPosition); + + if (actualFrom < 0 || actualTo < 0) return false; + if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false; + + localItems.add(actualTo, localItems.remove(actualFrom)); + notifyItemMoved(fromAdapterPosition, toAdapterPosition); + return true; + } + + public void clearStreamItemList() { + if (localItems.isEmpty()) { + return; + } + localItems.clear(); + notifyDataSetChanged(); + } + + public void setHeader(View header) { + boolean changed = header != this.header; + this.header = header; + if (changed) notifyDataSetChanged(); + } + + public void setFooter(View view) { + this.footer = view; + } + + public void showFooter(boolean show) { + if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]"); + if (show == showFooter) return; + + showFooter = show; + if (show) notifyItemInserted(sizeConsideringHeader()); + else notifyItemRemoved(sizeConsideringHeader()); + } + + private int offsetWithoutHeader(final int offset) { + return offset - (header != null ? 1 : 0); + } + + private int sizeConsideringHeader() { + return localItems.size() + (header != null ? 1 : 0); + } + + public ArrayList getItemsList() { + return localItems; + } + + @Override + public int getItemCount() { + int count = localItems.size(); + if (header != null) count++; + if (footer != null && showFooter) count++; + + if (DEBUG) { + Log.d(TAG, "getItemCount() called, count = " + count + + ", localItems.size() = " + localItems.size() + + ", header = " + header + ", footer = " + footer + + ", showFooter = " + showFooter); + } + return count; + } + + @Override + public int getItemViewType(int position) { + if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]"); + + if (header != null && position == 0) { + return HEADER_TYPE; + } else if (header != null) { + position--; + } + if (footer != null && position == localItems.size() && showFooter) { + return FOOTER_TYPE; + } + final LocalItem item = localItems.get(position); + + switch (item.getLocalItemType()) { + case PLAYLIST_ITEM: return PLAYLIST_HOLDER_TYPE; + case PLAYLIST_STREAM_ITEM: return STREAM_PLAYLIST_HOLDER_TYPE; + case STATISTIC_STREAM_ITEM: return STREAM_STATISTICS_HOLDER_TYPE; + default: + Log.e(TAG, "No holder type has been considered for item: [" + + item.getLocalItemType() + "]"); + return -1; + } + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) { + if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + + parent + "], type = [" + type + "]"); + switch (type) { + case HEADER_TYPE: + return new HeaderFooterHolder(header); + case FOOTER_TYPE: + return new HeaderFooterHolder(footer); + case PLAYLIST_HOLDER_TYPE: + return new LocalPlaylistItemHolder(localItemBuilder, parent); + case STREAM_PLAYLIST_HOLDER_TYPE: + return new LocalPlaylistStreamItemHolder(localItemBuilder, parent); + case STREAM_STATISTICS_HOLDER_TYPE: + return new LocalStatisticStreamItemHolder(localItemBuilder, parent); + default: + Log.e(TAG, "No view type has been considered for holder: [" + type + "]"); + return null; + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + + holder.getClass().getSimpleName() + "], position = [" + position + "]"); + + if (holder instanceof LocalItemHolder) { + // If header isn't null, offset the items by -1 + if (header != null) position--; + + ((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat); + } else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) { + ((HeaderFooterHolder) holder).view = header; + } else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader() + && footer != null && showFooter) { + ((HeaderFooterHolder) holder).view = footer; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 7ba5db7e1..2c4af25d9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -7,25 +7,29 @@ import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import java.util.ArrayList; @@ -37,7 +41,7 @@ import io.reactivex.disposables.CompositeDisposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class LocalPlaylistFragment extends BaseListFragment, Void> { +public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { private View headerRootLayout; private TextView headerTitleView; @@ -49,12 +53,14 @@ public class LocalPlaylistFragment extends BaseListFragment, private View headerBackgroundButton; @State - protected long playlistId; + protected Long playlistId; @State protected String name; @State protected Parcelable itemsListState; + private ItemTouchHelper itemTouchHelper; + /* Used for independent events */ private CompositeDisposable disposables = new CompositeDisposable(); private Subscription databaseSubscription; @@ -86,6 +92,9 @@ public class LocalPlaylistFragment extends BaseListFragment, @Override public void onPause() { super.onPause(); + + saveJoin(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); } @@ -115,8 +124,6 @@ public class LocalPlaylistFragment extends BaseListFragment, @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - infoListAdapter.useMiniItemVariants(true); - setFragmentTitle(name); } @@ -141,44 +148,61 @@ public class LocalPlaylistFragment extends BaseListFragment, protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(itemsList); + + itemListAdapter.setSelectedListener(new OnCustomItemGesture() { @Override - public void selected(StreamInfoItem selectedItem) { - // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openVideoDetailFragment(getFragmentManager(), - selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); + public void selected(LocalItem selectedItem) { + if (selectedItem instanceof PlaylistStreamEntry) { + final PlaylistStreamEntry item = (PlaylistStreamEntry) selectedItem; + NavigationHelper.openVideoDetailFragment(getFragmentManager(), + item.serviceId, item.url, item.title); + } } @Override - public void held(StreamInfoItem selectedItem) { - showStreamDialog(selectedItem); + public void held(LocalItem selectedItem) { + if (selectedItem instanceof PlaylistStreamEntry) { + showStreamDialog((PlaylistStreamEntry) selectedItem); + } + } + + @Override + public void drag(LocalItem selectedItem, RecyclerView.ViewHolder viewHolder) { + if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder); } }); - + headerTitleView.setOnClickListener(view -> createRenameDialog()); } - @Override - protected void showStreamDialog(final StreamInfoItem item) { + protected void showStreamDialog(final PlaylistStreamEntry item) { final Context context = getContext(); final Activity activity = getActivity(); if (context == null || context.getResources() == null || getActivity() == null) return; + final StreamInfoItem infoItem = item.toStreamInfoItem(); + final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_popup), context.getResources().getString(R.string.start_here_on_main), context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_popup), + "Set as Thumbnail", + context.getResources().getString(R.string.delete) }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { - final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, + new SinglePlayQueue(infoItem)); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new + SinglePlayQueue(infoItem)); break; case 2: NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); @@ -189,18 +213,56 @@ public class LocalPlaylistFragment extends BaseListFragment, case 4: NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); break; + case 5: + changeThumbnailUrl(item.thumbnailUrl); + break; + case 6: + itemListAdapter.removeItemAt(index); + setVideoCount(itemListAdapter.getItemsList().size()); + break; default: break; } }; - new InfoItemDialog(getActivity(), item, commands, actions).show(); + new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, + ItemTouchHelper.ACTION_STATE_IDLE) { + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, + RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType() || + itemListAdapter == null) { + return false; + } + + final int sourceIndex = source.getAdapterPosition(); + final int targetIndex = target.getAdapterPosition(); + return itemListAdapter.swapItems(sourceIndex, targetIndex); + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} + }; } private void resetFragment() { if (disposables != null) disposables.clear(); if (databaseSubscription != null) databaseSubscription.cancel(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); } /////////////////////////////////////////////////////////////////////////// @@ -224,8 +286,8 @@ public class LocalPlaylistFragment extends BaseListFragment, .subscribe(getPlaylistObserver()); } - private Subscriber> getPlaylistObserver() { - return new Subscriber>() { + private Subscriber> getPlaylistObserver() { + return new Subscriber>() { @Override public void onSubscribe(Subscription s) { showLoading(); @@ -236,7 +298,7 @@ public class LocalPlaylistFragment extends BaseListFragment, } @Override - public void onNext(List streams) { + public void onNext(List streams) { handleResult(streams); if (databaseSubscription != null) databaseSubscription.request(1); } @@ -253,9 +315,9 @@ public class LocalPlaylistFragment extends BaseListFragment, } @Override - public void handleResult(@NonNull List result) { + public void handleResult(@NonNull List result) { super.handleResult(result); - infoListAdapter.clearStreamItemList(); + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { showEmptyState(); @@ -265,15 +327,14 @@ public class LocalPlaylistFragment extends BaseListFragment, animateView(headerRootLayout, true, 100); animateView(itemsList, true, 300); - infoListAdapter.addInfoItemList(getStreamItems(result)); + itemListAdapter.addInfoItemList(result); if (itemsListState != null) { itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); itemsListState = null; } + setVideoCount(itemListAdapter.getItemsList().size()); playlistControl.setVisibility(View.VISIBLE); - headerStreamCount.setText( - getResources().getQuantityString(R.plurals.videos, result.size(), result.size())); headerPlayAllButton.setOnClickListener(view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); @@ -284,29 +345,6 @@ public class LocalPlaylistFragment extends BaseListFragment, hideLoading(); } - - private List getStreamItems(final List streams) { - List items = new ArrayList<>(streams.size()); - for (final StreamEntity stream : streams) { - items.add(stream.toStreamEntityInfoItem()); - } - return items; - } - - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void loadMoreItems() { - // Do nothing - } - - @Override - protected boolean hasMoreItems() { - return false; - } - /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @@ -325,13 +363,13 @@ public class LocalPlaylistFragment extends BaseListFragment, // Utils //////////////////////////////////////////////////////////////////////////*/ - protected void setInitialData(long playlistId, String name) { + private void setInitialData(long playlistId, String name) { this.playlistId = playlistId; this.name = !TextUtils.isEmpty(name) ? name : ""; } - protected void setFragmentTitle(final String title) { - if (activity.getSupportActionBar() != null) { + private void setFragmentTitle(final String title) { + if (activity != null && activity.getSupportActionBar() != null) { activity.getSupportActionBar().setTitle(title); } if (headerTitleView != null) { @@ -339,17 +377,80 @@ public class LocalPlaylistFragment extends BaseListFragment, } } + private void setVideoCount(final long count) { + if (activity != null && headerStreamCount != null) { + headerStreamCount.setText(Localization.localizeStreamCount(activity, count)); + } + } + private PlayQueue getPlayQueue() { return getPlayQueue(0); } private PlayQueue getPlayQueue(final int index) { - final List infoItems = infoListAdapter.getItemsList(); + final List infoItems = itemListAdapter.getItemsList(); List streamInfoItems = new ArrayList<>(infoItems.size()); - for (final InfoItem item : infoItems) { - if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + for (final LocalItem item : infoItems) { + if (item instanceof PlaylistStreamEntry) { + streamInfoItems.add(((PlaylistStreamEntry) item).toStreamInfoItem()); + } } return new SinglePlayQueue(streamInfoItems, index); } + + private void createRenameDialog() { + if (playlistId == null || name == null || getContext() == null) return; + + final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); + EditText nameEdit = dialogView.findViewById(R.id.playlist_name); + nameEdit.setText(name); + nameEdit.setSelection(nameEdit.getText().length()); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) + .setTitle(R.string.rename_playlist) + .setView(dialogView) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.create, (dialogInterface, i) -> { + name = nameEdit.getText().toString(); + setFragmentTitle(name); + + final LocalPlaylistManager playlistManager = + new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + final Toast successToast = Toast.makeText(getActivity(), + "Playlist renamed", + Toast.LENGTH_SHORT); + + playlistManager.renamePlaylist(playlistId, name) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> successToast.show()); + }); + + dialogBuilder.show(); + } + + private void changeThumbnailUrl(final String thumbnailUrl) { + final Toast successToast = Toast.makeText(getActivity(), + "Playlist thumbnail changed", + Toast.LENGTH_SHORT); + + playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignore -> successToast.show()); + } + + private void saveJoin() { + final List items = itemListAdapter.getItemsList(); + List streamIds = new ArrayList<>(items.size()); + for (final LocalItem item : items) { + if (item instanceof PlaylistStreamEntry) { + streamIds.add(((PlaylistStreamEntry) item).streamId); + } + } + + playlistManager.updateJoin(playlistId, streamIds) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index 4bc161c04..c266f5365 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -4,6 +4,7 @@ import android.support.annotation.Nullable; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; @@ -84,7 +85,7 @@ public class LocalPlaylistManager { return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); } - public Flowable> getPlaylistStreams(final long playlistId) { + public Flowable> getPlaylistStreams(final long playlistId) { return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java deleted file mode 100644 index 7862cf2f4..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.schabi.newpipe.fragments.local; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class MostPlayedFragment extends StatisticsPlaylistFragment { - @Override - protected String getName() { - return getString(R.string.title_most_played); - } - - @Override - protected List processResult(List results) { - Collections.sort(results, (left, right) -> - ((Long) right.watchCount).compareTo(left.watchCount)); - - List items = new ArrayList<>(results.size()); - for (final StreamStatisticsEntry stream : results) { - items.add(stream.toStreamStatisticsInfoItem()); - } - return items; - } - - @Override - protected String getAdditionalDetail(StreamStatisticsInfoItem infoItem) { - final int watchCount = (int) infoItem.getWatchCount(); - return getResources().getQuantityString(R.plurals.views, watchCount, watchCount); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java b/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java new file mode 100644 index 000000000..0b65c595a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java @@ -0,0 +1,19 @@ +package org.schabi.newpipe.fragments.local; + +import android.support.v7.widget.RecyclerView; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.extractor.InfoItem; + +public abstract class OnCustomItemGesture { + + public abstract void selected(T selectedItem); + + public void held(T selectedItem) { + // Optional gesture + } + + public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) { + // Optional gesture + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java deleted file mode 100644 index 2a4b8cfb0..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.schabi.newpipe.fragments.local; - -import android.text.format.DateFormat; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class WatchHistoryFragment extends StatisticsPlaylistFragment { - @Override - protected String getName() { - return getString(R.string.title_watch_history); - } - - @Override - protected List processResult(List results) { - Collections.sort(results, (left, right) -> - right.latestAccessDate.compareTo(left.latestAccessDate)); - - List items = new ArrayList<>(results.size()); - for (final StreamStatisticsEntry stream : results) { - items.add(stream.toStreamStatisticsInfoItem()); - } - return items; - } - - @Override - protected String getAdditionalDetail(StreamStatisticsInfoItem infoItem) { - return DateFormat.getLongDateFormat(getContext()).format(infoItem.getLatestAccessDate()); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java similarity index 74% rename from app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 769365dd8..01b433052 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -1,7 +1,7 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.bookmark; +import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; @@ -17,18 +17,15 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.info_list.OnInfoItemGesture; -import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; +import org.schabi.newpipe.fragments.local.LocalItemListAdapter; +import org.schabi.newpipe.fragments.local.LocalPlaylistManager; +import org.schabi.newpipe.fragments.local.OnCustomItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -42,7 +39,7 @@ public class BookmarkFragment extends BaseStateFragment() { + itemListAdapter.setSelectedListener(new OnCustomItemGesture() { @Override - public void selected(PlaylistInfoItem selectedItem) { + public void selected(LocalItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement - if (selectedItem instanceof LocalPlaylistInfoItem && getParentFragment() != null) { - final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); - + if (selectedItem instanceof PlaylistMetadataEntry && getParentFragment() != null) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); NavigationHelper.openLocalPlaylistFragment( - getParentFragment().getFragmentManager(), - playlistId, - selectedItem.getName() - ); + getParentFragment().getFragmentManager(), entry.uid, entry.name); } } @Override - public void held(PlaylistInfoItem selectedItem) { - if (selectedItem instanceof LocalPlaylistInfoItem) { - showPlaylistDialog((LocalPlaylistInfoItem) selectedItem); + public void held(LocalItem selectedItem) { + if (selectedItem instanceof PlaylistMetadataEntry) { + showDeleteDialog((PlaylistMetadataEntry) selectedItem); } } }); @@ -177,36 +168,25 @@ public class BookmarkFragment extends BaseStateFragment { - switch (i) { - case 0: + private void showDeleteDialog(final PlaylistMetadataEntry item) { + new AlertDialog.Builder(activity) + .setTitle(item.name) + .setMessage(R.string.delete_playlist_prompt) + .setCancelable(true) + .setPositiveButton(R.string.delete, (dialog, i) -> { final Toast deleteSuccessful = Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT); - disposables.add(localPlaylistManager.deletePlaylist(item.getPlaylistId()) + disposables.add(localPlaylistManager.deletePlaylist(item.uid) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> deleteSuccessful.show())); - break; - default: - break; - } - }; - - final String videoCount = getResources().getQuantityString(R.plurals.videos, - (int) item.getStreamCount(), (int) item.getStreamCount()); - new InfoItemDialog(getActivity(), commands, actions, item.getName(), videoCount).show(); + }) + .setNegativeButton(R.string.cancel, null) + .show(); } private void resetFragment() { if (disposables != null) disposables.clear(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); } /////////////////////////////////////////////////////////////////////////// @@ -254,12 +234,12 @@ public class BookmarkFragment extends BaseStateFragment result) { super.handleResult(result); - infoListAdapter.clearStreamItemList(); + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { showEmptyState(); } else { - infoListAdapter.addInfoItemList(infoItemsOf(result)); + itemListAdapter.addInfoItemList(infoItemsOf(result)); if (itemsListState != null) { itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); itemsListState = null; @@ -269,13 +249,9 @@ public class BookmarkFragment extends BaseStateFragment infoItemsOf(List playlists) { - List playlistInfoItems = new ArrayList<>(playlists.size()); - for (final PlaylistMetadataEntry playlist : playlists) { - playlistInfoItems.add(playlist.toStoredPlaylistInfoItem()); - } - Collections.sort(playlistInfoItems, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); - return playlistInfoItems; + private List infoItemsOf(List playlists) { + Collections.sort(playlists, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); + return playlists; } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java new file mode 100644 index 000000000..ed0d903a8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java @@ -0,0 +1,22 @@ +package org.schabi.newpipe.fragments.local.bookmark; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; + +import java.util.Collections; +import java.util.List; + +public class MostPlayedFragment extends StatisticsPlaylistFragment { + @Override + protected String getName() { + return getString(R.string.title_most_played); + } + + @Override + protected List processResult(List results) { + Collections.sort(results, (left, right) -> + ((Long) right.watchCount).compareTo(left.watchCount)); + return results; + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java similarity index 80% rename from app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 6eddc3a5c..5c2959d9c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.bookmark; import android.app.Activity; import android.content.Context; @@ -14,14 +14,13 @@ import android.view.ViewGroup; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.fragments.local.BaseLocalListFragment; +import org.schabi.newpipe.fragments.local.OnCustomItemGesture; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.OnInfoItemGesture; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; @@ -36,7 +35,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import static org.schabi.newpipe.util.AnimationUtils.animateView; public abstract class StatisticsPlaylistFragment - extends BaseListFragment, Void> { + extends BaseLocalListFragment, Void> { private View headerRootLayout; private View playlistControl; @@ -57,9 +56,7 @@ public abstract class StatisticsPlaylistFragment protected abstract String getName(); - protected abstract List processResult(final List results); - - protected abstract String getAdditionalDetail(final StreamStatisticsInfoItem infoItem); + protected abstract List processResult(final List results); /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle @@ -106,8 +103,6 @@ public abstract class StatisticsPlaylistFragment @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - infoListAdapter.useMiniItemVariants(true); - setFragmentTitle(getName()); } @@ -127,27 +122,31 @@ public abstract class StatisticsPlaylistFragment protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { + itemListAdapter.setSelectedListener(new OnCustomItemGesture() { @Override - public void selected(StreamInfoItem selectedItem) { - NavigationHelper.openVideoDetailFragment(getFragmentManager(), - selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); + public void selected(LocalItem selectedItem) { + if (selectedItem instanceof StreamStatisticsEntry) { + final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem; + NavigationHelper.openVideoDetailFragment(getFragmentManager(), + item.serviceId, item.url, item.title); + } } @Override - public void held(StreamInfoItem selectedItem) { - showStreamDialog(selectedItem); + public void held(LocalItem selectedItem) { + if (selectedItem instanceof StreamStatisticsEntry) { + showStreamDialog((StreamStatisticsEntry) selectedItem); + } } }); } - @Override - protected void showStreamDialog(final StreamInfoItem item) { + private void showStreamDialog(final StreamStatisticsEntry item) { final Context context = getContext(); final Activity activity = getActivity(); - if (context == null || context.getResources() == null - || getActivity() == null || !(item instanceof StreamStatisticsInfoItem)) return; + if (context == null || context.getResources() == null || getActivity() == null) return; + final StreamInfoItem infoItem = item.toStreamInfoItem(); final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), @@ -158,13 +157,13 @@ public abstract class StatisticsPlaylistFragment }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { - final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem)); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem)); break; case 2: NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); @@ -180,13 +179,12 @@ public abstract class StatisticsPlaylistFragment } }; - final String detail = getAdditionalDetail((StreamStatisticsInfoItem) item); - new InfoItemDialog(getActivity(), commands, actions, item.getName(), detail).show(); + new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); } private void resetFragment() { if (databaseSubscription != null) databaseSubscription.cancel(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); } /////////////////////////////////////////////////////////////////////////// @@ -241,7 +239,7 @@ public abstract class StatisticsPlaylistFragment @Override public void handleResult(@NonNull List result) { super.handleResult(result); - infoListAdapter.clearStreamItemList(); + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { showEmptyState(); @@ -251,7 +249,7 @@ public abstract class StatisticsPlaylistFragment animateView(headerRootLayout, true, 100); animateView(itemsList, true, 300); - infoListAdapter.addInfoItemList(processResult(result)); + itemListAdapter.addInfoItemList(processResult(result)); if (itemsListState != null) { itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); itemsListState = null; @@ -267,20 +265,6 @@ public abstract class StatisticsPlaylistFragment hideLoading(); } - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void loadMoreItems() { - // Do nothing - } - - @Override - protected boolean hasMoreItems() { - return false; - } - /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @@ -310,10 +294,12 @@ public abstract class StatisticsPlaylistFragment } private PlayQueue getPlayQueue(final int index) { - final List infoItems = infoListAdapter.getItemsList(); + final List infoItems = itemListAdapter.getItemsList(); List streamInfoItems = new ArrayList<>(infoItems.size()); - for (final InfoItem item : infoItems) { - if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + for (final LocalItem item : infoItems) { + if (item instanceof StreamStatisticsEntry) { + streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem()); + } } return new SinglePlayQueue(streamInfoItems, index); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java new file mode 100644 index 000000000..853029ae6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java @@ -0,0 +1,21 @@ +package org.schabi.newpipe.fragments.local.bookmark; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; + +import java.util.Collections; +import java.util.List; + +public class WatchHistoryFragment extends StatisticsPlaylistFragment { + @Override + protected String getName() { + return getString(R.string.title_watch_history); + } + + @Override + protected List processResult(List results) { + Collections.sort(results, (left, right) -> + right.latestAccessDate.compareTo(left.latestAccessDate)); + return results; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java new file mode 100644 index 000000000..64dc84472 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java @@ -0,0 +1,56 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; + +import java.text.DateFormat; + +/* + * Created by Christian Schabesberger on 12.02.17. + * + * Copyright (C) Christian Schabesberger 2016 + * InfoItemHolder.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 abstract class LocalItemHolder extends RecyclerView.ViewHolder { + protected final LocalItemBuilder itemBuilder; + + public LocalItemHolder(LocalItemBuilder itemBuilder, int layoutId, ViewGroup parent) { + super(LayoutInflater.from(itemBuilder.getContext()) + .inflate(layoutId, parent, false)); + this.itemBuilder = itemBuilder; + } + + public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); + + /*////////////////////////////////////////////////////////////////////////// + // ImageLoaderOptions + //////////////////////////////////////////////////////////////////////////*/ + + /** + * Base display options + */ + public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = + new DisplayImageOptions.Builder() + .cacheInMemory(true) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java new file mode 100644 index 000000000..d04fc123a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java @@ -0,0 +1,74 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; + +import java.text.DateFormat; + +public class LocalPlaylistItemHolder extends LocalItemHolder { + public final ImageView itemThumbnailView; + public final TextView itemStreamCountView; + public final TextView itemTitleView; + public final TextView itemUploaderView; + + public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, + int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemTitleView = itemView.findViewById(R.id.itemTitleView); + itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + } + + public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + } + + @Override + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistMetadataEntry)) return; + final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; + + itemTitleView.setText(item.name); + itemStreamCountView.setText(String.valueOf(item.streamCount)); + itemUploaderView.setVisibility(View.INVISIBLE); + + itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, + DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(item); + } + }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(item); + } + return true; + }); + } + + /** + * Display options for playlist thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java similarity index 58% rename from app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java index 8261d4760..712db8f8a 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,7 +1,6 @@ -package org.schabi.newpipe.info_list.holder; +package org.schabi.newpipe.fragments.local.holder; import android.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -11,40 +10,44 @@ import android.widget.TextView; import com.nostra13.universalimageloader.core.DisplayImageOptions; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; import org.schabi.newpipe.util.Localization; -public class StreamPlaylistInfoItemHolder extends InfoItemHolder { +import java.text.DateFormat; + +public class LocalPlaylistStreamItemHolder extends LocalItemHolder { public final ImageView itemThumbnailView; public final TextView itemVideoTitleView; - public final TextView itemUploaderView; + public final TextView itemAdditionalDetailsView; public final TextView itemDurationView; public final View itemHandleView; - StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); - itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails); itemDurationView = itemView.findViewById(R.id.itemDurationView); itemHandleView = itemView.findViewById(R.id.itemHandle); } - public StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { - this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_stream_playlist_item, parent); } @Override - public void updateFromItem(final InfoItem infoItem) { - if (!(infoItem instanceof StreamInfoItem)) return; - final StreamInfoItem item = (StreamInfoItem) infoItem; + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistStreamEntry)) return; + final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; - itemVideoTitleView.setText(item.getName()); - itemUploaderView.setText(item.uploader_name); + itemVideoTitleView.setText(item.title); + itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.uploader, + NewPipe.getNameOfService(item.serviceId))); if (item.duration > 0) { itemDurationView.setText(Localization.getDurationString(item.duration)); @@ -56,19 +59,19 @@ public class StreamPlaylistInfoItemHolder extends InfoItemHolder { } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.getImageLoader().displayImage(item.thumbnail_url, itemThumbnailView, - StreamPlaylistInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, + LocalPlaylistStreamItemHolder.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { - if (itemBuilder.getOnStreamSelectedListener() != null) { - itemBuilder.getOnStreamSelectedListener().selected(item); + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(item); } }); itemView.setLongClickable(true); itemView.setOnLongClickListener(view -> { - if (itemBuilder.getOnStreamSelectedListener() != null) { - itemBuilder.getOnStreamSelectedListener().held(item); + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(item); } return true; }); @@ -77,13 +80,13 @@ public class StreamPlaylistInfoItemHolder extends InfoItemHolder { itemHandleView.setOnTouchListener(getOnTouchListener(item)); } - private View.OnTouchListener getOnTouchListener(final StreamInfoItem item) { + private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) { return (view, motionEvent) -> { view.performClick(); if (itemBuilder != null && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - itemBuilder.getOnStreamSelectedListener() - .drag(item, StreamPlaylistInfoItemHolder.this); + itemBuilder.getOnItemSelectedListener().drag(item, + LocalPlaylistStreamItemHolder.this); } return false; }; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java new file mode 100644 index 000000000..bce6bab76 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java @@ -0,0 +1,119 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.support.v4.content.ContextCompat; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.Localization; + +import java.text.DateFormat; + +/* + * 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 LocalStatisticStreamItemHolder extends LocalItemHolder { + + public final ImageView itemThumbnailView; + public final TextView itemVideoTitleView; + public final TextView itemUploaderView; + public final TextView itemDurationView; + public final TextView itemAdditionalDetails; + + LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + itemDurationView = itemView.findViewById(R.id.itemDurationView); + itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); + } + + public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_stream_item, parent); + } + + private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, + final DateFormat dateFormat) { + final String watchCount = Localization.shortViewCount(itemBuilder.getContext(), + entry.watchCount); + final String uploadDate = dateFormat.format(entry.latestAccessDate); + final String serviceName = NewPipe.getNameOfService(entry.serviceId); + return Localization.concatenateStrings(watchCount, uploadDate, serviceName); + } + + @Override + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof StreamStatisticsEntry)) return; + final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; + + itemVideoTitleView.setText(item.title); + itemUploaderView.setText(item.uploader); + + if (item.duration > 0) { + itemDurationView.setText(Localization.getDurationString(item.duration)); + itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), + R.color.duration_background_color)); + itemDurationView.setVisibility(View.VISIBLE); + } else { + itemDurationView.setVisibility(View.GONE); + } + + itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); + + // Default thumbnail is shown on error, while loading and if the url is empty + itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, + DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(item); + } + }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(item); + } + return true; + }); + } + + /** + * Display options for stream thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnFail(R.drawable.dummy_thumbnail) + .showImageForEmptyUri(R.drawable.dummy_thumbnail) + .showImageOnLoading(R.drawable.dummy_thumbnail) + .build(); +} 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 462c12e61..5369c657c 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -32,6 +32,7 @@ import io.reactivex.Flowable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -169,8 +170,14 @@ public abstract class HistoryFragment extends BaseFragment private void clearHistory() { final Collection itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); - disposables.add(delete(itemsToDelete).observeOn(AndroidSchedulers.mainThread()) - .subscribe()); + + final Disposable deletion = delete(itemsToDelete) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + final Disposable cleanUp = historyRecordManager.removeOrphanedRecords() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + disposables.addAll(deletion, cleanUp); makeSnackbar(R.string.history_cleared); mHistoryAdapter.clear(); diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java index 1a5fe0525..9d9b74b30 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java @@ -48,6 +48,14 @@ public class HistoryRecordManager { streamHistoryKey = context.getString(R.string.enable_watch_history_key); } + public Single removeOrphanedRecords() { + return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); + } + + /////////////////////////////////////////////////////// + // Watch History + /////////////////////////////////////////////////////// + public Maybe onViewed(final StreamInfo info) { if (!isStreamHistoryEnabled()) return Maybe.empty(); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 1dc4442c7..dbf5d7556 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -55,10 +55,6 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; private boolean useMiniVariant = false; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java b/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java index 3e6fe2213..84634c1d9 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java @@ -1,7 +1,5 @@ package org.schabi.newpipe.info_list; -import android.support.v7.widget.RecyclerView; - import org.schabi.newpipe.extractor.InfoItem; public abstract class OnInfoItemGesture { @@ -11,8 +9,4 @@ public abstract class OnInfoItemGesture { public void held(T selectedItem) { // Optional gesture } - - public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) { - // Optional gesture - } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java deleted file mode 100644 index a54135211..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.schabi.newpipe.info_list.stored; - -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; - -public class StreamEntityInfoItem extends StreamInfoItem { - protected final long streamId; - - public StreamEntityInfoItem(final long streamId, final int serviceId, - final String url, final String name, final StreamType type) { - super(serviceId, url, name, type); - this.streamId = streamId; - } - - public long getStreamId() { - return streamId; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java deleted file mode 100644 index 6659b551a..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.schabi.newpipe.info_list.stored; - -import org.schabi.newpipe.extractor.stream.StreamType; - -import java.util.Date; - -public final class StreamStatisticsInfoItem extends StreamEntityInfoItem { - private Date latestAccessDate; - private long watchCount; - - public StreamStatisticsInfoItem(final long streamId, final int serviceId, - final String url, final String name, final StreamType type) { - super(streamId, serviceId, url, name, type); - } - - public Date getLatestAccessDate() { - return latestAccessDate; - } - - public void setLatestAccessDate(Date latestAccessDate) { - this.latestAccessDate = latestAccessDate; - } - - public long getWatchCount() { - return watchCount; - } - - public void setWatchCount(long watchCount) { - this.watchCount = watchCount; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 43ebc1677..c1e5c9ed4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.annotation.PluralsRes; import android.support.annotation.StringRes; import android.text.TextUtils; @@ -14,7 +15,9 @@ import java.text.DateFormat; import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Locale; /* @@ -39,9 +42,33 @@ import java.util.Locale; public class Localization { + public final static String DOT_SEPARATOR = " • "; + private Localization() { } + @NonNull + public static String concatenateStrings(final String... strings) { + return concatenateStrings(Arrays.asList(strings)); + } + + @NonNull + public static String concatenateStrings(final List strings) { + if (strings.isEmpty()) return ""; + + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(strings.get(0)); + + for (int i = 1; i < strings.size(); i++) { + final String string = strings.get(i); + if (!TextUtils.isEmpty(string)) { + stringBuilder.append(DOT_SEPARATOR).append(strings.get(i)); + } + } + + return stringBuilder.toString(); + } + public static Locale getPreferredLocale(Context context) { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 7ffbf07ed..3acfb6683 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -35,8 +35,8 @@ import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistFragment; -import org.schabi.newpipe.fragments.local.MostPlayedFragment; -import org.schabi.newpipe.fragments.local.WatchHistoryFragment; +import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; +import org.schabi.newpipe.fragments.local.bookmark.WatchHistoryFragment; import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; diff --git a/app/src/main/res/layout/list_stream_playlist_item.xml b/app/src/main/res/layout/list_stream_playlist_item.xml index 3a5b1b8e6..193b3fea4 100644 --- a/app/src/main/res/layout/list_stream_playlist_item.xml +++ b/app/src/main/res/layout/list_stream_playlist_item.xml @@ -70,7 +70,7 @@ tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique..."/> + tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur..." /> - + android:visible="true" + app:showAsAction="ifRoom"/> Delete One Delete All Checksum + Dismiss New mission @@ -380,6 +381,8 @@ Create New Playlist Delete Playlist + Rename Playlist Name Add To Playlist + Do you want to delete this playlist? From d31eeac49e1aef25b0ca506fe64cc32db813a54a Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 28 Jan 2018 18:26:19 -0800 Subject: [PATCH 15/52] -Condensed repeating entries on stream history. -Changed search history to show service name and stream history to show repeat count. -Removed history entry abstract and unused info items. --- .../schabi/newpipe/database/Migrations.java | 6 +- .../history/dao/SearchHistoryDAO.java | 5 +- .../history/dao/StreamHistoryDAO.java | 3 +- .../database/history/model/HistoryEntry.java | 60 ------------------- .../history/model/SearchHistoryEntry.java | 50 ++++++++++++++-- .../history/model/StreamHistoryEntity.java | 21 ++++++- .../history/model/StreamHistoryEntry.java | 13 +++- .../playlist/PlaylistMetadataEntry.java | 8 --- .../fragments/local/LocalItemBuilder.java | 6 +- .../fragments/local/LocalItemListAdapter.java | 2 +- .../local/LocalPlaylistFragment.java | 2 +- ...emGesture.java => OnLocalItemGesture.java} | 2 +- .../fragments/local/PlaylistAppendDialog.java | 26 +++----- .../local/bookmark/BookmarkFragment.java | 4 +- .../bookmark/StatisticsPlaylistFragment.java | 4 +- .../newpipe/history/HistoryEntryAdapter.java | 7 +++ .../newpipe/history/HistoryRecordManager.java | 11 +++- .../history/SearchHistoryFragment.java | 12 +++- .../history/WatchedHistoryFragment.java | 15 ++++- .../stored/LocalPlaylistInfoItem.java | 20 ------- .../org/schabi/newpipe/util/Constants.java | 1 - .../main/res/layout/item_search_history.xml | 2 +- 22 files changed, 140 insertions(+), 140 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/model/HistoryEntry.java rename app/src/main/java/org/schabi/newpipe/fragments/local/{OnCustomItemGesture.java => OnLocalItemGesture.java} (86%) delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 825ec5fd5..fdfd04a84 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -23,7 +23,7 @@ public class Migrations { database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); database.execSQL("CREATE INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)"); @@ -45,8 +45,8 @@ public class Migrations { // Once the streams have PKs, join them with the normalized history table // and populate it with the remaining data from watch history - database.execSQL("INSERT INTO stream_history (stream_id, access_date)" + - "SELECT uid, creation_date " + + database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" + + "SELECT uid, creation_date, 1 " + "FROM watch_history INNER JOIN streams " + "ON watch_history.service_id == streams.service_id " + "AND watch_history.url == streams.url " + diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java index 257c1ec3d..b0a3c3a3c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java @@ -4,6 +4,7 @@ import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; import android.support.annotation.Nullable; +import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import java.util.List; @@ -21,8 +22,8 @@ public interface SearchHistoryDAO extends HistoryDAO { String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; - @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") - @Override + @Query("SELECT * FROM " + TABLE_NAME + + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") @Nullable SearchHistoryEntry getLatestEntry(); diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index fe19d362e..fd7a1b96f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -14,6 +14,7 @@ import java.util.List; import io.reactivex.Flowable; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT; import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; @@ -59,7 +60,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO onSelectedListener; + private OnLocalItemGesture onSelectedListener; public LocalItemBuilder(Context context) { this.context = context; @@ -46,11 +46,11 @@ public class LocalItemBuilder { return imageLoader; } - public OnCustomItemGesture getOnItemSelectedListener() { + public OnLocalItemGesture getOnItemSelectedListener() { return onSelectedListener; } - public void setOnItemSelectedListener(OnCustomItemGesture listener) { + public void setOnItemSelectedListener(OnLocalItemGesture listener) { this.onSelectedListener = listener; } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index 9c621a55c..af1a0f666 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -66,7 +66,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter listener) { + public void setSelectedListener(OnLocalItemGesture listener) { localItemBuilder.setOnItemSelectedListener(listener); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 2c4af25d9..d54a4b4ae 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -151,7 +151,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { + itemListAdapter.setSelectedListener(new OnLocalItemGesture() { @Override public void selected(LocalItem selectedItem) { if (selectedItem instanceof PlaylistStreamEntry) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java b/app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java similarity index 86% rename from app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java index 0b65c595a..5cede4c67 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java @@ -5,7 +5,7 @@ import android.support.v7.widget.RecyclerView; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.extractor.InfoItem; -public abstract class OnCustomItemGesture { +public abstract class OnLocalItemGesture { public abstract void selected(T selectedItem); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index 6ed357e36..302a37002 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -13,15 +13,11 @@ import android.widget.Toast; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.info_list.OnInfoItemGesture; -import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; import java.util.ArrayList; @@ -34,7 +30,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog { private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); private RecyclerView playlistRecyclerView; - private InfoListAdapter playlistAdapter; + private LocalItemListAdapter playlistAdapter; public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) { PlaylistAppendDialog dialog = new PlaylistAppendDialog(); @@ -69,8 +65,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog { @Override public void onAttach(Context context) { super.onAttach(context); - playlistAdapter = new InfoListAdapter(getActivity()); - playlistAdapter.useMiniItemVariants(true); + playlistAdapter = new LocalItemListAdapter(getActivity()); } /*////////////////////////////////////////////////////////////////////////// @@ -97,13 +92,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog { newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); - playlistAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture() { + playlistAdapter.setSelectedListener(new OnLocalItemGesture() { @Override - public void selected(PlaylistInfoItem selectedItem) { - if (!(selectedItem instanceof LocalPlaylistInfoItem) || getStreams() == null) + public void selected(LocalItem selectedItem) { + if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) return; - final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); + final long playlistId = ((PlaylistMetadataEntry) selectedItem).uid; final Toast successToast = Toast.makeText(getContext(), "Added", Toast.LENGTH_SHORT); @@ -123,13 +118,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog { return; } - List playlistInfoItems = new ArrayList<>(metadataEntries.size()); - for (final PlaylistMetadataEntry metadataEntry : metadataEntries) { - playlistInfoItems.add(metadataEntry.toStoredPlaylistInfoItem()); - } - playlistAdapter.clearStreamItemList(); - playlistAdapter.addInfoItemList(playlistInfoItems); + playlistAdapter.addInfoItemList(metadataEntries); playlistRecyclerView.setVisibility(View.VISIBLE); }); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 01b433052..0bd0fa00f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -22,7 +22,7 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.local.LocalItemListAdapter; import org.schabi.newpipe.fragments.local.LocalPlaylistManager; -import org.schabi.newpipe.fragments.local.OnCustomItemGesture; +import org.schabi.newpipe.fragments.local.OnLocalItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; @@ -136,7 +136,7 @@ public class BookmarkFragment extends BaseStateFragment() { + itemListAdapter.setSelectedListener(new OnLocalItemGesture() { @Override public void selected(LocalItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 5c2959d9c..cb2d671cc 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -18,7 +18,7 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.local.BaseLocalListFragment; -import org.schabi.newpipe.fragments.local.OnCustomItemGesture; +import org.schabi.newpipe.fragments.local.OnLocalItemGesture; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.playlist.PlayQueue; @@ -122,7 +122,7 @@ public abstract class StatisticsPlaylistFragment protected void initListeners() { super.initListeners(); - itemListAdapter.setSelectedListener(new OnCustomItemGesture() { + itemListAdapter.setSelectedListener(new OnLocalItemGesture() { @Override public void selected(LocalItem selectedItem) { if (selectedItem instanceof StreamStatisticsEntry) { diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java index f61e8eb7d..4170a1139 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.history; import android.content.Context; +import android.content.res.Resources; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; @@ -22,11 +23,13 @@ public abstract class HistoryEntryAdapter private final ArrayList mEntries; private final DateFormat mDateFormat; + private final Context mContext; private OnHistoryItemClickListener onHistoryItemClickListener = null; public HistoryEntryAdapter(Context context) { super(); + mContext = context; mEntries = new ArrayList<>(); mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, Localization.getPreferredLocale(context)); @@ -51,6 +54,10 @@ public abstract class HistoryEntryAdapter return mDateFormat.format(date); } + protected String getFormattedViewString(final long viewCount) { + return Localization.shortViewCount(mContext, viewCount); + } + @Override public int getItemCount() { return mEntries.size(); diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java index 9d9b74b30..839b8c89b 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java @@ -62,7 +62,16 @@ public class HistoryRecordManager { final Date currentTime = new Date(); return Maybe.fromCallable(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); - return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); + StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(); + + if (latestEntry != null && latestEntry.getStreamUid() == streamId) { + streamHistoryTable.delete(latestEntry); + latestEntry.setAccessDate(currentTime); + latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1); + return streamHistoryTable.insert(latestEntry); + } else { + return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); + } })).subscribeOn(Schedulers.io()); } 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 a8bba0573..e40a79368 100644 --- a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java @@ -14,6 +14,8 @@ import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import java.util.Collection; @@ -99,12 +101,12 @@ public class SearchHistoryFragment extends HistoryFragment { private static class ViewHolder extends RecyclerView.ViewHolder { private final TextView search; - private final TextView time; + private final TextView info; public ViewHolder(View itemView) { super(itemView); search = itemView.findViewById(R.id.search); - time = itemView.findViewById(R.id.time); + info = itemView.findViewById(R.id.info); } } @@ -125,7 +127,11 @@ public class SearchHistoryFragment extends HistoryFragment { @Override void onBindViewHolder(ViewHolder holder, SearchHistoryEntry entry, int position) { holder.search.setText(entry.getSearch()); - holder.time.setText(getFormattedDate(entry.getCreationDate())); + + final String info = Localization.concatenateStrings( + getFormattedDate(entry.getCreationDate()), + NewPipe.getNameOfService(entry.getServiceId())); + holder.info.setText(info); } } } 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 026d5ee16..7913c9a28 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java @@ -123,7 +123,16 @@ public class WatchedHistoryFragment extends HistoryFragment @Override void onBindViewHolder(ViewHolder holder, StreamHistoryEntry entry, int position) { - holder.date.setText(getFormattedDate(entry.accessDate)); + final String formattedDate = getFormattedDate(entry.accessDate); + final String info; + if (entry.repeatCount > 1) { + info = Localization.concatenateStrings(formattedDate, + getFormattedViewString(entry.repeatCount)); + } else { + info = formattedDate; + } + + holder.info.setText(info); holder.streamTitle.setText(entry.title); holder.uploader.setText(entry.uploader); holder.duration.setText(Localization.getDurationString(entry.duration)); @@ -133,7 +142,7 @@ public class WatchedHistoryFragment extends HistoryFragment } private static class ViewHolder extends RecyclerView.ViewHolder { - private final TextView date; + private final TextView info; private final TextView streamTitle; private final ImageView thumbnailView; private final TextView uploader; @@ -142,7 +151,7 @@ public class WatchedHistoryFragment extends HistoryFragment public ViewHolder(View itemView) { super(itemView); thumbnailView = itemView.findViewById(R.id.itemThumbnailView); - date = itemView.findViewById(R.id.itemAdditionalDetails); + info = itemView.findViewById(R.id.itemAdditionalDetails); streamTitle = itemView.findViewById(R.id.itemVideoTitleView); uploader = itemView.findViewById(R.id.itemUploaderView); duration = itemView.findViewById(R.id.itemDurationView); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java deleted file mode 100644 index b0afe1948..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.schabi.newpipe.info_list.stored; - -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; - -import static org.schabi.newpipe.util.Constants.NO_SERVICE_ID; -import static org.schabi.newpipe.util.Constants.NO_URL; - -public final class LocalPlaylistInfoItem extends PlaylistInfoItem { - private final long playlistId; - - public LocalPlaylistInfoItem(final long playlistId, final String name) { - super(NO_SERVICE_ID, NO_URL, name); - - this.playlistId = playlistId; - } - - public long getPlaylistId() { - return playlistId; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java index 4238424d9..a6aec96e2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Constants.java +++ b/app/src/main/java/org/schabi/newpipe/util/Constants.java @@ -12,5 +12,4 @@ public class Constants { public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; public static final int NO_SERVICE_ID = -1; - public static final String NO_URL = ""; } diff --git a/app/src/main/res/layout/item_search_history.xml b/app/src/main/res/layout/item_search_history.xml index a89882c0c..2c40ca1d1 100644 --- a/app/src/main/res/layout/item_search_history.xml +++ b/app/src/main/res/layout/item_search_history.xml @@ -13,7 +13,7 @@ android:paddingTop="8dp"> From 9b4a07de345eb76c8ecc1ab944614affc48227e4 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 28 Jan 2018 23:01:06 -0800 Subject: [PATCH 16/52] -Redone control panel in video detail fragment. -Added playlist append menu item to channel and playlist fragments. -Added debouncing to local playlist fragment to allow saving join when list is reordered or item is deleted. -Extracted hardcoded strings. --- .../fragments/detail/VideoDetailFragment.java | 1 + .../fragments/list/BaseListFragment.java | 18 ++ .../list/channel/ChannelFragment.java | 19 +- .../list/playlist/PlaylistFragment.java | 19 +- .../fragments/local/LocalItemListAdapter.java | 20 +- .../local/LocalPlaylistFragment.java | 46 ++- .../fragments/local/PlaylistAppendDialog.java | 4 +- .../local/PlaylistCreationDialog.java | 2 +- .../local/bookmark/BookmarkFragment.java | 4 +- .../bookmark/StatisticsPlaylistFragment.java | 2 +- app/src/main/res/layout/dialog_playlists.xml | 2 +- .../main/res/layout/fragment_video_detail.xml | 288 +++++++++--------- app/src/main/res/menu/menu_channel.xml | 8 + app/src/main/res/menu/menu_playlist.xml | 11 +- app/src/main/res/values/strings.xml | 7 + 15 files changed, 279 insertions(+), 172 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index b134bc98d..6907f3266 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1031,6 +1031,7 @@ public class VideoDetailFragment extends BaseStateFragment implement if (!TextUtils.isEmpty(info.getUploaderName())) { uploaderTextView.setText(info.getUploaderName()); uploaderTextView.setVisibility(View.VISIBLE); + uploaderTextView.setSelected(true); } else { uploaderTextView.setVisibility(View.GONE); } 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 9e4fe89ab..82b45c76e 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 @@ -20,6 +20,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.OnInfoItemGesture; @@ -27,6 +28,7 @@ import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.StateSaver; +import java.util.ArrayList; import java.util.List; import java.util.Queue; @@ -283,4 +285,20 @@ public abstract class BaseListFragment extends BaseStateFragment implem public void handleNextItems(N result) { isLoading.set(false); } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + protected void appendToPlaylist(final android.support.v4.app.FragmentManager manager, + final String tag) { + if (infoListAdapter == null) return; + List streams = new ArrayList<>(); + for (final InfoItem item : infoListAdapter.getItemsList()) { + if (item instanceof StreamInfoItem) { + streams.add((StreamInfoItem) item); + } + } + PlaylistAppendDialog.fromStreamInfoItems(streams).show(manager, tag); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 1b24a5dce..641b26299 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -84,6 +84,7 @@ public class ChannelFragment extends BaseListInfoFragment { private LinearLayout headerBackgroundButton; private MenuItem menuRssButton; + private MenuItem playlistAppendButton; public static ChannelFragment getInstance(int serviceId, String url, String name) { ChannelFragment instance = new ChannelFragment(); @@ -194,17 +195,20 @@ public class ChannelFragment extends BaseListInfoFragment { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); - if(useAsFrontPage) { + if(useAsFrontPage && supportActionBar != null) { supportActionBar.setDisplayHomeAsUpEnabled(false); } else { inflater.inflate(R.menu.menu_channel, menu); - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + + "], inflater = [" + inflater + "]"); menuRssButton = menu.findItem(R.id.menu_item_rss); + playlistAppendButton = menu.findItem(R.id.menu_append_playlist); + if (currentInfo != null) { menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl())); + playlistAppendButton.setVisible(!currentInfo.getRelatedStreams().isEmpty()); } - } } @@ -225,10 +229,12 @@ public class ChannelFragment extends BaseListInfoFragment { case R.id.menu_item_openInBrowser: openUrlInBrowser(url); break; - case R.id.menu_item_share: { + case R.id.menu_item_share: shareUrl(name, url); break; - } + case R.id.menu_append_playlist: + appendToPlaylist(getFragmentManager(), TAG); + break; default: return super.onOptionsItemSelected(item); } @@ -428,6 +434,9 @@ public class ChannelFragment extends BaseListInfoFragment { } else headerSubscribersTextView.setVisibility(View.GONE); if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); + if (playlistAppendButton != null) playlistAppendButton + .setVisible(!currentInfo.getRelatedStreams().isEmpty()); + playlistCtrl.setVisibility(View.VISIBLE); if (!result.errors.isEmpty()) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 52eeb337c..15255618b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -54,6 +54,8 @@ public class PlaylistFragment extends BaseListInfoFragment { private View headerPopupButton; private View headerBackgroundButton; + private MenuItem playlistAppendButton; + public static PlaylistFragment getInstance(int serviceId, String url, String name) { PlaylistFragment instance = new PlaylistFragment(); instance.setInitialData(serviceId, url, name); @@ -141,9 +143,15 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + + "], inflater = [" + inflater + "]"); super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_playlist, menu); + + playlistAppendButton = menu.findItem(R.id.menu_append_playlist); + if (currentInfo != null) { + playlistAppendButton.setVisible(!currentInfo.getRelatedStreams().isEmpty()); + } } /*////////////////////////////////////////////////////////////////////////// @@ -166,10 +174,12 @@ public class PlaylistFragment extends BaseListInfoFragment { case R.id.menu_item_openInBrowser: openUrlInBrowser(url); break; - case R.id.menu_item_share: { + case R.id.menu_item_share: shareUrl(name, url); break; - } + case R.id.menu_append_playlist: + appendToPlaylist(getFragmentManager(), TAG); + break; default: return super.onOptionsItemSelected(item); } @@ -215,6 +225,9 @@ public class PlaylistFragment extends BaseListInfoFragment { imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS); headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count)); + if (playlistAppendButton != null) playlistAppendButton + .setVisible(!currentInfo.getRelatedStreams().isEmpty()); + if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index af1a0f666..0e012aad7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -58,7 +58,6 @@ public class LocalItemListAdapter extends RecyclerView.Adapter(); @@ -99,21 +98,16 @@ public class LocalItemListAdapter extends RecyclerView.Adapter= localItems.size()) return; - - localItems.remove(infoListPosition); - - notifyItemRemoved(infoListPosition + (header != null ? 1 : 0)); + localItems.remove(index); + notifyItemRemoved(index + (header != null ? 1 : 0)); } public boolean swapItems(int fromAdapterPosition, int toAdapterPosition) { - final int actualFrom = offsetWithoutHeader(fromAdapterPosition); - final int actualTo = offsetWithoutHeader(toAdapterPosition); + final int actualFrom = adapterOffsetWithoutHeader(fromAdapterPosition); + final int actualTo = adapterOffsetWithoutHeader(toAdapterPosition); if (actualFrom < 0 || actualTo < 0) return false; if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false; @@ -150,7 +144,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter, Void> { + private static final long SAVE_DEBOUNCE_MILLIS = 1000; + private View headerRootLayout; private TextView headerTitleView; private TextView headerStreamCount; @@ -66,6 +71,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment debouncedSaveSignal; + private Disposable debouncedSaver; + public static LocalPlaylistFragment getInstance(long playlistId, String name) { LocalPlaylistFragment instance = new LocalPlaylistFragment(); instance.setInitialData(playlistId, name); @@ -89,11 +97,22 @@ public class LocalPlaylistFragment extends BaseLocalListFragment successToast.show()); } + private void saveDebounced() { + debouncedSaveSignal.onNext(System.currentTimeMillis()); + } + + private Disposable getDebouncedSaver() { + return debouncedSaveSignal + .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .doOnDispose(this::saveJoin) + .subscribe(ignored -> saveJoin()); + } + private void saveJoin() { final List items = itemListAdapter.getItemsList(); List streamIds = new ArrayList<>(items.size()); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index 302a37002..d4b6bd964 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -99,8 +99,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog { return; final long playlistId = ((PlaylistMetadataEntry) selectedItem).uid; - final Toast successToast = - Toast.makeText(getContext(), "Added", Toast.LENGTH_SHORT); + final Toast successToast = Toast.makeText(getContext(), + R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); playlistManager.appendToPlaylist(playlistId, getStreams()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index 791e90fa2..670ae9819 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -48,7 +48,7 @@ public final class PlaylistCreationDialog extends PlaylistDialog { final LocalPlaylistManager playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); final Toast successToast = Toast.makeText(getActivity(), - "Playlist successfully created", + R.string.playlist_creation_success, Toast.LENGTH_SHORT); playlistManager.createPlaylist(name, getStreams()) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 0bd0fa00f..ce80bcf0d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -174,8 +174,8 @@ public class BookmarkFragment extends BaseStateFragment { - final Toast deleteSuccessful = - Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT); + final Toast deleteSuccessful = Toast.makeText(getContext(), + R.string.playlist_delete_success, Toast.LENGTH_SHORT); disposables.add(localPlaylistManager.deletePlaylist(item.uid) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> deleteSuccessful.show())); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index cb2d671cc..1a872f382 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -275,7 +275,7 @@ public abstract class StatisticsPlaylistFragment if (super.onError(exception)) return true; onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, - "none", "History", R.string.general_error); + "none", "History Statistics", R.string.general_error); return true; } diff --git a/app/src/main/res/layout/dialog_playlists.xml b/app/src/main/res/layout/dialog_playlists.xml index 5abe91a8e..8c639fff6 100644 --- a/app/src/main/res/layout/dialog_playlists.xml +++ b/app/src/main/res/layout/dialog_playlists.xml @@ -28,7 +28,7 @@ android:layout_height="50dp" android:layout_toRightOf="@+id/newPlaylistIcon" android:gravity="left|center" - android:text="Create New Playlist" + android:text="@string/create_playlist" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="15sp" android:textStyle="bold" diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index cf555ffa5..3861a380d 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -159,112 +159,175 @@ android:baselineAligned="false" android:orientation="horizontal"> + + + + + + + + + + - + android:layout_height="match_parent" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:paddingLeft="6dp" + android:paddingRight="6dp"> + - + - + - + - + - + + + + + - - - - - - - - - - - + + diff --git a/app/src/main/res/menu/menu_playlist.xml b/app/src/main/res/menu/menu_playlist.xml index f125c3fc7..a12fb2f49 100644 --- a/app/src/main/res/menu/menu_playlist.xml +++ b/app/src/main/res/menu/menu_playlist.xml @@ -1,6 +1,7 @@

+ xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c3604f953..1e6d3e641 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -384,5 +384,12 @@ Rename Playlist Name Add To Playlist + Set as Playlist Thumbnail + Do you want to delete this playlist? + Playlist successfully created + Added to playlist + Playlist thumbnail changed + Playlist renamed + Playlist deleted From d3160eed9d17703a70d0278f120aac40e2f49f29 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 29 Jan 2018 14:08:26 -0800 Subject: [PATCH 17/52] -Added state saving for streams on skip and player exception events. -Added query for loading saved stream states. -Modified orphan record removal to no longer consider stream table records. --- .../database/stream/dao/StreamDAO.java | 4 -- .../database/stream/dao/StreamStateDAO.java | 15 +++++++ .../newpipe/history/HistoryFragment.java | 2 +- .../newpipe/history/HistoryRecordManager.java | 43 +++++++++++++++---- .../org/schabi/newpipe/player/BasePlayer.java | 24 +++++++++++ 5 files changed, 75 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java index b699e0b6b..63f9e5940 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -92,10 +92,6 @@ public abstract class StreamDAO implements BasicDAO { " ON " + STREAM_ID + " = " + StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID + - " LEFT JOIN " + STREAM_STATE_TABLE + - " ON " + STREAM_ID + " = " + - StreamStateEntity.STREAM_STATE_TABLE + "." + StreamStateEntity.JOIN_STREAM_ID + - " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + " ON " + STREAM_ID + " = " + PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID + diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java index f89f2f7ef..1c06f4df9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java @@ -1,7 +1,10 @@ package org.schabi.newpipe.database.stream.dao; import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.stream.model.StreamStateEntity; @@ -28,6 +31,18 @@ public abstract class StreamStateDAO implements BasicDAO { throw new UnsupportedOperationException(); } + @Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") + public abstract Flowable> getState(final long streamId); + @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") public abstract int deleteState(final long streamId); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract void silentInsertInternal(final StreamStateEntity streamState); + + @Transaction + public long upsert(StreamStateEntity stream) { + silentInsertInternal(stream); + return update(stream); + } } 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 5369c657c..ac5cf4cc3 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -156,7 +156,7 @@ public abstract class HistoryFragment extends BaseFragment .setMessage(R.string.delete_all_history_prompt) .setCancelable(true) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.delete, (dialog, i) -> clearHistory()) + .setPositiveButton(R.string.delete_all, (dialog, i) -> clearHistory()) .show(); } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java index 839b8c89b..9d3ffaffe 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java @@ -3,23 +3,25 @@ package org.schabi.newpipe.history; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; -import org.schabi.newpipe.database.history.model.StreamHistoryEntry; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.List; @@ -34,6 +36,7 @@ public class HistoryRecordManager { private final StreamDAO streamTable; private final StreamHistoryDAO streamHistoryTable; private final SearchHistoryDAO searchHistoryTable; + private final StreamStateDAO streamStateTable; private final SharedPreferences sharedPreferences; private final String searchHistoryKey; private final String streamHistoryKey; @@ -43,15 +46,12 @@ public class HistoryRecordManager { streamTable = database.streamDAO(); streamHistoryTable = database.streamHistoryDAO(); searchHistoryTable = database.searchHistoryDAO(); + streamStateTable = database.streamStateDAO(); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); searchHistoryKey = context.getString(R.string.enable_search_history_key); streamHistoryKey = context.getString(R.string.enable_watch_history_key); } - public Single removeOrphanedRecords() { - return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); - } - /////////////////////////////////////////////////////// // Watch History /////////////////////////////////////////////////////// @@ -161,4 +161,31 @@ public class HistoryRecordManager { private boolean isSearchHistoryEnabled() { return sharedPreferences.getBoolean(searchHistoryKey, false); } + + /////////////////////////////////////////////////////// + // Stream State History + /////////////////////////////////////////////////////// + + @SuppressWarnings("unused") + public Maybe loadStreamState(final StreamInfo info) { + return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info))) + .flatMap(streamId -> streamStateTable.getState(streamId).firstElement()) + .flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0))) + .subscribeOn(Schedulers.io()); + } + + public Maybe saveStreamState(@NonNull final StreamInfo info, final long progressTime) { + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return streamStateTable.upsert(new StreamStateEntity(streamId, progressTime)); + })).subscribeOn(Schedulers.io()); + } + + /////////////////////////////////////////////////////// + // Utility + /////////////////////////////////////////////////////// + + public Single removeOrphanedRecords() { + return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); + } } 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 8260adc6e..369b15509 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -581,6 +581,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen errorToast = null; } + savePlaybackState(); + switch (error.type) { case ExoPlaybackException.TYPE_SOURCE: if (simpleExoPlayer.getCurrentPosition() < @@ -758,6 +760,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (simpleExoPlayer == null || playQueue == null) return; if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); + savePlaybackState(); + /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track. * Also restart the track if the current track is the first in a queue.*/ if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) { @@ -772,6 +776,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (playQueue == null) return; if (DEBUG) Log.d(TAG, "onPlayNext() called"); + savePlaybackState(); + playQueue.offsetIndex(+1); } @@ -833,6 +839,24 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen ); } + protected void savePlaybackState(final StreamInfo info, final long progress) { + if (context == null || info == null || databaseUpdateReactor == null) return; + final Disposable stateSaver = recordManager.saveStreamState(info, progress) + .observeOn(AndroidSchedulers.mainThread()) + .onErrorComplete() + .subscribe(); + databaseUpdateReactor.add(stateSaver); + } + + private void savePlaybackState() { + if (simpleExoPlayer == null || currentInfo == null) return; + + if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD && + simpleExoPlayer.getCurrentPosition() < + simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { + savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); + } + } /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ From 6f9deea873d9504eb06182b12aef6f3f22cf2e39 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 29 Jan 2018 18:06:48 -0800 Subject: [PATCH 18/52] -Fixed memory leak due to image loader overusing memory cache. -Added disk cache for local item loading. --- app/src/main/java/org/schabi/newpipe/App.java | 5 ++++- .../newpipe/fragments/local/LocalItemBuilder.java | 9 +++++++-- .../newpipe/fragments/local/LocalItemListAdapter.java | 8 ++++---- .../fragments/local/LocalPlaylistFragment.java | 2 +- .../newpipe/fragments/local/PlaylistAppendDialog.java | 2 +- .../fragments/local/bookmark/BookmarkFragment.java | 2 +- .../local/bookmark/StatisticsPlaylistFragment.java | 2 +- .../fragments/local/holder/LocalItemHolder.java | 7 +++++++ .../local/holder/LocalPlaylistItemHolder.java | 3 +-- .../local/holder/LocalPlaylistStreamItemHolder.java | 5 +++-- .../local/holder/LocalStatisticStreamItemHolder.java | 11 +++-------- 11 files changed, 33 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 49f73853b..c182bfcfe 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -10,6 +10,7 @@ import android.content.Intent; import android.os.Build; import android.util.Log; +import com.nostra13.universalimageloader.cache.memory.impl.WeakMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; @@ -80,7 +81,9 @@ public class App extends Application { initNotificationChannel(); // Initialize image loader - ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build(); + ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this) + .memoryCache(new WeakMemoryCache()) + .build(); ImageLoader.getInstance().init(config); configureRxJavaErrorHandler(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java index ca200cc8a..128daf435 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java @@ -1,8 +1,12 @@ package org.schabi.newpipe.fragments.local; import android.content.Context; +import android.graphics.Bitmap; +import android.widget.ImageView; +import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.process.BitmapProcessor; import org.schabi.newpipe.database.LocalItem; @@ -42,8 +46,9 @@ public class LocalItemBuilder { return context; } - public ImageLoader getImageLoader() { - return imageLoader; + public void displayImage(final String url, final ImageView view, + final DisplayImageOptions options) { + imageLoader.displayImage(url, view, options); } public OnLocalItemGesture getOnItemSelectedListener() { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index 0e012aad7..35112a6a5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -69,10 +69,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { + public void addItems(List data) { if (data != null) { if (DEBUG) { - Log.d(TAG, "addInfoItemList() before > localItems.size() = " + + Log.d(TAG, "addItems() before > localItems.size() = " + localItems.size() + ", data.size() = " + data.size()); } @@ -80,7 +80,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter offsetStart = " + offsetStart + + Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + ", localItems.size() = " + localItems.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); @@ -92,7 +92,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter { if (itemBuilder.getOnItemSelectedListener() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java index 712db8f8a..4fe577aaf 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.fragments.local.holder; +import android.graphics.Bitmap; import android.support.v4.content.ContextCompat; import android.view.MotionEvent; import android.view.View; @@ -8,6 +9,7 @@ import android.widget.ImageView; import android.widget.TextView; import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.assist.ImageScaleType; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; @@ -59,8 +61,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, - LocalPlaylistStreamItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java index bce6bab76..cd0630b37 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java @@ -45,8 +45,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { public final TextView itemDurationView; public final TextView itemAdditionalDetails; - LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { - super(infoItemBuilder, layoutId, parent); + public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + super(infoItemBuilder, R.layout.list_stream_item, parent); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); @@ -55,10 +55,6 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); } - public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { - this(infoItemBuilder, R.layout.list_stream_item, parent); - } - private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, final DateFormat dateFormat) { final String watchCount = Localization.shortViewCount(itemBuilder.getContext(), @@ -88,8 +84,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, - DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { From 62814f083ec7ca2eb0179fd812703b5259014016 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 29 Jan 2018 20:42:52 -0800 Subject: [PATCH 19/52] -Fixed memory leak in playlist append dialog due to rogue flowables. -Changed image loader memory cache to use limited LRU. --- app/src/main/java/org/schabi/newpipe/App.java | 13 +++++--- .../fragments/local/PlaylistAppendDialog.java | 33 +++++++++++++++---- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index c182bfcfe..2ae21137f 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -10,6 +10,8 @@ import android.content.Intent; import android.os.Build; import android.util.Log; +import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; +import com.nostra13.universalimageloader.cache.memory.impl.LruMemoryCache; import com.nostra13.universalimageloader.cache.memory.impl.WeakMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; @@ -81,10 +83,7 @@ public class App extends Application { initNotificationChannel(); // Initialize image loader - ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this) - .memoryCache(new WeakMemoryCache()) - .build(); - ImageLoader.getInstance().init(config); + ImageLoader.getInstance().init(getImageLoaderConfigurations(10)); configureRxJavaErrorHandler(); } @@ -122,6 +121,12 @@ public class App extends Application { }); } + private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb) { + return new ImageLoaderConfiguration.Builder(this) + .memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024)) + .build(); + } + private void initACRA() { try { final ACRAConfiguration acraConfig = new ConfigurationBuilder(this) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index 6aca5ae70..7145d91d7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.fragments.local; +import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; @@ -25,6 +26,7 @@ import java.util.Collections; import java.util.List; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; public final class PlaylistAppendDialog extends PlaylistDialog { private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); @@ -32,6 +34,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog { private RecyclerView playlistRecyclerView; private LocalItemListAdapter playlistAdapter; + private Disposable playlistReactor; + public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) { PlaylistAppendDialog dialog = new PlaylistAppendDialog(); dialog.setInfo(Collections.singletonList(new StreamEntity(info))); @@ -68,6 +72,15 @@ public final class PlaylistAppendDialog extends PlaylistDialog { playlistAdapter = new LocalItemListAdapter(getActivity()); } + @Override + public void onDestroy() { + super.onDestroy(); + if (playlistReactor != null) playlistReactor.dispose(); + playlistReactor = null; + playlistRecyclerView = null; + playlistAdapter = null; + } + /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ @@ -99,18 +112,20 @@ public final class PlaylistAppendDialog extends PlaylistDialog { return; final long playlistId = ((PlaylistMetadataEntry) selectedItem).uid; - final Toast successToast = Toast.makeText(getContext(), - R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); + @SuppressLint("ShowToast") + final Toast successToast = Toast.makeText(getContext(), R.string.playlist_add_stream_success, + Toast.LENGTH_SHORT); playlistManager.appendToPlaylist(playlistId, getStreams()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> successToast.show()); + .doOnDispose(successToast::show) + .subscribe(ignored -> {}); getDialog().dismiss(); } }); - playlistManager.getPlaylists() + playlistReactor = playlistManager.getPlaylists() .observeOn(AndroidSchedulers.mainThread()) .subscribe(metadataEntries -> { if (metadataEntries.isEmpty()) { @@ -118,9 +133,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog { return; } - playlistAdapter.clearStreamItemList(); - playlistAdapter.addItems(metadataEntries); - playlistRecyclerView.setVisibility(View.VISIBLE); + if (playlistAdapter != null) { + playlistAdapter.clearStreamItemList(); + playlistAdapter.addItems(metadataEntries); + } + if (playlistRecyclerView != null) { + playlistRecyclerView.setVisibility(View.VISIBLE); + } }); } From 75a58d6381290453ffe8cb72e6c11054e4734bff Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 30 Jan 2018 08:06:12 -0800 Subject: [PATCH 20/52] -Fixed memory leak on rogue observable in history fragment. -Removed stream id from playlist stream join table since only foreign constraint is needed. -Added bar to playlist control UI. -Modified local playlist fragment to no longer save when out of focus. --- .../schabi/newpipe/database/Migrations.java | 2 +- .../playlist/model/PlaylistStreamEntity.java | 2 +- .../local/LocalPlaylistFragment.java | 1 - .../newpipe/history/HistoryFragment.java | 7 +- app/src/main/res/layout/playlist_control.xml | 165 ++++++++++-------- .../main/res/layout/subscription_header.xml | 3 +- 6 files changed, 101 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index fdfd04a84..c6b472f7f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -28,7 +28,7 @@ public class Migrations { database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)"); database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `stream_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)"); database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)"); diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java index 3d71f7e70..a5b2e8248 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java @@ -14,7 +14,7 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JO import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; @Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE, - primaryKeys = {JOIN_PLAYLIST_ID, JOIN_STREAM_ID, JOIN_INDEX}, + primaryKeys = {JOIN_PLAYLIST_ID, JOIN_INDEX}, indices = { @Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true), @Index(value = {JOIN_STREAM_ID}) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index e9d24357d..6528b8923 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -469,7 +469,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment saveJoin()); } 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 ac5cf4cc3..3fa8076f3 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -222,11 +222,16 @@ public abstract class HistoryFragment extends BaseFragment @Override public void onDestroy() { super.onDestroy(); + + if (disposables != null) disposables.dispose(); + if (historySubscription != null) historySubscription.cancel(); + mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); mSharedPreferences = null; mHistoryIsEnabledChangeListener = null; mHistoryIsEnabledKey = null; - if (disposables != null) disposables.dispose(); + historySubscription = null; + disposables = null; } @Override diff --git a/app/src/main/res/layout/playlist_control.xml b/app/src/main/res/layout/playlist_control.xml index 821158bba..01632f9fc 100644 --- a/app/src/main/res/layout/playlist_control.xml +++ b/app/src/main/res/layout/playlist_control.xml @@ -1,83 +1,100 @@ - + android:visibility="invisible" + tools:visibility="visible"> - + android:layout_height="@dimen/playlist_ctrl_height"> + + + + + + + + + + + + + + + - - - - - - - - - - - - \ No newline at end of file + android:layout_height="1dp" + android:layout_below="@+id/playlist_panel" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:background="?attr/separator_color"/> + diff --git a/app/src/main/res/layout/subscription_header.xml b/app/src/main/res/layout/subscription_header.xml index 432be08b7..84a5a6216 100644 --- a/app/src/main/res/layout/subscription_header.xml +++ b/app/src/main/res/layout/subscription_header.xml @@ -6,7 +6,8 @@ android:layout_height="wrap_content" android:layout_marginBottom="12dp" android:background="?attr/selectableItemBackground" - android:clickable="true"> + android:clickable="true" + android:focusable="true"> Date: Tue, 30 Jan 2018 16:01:11 -0800 Subject: [PATCH 21/52] -Modified BaseLocalItemFragment to no longer cache items when going into background. -Refactored and restructured all LocalItem related fragments and dialogs. -Added error logging to unmonitored single-use observables. -Modified playlist metadata query to return by alphabetical order. -Removed sending toast when playlist is renamed or deleted as it is obvious. -Removed unused code in main fragment. --- .../playlist/dao/PlaylistStreamDAO.java | 3 +- .../newpipe/fragments/MainFragment.java | 4 - .../local/BaseLocalListFragment.java | 122 +++-- .../local/LocalPlaylistFragment.java | 484 +++++++++--------- .../fragments/local/PlaylistAppendDialog.java | 109 ++-- .../local/bookmark/BookmarkFragment.java | 214 ++++---- .../local/bookmark/MostPlayedFragment.java | 2 +- .../bookmark/StatisticsPlaylistFragment.java | 265 +++++----- .../local/bookmark/WatchHistoryFragment.java | 2 +- .../newpipe/history/HistoryFragment.java | 14 +- .../history/SearchHistoryFragment.java | 24 +- .../history/WatchedHistoryFragment.java | 22 +- .../org/schabi/newpipe/player/BasePlayer.java | 11 +- .../main/res/layout/local_playlist_header.xml | 13 +- app/src/main/res/values/settings_keys.xml | 2 - app/src/main/res/values/strings.xml | 3 +- 16 files changed, 647 insertions(+), 647 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java index 2d645e793..dd2994d29 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -63,6 +63,7 @@ public abstract class PlaylistStreamDAO implements BasicDAO> getPlaylistMetadata(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index fc4913114..4512e316f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -229,10 +229,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name); fragment.useAsFrontPage(true); return fragment; - } else if (setMainPage.equals(getString(R.string.bookmark_page_key))) { - final BookmarkFragment fragment = new BookmarkFragment(); - fragment.useAsFrontPage(true); - return fragment; } else { return new BlankFragment(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java index afc67aa68..7db54db6c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java @@ -1,8 +1,7 @@ package org.schabi.newpipe.fragments.local; -import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; import android.support.v7.app.ActionBar; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -12,86 +11,44 @@ import android.view.MenuInflater; import android.view.View; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.ListViewContract; -import org.schabi.newpipe.util.StateSaver; - -import java.util.List; -import java.util.Queue; import static org.schabi.newpipe.util.AnimationUtils.animateView; +/** + * This fragment is design to be used with persistent data such as + * {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained + * in the list adapter to avoid extra writes when the it exits or re-enters its lifecycle. + * + * This fragment destroys its adapter and views when {@link Fragment#onDestroyView()} is + * called and is memory efficient when in backstack. + * */ public abstract class BaseLocalListFragment extends BaseStateFragment - implements ListViewContract, StateSaver.WriteRead { + implements ListViewContract { /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ + protected View headerRootView; + protected View footerRootView; + protected LocalItemListAdapter itemListAdapter; protected RecyclerView itemsList; /*////////////////////////////////////////////////////////////////////////// - // LifeCycle + // Lifecycle - Creation //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onAttach(Context context) { - super.onAttach(context); - itemListAdapter = new LocalItemListAdapter(activity); - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } - @Override - public void onDestroy() { - super.onDestroy(); - StateSaver.onDestroy(savedState); - } - /*////////////////////////////////////////////////////////////////////////// - // State Saving - //////////////////////////////////////////////////////////////////////////*/ - - protected StateSaver.SavedState savedState; - - @Override - public String generateSuffix() { - // Naive solution, but it's good for now (the items don't change) - return "." + itemListAdapter.getItemsList().size() + ".list"; - } - - @Override - public void writeTo(Queue objectsToSave) { - objectsToSave.add(itemListAdapter.getItemsList()); - } - - @Override - @SuppressWarnings("unchecked") - public void readFrom(@NonNull Queue savedObjects) throws Exception { - itemListAdapter.getItemsList().clear(); - itemListAdapter.getItemsList().addAll((List) savedObjects.poll()); - } - - @Override - public void onSaveInstanceState(Bundle bundle) { - super.onSaveInstanceState(bundle); - savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this); - } - - @Override - protected void onRestoreInstanceState(@NonNull Bundle bundle) { - super.onRestoreInstanceState(bundle); - savedState = StateSaver.tryToRestore(bundle, this); - } - - /*////////////////////////////////////////////////////////////////////////// - // Init + // Lifecycle - View //////////////////////////////////////////////////////////////////////////*/ protected View getListHeader() { @@ -113,8 +70,9 @@ public abstract class BaseLocalListFragment extends BaseStateFragment itemsList = rootView.findViewById(R.id.items_list); itemsList.setLayoutManager(getListLayoutManager()); - itemListAdapter.setFooter(getListFooter()); - itemListAdapter.setHeader(getListHeader()); + itemListAdapter = new LocalItemListAdapter(activity); + itemListAdapter.setHeader(headerRootView = getListHeader()); + itemListAdapter.setFooter(footerRootView = getListFooter()); itemsList.setAdapter(itemListAdapter); } @@ -125,12 +83,13 @@ public abstract class BaseLocalListFragment extends BaseStateFragment } /*////////////////////////////////////////////////////////////////////////// - // Menu + // Lifecycle - Menu //////////////////////////////////////////////////////////////////////////*/ @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + + "], inflater = [" + inflater + "]"); super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { @@ -143,27 +102,48 @@ public abstract class BaseLocalListFragment extends BaseStateFragment } } + /*////////////////////////////////////////////////////////////////////////// + // Lifecycle - Destruction + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onDestroyView() { + super.onDestroyView(); + itemsList = null; + itemListAdapter = null; + } + /*////////////////////////////////////////////////////////////////////////// // Contract //////////////////////////////////////////////////////////////////////////*/ + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + resetFragment(); + } + @Override public void showLoading() { super.showLoading(); - // animateView(itemsList, false, 400); + animateView(itemsList, false, 200); + if (headerRootView != null) animateView(headerRootView, false, 200); } @Override public void hideLoading() { super.hideLoading(); - animateView(itemsList, true, 300); + animateView(itemsList, true, 200); + if (headerRootView != null) animateView(headerRootView, true, 200); } @Override public void showError(String message, boolean showRetryButton) { super.showError(message, showRetryButton); showListFooter(false); + animateView(itemsList, false, 200); + if (headerRootView != null) animateView(headerRootView, false, 200); } @Override @@ -181,4 +161,18 @@ public abstract class BaseLocalListFragment extends BaseStateFragment public void handleNextItems(N result) { isLoading.set(false); } + + /*////////////////////////////////////////////////////////////////////////// + // Error handling + //////////////////////////////////////////////////////////////////////////*/ + + protected void resetFragment() { + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); + } + + @Override + protected boolean onError(Throwable exception) { + resetFragment(); + return super.onError(exception); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 6528b8923..7ec24337a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -11,6 +11,7 @@ import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.text.TextUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -38,7 +39,6 @@ import java.util.concurrent.TimeUnit; import icepick.State; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.subjects.PublishSubject; @@ -51,8 +51,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment debouncedSaveSignal; private Disposable debouncedSaver; @@ -81,13 +79,14 @@ public class LocalPlaylistFragment extends BaseLocalListFragment createRenameDialog()); + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper.attachToRecyclerView(itemsList); @@ -192,9 +164,236 @@ public class LocalPlaylistFragment extends BaseLocalListFragment createRenameDialog()); } + /////////////////////////////////////////////////////////////////////////// + // Fragment Lifecycle - Loading + /////////////////////////////////////////////////////////////////////////// + + @Override + public void showLoading() { + super.showLoading(); + animateView(headerRootLayout, false, 200); + animateView(playlistControl, false, 200); + } + + @Override + public void hideLoading() { + super.hideLoading(); + animateView(headerRootLayout, true, 200); + animateView(playlistControl, true, 200); + } + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + + if (debouncedSaver != null) debouncedSaver.dispose(); + debouncedSaver = getDebouncedSaver(); + + playlistManager.getPlaylistStreams(playlistId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistObserver()); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Lifecycle - Destruction + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (databaseSubscription != null) databaseSubscription.cancel(); + if (debouncedSaver != null) debouncedSaver.dispose(); + + databaseSubscription = null; + debouncedSaver = null; + itemTouchHelper = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (debouncedSaveSignal != null) debouncedSaveSignal.onComplete(); + + debouncedSaveSignal = null; + playlistManager = null; + } + + /////////////////////////////////////////////////////////////////////////// + // Playlist Stream Loader + /////////////////////////////////////////////////////////////////////////// + + private Subscriber> getPlaylistObserver() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List streams) { + // Do not allow saving while the result is being updated + if (debouncedSaver != null) debouncedSaver.dispose(); + handleResult(streams); + debouncedSaver = getDebouncedSaver(); + + if (databaseSubscription != null) databaseSubscription.request(1); + } + + @Override + public void onError(Throwable exception) { + LocalPlaylistFragment.this.onError(exception); + } + + @Override + public void onComplete() {} + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + itemListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + return; + } + + itemListAdapter.addItems(result); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + setVideoCount(itemListAdapter.getItemsList().size()); + + headerPlayAllButton.setOnClickListener(view -> + NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener(view -> + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener(view -> + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + + hideLoading(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void resetFragment() { + super.resetFragment(); + if (databaseSubscription != null) databaseSubscription.cancel(); + } + + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, + "none", "Local Playlist", R.string.general_error); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Playlist Metadata/Streams Manipulation + //////////////////////////////////////////////////////////////////////////*/ + + private void createRenameDialog() { + if (playlistId == null || name == null || getContext() == null) return; + + final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); + EditText nameEdit = dialogView.findViewById(R.id.playlist_name); + nameEdit.setText(name); + nameEdit.setSelection(nameEdit.getText().length()); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) + .setTitle(R.string.rename_playlist) + .setView(dialogView) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.create, (dialogInterface, i) -> + changePlaylistName(nameEdit.getText().toString()) + ); + + dialogBuilder.show(); + } + + private void changePlaylistName(final String name) { + this.name = name; + setTitle(name); + + Log.e(TAG, "Updating playlist id=[" + playlistId + + "] with new name=[" + name + "] items"); + + playlistManager.renamePlaylist(playlistId, name) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> {/*Do nothing on success*/}, this::onError); + } + + private void changeThumbnailUrl(final String thumbnailUrl) { + final Toast successToast = Toast.makeText(getActivity(), + R.string.playlist_thumbnail_change_success, + Toast.LENGTH_SHORT); + + Log.e(TAG, "Updating playlist id=[" + playlistId + + "] with new thumbnail url=[" + thumbnailUrl + "]"); + + playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignore -> successToast.show(), this::onError); + } + + private void deleteItem(final PlaylistStreamEntry item) { + itemListAdapter.removeItem(item); + setVideoCount(itemListAdapter.getItemsList().size()); + saveDebounced(); + } + + private void saveDebounced() { + debouncedSaveSignal.onNext(System.currentTimeMillis()); + } + + private Disposable getDebouncedSaver() { + return debouncedSaveSignal + .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> saveJoin()); + } + + private void saveJoin() { + final List items = itemListAdapter.getItemsList(); + List streamIds = new ArrayList<>(items.size()); + for (final LocalItem item : items) { + if (item instanceof PlaylistStreamEntry) { + streamIds.add(((PlaylistStreamEntry) item).streamId); + } + } + + Log.e(TAG, "Updating playlist id=[" + playlistId + + "] with [" + streamIds.size() + "] items"); + + playlistManager.updateJoin(playlistId, streamIds) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> {/*Do nothing on success*/}, this::onError); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + protected void showStreamDialog(final PlaylistStreamEntry item) { final Context context = getContext(); final Activity activity = getActivity(); @@ -236,9 +435,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment> getPlaylistObserver() { - return new Subscriber>() { - @Override - public void onSubscribe(Subscription s) { - showLoading(); - - if (databaseSubscription != null) databaseSubscription.cancel(); - databaseSubscription = s; - databaseSubscription.request(1); - } - - @Override - public void onNext(List streams) { - handleResult(streams); - if (databaseSubscription != null) databaseSubscription.request(1); - } - - @Override - public void onError(Throwable exception) { - LocalPlaylistFragment.this.onError(exception); - } - - @Override - public void onComplete() { - } - }; - } - - @Override - public void handleResult(@NonNull List result) { - super.handleResult(result); - itemListAdapter.clearStreamItemList(); - - if (result.isEmpty()) { - showEmptyState(); - return; - } - - animateView(headerRootLayout, true, 100); - animateView(itemsList, true, 300); - - itemListAdapter.addItems(result); - if (itemsListState != null) { - itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); - itemsListState = null; - } - setVideoCount(itemListAdapter.getItemsList().size()); - - playlistControl.setVisibility(View.VISIBLE); - - headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); - headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); - headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); - hideLoading(); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - /////////////////////////////////////////////////////////////////////////// - - @Override - protected boolean onError(Throwable exception) { - resetFragment(); - if (super.onError(exception)) return true; - - onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, - "none", "Local Playlist", R.string.general_error); - return true; - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - private void setInitialData(long playlistId, String name) { this.playlistId = playlistId; this.name = !TextUtils.isEmpty(name) ? name : ""; } - private void setFragmentTitle(final String title) { - if (activity != null && activity.getSupportActionBar() != null) { - activity.getSupportActionBar().setTitle(title); - } - if (headerTitleView != null) { - headerTitleView.setText(title); - } - } - private void setVideoCount(final long count) { if (activity != null && headerStreamCount != null) { headerStreamCount.setText(Localization.localizeStreamCount(activity, count)); @@ -419,71 +503,5 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { - name = nameEdit.getText().toString(); - setFragmentTitle(name); - - final LocalPlaylistManager playlistManager = - new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); - final Toast successToast = Toast.makeText(getActivity(), - R.string.playlist_rename_success, - Toast.LENGTH_SHORT); - - playlistManager.renamePlaylist(playlistId, name) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(longs -> successToast.show()); - }); - - dialogBuilder.show(); - } - - private void changeThumbnailUrl(final String thumbnailUrl) { - final Toast successToast = Toast.makeText(getActivity(), - R.string.playlist_thumbnail_change_success, - Toast.LENGTH_SHORT); - - playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignore -> successToast.show()); - } - - private void saveDebounced() { - debouncedSaveSignal.onNext(System.currentTimeMillis()); - } - - private Disposable getDebouncedSaver() { - return debouncedSaveSignal - .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> saveJoin()); - } - - private void saveJoin() { - final List items = itemListAdapter.getItemsList(); - List streamIds = new ArrayList<>(items.size()); - for (final LocalItem item : items) { - if (item instanceof PlaylistStreamEntry) { - streamIds.add(((PlaylistStreamEntry) item).streamId); - } - } - - playlistManager.updateJoin(playlistId, streamIds) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(); - } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index 7145d91d7..b01dcabeb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.fragments.local; import android.annotation.SuppressLint; -import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -25,6 +24,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import javax.annotation.Nonnull; + import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -63,26 +64,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog { } /*////////////////////////////////////////////////////////////////////////// - // LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onAttach(Context context) { - super.onAttach(context); - playlistAdapter = new LocalItemListAdapter(getActivity()); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (playlistReactor != null) playlistReactor.dispose(); - playlistReactor = null; - playlistRecyclerView = null; - playlistAdapter = null; - } - - /*////////////////////////////////////////////////////////////////////////// - // Views + // LifeCycle - Creation //////////////////////////////////////////////////////////////////////////*/ @Override @@ -95,52 +77,44 @@ public final class PlaylistAppendDialog extends PlaylistDialog { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - final View newPlaylistButton = view.findViewById(R.id.newPlaylist); - playlistRecyclerView = view.findViewById(R.id.playlist_list); - playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - playlistRecyclerView.setAdapter(playlistAdapter); - final LocalPlaylistManager playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); - newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); - + playlistAdapter = new LocalItemListAdapter(getActivity()); playlistAdapter.setSelectedListener(new OnLocalItemGesture() { @Override public void selected(LocalItem selectedItem) { if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) return; - - final long playlistId = ((PlaylistMetadataEntry) selectedItem).uid; - @SuppressLint("ShowToast") - final Toast successToast = Toast.makeText(getContext(), R.string.playlist_add_stream_success, - Toast.LENGTH_SHORT); - - playlistManager.appendToPlaylist(playlistId, getStreams()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnDispose(successToast::show) - .subscribe(ignored -> {}); - - getDialog().dismiss(); + onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, + getStreams()); } }); + playlistRecyclerView = view.findViewById(R.id.playlist_list); + playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + playlistRecyclerView.setAdapter(playlistAdapter); + + final View newPlaylistButton = view.findViewById(R.id.newPlaylist); + newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); + playlistReactor = playlistManager.getPlaylists() .observeOn(AndroidSchedulers.mainThread()) - .subscribe(metadataEntries -> { - if (metadataEntries.isEmpty()) { - openCreatePlaylistDialog(); - return; - } + .subscribe(this::onPlaylistsReceived); + } - if (playlistAdapter != null) { - playlistAdapter.clearStreamItemList(); - playlistAdapter.addItems(metadataEntries); - } - if (playlistRecyclerView != null) { - playlistRecyclerView.setVisibility(View.VISIBLE); - } - }); + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle - Destruction + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (playlistReactor != null) playlistReactor.dispose(); + + playlistReactor = null; + playlistRecyclerView = null; + playlistAdapter = null; } /*////////////////////////////////////////////////////////////////////////// @@ -153,4 +127,33 @@ public final class PlaylistAppendDialog extends PlaylistDialog { PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG); getDialog().dismiss(); } + + private void onPlaylistsReceived(@NonNull final List playlists) { + if (playlists.isEmpty()) { + openCreatePlaylistDialog(); + return; + } + + if (playlistAdapter != null && playlistRecyclerView != null) { + playlistAdapter.clearStreamItemList(); + playlistAdapter.addItems(playlists); + playlistRecyclerView.setVisibility(View.VISIBLE); + } + } + + private void onPlaylistSelected(@NonNull LocalPlaylistManager manager, + @NonNull PlaylistMetadataEntry playlist, + @Nonnull List streams) { + if (getStreams() == null) return; + + @SuppressLint("ShowToast") + final Toast successToast = Toast.makeText(getContext(), + R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); + + manager.appendToPlaylist(playlist.uid, streams) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> successToast.show()); + + getDialog().dismiss(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 613e77cfe..393e128f7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -1,17 +1,14 @@ package org.schabi.newpipe.fragments.local.bookmark; import android.app.AlertDialog; -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; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -19,29 +16,25 @@ import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; -import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.fragments.local.LocalItemListAdapter; +import org.schabi.newpipe.fragments.local.BaseLocalListFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistManager; import org.schabi.newpipe.fragments.local.OnLocalItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; -import java.util.Collections; import java.util.List; import icepick.State; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; -import static org.schabi.newpipe.util.AnimationUtils.animateView; +public final class BookmarkFragment + extends BaseLocalListFragment, Void> { -public class BookmarkFragment extends BaseStateFragment> { private View watchHistoryButton; private View mostWatchedButton; - private LocalItemListAdapter itemListAdapter; - private RecyclerView itemsList; - @State protected Parcelable itemsListState; @@ -50,23 +43,14 @@ public class BookmarkFragment extends BaseStateFragment { - final Toast deleteSuccessful = Toast.makeText(getContext(), - R.string.playlist_delete_success, Toast.LENGTH_SHORT); - disposables.add(localPlaylistManager.deletePlaylist(item.uid) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> deleteSuccessful.show())); - }) - .setNegativeButton(R.string.cancel, null) - .show(); - } - - private void resetFragment() { - if (disposables != null) disposables.clear(); - if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); - } - /////////////////////////////////////////////////////////////////////////// - // Subscriptions Loader + // Fragment LifeCycle - Loading /////////////////////////////////////////////////////////////////////////// @Override public void startLoading(boolean forceLoad) { super.startLoading(forceLoad); - resetFragment(); - localPlaylistManager.getPlaylists() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getSubscriptionSubscriber()); } + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle - Destruction + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (disposables != null) disposables.clear(); + if (databaseSubscription != null) databaseSubscription.cancel(); + + databaseSubscription = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposables != null) disposables.dispose(); + + disposables = null; + localPlaylistManager = null; + itemsListState = null; + } + + /////////////////////////////////////////////////////////////////////////// + // Subscriptions Loader + /////////////////////////////////////////////////////////////////////////// + private Subscriber> getSubscriptionSubscriber() { return new Subscriber>() { @Override @@ -238,55 +207,58 @@ public class BookmarkFragment extends BaseStateFragment infoItemsOf(List playlists) { - Collections.sort(playlists, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); - return playlists; - } - - /*////////////////////////////////////////////////////////////////////////// - // 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(); - } - /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @Override protected boolean onError(Throwable exception) { - resetFragment(); if (super.onError(exception)) return true; onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Bookmark", R.string.general_error); return true; } + + @Override + protected void resetFragment() { + super.resetFragment(); + if (disposables != null) disposables.clear(); + } + + /////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////// + + private void showDeleteDialog(final PlaylistMetadataEntry item) { + new AlertDialog.Builder(activity) + .setTitle(item.name) + .setMessage(R.string.delete_playlist_prompt) + .setCancelable(true) + .setPositiveButton(R.string.delete, (dialog, i) -> + disposables.add(deletePlaylist(item.uid)) + ) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + private Disposable deletePlaylist(final long playlistId) { + return localPlaylistManager.deletePlaylist(playlistId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> {/*Do nothing on success*/}, + throwable -> Log.e(TAG, "Playlist deletion failed, id=[" + + playlistId + "]") + ); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java index ed0d903a8..cba9e9c64 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java @@ -6,7 +6,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import java.util.Collections; import java.util.List; -public class MostPlayedFragment extends StatisticsPlaylistFragment { +public final class MostPlayedFragment extends StatisticsPlaylistFragment { @Override protected String getName() { return getString(R.string.title_most_played); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 78038c3de..4843034eb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -32,13 +32,9 @@ import java.util.List; import icepick.State; import io.reactivex.android.schedulers.AndroidSchedulers; -import static org.schabi.newpipe.util.AnimationUtils.animateView; - public abstract class StatisticsPlaylistFragment extends BaseLocalListFragment, Void> { - private View headerRootLayout; - private View playlistControl; private View headerPlayAllButton; private View headerPopupButton; private View headerBackgroundButton; @@ -59,13 +55,13 @@ public abstract class StatisticsPlaylistFragment protected abstract List processResult(final List results); /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle + // Fragment LifeCycle - Creation /////////////////////////////////////////////////////////////////////////// @Override - public void onAttach(Context context) { - super.onAttach(context); - recordManager = new HistoryRecordManager(context); + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + recordManager = new HistoryRecordManager(getContext()); } @Override @@ -75,46 +71,23 @@ public abstract class StatisticsPlaylistFragment return inflater.inflate(R.layout.fragment_playlist, container, false); } - @Override - public void onPause() { - super.onPause(); - itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); - } - - @Override - public void onDestroyView() { - if (databaseSubscription != null) databaseSubscription.cancel(); - super.onDestroyView(); - } - - @Override - public void onDestroy() { - if (databaseSubscription != null) databaseSubscription.cancel(); - databaseSubscription = null; - recordManager = null; - - super.onDestroy(); - } - /////////////////////////////////////////////////////////////////////////// - // Fragment Views + // Fragment LifeCycle - Views /////////////////////////////////////////////////////////////////////////// @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - setFragmentTitle(getName()); + setTitle(getName()); } @Override protected View getListHeader() { - headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control, + final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control, itemsList, false); - playlistControl = headerRootLayout.findViewById(R.id.playlist_control); headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); - return headerRootLayout; } @@ -139,9 +112,124 @@ public abstract class StatisticsPlaylistFragment } } }); - } + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle - Loading + /////////////////////////////////////////////////////////////////////////// + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + recordManager.getStreamStatistics() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getHistoryObserver()); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle - Destruction + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + recordManager = null; + itemsListState = null; + } + + /////////////////////////////////////////////////////////////////////////// + // Statistics Loader + /////////////////////////////////////////////////////////////////////////// + + private Subscriber> getHistoryObserver() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List streams) { + handleResult(streams); + if (databaseSubscription != null) databaseSubscription.request(1); + } + + @Override + public void onError(Throwable exception) { + StatisticsPlaylistFragment.this.onError(exception); + } + + @Override + public void onComplete() { + } + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + itemListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + return; + } + + itemListAdapter.addItems(processResult(result)); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + + headerPlayAllButton.setOnClickListener(view -> + NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener(view -> + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener(view -> + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + + hideLoading(); + } + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void resetFragment() { + super.resetFragment(); + if (databaseSubscription != null) databaseSubscription.cancel(); + } + + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, + "none", "History Statistics", R.string.general_error); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + private void showStreamDialog(final StreamStatisticsEntry item) { final Context context = getContext(); final Activity activity = getActivity(); @@ -182,113 +270,6 @@ public abstract class StatisticsPlaylistFragment new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); } - private void resetFragment() { - if (databaseSubscription != null) databaseSubscription.cancel(); - if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); - } - - /////////////////////////////////////////////////////////////////////////// - // Loader - /////////////////////////////////////////////////////////////////////////// - - @Override - public void showLoading() { - super.showLoading(); - animateView(headerRootLayout, false, 200); - animateView(itemsList, false, 100); - } - - @Override - public void startLoading(boolean forceLoad) { - super.startLoading(forceLoad); - resetFragment(); - - recordManager.getStreamStatistics() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getHistoryObserver()); - } - - private Subscriber> getHistoryObserver() { - return new Subscriber>() { - @Override - public void onSubscribe(Subscription s) { - showLoading(); - - if (databaseSubscription != null) databaseSubscription.cancel(); - databaseSubscription = s; - databaseSubscription.request(1); - } - - @Override - public void onNext(List streams) { - handleResult(streams); - if (databaseSubscription != null) databaseSubscription.request(1); - } - - @Override - public void onError(Throwable exception) { - StatisticsPlaylistFragment.this.onError(exception); - } - - @Override - public void onComplete() { - } - }; - } - - @Override - public void handleResult(@NonNull List result) { - super.handleResult(result); - itemListAdapter.clearStreamItemList(); - - if (result.isEmpty()) { - showEmptyState(); - return; - } - - animateView(headerRootLayout, true, 100); - animateView(itemsList, true, 300); - - itemListAdapter.addItems(processResult(result)); - if (itemsListState != null) { - itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); - itemsListState = null; - } - - playlistControl.setVisibility(View.VISIBLE); - headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); - headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); - headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); - hideLoading(); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - /////////////////////////////////////////////////////////////////////////// - - @Override - protected boolean onError(Throwable exception) { - resetFragment(); - if (super.onError(exception)) return true; - - onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, - "none", "History Statistics", R.string.general_error); - return true; - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - protected void setFragmentTitle(final String title) { - if (activity.getSupportActionBar() != null) { - activity.getSupportActionBar().setTitle(title); - } - } - private PlayQueue getPlayQueue() { return getPlayQueue(0); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java index 853029ae6..84126ad4b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java @@ -6,7 +6,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import java.util.Collections; import java.util.List; -public class WatchHistoryFragment extends StatisticsPlaylistFragment { +public final class WatchHistoryFragment extends StatisticsPlaylistFragment { @Override protected String getName() { return getString(R.string.title_watch_history); 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 3fa8076f3..14bd93c57 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -14,6 +14,7 @@ import android.support.design.widget.Snackbar; import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -173,10 +174,19 @@ public abstract class HistoryFragment extends BaseFragment final Disposable deletion = delete(itemsToDelete) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(); + .subscribe( + ignored -> Log.d(TAG, "Clear history deleted [" + + itemsToDelete.size() + "] items."), + error -> Log.e(TAG, "Clear history delete step failed", error) + ); + final Disposable cleanUp = historyRecordManager.removeOrphanedRecords() .observeOn(AndroidSchedulers.mainThread()) - .subscribe(); + .subscribe( + ignored -> Log.d(TAG, "Clear history deleted orphaned stream records"), + error -> Log.e(TAG, "Clear history remove orphaned records failed", error) + ); + disposables.addAll(deletion, cleanUp); makeSnackbar(R.string.history_cleared); 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 e40a79368..25098fac8 100644 --- a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java @@ -7,6 +7,7 @@ import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -83,17 +84,25 @@ public class SearchHistoryFragment extends HistoryFragment { .setCancelable(true) .setNeutralButton(R.string.cancel, null) .setPositiveButton(R.string.delete_one, (dialog, i) -> { - final Single onDelete = historyRecordManager + final Disposable onDelete = historyRecordManager .deleteSearches(Collections.singleton(item)) - .observeOn(AndroidSchedulers.mainThread()); - disposables.add(onDelete.subscribe()); + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + ignored -> {/*successful*/}, + error -> Log.e(TAG, "Search history Delete One failed:", error) + ); + disposables.add(onDelete); makeSnackbar(R.string.item_deleted); }) .setNegativeButton(R.string.delete_all, (dialog, i) -> { - final Single onDeleteAll = historyRecordManager + final Disposable onDeleteAll = historyRecordManager .deleteSearchHistory(item.getSearch()) - .observeOn(AndroidSchedulers.mainThread()); - disposables.add(onDeleteAll.subscribe()); + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + ignored -> {/*successful*/}, + error -> Log.e(TAG, "Search history Delete All failed:", error) + ); + disposables.add(onDeleteAll); makeSnackbar(R.string.item_deleted); }) .show(); @@ -112,8 +121,7 @@ public class SearchHistoryFragment extends HistoryFragment { protected class SearchHistoryAdapter extends HistoryEntryAdapter { - - public SearchHistoryAdapter(Context context) { + SearchHistoryAdapter(Context context) { super(context); } 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 7913c9a28..0fabc594a 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java @@ -8,6 +8,7 @@ import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -29,6 +30,7 @@ import java.util.List; import io.reactivex.Flowable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; public class WatchedHistoryFragment extends HistoryFragment { @@ -85,17 +87,25 @@ public class WatchedHistoryFragment extends HistoryFragment .setCancelable(true) .setNeutralButton(R.string.cancel, null) .setPositiveButton(R.string.delete_one, (dialog, i) -> { - final Single onDelete = historyRecordManager + final Disposable onDelete = historyRecordManager .deleteStreamHistory(Collections.singleton(item)) - .observeOn(AndroidSchedulers.mainThread()); - disposables.add(onDelete.subscribe()); + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + ignored -> {/*successful*/}, + error -> Log.e(TAG, "Watch history Delete One failed:", error) + ); + disposables.add(onDelete); makeSnackbar(R.string.item_deleted); }) .setNegativeButton(R.string.delete_all, (dialog, i) -> { - final Single onDeleteAll = historyRecordManager + final Disposable onDeleteAll = historyRecordManager .deleteStreamHistory(item.streamId) - .observeOn(AndroidSchedulers.mainThread()); - disposables.add(onDeleteAll.subscribe()); + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + ignored -> {/*successful*/}, + error -> Log.e(TAG, "Watch history Delete All failed:", error) + ); + disposables.add(onDeleteAll); makeSnackbar(R.string.item_deleted); }) .show(); 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 369b15509..7558f1375 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -676,7 +676,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } // TODO: update exoplayer to 2.6.x in order to register view count on repeated streams - databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe()); + databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() + .subscribe( + ignored -> {/* successful */}, + error -> Log.e(TAG, "Player onViewed() failure: ", error) + )); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } @@ -844,7 +848,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen final Disposable stateSaver = recordManager.saveStreamState(info, progress) .observeOn(AndroidSchedulers.mainThread()) .onErrorComplete() - .subscribe(); + .subscribe( + ignored -> {/* successful */}, + error -> Log.e(TAG, "savePlaybackState() failure: ", error) + ); databaseUpdateReactor.add(stateSaver); } diff --git a/app/src/main/res/layout/local_playlist_header.xml b/app/src/main/res/layout/local_playlist_header.xml index 4ba611681..4d686c515 100644 --- a/app/src/main/res/layout/local_playlist_header.xml +++ b/app/src/main/res/layout/local_playlist_header.xml @@ -11,9 +11,12 @@ android:id="@+id/playlist_title_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginLeft="8dp" - android:layout_marginRight="8dp" - android:layout_marginTop="6dp" + android:paddingTop="6dp" + android:paddingBottom="6dp" + android:paddingLeft="12dp" + android:paddingRight="12dp" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_toLeftOf="@id/playlist_stream_count" android:layout_toStartOf="@id/playlist_stream_count" android:background="?attr/selectableItemBackground" @@ -26,7 +29,7 @@ android:singleLine="true" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="@dimen/playlist_detail_title_text_size" - tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur..." /> + tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur..."/> subscription_page_key kiosk_page channel_page - bookmark_page @string/blank_page_key @string/kiosk_page_key @string/feed_page_key @string/subscription_page_key @string/channel_page_key - @string/bookmark_page_key main_page_selected_service main_page_selected_channel_name diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e6d3e641..577d85ce5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -390,6 +390,5 @@ Playlist successfully created Added to playlist Playlist thumbnail changed - Playlist renamed - Playlist deleted + Failed to delete playlist From 1ff8b5fb9f5afaa01ed7e15df9c104a520a5e394 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 30 Jan 2018 16:21:50 -0800 Subject: [PATCH 22/52] -Refactored info item and local item click gestures into the same OnClickGesture. --- .../fragments/detail/VideoDetailFragment.java | 7 ++----- .../fragments/list/BaseListFragment.java | 8 ++++---- .../fragments/local/LocalItemBuilder.java | 7 ++++--- .../fragments/local/LocalItemListAdapter.java | 3 ++- .../local/LocalPlaylistFragment.java | 3 ++- .../fragments/local/PlaylistAppendDialog.java | 3 ++- .../local/bookmark/BookmarkFragment.java | 4 ++-- .../bookmark/StatisticsPlaylistFragment.java | 4 ++-- .../subscription/SubscriptionFragment.java | 4 ++-- .../newpipe/info_list/InfoItemBuilder.java | 19 ++++++++++--------- .../newpipe/info_list/InfoListAdapter.java | 7 ++++--- .../newpipe/info_list/OnInfoItemGesture.java | 12 ------------ .../OnClickGesture.java} | 7 ++----- 13 files changed, 38 insertions(+), 50 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java rename app/src/main/java/org/schabi/newpipe/{fragments/local/OnLocalItemGesture.java => util/OnClickGesture.java} (58%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 6907f3266..923feeba0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -44,7 +44,6 @@ import com.nirhart.parallaxscroll.views.ParallaxScrollView; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; @@ -60,11 +59,8 @@ import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; -import org.schabi.newpipe.history.HistoryListener; -import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.player.helper.PlayerHelper; @@ -79,6 +75,7 @@ import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -474,7 +471,7 @@ public class VideoDetailFragment extends BaseStateFragment implement @Override protected void initListeners() { super.initListeners(); - infoItemBuilder.setOnStreamSelectedListener(new OnInfoItemGesture() { + infoItemBuilder.setOnStreamSelectedListener(new OnClickGesture() { @Override public void selected(StreamInfoItem selectedItem) { selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); 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 82b45c76e..aad6e95fa 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 @@ -23,9 +23,9 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; import java.util.ArrayList; @@ -137,7 +137,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { + infoListAdapter.setOnStreamSelectedListener(new OnClickGesture() { @Override public void selected(StreamInfoItem selectedItem) { onItemSelected(selectedItem); @@ -152,7 +152,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem } }); - infoListAdapter.setOnChannelSelectedListener(new OnInfoItemGesture() { + infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() { @Override public void selected(ChannelInfoItem selectedItem) { onItemSelected(selectedItem); @@ -162,7 +162,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem } }); - infoListAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture() { + infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture() { @Override public void selected(PlaylistInfoItem selectedItem) { onItemSelected(selectedItem); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java index 128daf435..4794def97 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java @@ -9,6 +9,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.process.BitmapProcessor; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.util.OnClickGesture; /* * Created by Christian Schabesberger on 26.09.16. @@ -36,7 +37,7 @@ public class LocalItemBuilder { private final Context context; private ImageLoader imageLoader = ImageLoader.getInstance(); - private OnLocalItemGesture onSelectedListener; + private OnClickGesture onSelectedListener; public LocalItemBuilder(Context context) { this.context = context; @@ -51,11 +52,11 @@ public class LocalItemBuilder { imageLoader.displayImage(url, view, options); } - public OnLocalItemGesture getOnItemSelectedListener() { + public OnClickGesture getOnItemSelectedListener() { return onSelectedListener; } - public void setOnItemSelectedListener(OnLocalItemGesture listener) { + public void setOnItemSelectedListener(OnClickGesture listener) { this.onSelectedListener = listener; } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index 35112a6a5..807599678 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -12,6 +12,7 @@ import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder; import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder; import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.OnClickGesture; import java.text.DateFormat; import java.util.ArrayList; @@ -65,7 +66,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter listener) { + public void setSelectedListener(OnClickGesture listener) { localItemBuilder.setOnItemSelectedListener(listener); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 7ec24337a..7d73b14b9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -32,6 +32,7 @@ import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; @@ -142,7 +143,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { + itemListAdapter.setSelectedListener(new OnClickGesture() { @Override public void selected(LocalItem selectedItem) { if (selectedItem instanceof PlaylistStreamEntry) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index b01dcabeb..6d18e9155 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -19,6 +19,7 @@ import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; +import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.Collections; @@ -81,7 +82,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog { new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); playlistAdapter = new LocalItemListAdapter(getActivity()); - playlistAdapter.setSelectedListener(new OnLocalItemGesture() { + playlistAdapter.setSelectedListener(new OnClickGesture() { @Override public void selected(LocalItem selectedItem) { if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 393e128f7..5c863590f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -18,9 +18,9 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.fragments.local.BaseLocalListFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistManager; -import org.schabi.newpipe.fragments.local.OnLocalItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import java.util.List; @@ -95,7 +95,7 @@ public final class BookmarkFragment protected void initListeners() { super.initListeners(); - itemListAdapter.setSelectedListener(new OnLocalItemGesture() { + itemListAdapter.setSelectedListener(new OnClickGesture() { @Override public void selected(LocalItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 4843034eb..d4e888c30 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -18,13 +18,13 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.local.BaseLocalListFragment; -import org.schabi.newpipe.fragments.local.OnLocalItemGesture; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; @@ -95,7 +95,7 @@ public abstract class StatisticsPlaylistFragment protected void initListeners() { super.initListeners(); - itemListAdapter.setSelectedListener(new OnLocalItemGesture() { + itemListAdapter.setSelectedListener(new OnClickGesture() { @Override public void selected(LocalItem selectedItem) { if (selectedItem instanceof StreamStatisticsEntry) { 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 8db5d5f00..a91cca908 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 @@ -17,9 +17,9 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.Collections; @@ -125,7 +125,7 @@ public class SubscriptionFragment extends BaseStateFragment() { + infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() { @Override public void selected(ChannelInfoItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index cdad31674..218895983 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -19,6 +19,7 @@ import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; +import org.schabi.newpipe.util.OnClickGesture; /* * Created by Christian Schabesberger on 26.09.16. @@ -46,9 +47,9 @@ public class InfoItemBuilder { private final Context context; private ImageLoader imageLoader = ImageLoader.getInstance(); - private OnInfoItemGesture onStreamSelectedListener; - private OnInfoItemGesture onChannelSelectedListener; - private OnInfoItemGesture onPlaylistSelectedListener; + private OnClickGesture onStreamSelectedListener; + private OnClickGesture onChannelSelectedListener; + private OnClickGesture onPlaylistSelectedListener; public InfoItemBuilder(Context context) { this.context = context; @@ -86,27 +87,27 @@ public class InfoItemBuilder { return imageLoader; } - public OnInfoItemGesture getOnStreamSelectedListener() { + public OnClickGesture getOnStreamSelectedListener() { return onStreamSelectedListener; } - public void setOnStreamSelectedListener(OnInfoItemGesture listener) { + public void setOnStreamSelectedListener(OnClickGesture listener) { this.onStreamSelectedListener = listener; } - public OnInfoItemGesture getOnChannelSelectedListener() { + public OnClickGesture getOnChannelSelectedListener() { return onChannelSelectedListener; } - public void setOnChannelSelectedListener(OnInfoItemGesture listener) { + public void setOnChannelSelectedListener(OnClickGesture listener) { this.onChannelSelectedListener = listener; } - public OnInfoItemGesture getOnPlaylistSelectedListener() { + public OnClickGesture getOnPlaylistSelectedListener() { return onPlaylistSelectedListener; } - public void setOnPlaylistSelectedListener(OnInfoItemGesture listener) { + public void setOnPlaylistSelectedListener(OnClickGesture listener) { this.onPlaylistSelectedListener = listener; } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index dbf5d7556..4b9914397 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -17,6 +17,7 @@ import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; +import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; @@ -76,15 +77,15 @@ public class InfoListAdapter extends RecyclerView.Adapter(); } - public void setOnStreamSelectedListener(OnInfoItemGesture listener) { + public void setOnStreamSelectedListener(OnClickGesture listener) { infoItemBuilder.setOnStreamSelectedListener(listener); } - public void setOnChannelSelectedListener(OnInfoItemGesture listener) { + public void setOnChannelSelectedListener(OnClickGesture listener) { infoItemBuilder.setOnChannelSelectedListener(listener); } - public void setOnPlaylistSelectedListener(OnInfoItemGesture listener) { + public void setOnPlaylistSelectedListener(OnClickGesture listener) { infoItemBuilder.setOnPlaylistSelectedListener(listener); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java b/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java deleted file mode 100644 index 84634c1d9..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.schabi.newpipe.info_list; - -import org.schabi.newpipe.extractor.InfoItem; - -public abstract class OnInfoItemGesture { - - public abstract void selected(T selectedItem); - - public void held(T selectedItem) { - // Optional gesture - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java b/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java similarity index 58% rename from app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java rename to app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java index 5cede4c67..01416b279 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java +++ b/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java @@ -1,11 +1,8 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.util; import android.support.v7.widget.RecyclerView; -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.extractor.InfoItem; - -public abstract class OnLocalItemGesture { +public abstract class OnClickGesture { public abstract void selected(T selectedItem); From 53a1833e26e949e3ec541f40a549c3b8ca3d3247 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 30 Jan 2018 18:17:27 -0800 Subject: [PATCH 23/52] -Increased save join debounce time to 2 seconds. -Added add to playlist option for videos available as base list items. -Moved video count to second row on local playlist header. -Removed bottom line on playlist control UI. --- .../fragments/list/BaseListFragment.java | 33 ++-- .../local/LocalPlaylistFragment.java | 2 +- .../main/res/layout/local_playlist_header.xml | 17 +- app/src/main/res/layout/playlist_control.xml | 157 ++++++++---------- 4 files changed, 100 insertions(+), 109 deletions(-) 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 aad6e95fa..8bc68a4c7 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 @@ -29,6 +29,7 @@ import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Queue; @@ -194,22 +195,26 @@ public abstract class BaseListFragment extends BaseStateFragment implem final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), - context.getResources().getString(R.string.enqueue_on_popup) + context.getResources().getString(R.string.enqueue_on_popup), + context.getResources().getString(R.string.append_playlist) }; - final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - switch (i) { - case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); - break; - case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); - break; - default: - break; - } + final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + break; + case 2: + if (getFragmentManager() != null) { + PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) + .show(getFragmentManager(), TAG); + } + break; + default: + break; } }; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 7d73b14b9..0a6f9158e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -47,7 +47,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { - private static final long SAVE_DEBOUNCE_MILLIS = 1000; + private static final long SAVE_DEBOUNCE_MILLIS = 2000; private View headerRootLayout; private TextView headerTitleView; diff --git a/app/src/main/res/layout/local_playlist_header.xml b/app/src/main/res/layout/local_playlist_header.xml index 4d686c515..ab5dd4440 100644 --- a/app/src/main/res/layout/local_playlist_header.xml +++ b/app/src/main/res/layout/local_playlist_header.xml @@ -5,20 +5,17 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:padding="6dp" android:background="?attr/contrast_background_color"> - - - - - - - - - - - - - - - - + android:layout_height="@dimen/playlist_ctrl_height" + android:layout_weight="1" + android:gravity="center" + android:clickable="true" + android:focusable="true" + android:background="?attr/selectableItemBackground"> + - + + - + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center" + android:clickable="true" + android:focusable="true" + android:background="?attr/selectableItemBackground" + android:id="@+id/playlist_ctrl_play_all_button"> + + + + + + + + + From 268762166af3a74fe4a9f9d53fa89f0729c5591c Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 30 Jan 2018 19:39:41 -0800 Subject: [PATCH 24/52] -Added save on exit to local playlist fragment. -Improved drag reordering experience by setting minimum velocity. -Increased save debounce to 10 seconds. --- .../local/LocalPlaylistFragment.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 0a6f9158e..e84ee41ff 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -47,7 +47,9 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { - private static final long SAVE_DEBOUNCE_MILLIS = 2000; + // Save the list 10 seconds after the last change occurred + private static final long SAVE_DEBOUNCE_MILLIS = 10000; + private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 15; private View headerRootLayout; private TextView headerTitleView; @@ -205,6 +207,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment saveJoin()); + .subscribe(ignored -> saveImmediate()); } - private void saveJoin() { + private void saveImmediate() { final List items = itemListAdapter.getItemsList(); List streamIds = new ArrayList<>(items.size()); for (final LocalItem item : items) { @@ -449,6 +452,17 @@ public class LocalPlaylistFragment extends BaseLocalListFragment Date: Wed, 31 Jan 2018 11:51:47 -0800 Subject: [PATCH 25/52] -Fixed database updates cause outdated record to overwrite reordered local playlist when fragment is active. -Fixed save on exit causes empty list being saved after orientation changes on older devices. -Fixed NPE on animating garbage collected views on local item fragments. -Reduced drag speed from 15 to 12 items per second. --- .../playlist/dao/PlaylistStreamDAO.java | 4 +- .../local/BaseLocalListFragment.java | 6 +- .../local/LocalPlaylistFragment.java | 156 +++++++++++------- 3 files changed, 100 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java index dd2994d29..8bf1ea696 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -47,8 +47,8 @@ public abstract class PlaylistStreamDAO implements BasicDAO saveImmediate()); + .subscribe(ignored -> saveImmediate(), this::onError); } private void saveImmediate() { + // List must be loaded and modified in order to save + if (isLoadingComplete == null || isModified == null || + !isLoadingComplete.get() || !isModified.get()) { + Log.w(TAG, "Attempting to save playlist when local playlist " + + "is not loaded or not modified: playlist id=[" + playlistId + "]"); + return; + } + final List items = itemListAdapter.getItemsList(); List streamIds = new ArrayList<>(items.size()); for (final LocalItem item : items) { @@ -386,12 +416,60 @@ public class LocalPlaylistFragment extends BaseLocalListFragment {/*Do nothing on success*/}, this::onError); + .subscribe( + () -> { if (isModified != null) isModified.set(false); }, + this::onError + ); + } + + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, + ItemTouchHelper.ACTION_STATE_IDLE) { + @Override + public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, + int viewSizeOutOfBounds, int totalSize, + long msSinceStartScroll) { + final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, + viewSizeOutOfBounds, totalSize, msSinceStartScroll); + final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, + Math.abs(standardSpeed)); + return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, + RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType() || + itemListAdapter == null) { + return false; + } + + final int sourceIndex = source.getAdapterPosition(); + final int targetIndex = target.getAdapterPosition(); + final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); + if (isSwapped) saveChanges(); + return isSwapped; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} + }; } /*////////////////////////////////////////////////////////////////////////// @@ -449,50 +527,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment Date: Sat, 3 Feb 2018 09:36:40 -0800 Subject: [PATCH 26/52] -Fixed NPE issues when button views are clicked on local playlist and statistics playlist fragments are out of focus. -Added disk cache size limit for image loader. -Fixed button names for playlist rename dialog. --- app/src/main/java/org/schabi/newpipe/App.java | 8 ++++---- .../fragments/local/LocalPlaylistFragment.java | 11 ++++++++++- .../local/bookmark/StatisticsPlaylistFragment.java | 7 +++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 2ae21137f..3a22bf511 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -11,8 +11,6 @@ import android.os.Build; import android.util.Log; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; -import com.nostra13.universalimageloader.cache.memory.impl.LruMemoryCache; -import com.nostra13.universalimageloader.cache.memory.impl.WeakMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; @@ -83,7 +81,7 @@ public class App extends Application { initNotificationChannel(); // Initialize image loader - ImageLoader.getInstance().init(getImageLoaderConfigurations(10)); + ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50)); configureRxJavaErrorHandler(); } @@ -121,9 +119,11 @@ public class App extends Application { }); } - private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb) { + private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb, + final int diskCacheSizeMb) { return new ImageLoaderConfiguration.Builder(this) .memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024)) + .diskCacheSize(diskCacheSizeMb * 1024 * 1024) .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 058dc43b2..ea7242055 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -35,6 +35,7 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -289,6 +290,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment result) { super.handleResult(result); + if (itemListAdapter == null) return; + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { @@ -349,7 +352,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment + .setPositiveButton(R.string.rename, (dialogInterface, i) -> changePlaylistName(nameEdit.getText().toString()) ); @@ -382,6 +385,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment infoItems = itemListAdapter.getItemsList(); List streamInfoItems = new ArrayList<>(infoItems.size()); for (final LocalItem item : infoItems) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index d4e888c30..ec2dda0a4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -27,6 +27,7 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import icepick.State; @@ -185,6 +186,8 @@ public abstract class StatisticsPlaylistFragment @Override public void handleResult(@NonNull List result) { super.handleResult(result); + if (itemListAdapter == null) return; + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { @@ -275,6 +278,10 @@ public abstract class StatisticsPlaylistFragment } private PlayQueue getPlayQueue(final int index) { + if (itemListAdapter == null) { + return new SinglePlayQueue(Collections.emptyList(), 0); + } + final List infoItems = itemListAdapter.getItemsList(); List streamInfoItems = new ArrayList<>(infoItems.size()); for (final LocalItem item : infoItems) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 577d85ce5..05db15da1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -233,6 +233,7 @@ Delete All Checksum Dismiss + Rename New mission From c0a75f5b98606091070ac6a37d82658db5b06afc Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 5 Feb 2018 21:32:23 -0800 Subject: [PATCH 27/52] -Added ability to save playlist as remote playlist link rather than storing it in database. -Added LeakCanary as part of debug build. -Modified bookmark list to show both remote and local playlists. -Removed ability to save channel items as local playlist, in favor of subscribe. --- app/build.gradle | 2 + .../java/org/schabi/newpipe/DebugApp.java | 8 + app/src/main/java/org/schabi/newpipe/App.java | 3 - .../schabi/newpipe/database/AppDatabase.java | 6 +- .../schabi/newpipe/database/LocalItem.java | 6 +- .../database/playlist/PlaylistLocalItem.java | 7 + .../playlist/PlaylistMetadataEntry.java | 11 +- .../playlist/dao/PlaylistRemoteDAO.java | 60 +++++ .../playlist/model/PlaylistRemoteEntity.java | 138 ++++++++++++ .../fragments/list/BaseListFragment.java | 16 -- .../list/channel/ChannelFragment.java | 12 - .../list/playlist/PlaylistFragment.java | 205 +++++++++++++----- .../fragments/local/LocalItemListAdapter.java | 15 +- .../local/RemotePlaylistManager.java | 48 ++++ .../local/bookmark/BookmarkFragment.java | 93 +++++--- .../local/holder/LocalPlaylistItemHolder.java | 43 +--- .../local/holder/PlaylistItemHolder.java | 62 ++++++ .../holder/RemotePlaylistItemHolder.java | 33 +++ .../ic_playlist_add_check_black_24dp.png | Bin 0 -> 163 bytes .../ic_playlist_add_check_white_24dp.png | Bin 0 -> 159 bytes .../ic_playlist_add_check_black_24dp.png | Bin 0 -> 122 bytes .../ic_playlist_add_check_white_24dp.png | Bin 0 -> 124 bytes .../ic_playlist_add_check_black_24dp.png | Bin 0 -> 163 bytes .../ic_playlist_add_check_white_24dp.png | Bin 0 -> 163 bytes .../ic_playlist_add_check_black_24dp.png | Bin 0 -> 236 bytes .../ic_playlist_add_check_white_24dp.png | Bin 0 -> 236 bytes .../ic_playlist_add_check_black_24dp.png | Bin 0 -> 283 bytes .../ic_playlist_add_check_white_24dp.png | Bin 0 -> 289 bytes .../main/res/layout/fragment_video_detail.xml | 2 +- app/src/main/res/menu/menu_channel.xml | 8 - app/src/main/res/menu/menu_play_queue.xml | 2 +- app/src/main/res/menu/menu_playlist.xml | 16 +- app/src/main/res/values/attrs.xml | 3 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 6 +- 35 files changed, 625 insertions(+), 183 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/RemotePlaylistManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java create mode 100644 app/src/main/res/drawable-hdpi/ic_playlist_add_check_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_playlist_add_check_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_playlist_add_check_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_playlist_add_check_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_playlist_add_check_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_white_24dp.png diff --git a/app/build.gradle b/app/build.gradle index 86d6542e0..748bbb9c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,4 +89,6 @@ dependencies { implementation 'frankiesardo:icepick:3.2.0' annotationProcessor 'frankiesardo:icepick-processor:3.2.0' + + debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' } diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index 1a507b4e5..fbf414738 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -4,6 +4,7 @@ import android.content.Context; import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; +import com.squareup.leakcanary.LeakCanary; public class DebugApp extends App { private static final String TAG = DebugApp.class.toString(); @@ -18,6 +19,13 @@ public class DebugApp extends App { public void onCreate() { super.onCreate(); + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; + } + LeakCanary.install(this); + initStetho(); } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 3a22bf511..79221db7f 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -1,12 +1,9 @@ package org.schabi.newpipe; -import android.app.AlarmManager; import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.Context; -import android.content.Intent; import android.os.Build; import android.util.Log; diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index 086e1bed0..145a77c70 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -9,8 +9,10 @@ import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.dao.StreamDAO; import org.schabi.newpipe.database.stream.dao.StreamStateDAO; @@ -26,7 +28,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_12_0; entities = { SubscriptionEntity.class, SearchHistoryEntry.class, StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, - PlaylistEntity.class, PlaylistStreamEntity.class + PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class }, version = DB_VER_12_0, exportSchema = false @@ -48,4 +50,6 @@ public abstract class AppDatabase extends RoomDatabase { public abstract PlaylistDAO playlistDAO(); public abstract PlaylistStreamDAO playlistStreamDAO(); + + public abstract PlaylistRemoteDAO playlistRemoteDAO(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java index 95d0d9213..e121739ab 100644 --- a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java @@ -2,9 +2,11 @@ package org.schabi.newpipe.database; public interface LocalItem { enum LocalItemType { - PLAYLIST_ITEM, + PLAYLIST_LOCAL_ITEM, + PLAYLIST_REMOTE_ITEM, + PLAYLIST_STREAM_ITEM, - STATISTIC_STREAM_ITEM + STATISTIC_STREAM_ITEM, } LocalItemType getLocalItemType(); diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java new file mode 100644 index 000000000..fd99f84a1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.database.playlist; + +import org.schabi.newpipe.database.LocalItem; + +public interface PlaylistLocalItem extends LocalItem { + String getOrderingName(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java index 205c5108d..6d9fc2213 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -2,13 +2,11 @@ package org.schabi.newpipe.database.playlist; import android.arch.persistence.room.ColumnInfo; -import org.schabi.newpipe.database.LocalItem; - import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; -public class PlaylistMetadataEntry implements LocalItem { +public class PlaylistMetadataEntry implements PlaylistLocalItem { final public static String PLAYLIST_STREAM_COUNT = "streamCount"; @ColumnInfo(name = PLAYLIST_ID) @@ -29,6 +27,11 @@ public class PlaylistMetadataEntry implements LocalItem { @Override public LocalItemType getLocalItemType() { - return LocalItemType.PLAYLIST_ITEM; + return LocalItemType.PLAYLIST_LOCAL_ITEM; + } + + @Override + public String getOrderingName() { + return name; } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java new file mode 100644 index 000000000..82d767b07 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java @@ -0,0 +1,60 @@ +package org.schabi.newpipe.database.playlist.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL; + +@Dao +public abstract class PlaylistRemoteDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE) + public abstract int deleteAll(); + + @Override + @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + + " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + public abstract Flowable> listByService(int serviceId); + + @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " + + REMOTE_PLAYLIST_URL + " = :url AND " + + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + public abstract Flowable> getPlaylist(long serviceId, String url); + + @Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE + + " WHERE " + + REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + abstract Long getPlaylistIdInternal(long serviceId, String url); + + @Transaction + public long upsert(PlaylistRemoteEntity playlist) { + final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl()); + + if (playlistId == null) { + return insert(playlist); + } else { + playlist.setUid(playlistId); + update(playlist); + return playlistId; + } + } + + @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE + + " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId") + public abstract int deletePlaylist(final long playlistId); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java new file mode 100644 index 000000000..5e3db62a9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java @@ -0,0 +1,138 @@ +package org.schabi.newpipe.database.playlist.model; + +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.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistLocalItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.util.Constants; + +import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL; + +@Entity(tableName = REMOTE_PLAYLIST_TABLE, + indices = { + @Index(value = {REMOTE_PLAYLIST_NAME}), + @Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true) + }) +public class PlaylistRemoteEntity implements PlaylistLocalItem { + final public static String REMOTE_PLAYLIST_TABLE = "remote_playlists"; + final public static String REMOTE_PLAYLIST_ID = "uid"; + final public static String REMOTE_PLAYLIST_SERVICE_ID = "service_id"; + final public static String REMOTE_PLAYLIST_NAME = "name"; + final public static String REMOTE_PLAYLIST_URL = "url"; + final public static String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; + final public static String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader"; + final public static String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = REMOTE_PLAYLIST_ID) + private long uid = 0; + + @ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID) + private int serviceId = Constants.NO_SERVICE_ID; + + @ColumnInfo(name = REMOTE_PLAYLIST_NAME) + private String name; + + @ColumnInfo(name = REMOTE_PLAYLIST_URL) + private String url; + + @ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL) + private String thumbnailUrl; + + @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME) + private String uploader; + + @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) + private Long streamCount; + + public PlaylistRemoteEntity(int serviceId, String name, String url, String thumbnailUrl, + String uploader, Long streamCount) { + this.serviceId = serviceId; + this.name = name; + this.url = url; + this.thumbnailUrl = thumbnailUrl; + this.uploader = uploader; + this.streamCount = streamCount; + } + + @Ignore + public PlaylistRemoteEntity(final PlaylistInfo info) { + this(info.getServiceId(), info.getName(), info.getUrl(), info.getThumbnailUrl(), + info.getUploaderName(), info.getStreamCount()); + } + + public long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + + public int getServiceId() { + return serviceId; + } + + public void setServiceId(int serviceId) { + this.serviceId = serviceId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public void setThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUploader() { + return uploader; + } + + public void setUploader(String uploader) { + this.uploader = uploader; + } + + public Long getStreamCount() { + return streamCount; + } + + public void setStreamCount(Long streamCount) { + this.streamCount = streamCount; + } + + @Override + public LocalItemType getLocalItemType() { + return PLAYLIST_REMOTE_ITEM; + } + + @Override + public String getOrderingName() { + return name; + } +} 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 8bc68a4c7..1a0a836c5 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 @@ -290,20 +290,4 @@ public abstract class BaseListFragment extends BaseStateFragment implem public void handleNextItems(N result) { isLoading.set(false); } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - protected void appendToPlaylist(final android.support.v4.app.FragmentManager manager, - final String tag) { - if (infoListAdapter == null) return; - List streams = new ArrayList<>(); - for (final InfoItem item : infoListAdapter.getItemsList()) { - if (item instanceof StreamInfoItem) { - streams.add((StreamInfoItem) item); - } - } - PlaylistAppendDialog.fromStreamInfoItems(streams).show(manager, tag); - } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 641b26299..a7f513de9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -84,7 +84,6 @@ public class ChannelFragment extends BaseListInfoFragment { private LinearLayout headerBackgroundButton; private MenuItem menuRssButton; - private MenuItem playlistAppendButton; public static ChannelFragment getInstance(int serviceId, String url, String name) { ChannelFragment instance = new ChannelFragment(); @@ -203,12 +202,6 @@ public class ChannelFragment extends BaseListInfoFragment { if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); menuRssButton = menu.findItem(R.id.menu_item_rss); - playlistAppendButton = menu.findItem(R.id.menu_append_playlist); - - if (currentInfo != null) { - menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl())); - playlistAppendButton.setVisible(!currentInfo.getRelatedStreams().isEmpty()); - } } } @@ -232,9 +225,6 @@ public class ChannelFragment extends BaseListInfoFragment { case R.id.menu_item_share: shareUrl(name, url); break; - case R.id.menu_append_playlist: - appendToPlaylist(getFragmentManager(), TAG); - break; default: return super.onOptionsItemSelected(item); } @@ -434,8 +424,6 @@ public class ChannelFragment extends BaseListInfoFragment { } else headerSubscribersTextView.setVisibility(View.GONE); if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); - if (playlistAppendButton != null) playlistAppendButton - .setVisible(!currentInfo.getRelatedStreams().isEmpty()); playlistCtrl.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 15255618b..39c88f8d3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -17,13 +17,18 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.fragments.local.RemotePlaylistManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlaylistPlayQueue; @@ -32,12 +37,21 @@ import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import java.util.List; + import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class PlaylistFragment extends BaseListInfoFragment { + private CompositeDisposable disposables; + private Subscription bookmarkReactor; + + private RemotePlaylistManager remotePlaylistManager; + private PlaylistRemoteEntity playlistEntity; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ @@ -54,7 +68,8 @@ public class PlaylistFragment extends BaseListInfoFragment { private View headerPopupButton; private View headerBackgroundButton; - private MenuItem playlistAppendButton; + private MenuItem playlistBookmarkButton; + private MenuItem playlistUnbookmarkButton; public static PlaylistFragment getInstance(int serviceId, String url, String name) { PlaylistFragment instance = new PlaylistFragment(); @@ -67,7 +82,15 @@ public class PlaylistFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + disposables = new CompositeDisposable(); + remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext())); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_playlist, container, false); } @@ -96,6 +119,11 @@ public class PlaylistFragment extends BaseListInfoFragment { super.initViews(rootView, savedInstanceState); infoListAdapter.useMiniItemVariants(true); + + remotePlaylistManager.getPlaylist(serviceId, url) + .onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistBookmarkSubscriber()); } @Override @@ -112,29 +140,26 @@ public class PlaylistFragment extends BaseListInfoFragment { context.getResources().getString(R.string.start_here_on_popup), }; - final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); - switch (i) { - case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); - break; - case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); - break; - case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); - break; - case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); - break; - case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); - break; - default: - break; - } + final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { + final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + break; + case 2: + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + break; + case 3: + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + break; + case 4: + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + break; + default: + break; } }; @@ -148,10 +173,28 @@ public class PlaylistFragment extends BaseListInfoFragment { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_playlist, menu); - playlistAppendButton = menu.findItem(R.id.menu_append_playlist); - if (currentInfo != null) { - playlistAppendButton.setVisible(!currentInfo.getRelatedStreams().isEmpty()); - } + playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark); + playlistUnbookmarkButton = menu.findItem(R.id.menu_item_unbookmark); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (disposables != null) disposables.clear(); + if (bookmarkReactor != null) bookmarkReactor.cancel(); + + bookmarkReactor = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (disposables != null) disposables.dispose(); + + disposables = null; + remotePlaylistManager = null; + playlistEntity = null; } /*////////////////////////////////////////////////////////////////////////// @@ -177,8 +220,11 @@ public class PlaylistFragment extends BaseListInfoFragment { case R.id.menu_item_share: shareUrl(name, url); break; - case R.id.menu_append_playlist: - appendToPlaylist(getFragmentManager(), TAG); + case R.id.menu_item_bookmark: + bookmarkPlaylist(); + break; + case R.id.menu_item_unbookmark: + unbookmarkPlaylist(); break; default: return super.onOptionsItemSelected(item); @@ -211,12 +257,11 @@ public class PlaylistFragment extends BaseListInfoFragment { if (!TextUtils.isEmpty(result.getUploaderName())) { headerUploaderName.setText(result.getUploaderName()); if (!TextUtils.isEmpty(result.getUploaderUrl())) { - headerUploaderLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - NavigationHelper.openChannelFragment(getFragmentManager(), result.getServiceId(), result.getUploaderUrl(), result.getUploaderName()); - } - }); + headerUploaderLayout.setOnClickListener(v -> + NavigationHelper.openChannelFragment(getFragmentManager(), + result.getServiceId(), result.getUploaderUrl(), + result.getUploaderName()) + ); } } @@ -225,31 +270,20 @@ public class PlaylistFragment extends BaseListInfoFragment { imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS); headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count)); - if (playlistAppendButton != null) playlistAppendButton - .setVisible(!currentInfo.getRelatedStreams().isEmpty()); - if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); } - headerPlayAllButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.playOnMainPlayer(activity, getPlayQueue()); - } - }); - headerPopupButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()); - } - }); - headerBackgroundButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()); - } - }); + remotePlaylistManager.onUpdate(result) + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe(integer -> {/* Do nothing*/}, this::onError); + + headerPlayAllButton.setOnClickListener(view -> + NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener(view -> + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener(view -> + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); } private PlayQueue getPlayQueue() { @@ -293,9 +327,64 @@ public class PlaylistFragment extends BaseListInfoFragment { // Utils //////////////////////////////////////////////////////////////////////////*/ + private Subscriber> getPlaylistBookmarkSubscriber() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + if (bookmarkReactor != null) bookmarkReactor.cancel(); + bookmarkReactor = s; + bookmarkReactor.request(1); + } + + @Override + public void onNext(List playlist) { + if (playlistBookmarkButton == null || playlistUnbookmarkButton == null) return; + + playlistBookmarkButton.setVisible(playlist.isEmpty()); + playlistUnbookmarkButton.setVisible(!playlist.isEmpty()); + playlistEntity = playlist.isEmpty() ? null : playlist.get(0); + + if (bookmarkReactor != null) bookmarkReactor.request(1); + } + + @Override + public void onError(Throwable t) { + PlaylistFragment.this.onError(t); + } + + @Override + public void onComplete() { + + } + }; + } + @Override public void setTitle(String title) { super.setTitle(title); headerTitleView.setText(title); } + + private void bookmarkPlaylist() { + if (remotePlaylistManager == null || currentInfo == null) return; + + playlistBookmarkButton.setVisible(false); + playlistUnbookmarkButton.setVisible(false); + + remotePlaylistManager.onBookmark(currentInfo) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> {/* Do nothing */}, this::onError); + } + + private void unbookmarkPlaylist() { + if (remotePlaylistManager == null || playlistEntity == null) return; + + playlistBookmarkButton.setVisible(false); + playlistUnbookmarkButton.setVisible(false); + + remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) + .observeOn(AndroidSchedulers.mainThread()) + .doFinally(() -> playlistEntity = null) + .subscribe(ignored -> {/* Do nothing */}, this::onError); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index 807599678..0ccc13446 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -11,12 +11,12 @@ import org.schabi.newpipe.fragments.local.holder.LocalItemHolder; import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder; import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder; import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder; +import org.schabi.newpipe.fragments.local.holder.RemotePlaylistItemHolder; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.OnClickGesture; import java.text.DateFormat; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /* @@ -49,8 +49,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter localItems; private final DateFormat dateFormat; @@ -187,7 +188,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter> getPlaylists() { + return playlistRemoteTable.getAll().subscribeOn(Schedulers.io()); + } + + public Flowable> getPlaylist(final int serviceId, final String url) { + return playlistRemoteTable.getPlaylist(serviceId, url).subscribeOn(Schedulers.io()); + } + + public Single deletePlaylist(final long playlistId) { + return Single.fromCallable(() -> playlistRemoteTable.deletePlaylist(playlistId)) + .subscribeOn(Schedulers.io()); + } + + public Single onBookmark(final PlaylistInfo playlistInfo) { + return Single.fromCallable(() -> { + final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); + return playlistRemoteTable.upsert(playlist); + }).subscribeOn(Schedulers.io()); + } + + public Single onUpdate(final PlaylistInfo playlistInfo) { + return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo))) + .subscribeOn(Schedulers.io()); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 5c863590f..51bd312b0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -5,7 +5,7 @@ import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; +import android.support.v4.app.FragmentManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -14,23 +14,30 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.fragments.local.BaseLocalListFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistManager; +import org.schabi.newpipe.fragments.local.RemotePlaylistManager; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import icepick.State; +import io.reactivex.Flowable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; public final class BookmarkFragment - extends BaseLocalListFragment, Void> { + extends BaseLocalListFragment, Void> { private View watchHistoryButton; private View mostWatchedButton; @@ -41,6 +48,7 @@ public final class BookmarkFragment private Subscription databaseSubscription; private CompositeDisposable disposables = new CompositeDisposable(); private LocalPlaylistManager localPlaylistManager; + private RemotePlaylistManager remotePlaylistManager; /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle - Creation @@ -49,7 +57,9 @@ public final class BookmarkFragment @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - localPlaylistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + final AppDatabase database = NewPipeDatabase.getInstance(getContext()); + localPlaylistManager = new LocalPlaylistManager(database); + remotePlaylistManager = new RemotePlaylistManager(database); disposables = new CompositeDisposable(); } @@ -99,17 +109,28 @@ public final class BookmarkFragment @Override public void selected(LocalItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement - if (selectedItem instanceof PlaylistMetadataEntry && getParentFragment() != null) { + if (getParentFragment() == null) return; + final FragmentManager fragmentManager = getParentFragment().getFragmentManager(); + + if (selectedItem instanceof PlaylistMetadataEntry) { final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - NavigationHelper.openLocalPlaylistFragment( - getParentFragment().getFragmentManager(), entry.uid, entry.name); + NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid, + entry.name); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(), + entry.getUrl(), entry.getName()); } } @Override public void held(LocalItem selectedItem) { if (selectedItem instanceof PlaylistMetadataEntry) { - showDeleteDialog((PlaylistMetadataEntry) selectedItem); + showLocalDeleteDialog((PlaylistMetadataEntry) selectedItem); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem); } } }); @@ -134,9 +155,14 @@ public final class BookmarkFragment @Override public void startLoading(boolean forceLoad) { super.startLoading(forceLoad); - localPlaylistManager.getPlaylists() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscriptionSubscriber()); + + Flowable.combineLatest( + localPlaylistManager.getPlaylists(), + remotePlaylistManager.getPlaylists(), + BookmarkFragment::merge + ).onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistsSubscriber()); } /////////////////////////////////////////////////////////////////////////// @@ -165,6 +191,7 @@ public final class BookmarkFragment disposables = null; localPlaylistManager = null; + remotePlaylistManager = null; itemsListState = null; } @@ -172,8 +199,8 @@ public final class BookmarkFragment // Subscriptions Loader /////////////////////////////////////////////////////////////////////////// - private Subscriber> getSubscriptionSubscriber() { - return new Subscriber>() { + private Subscriber> getPlaylistsSubscriber() { + return new Subscriber>() { @Override public void onSubscribe(Subscription s) { showLoading(); @@ -183,7 +210,7 @@ public final class BookmarkFragment } @Override - public void onNext(List subscriptions) { + public void onNext(List subscriptions) { handleResult(subscriptions); if (databaseSubscription != null) databaseSubscription.request(1); } @@ -200,7 +227,7 @@ public final class BookmarkFragment } @Override - public void handleResult(@NonNull List result) { + public void handleResult(@NonNull List result) { super.handleResult(result); itemListAdapter.clearStreamItemList(); @@ -240,25 +267,41 @@ public final class BookmarkFragment // Utils /////////////////////////////////////////////////////////////////////////// - private void showDeleteDialog(final PlaylistMetadataEntry item) { + private void showLocalDeleteDialog(final PlaylistMetadataEntry item) { + showDeleteDialog(item.name, localPlaylistManager.deletePlaylist(item.uid)); + } + + private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) { + showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid())); + } + + private void showDeleteDialog(final String name, final Single deleteReactor) { + if (activity == null || disposables == null) return; + new AlertDialog.Builder(activity) - .setTitle(item.name) + .setTitle(name) .setMessage(R.string.delete_playlist_prompt) .setCancelable(true) .setPositiveButton(R.string.delete, (dialog, i) -> - disposables.add(deletePlaylist(item.uid)) + disposables.add(deleteReactor + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> {/*Do nothing on success*/}, this::onError)) ) .setNegativeButton(R.string.cancel, null) .show(); } - private Disposable deletePlaylist(final long playlistId) { - return localPlaylistManager.deletePlaylist(playlistId) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> {/*Do nothing on success*/}, - throwable -> Log.e(TAG, "Playlist deletion failed, id=[" - + playlistId + "]") - ); + private static List merge(final List localPlaylists, + final List remotePlaylists) { + List items = new ArrayList<>( + localPlaylists.size() + remotePlaylists.size()); + items.addAll(localPlaylists); + items.addAll(remotePlaylists); + + Collections.sort(items, (left, right) -> + left.getOrderingName().compareToIgnoreCase(right.getOrderingName())); + + return items; } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java index cbc1d07aa..1fbea6cc4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java @@ -14,24 +14,10 @@ import org.schabi.newpipe.fragments.local.LocalItemBuilder; import java.text.DateFormat; -public class LocalPlaylistItemHolder extends LocalItemHolder { - public final ImageView itemThumbnailView; - public final TextView itemStreamCountView; - public final TextView itemTitleView; - public final TextView itemUploaderView; - - public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, - int layoutId, ViewGroup parent) { - super(infoItemBuilder, layoutId, parent); - - itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); - itemTitleView = itemView.findViewById(R.id.itemTitleView); - itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); - itemUploaderView = itemView.findViewById(R.id.itemUploaderView); - } +public class LocalPlaylistItemHolder extends PlaylistItemHolder { public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { - this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + super(infoItemBuilder, parent); } @Override @@ -45,29 +31,6 @@ public class LocalPlaylistItemHolder extends LocalItemHolder { itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); - itemView.setOnClickListener(view -> { - if (itemBuilder.getOnItemSelectedListener() != null) { - itemBuilder.getOnItemSelectedListener().selected(item); - } - }); - - itemView.setLongClickable(true); - itemView.setOnLongClickListener(view -> { - if (itemBuilder.getOnItemSelectedListener() != null) { - itemBuilder.getOnItemSelectedListener().held(item); - } - return true; - }); + super.updateFromItem(localItem, dateFormat); } - - /** - * Display options for playlist thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) - .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) - .showImageOnFail(R.drawable.dummy_thumbnail_playlist) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java new file mode 100644 index 000000000..bab76ddcb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java @@ -0,0 +1,62 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; + +import java.text.DateFormat; + +public abstract class PlaylistItemHolder extends LocalItemHolder { + public final ImageView itemThumbnailView; + public final TextView itemStreamCountView; + public final TextView itemTitleView; + public final TextView itemUploaderView; + + public PlaylistItemHolder(LocalItemBuilder infoItemBuilder, + int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemTitleView = itemView.findViewById(R.id.itemTitleView); + itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + } + + public PlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + } + + @Override + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(localItem); + } + }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(localItem); + } + return true; + }); + } + + /** + * Display options for playlist thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java new file mode 100644 index 000000000..0f7b00e6d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java @@ -0,0 +1,33 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.view.ViewGroup; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.Localization; + +import java.text.DateFormat; + +public class RemotePlaylistItemHolder extends PlaylistItemHolder { + public RemotePlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + super(infoItemBuilder, parent); + } + + @Override + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistRemoteEntity)) return; + final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; + + itemTitleView.setText(item.getName()); + itemStreamCountView.setText(String.valueOf(item.getStreamCount())); + itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(), + NewPipe.getNameOfService(item.getServiceId()))); + + itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, + DISPLAY_THUMBNAIL_OPTIONS); + + super.updateFromItem(localItem, dateFormat); + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_check_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..92448842b50fc2c58ba7785c14447e69a0e51303 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8mZytjNCo5Di>!=?4Fp;rsy|)g z6QnX{i)ZH~$CAQ}43wdkdu9 zIA`fiS`h8VD0KLji>&0=iiJ5dbW;!KXn3c28|ox`r#TyTt^CS-z39@>vuhYP0&QjR MboFyt=akR{04w@A{{R30 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_check_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..bd23b9c481abe4fdafac75b22f43e01ffd4f0621 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8nx~6nNCo5Di|&j^40u=rodu_~ zrf@WE3pkT%#UgxxC8Dzb1+t*q%76?zVy4kHv*A`Z?Vk zE(aV`QC{E^evoDHA$6^D8d~Q#yo_f>o%OkLWX*CL+hyyy9$nb`;^6dkia;9~JYD@< J);T3K0RYYNJH-G1 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_check_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..416490774d7064fd48305d40e9124de6ea0d2512 GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1M^6{WkP60R1*x8<|2Gs*O0_MN zm+>feWxmkBc4>;TNkXX?vp|x@@wEpTm$59ITI UbyV1%1{%iT>FVdQ&MBb@0Gt;jp#T5? literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_check_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0e35fe739f9ea8e3fe8f1365a79bfbcfd328e8f0 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1XHOT$kP60R1+ku{{}v1bP0l+XkKFxDtK literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..24855e94f1f97621c1dc74ba1e24d80106190f4a GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DEKe85kP61PQ;rKBF%W4D`L(*{ z`onWQlZ^jZ-|%vEc@p+VdN~(!PX#|u%3S7?{}?U9_WSl-Eqa&fY0qDsoEQ9kWJ=y0{*^3b`R{7wqur4|KXspAa%(MHucSQn0MJ$j MPgg&ebxsLQ0Ig_0g8%>k literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a94c5d035a7b8fd290e821336729a300b8c14b75 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DEKe85kP61PQ&<@p964C*f0@~H zHhkVEoKVoG?#Q92%)u*DwdCe=xJC{Ou%kl_%AYpq3t zfm2{SzX2Od?Q;dz+H?-b`{GU)<_j>X%U%7M&v^dd?5pSfIc9!Z!(taW>yZ}FRt8U3 KKbLh*2~7YA6fmm* literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ac03e19abfc714e97ba924b57fe4770076412b88 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawHha1_hEy=Vy?T6Ri-Ca4Lp5Wj z!_N0^F=VAL`e>oE+DYM$mGmFOk99B>FDHexj6m?B_BcpoL z(rZ(}E60LW_3xDnmsd9hGhVW`e`WDip;j>6C-dCRl2-zK6Y7eV*)53N7E`qHt?KfB zoSG}A1y7&!ZU)aBmdxK@zQ`<5zveF^eDZ+cq}j@E>-_pkxQ{!0DqM2*+(a{L_gimd jGJB7oP*hSf`oezVmbc!XhskC@=QDV^`njxgN@xNA0d!#h literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..290088718fb6f86b6efbd14b59a6f5b204140477 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawHha1_hEy=Vy?Q;d)j@>$qI;^_H=VWYVN=Z2sx$0t6xc;;bA33QCXknORoMyCu(hNhPz&wYYE2lxG&2=TA=V zDQ04M{^skoB`>4TMLe3r@bk>_&vP~_-0z6=ooO8ZtJLDS2W#J~Xosw)x^`3Ate$TY zExP1+?&e8uW9`O9YNzp7H#O$LiuHmFFy%1RjfcIQC8K kne4xJ8-u!ppx{Mmr`EGqYws>B13I6<)78&qol`;+0EvQPkpKVy literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..068c596a3476e67afd160d396e36b493743c2b47 GIT binary patch literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg9(lSrhEy=Vz3R%;93XJ)!NYqN zb9U=(b_qUveA}e`hq|>rC#e9@%M)>Y1r5v*KtjcH-VqSH;WT6Jmismf_S)_4)t~b7 z4~xzG`3=uM3!kW|W17IvXK=sffx_*Qu4QSib`}c$feQ2GC)l^?b&3A``KID;!Z;1E}_b4uhM*J16#%u^)DMuQY7Y{_g}Gm|8oC%49`9b zA6sz&WO#q(%bGan=bvZspKw>_nLm5Jl-2y}7k=)Ub?*7q61(-wK3AH~J-<3BX1YGe a@#-e|w{E5=|GEwIErX}4pUXO@geCxg0eV3I literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..767d066de4a1a5067ead23b6ef0276f34d58a24a GIT binary patch literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgUU<4VhEy=Vy~>_-I6=TA@%25u ztnc$SY`MTzw@K&Yf!}@x=cjGq2Py=D1sM{IECL4Y3``6R4}{JuG%zr6Tv%so;jsJE zzY_`1rrTE6-Q4^fOgTS4&u%dPXV$Bji}AmH{QS)Luj}0T_v|-*KD}^lM*W(r=0AUC zbJp$KR?l%EuWie-^Xxx7fvGiIRE5Ac^oF^bLDVb%unh^MQc%Q~loCIB>Wg(m<2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 3861a380d..2d39d3d70 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -314,7 +314,7 @@ android:clickable="true" android:focusable="true" android:contentDescription="@string/append_playlist" - android:drawableTop="?attr/playlist_add" + android:drawableTop="?attr/ic_playlist_add" android:gravity="center" android:paddingBottom="6dp" android:paddingTop="6dp" diff --git a/app/src/main/res/menu/menu_channel.xml b/app/src/main/res/menu/menu_channel.xml index 79a0fd5c9..cc6a9ed71 100644 --- a/app/src/main/res/menu/menu_channel.xml +++ b/app/src/main/res/menu/menu_channel.xml @@ -22,12 +22,4 @@ android:icon="?attr/share" android:title="@string/share" app:showAsAction="ifRoom"/> - - diff --git a/app/src/main/res/menu/menu_play_queue.xml b/app/src/main/res/menu/menu_play_queue.xml index 31e2ebe72..6261b8c18 100644 --- a/app/src/main/res/menu/menu_play_queue.xml +++ b/app/src/main/res/menu/menu_play_queue.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/res/menu/menu_playlist.xml b/app/src/main/res/menu/menu_playlist.xml index a12fb2f49..e0e7ebe18 100644 --- a/app/src/main/res/menu/menu_playlist.xml +++ b/app/src/main/res/menu/menu_playlist.xml @@ -15,10 +15,18 @@ app:showAsAction="ifRoom"/> + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index e770cf102..794365a3d 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -27,7 +27,8 @@ - + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05db15da1..032e56b22 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -387,6 +387,9 @@ Add To Playlist Set as Playlist Thumbnail + Bookmark Playlist + Remove Bookmark + Do you want to delete this playlist? Playlist successfully created Added to playlist diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index bcbc759d2..b16958ae6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -42,7 +42,8 @@ @drawable/ic_whatshot_black_24dp @drawable/ic_channel_black_24dp @drawable/ic_bookmark_black_24dp - @drawable/ic_playlist_add_black_24dp + @drawable/ic_playlist_add_black_24dp + @drawable/ic_playlist_add_check_black_24dp @color/light_separator_color @color/light_contrast_background_color @@ -91,7 +92,8 @@ @drawable/ic_whatshot_white_24dp @drawable/ic_channel_white_24dp @drawable/ic_bookmark_white_24dp - @drawable/ic_playlist_add_white_24dp + @drawable/ic_playlist_add_white_24dp + @drawable/ic_playlist_add_check_white_24dp @color/dark_separator_color @color/dark_contrast_background_color From 7ab41e0c3aefd33317ecf50d128bcc8b6e4d12bf Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 7 Feb 2018 14:20:16 -0800 Subject: [PATCH 28/52] -Added listener unregistration to local item adapters to release dependency and avoid memory leak. -Added listener unregistration on all listeners using contexts in local item related fragments. --- .../newpipe/fragments/local/LocalItemListAdapter.java | 4 ++++ .../newpipe/fragments/local/LocalPlaylistFragment.java | 5 +++++ .../newpipe/fragments/local/PlaylistAppendDialog.java | 1 + .../newpipe/fragments/local/bookmark/BookmarkFragment.java | 3 +++ .../local/bookmark/StatisticsPlaylistFragment.java | 6 ++++++ .../local/holder/LocalPlaylistStreamItemHolder.java | 2 +- 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index 0ccc13446..d36f56733 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -71,6 +71,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { if (data != null) { if (DEBUG) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index ea7242055..abc12fb14 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -230,6 +230,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { view.performClick(); - if (itemBuilder != null && + if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { itemBuilder.getOnItemSelectedListener().drag(item, LocalPlaylistStreamItemHolder.this); From 6020dc2b2d999a950995df9239205f73dd955dd4 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 7 Feb 2018 14:37:05 -0800 Subject: [PATCH 29/52] -Renamed "watch history" fragment under bookmark to "last played". -Renamed "watched history" fragment under history to "watch history". --- .../local/bookmark/BookmarkFragment.java | 18 +++++++-------- ...yFragment.java => LastPlayedFragment.java} | 4 ++-- .../newpipe/history/HistoryActivity.java | 5 +---- ...ragment.java => WatchHistoryFragment.java} | 6 ++--- .../schabi/newpipe/util/NavigationHelper.java | 6 ++--- app/src/main/res/layout/bookmark_header.xml | 22 +++++++++---------- app/src/main/res/values/strings.xml | 2 +- 7 files changed, 30 insertions(+), 33 deletions(-) rename app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/{WatchHistoryFragment.java => LastPlayedFragment.java} (79%) rename app/src/main/java/org/schabi/newpipe/history/{WatchedHistoryFragment.java => WatchHistoryFragment.java} (97%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 93b0dea29..2aa648fa9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -39,8 +39,8 @@ import io.reactivex.disposables.CompositeDisposable; public final class BookmarkFragment extends BaseLocalListFragment, Void> { - private View watchHistoryButton; - private View mostWatchedButton; + private View lastPlayedButton; + private View mostPlayedButton; @State protected Parcelable itemsListState; @@ -96,8 +96,8 @@ public final class BookmarkFragment protected View getListHeader() { final View headerRootLayout = activity.getLayoutInflater() .inflate(R.layout.bookmark_header, itemsList, false); - watchHistoryButton = headerRootLayout.findViewById(R.id.watchHistory); - mostWatchedButton = headerRootLayout.findViewById(R.id.mostWatched); + lastPlayedButton = headerRootLayout.findViewById(R.id.lastPlayed); + mostPlayedButton = headerRootLayout.findViewById(R.id.mostPlayed); return headerRootLayout; } @@ -135,13 +135,13 @@ public final class BookmarkFragment } }); - watchHistoryButton.setOnClickListener(view -> { + lastPlayedButton.setOnClickListener(view -> { if (getParentFragment() != null) { - NavigationHelper.openWatchHistoryFragment(getParentFragment().getFragmentManager()); + NavigationHelper.openLastPlayedFragment(getParentFragment().getFragmentManager()); } }); - mostWatchedButton.setOnClickListener(view -> { + mostPlayedButton.setOnClickListener(view -> { if (getParentFragment() != null) { NavigationHelper.openMostPlayedFragment(getParentFragment().getFragmentManager()); } @@ -178,8 +178,8 @@ public final class BookmarkFragment @Override public void onDestroyView() { super.onDestroyView(); - if (mostWatchedButton != null) mostWatchedButton.setOnClickListener(null); - if (watchHistoryButton != null) watchHistoryButton.setOnClickListener(null); + if (mostPlayedButton != null) mostPlayedButton.setOnClickListener(null); + if (lastPlayedButton != null) lastPlayedButton.setOnClickListener(null); if (disposables != null) disposables.clear(); if (databaseSubscription != null) databaseSubscription.cancel(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LastPlayedFragment.java similarity index 79% rename from app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LastPlayedFragment.java index 84126ad4b..a5b62c63e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LastPlayedFragment.java @@ -6,10 +6,10 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import java.util.Collections; import java.util.List; -public final class WatchHistoryFragment extends StatisticsPlaylistFragment { +public final class LastPlayedFragment extends StatisticsPlaylistFragment { @Override protected String getName() { - return getString(R.string.title_watch_history); + return getString(R.string.title_last_played); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java index 30589a22c..267d07065 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java @@ -9,10 +9,8 @@ import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; -import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -23,7 +21,6 @@ import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.util.ThemeHelper; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.functions.Consumer; public class HistoryActivity extends AppCompatActivity { @@ -116,7 +113,7 @@ public class HistoryActivity extends AppCompatActivity { fragment = SearchHistoryFragment.newInstance(); break; case 1: - fragment = WatchedHistoryFragment.newInstance(); + fragment = WatchHistoryFragment.newInstance(); break; default: throw new IllegalArgumentException("position: " + position); diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java similarity index 97% rename from app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java rename to app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java index 0fabc594a..4830ed33b 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java @@ -33,11 +33,11 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; -public class WatchedHistoryFragment extends HistoryFragment { +public class WatchHistoryFragment extends HistoryFragment { @NonNull - public static WatchedHistoryFragment newInstance() { - return new WatchedHistoryFragment(); + public static WatchHistoryFragment newInstance() { + return new WatchHistoryFragment(); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 3acfb6683..42cf135e3 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -36,7 +36,7 @@ import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistFragment; import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; -import org.schabi.newpipe.fragments.local.bookmark.WatchHistoryFragment; +import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; @@ -335,10 +335,10 @@ public class NavigationHelper { .commit(); } - public static void openWatchHistoryFragment(FragmentManager fragmentManager) { + public static void openLastPlayedFragment(FragmentManager fragmentManager) { fragmentManager.beginTransaction() .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) - .replace(R.id.fragment_holder, new WatchHistoryFragment()) + .replace(R.id.fragment_holder, new LastPlayedFragment()) .addToBackStack(null) .commit(); } diff --git a/app/src/main/res/layout/bookmark_header.xml b/app/src/main/res/layout/bookmark_header.xml index b087a5157..8ca5c1228 100644 --- a/app/src/main/res/layout/bookmark_header.xml +++ b/app/src/main/res/layout/bookmark_header.xml @@ -8,14 +8,14 @@ android:background="?attr/selectableItemBackground"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 032e56b22..21579d3e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -313,7 +313,7 @@ Do you want to delete this item from search history? Do you want to delete this item from watch history? Are you sure you want to delete all items from history? - Watch History + Last Played Most Played From c3941d5becccb5c2e960f9ed0c861fe36612ba93 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 7 Feb 2018 17:33:29 -0800 Subject: [PATCH 30/52] -Added remote playlist table creation to migrations. --- app/src/main/java/org/schabi/newpipe/database/Migrations.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index c6b472f7f..239fc02bb 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -31,6 +31,9 @@ public class Migrations { database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)"); database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)"); + database.execSQL("CREATE INDEX `index_remote_playlists_name` ON `remote_playlists` (`name`)"); + database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `remote_playlists` (`service_id`, `url`)"); // Populate streams table with existing entries in watch history // Latest data first, thus ignoring older entries with the same indices From 0630423c8e41042521702a2b087391a1a1265bdc Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 8 Feb 2018 10:13:29 -0800 Subject: [PATCH 31/52] -Fixed bookmark fragment in main pager not showing hamburger menu. --- .../local/BaseLocalListFragment.java | 29 +++++++++++++------ .../local/bookmark/BookmarkFragment.java | 9 ++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java index c0c4362eb..53786d2ac 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java @@ -86,19 +86,30 @@ public abstract class BaseLocalListFragment extends BaseStateFragment // Lifecycle - Menu //////////////////////////////////////////////////////////////////////////*/ + /** Determines if the fragment is part of the main fragment view pager. + * If so, then this method must be overriden to return true + * in order to show the hamburger menu. */ + protected boolean isPartOfFrontPager() { + return false; + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, 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); - if(useAsFrontPage) { - supportActionBar.setDisplayHomeAsUpEnabled(false); - } else { - supportActionBar.setDisplayHomeAsUpEnabled(true); - } + + final ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar == null) return; + + supportActionBar.setDisplayShowTitleEnabled(true); + + // Show up arrow icon if the fragment is not used as front page or part of the front pager + if (!useAsFrontPage && !isPartOfFrontPager()) { + // If set true, an up arrow icon will be displayed. + // If set false, no icon will be shown. + // If unset, show hamburger menu + supportActionBar.setDisplayHomeAsUpEnabled(true); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 2aa648fa9..a2e00429b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -148,6 +148,15 @@ public final class BookmarkFragment }); } + /*////////////////////////////////////////////////////////////////////////// + // Fragment Lifecycle - Menu + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected boolean isPartOfFrontPager() { + return true; + } + /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle - Loading /////////////////////////////////////////////////////////////////////////// From 490b250db687f2847e4b4371149c9786b03f627c Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 8 Feb 2018 11:53:08 -0800 Subject: [PATCH 32/52] -Removed Leak Canary dependency. -Fixed local playlist header margins. --- app/build.gradle | 2 -- app/src/debug/java/org/schabi/newpipe/DebugApp.java | 9 --------- app/src/main/res/layout/local_playlist_header.xml | 9 +++++---- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 748bbb9c6..86d6542e0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,6 +89,4 @@ dependencies { implementation 'frankiesardo:icepick:3.2.0' annotationProcessor 'frankiesardo:icepick-processor:3.2.0' - - debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' } diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index fbf414738..4d37094ba 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -4,7 +4,6 @@ import android.content.Context; import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; -import com.squareup.leakcanary.LeakCanary; public class DebugApp extends App { private static final String TAG = DebugApp.class.toString(); @@ -18,14 +17,6 @@ public class DebugApp extends App { @Override public void onCreate() { super.onCreate(); - - if (LeakCanary.isInAnalyzerProcess(this)) { - // This process is dedicated to LeakCanary for heap analysis. - // You should not init your app in this process. - return; - } - LeakCanary.install(this); - initStetho(); } diff --git a/app/src/main/res/layout/local_playlist_header.xml b/app/src/main/res/layout/local_playlist_header.xml index ab5dd4440..420da04ee 100644 --- a/app/src/main/res/layout/local_playlist_header.xml +++ b/app/src/main/res/layout/local_playlist_header.xml @@ -5,13 +5,15 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="6dp" + android:paddingTop="6dp" android:background="?attr/contrast_background_color"> Date: Thu, 8 Feb 2018 15:58:48 -0800 Subject: [PATCH 33/52] -Fixed playlist bookmark button not showing out when activity / playlist fragment is created by external share. --- .../fragments/list/playlist/PlaylistFragment.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 39c88f8d3..9b57e7181 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -111,6 +111,7 @@ public class PlaylistFragment extends BaseListInfoFragment { headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); + return headerRootLayout; } @@ -175,6 +176,8 @@ public class PlaylistFragment extends BaseListInfoFragment { playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark); playlistUnbookmarkButton = menu.findItem(R.id.menu_item_unbookmark); + + updateBookmarkButtonsVisibility(); } @Override @@ -338,11 +341,8 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override public void onNext(List playlist) { - if (playlistBookmarkButton == null || playlistUnbookmarkButton == null) return; - - playlistBookmarkButton.setVisible(playlist.isEmpty()); - playlistUnbookmarkButton.setVisible(!playlist.isEmpty()); playlistEntity = playlist.isEmpty() ? null : playlist.get(0); + updateBookmarkButtonsVisibility(); if (bookmarkReactor != null) bookmarkReactor.request(1); } @@ -387,4 +387,11 @@ public class PlaylistFragment extends BaseListInfoFragment { .doFinally(() -> playlistEntity = null) .subscribe(ignored -> {/* Do nothing */}, this::onError); } + + private void updateBookmarkButtonsVisibility() { + if (playlistBookmarkButton == null || playlistUnbookmarkButton == null) return; + + playlistBookmarkButton.setVisible(playlistEntity == null); + playlistUnbookmarkButton.setVisible(playlistEntity != null); + } } From 43ab0283d934babeb9f097a9c0e8a5fbfe5cfb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 9 Feb 2018 02:53:59 +0100 Subject: [PATCH 34/52] ZIP, Warning: --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d05d088d..b23e1db93 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -323,8 +323,8 @@ Select a kiosk Export complete Import complete - No valid Zip file - WARNING: Could not import all files. + No valid ZIP file + Warning: Could not import all files. This will override your current setup. From d0808ce1590b6d43f15ef1984a1a55807d3c73c5 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 8 Feb 2018 18:48:36 -0800 Subject: [PATCH 35/52] -Fixed playlist creation icon in playlist append dialog. -Fixed bookmarking disposable not part of playlist fragment lifecycle. -Rearranged local fragment directory structure. --- .../newpipe/fragments/detail/VideoDetailFragment.java | 2 +- .../schabi/newpipe/fragments/list/BaseListFragment.java | 3 +-- .../newpipe/fragments/list/playlist/PlaylistFragment.java | 7 +++++-- .../local/{ => bookmark}/BaseLocalListFragment.java | 3 ++- .../newpipe/fragments/local/bookmark/BookmarkFragment.java | 1 - .../local/{ => bookmark}/LocalPlaylistFragment.java | 3 ++- .../local/bookmark/StatisticsPlaylistFragment.java | 1 - .../fragments/local/{ => dialog}/PlaylistAppendDialog.java | 4 +++- .../local/{ => dialog}/PlaylistCreationDialog.java | 3 ++- .../fragments/local/{ => dialog}/PlaylistDialog.java | 2 +- .../org/schabi/newpipe/player/ServicePlayerActivity.java | 2 +- .../java/org/schabi/newpipe/util/NavigationHelper.java | 2 +- app/src/main/res/layout/dialog_playlists.xml | 2 +- 13 files changed, 20 insertions(+), 15 deletions(-) rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => bookmark}/BaseLocalListFragment.java (98%) rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => bookmark}/LocalPlaylistFragment.java (99%) rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => dialog}/PlaylistAppendDialog.java (97%) rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => dialog}/PlaylistCreationDialog.java (95%) rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => dialog}/PlaylistDialog.java (97%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 923feeba0..89f35c306 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -58,7 +58,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; +import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.player.MainVideoPlayer; 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 1a0a836c5..8c9945149 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 @@ -20,7 +20,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; +import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.playlist.SinglePlayQueue; @@ -28,7 +28,6 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Queue; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 9b57e7181..15c9d4b38 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -42,6 +42,7 @@ import java.util.List; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -371,9 +372,10 @@ public class PlaylistFragment extends BaseListInfoFragment { playlistBookmarkButton.setVisible(false); playlistUnbookmarkButton.setVisible(false); - remotePlaylistManager.onBookmark(currentInfo) + final Disposable disposable = remotePlaylistManager.onBookmark(currentInfo) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> {/* Do nothing */}, this::onError); + disposables.add(disposable); } private void unbookmarkPlaylist() { @@ -382,10 +384,11 @@ public class PlaylistFragment extends BaseListInfoFragment { playlistBookmarkButton.setVisible(false); playlistUnbookmarkButton.setVisible(false); - remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) + final Disposable disposable = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) .observeOn(AndroidSchedulers.mainThread()) .doFinally(() -> playlistEntity = null) .subscribe(ignored -> {/* Do nothing */}, this::onError); + disposables.add(disposable); } private void updateBookmarkButtonsVisibility() { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java index 53786d2ac..261f28d7c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.bookmark; import android.os.Bundle; import android.support.v4.app.Fragment; @@ -13,6 +13,7 @@ import android.view.View; import org.schabi.newpipe.R; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.ListViewContract; +import org.schabi.newpipe.fragments.local.LocalItemListAdapter; import static org.schabi.newpipe.util.AnimationUtils.animateView; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index a2e00429b..4166f462b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -19,7 +19,6 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.fragments.local.BaseLocalListFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistManager; import org.schabi.newpipe.fragments.local.RemotePlaylistManager; import org.schabi.newpipe.report.UserAction; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java similarity index 99% rename from app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java index abc12fb14..a3a78e46e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.bookmark; import android.app.Activity; import android.content.Context; @@ -26,6 +26,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.local.LocalPlaylistManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 543fc03eb..d9bbc68c8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -17,7 +17,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.local.BaseLocalListFragment; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.playlist.PlayQueue; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistAppendDialog.java similarity index 97% rename from app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistAppendDialog.java index 034411e4a..40637e149 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistAppendDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.dialog; import android.annotation.SuppressLint; import android.os.Bundle; @@ -18,6 +18,8 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.local.LocalItemListAdapter; +import org.schabi.newpipe.fragments.local.LocalPlaylistManager; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.OnClickGesture; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistCreationDialog.java similarity index 95% rename from app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistCreationDialog.java index 670ae9819..f721e7701 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistCreationDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.dialog; import android.app.AlertDialog; import android.app.Dialog; @@ -12,6 +12,7 @@ import android.widget.Toast; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.fragments.local.LocalPlaylistManager; import java.util.List; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistDialog.java similarity index 97% rename from app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistDialog.java index 010ba0181..a632988c4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.dialog; import android.os.Bundle; import android.support.annotation.NonNull; diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 6e0f5c1d7..5518357a8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.Player; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; +import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 42cf135e3..4c7b32954 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -34,7 +34,7 @@ import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; -import org.schabi.newpipe.fragments.local.LocalPlaylistFragment; +import org.schabi.newpipe.fragments.local.bookmark.LocalPlaylistFragment; import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; import org.schabi.newpipe.history.HistoryActivity; diff --git a/app/src/main/res/layout/dialog_playlists.xml b/app/src/main/res/layout/dialog_playlists.xml index 8c639fff6..c08aa315e 100644 --- a/app/src/main/res/layout/dialog_playlists.xml +++ b/app/src/main/res/layout/dialog_playlists.xml @@ -19,7 +19,7 @@ android:layout_centerVertical="true" android:layout_marginLeft="12dp" android:layout_marginRight="12dp" - android:src="?attr/palette" + android:src="?attr/ic_playlist_add" tools:ignore="ContentDescription,RtlHardcoded"/> Date: Thu, 8 Feb 2018 19:53:04 -0800 Subject: [PATCH 36/52] -Merged bookmark buttons on playlist fragment into one. -Fixed bookmark button flickering on visibility toggling. -Removed toolbar up button control from local fragments, delegating functionality back to main fragment. -Updated extractor to latest. --- app/build.gradle | 2 +- .../list/playlist/PlaylistFragment.java | 72 ++++++++++--------- .../local/bookmark/BaseLocalListFragment.java | 15 ---- .../local/bookmark/BookmarkFragment.java | 9 --- app/src/main/res/menu/menu_playlist.xml | 12 +--- 5 files changed, 43 insertions(+), 67 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 86d6542e0..273616f91 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:7fd21ec08581d' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:4fb49d54b5' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 15c9d4b38..2c0b94c69 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -36,13 +36,16 @@ import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ThemeHelper; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.Disposables; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -50,6 +53,7 @@ public class PlaylistFragment extends BaseListInfoFragment { private CompositeDisposable disposables; private Subscription bookmarkReactor; + private AtomicBoolean isBookmarkButtonReady; private RemotePlaylistManager remotePlaylistManager; private PlaylistRemoteEntity playlistEntity; @@ -70,7 +74,6 @@ public class PlaylistFragment extends BaseListInfoFragment { private View headerBackgroundButton; private MenuItem playlistBookmarkButton; - private MenuItem playlistUnbookmarkButton; public static PlaylistFragment getInstance(int serviceId, String url, String name) { PlaylistFragment instance = new PlaylistFragment(); @@ -86,6 +89,7 @@ public class PlaylistFragment extends BaseListInfoFragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); disposables = new CompositeDisposable(); + isBookmarkButtonReady = new AtomicBoolean(false); remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext())); } @@ -176,14 +180,14 @@ public class PlaylistFragment extends BaseListInfoFragment { inflater.inflate(R.menu.menu_playlist, menu); playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark); - playlistUnbookmarkButton = menu.findItem(R.id.menu_item_unbookmark); - - updateBookmarkButtonsVisibility(); + updateBookmarkButtons(); } @Override public void onDestroyView() { super.onDestroyView(); + if (isBookmarkButtonReady != null) isBookmarkButtonReady.set(false); + if (disposables != null) disposables.clear(); if (bookmarkReactor != null) bookmarkReactor.cancel(); @@ -199,6 +203,7 @@ public class PlaylistFragment extends BaseListInfoFragment { disposables = null; remotePlaylistManager = null; playlistEntity = null; + isBookmarkButtonReady = null; } /*////////////////////////////////////////////////////////////////////////// @@ -225,10 +230,7 @@ public class PlaylistFragment extends BaseListInfoFragment { shareUrl(name, url); break; case R.id.menu_item_bookmark: - bookmarkPlaylist(); - break; - case R.id.menu_item_unbookmark: - unbookmarkPlaylist(); + onBookmarkClicked(); break; default: return super.onOptionsItemSelected(item); @@ -343,7 +345,9 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override public void onNext(List playlist) { playlistEntity = playlist.isEmpty() ? null : playlist.get(0); - updateBookmarkButtonsVisibility(); + + updateBookmarkButtons(); + isBookmarkButtonReady.set(true); if (bookmarkReactor != null) bookmarkReactor.request(1); } @@ -366,35 +370,39 @@ public class PlaylistFragment extends BaseListInfoFragment { headerTitleView.setText(title); } - private void bookmarkPlaylist() { - if (remotePlaylistManager == null || currentInfo == null) return; + private void onBookmarkClicked() { + if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get() || + remotePlaylistManager == null) + return; - playlistBookmarkButton.setVisible(false); - playlistUnbookmarkButton.setVisible(false); + final Disposable action; - final Disposable disposable = remotePlaylistManager.onBookmark(currentInfo) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> {/* Do nothing */}, this::onError); - disposables.add(disposable); + if (currentInfo != null && playlistEntity == null) { + action = remotePlaylistManager.onBookmark(currentInfo) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> {/* Do nothing */}, this::onError); + } else if (playlistEntity != null) { + action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) + .observeOn(AndroidSchedulers.mainThread()) + .doFinally(() -> playlistEntity = null) + .subscribe(ignored -> {/* Do nothing */}, this::onError); + } else { + action = Disposables.empty(); + } + + disposables.add(action); } - private void unbookmarkPlaylist() { - if (remotePlaylistManager == null || playlistEntity == null) return; + private void updateBookmarkButtons() { + if (playlistBookmarkButton == null || activity == null) return; - playlistBookmarkButton.setVisible(false); - playlistUnbookmarkButton.setVisible(false); + final int iconAttr = playlistEntity == null ? + R.attr.ic_playlist_add : R.attr.ic_playlist_check; - final Disposable disposable = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) - .observeOn(AndroidSchedulers.mainThread()) - .doFinally(() -> playlistEntity = null) - .subscribe(ignored -> {/* Do nothing */}, this::onError); - disposables.add(disposable); - } + final int titleRes = playlistEntity == null ? + R.string.bookmark_playlist : R.string.unbookmark_playlist; - private void updateBookmarkButtonsVisibility() { - if (playlistBookmarkButton == null || playlistUnbookmarkButton == null) return; - - playlistBookmarkButton.setVisible(playlistEntity == null); - playlistUnbookmarkButton.setVisible(playlistEntity != null); + playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); + playlistBookmarkButton.setTitle(titleRes); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java index 261f28d7c..d2c4e1b14 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java @@ -87,13 +87,6 @@ public abstract class BaseLocalListFragment extends BaseStateFragment // Lifecycle - Menu //////////////////////////////////////////////////////////////////////////*/ - /** Determines if the fragment is part of the main fragment view pager. - * If so, then this method must be overriden to return true - * in order to show the hamburger menu. */ - protected boolean isPartOfFrontPager() { - return false; - } - @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); @@ -104,14 +97,6 @@ public abstract class BaseLocalListFragment extends BaseStateFragment if (supportActionBar == null) return; supportActionBar.setDisplayShowTitleEnabled(true); - - // Show up arrow icon if the fragment is not used as front page or part of the front pager - if (!useAsFrontPage && !isPartOfFrontPager()) { - // If set true, an up arrow icon will be displayed. - // If set false, no icon will be shown. - // If unset, show hamburger menu - supportActionBar.setDisplayHomeAsUpEnabled(true); - } } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 4166f462b..21aceade8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -147,15 +147,6 @@ public final class BookmarkFragment }); } - /*////////////////////////////////////////////////////////////////////////// - // Fragment Lifecycle - Menu - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected boolean isPartOfFrontPager() { - return true; - } - /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle - Loading /////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/res/menu/menu_playlist.xml b/app/src/main/res/menu/menu_playlist.xml index e0e7ebe18..4583ff719 100644 --- a/app/src/main/res/menu/menu_playlist.xml +++ b/app/src/main/res/menu/menu_playlist.xml @@ -18,15 +18,7 @@ android:id="@+id/menu_item_bookmark" android:icon="?attr/ic_playlist_add" android:title="@string/bookmark_playlist" - android:visible="false" - app:showAsAction="always" - tools:visible="true"/> - - \ No newline at end of file From cb41afb11fd5fb5d7de9b5c5f581da150b4a672a Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 10 Feb 2018 17:20:56 -0800 Subject: [PATCH 37/52] -Fixed Soundcloud playlist bookmark button not working when entered from search page. -Fixed NPE when playlist fragment is destroyed while renaming. -Fixed remote playlist thumbnail to use uploader avatar when thumbnail url is unavailable. -Added dispose on exit to all database requests in local playlist fragment. --- .../playlist/model/PlaylistRemoteEntity.java | 3 +- .../list/playlist/PlaylistFragment.java | 10 ++--- .../local/RemotePlaylistManager.java | 5 ++- .../local/bookmark/LocalPlaylistFragment.java | 41 ++++++++++++++----- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java index 5e3db62a9..486350fc9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java @@ -66,7 +66,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { @Ignore public PlaylistRemoteEntity(final PlaylistInfo info) { - this(info.getServiceId(), info.getName(), info.getUrl(), info.getThumbnailUrl(), + this(info.getServiceId(), info.getName(), info.getUrl(), + info.getThumbnailUrl() == null ? info.getUploaderAvatarUrl() : info.getThumbnailUrl(), info.getUploaderName(), info.getStreamCount()); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 2c0b94c69..db382ef5d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -125,11 +125,6 @@ public class PlaylistFragment extends BaseListInfoFragment { super.initViews(rootView, savedInstanceState); infoListAdapter.useMiniItemVariants(true); - - remotePlaylistManager.getPlaylist(serviceId, url) - .onBackpressureLatest() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getPlaylistBookmarkSubscriber()); } @Override @@ -280,6 +275,11 @@ public class PlaylistFragment extends BaseListInfoFragment { showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); } + remotePlaylistManager.getPlaylist(result) + .onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistBookmarkSubscriber()); + remotePlaylistManager.onUpdate(result) .subscribeOn(AndroidSchedulers.mainThread()) .subscribe(integer -> {/* Do nothing*/}, this::onError); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/RemotePlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/RemotePlaylistManager.java index 3012f3d73..1e9be5638 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/RemotePlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/RemotePlaylistManager.java @@ -25,8 +25,9 @@ public class RemotePlaylistManager { return playlistRemoteTable.getAll().subscribeOn(Schedulers.io()); } - public Flowable> getPlaylist(final int serviceId, final String url) { - return playlistRemoteTable.getPlaylist(serviceId, url).subscribeOn(Schedulers.io()); + public Flowable> getPlaylist(final PlaylistInfo info) { + return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl()) + .subscribeOn(Schedulers.io()); } public Single deletePlaylist(final long playlistId) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java index a3a78e46e..20eee38fc 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java @@ -43,7 +43,9 @@ import java.util.concurrent.atomic.AtomicBoolean; import icepick.State; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.Disposables; import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -76,7 +78,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment debouncedSaveSignal; - private Disposable debouncedSaver; + private CompositeDisposable disposables; /* Has the playlist been fully loaded from db */ private AtomicBoolean isLoadingComplete; @@ -99,6 +101,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment - changePlaylistName(nameEdit.getText().toString()) - ); + .setPositiveButton(R.string.rename, (dialogInterface, i) -> { + changePlaylistName(nameEdit.getText().toString()); + }); dialogBuilder.show(); } private void changePlaylistName(final String name) { + if (playlistManager == null) return; + this.name = name; setTitle(name); Log.d(TAG, "Updating playlist id=[" + playlistId + "] with new name=[" + name + "] items"); - playlistManager.renamePlaylist(playlistId, name) + final Disposable disposable = playlistManager.renamePlaylist(playlistId, name) .observeOn(AndroidSchedulers.mainThread()) .subscribe(longs -> {/*Do nothing on success*/}, this::onError); + disposables.add(disposable); } private void changeThumbnailUrl(final String thumbnailUrl) { + if (playlistManager == null) return; + final Toast successToast = Toast.makeText(getActivity(), R.string.playlist_thumbnail_change_success, Toast.LENGTH_SHORT); @@ -385,9 +395,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment successToast.show(), this::onError); + disposables.add(disposable); } private void deleteItem(final PlaylistStreamEntry item) { @@ -399,11 +411,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { if (isModified != null) isModified.set(false); }, this::onError ); + disposables.add(disposable); } From 5773152ed35a87ca10df7bc39215a966d0fc2e02 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 3 Feb 2018 14:39:03 -0800 Subject: [PATCH 38/52] -Added subtitles loading and display. -Added subtitles switching button to popup and main players. -Added aspect ratio switching button to popup pand main players. --- .../newpipe/player/PopupVideoPlayer.java | 3 +- .../schabi/newpipe/player/VideoPlayer.java | 164 ++++++++++++++++-- .../newpipe/player/helper/PlayerHelper.java | 45 +++++ .../main/res/layout/activity_main_player.xml | 40 ++++- app/src/main/res/layout/player_popup.xml | 32 ++++ 5 files changed, 272 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index c3803f0d5..e8ccba7d0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -49,6 +49,7 @@ import android.widget.RemoteViews; import android.widget.SeekBar; import android.widget.TextView; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -642,7 +643,7 @@ public final class PopupVideoPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ /*package-private*/ void enableVideoRenderer(final boolean enable) { - final int videoRendererIndex = getVideoRendererIndex(); + final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); if (trackSelector != null && videoRendererIndex != -1) { trackSelector.setRendererDisabled(videoRendererIndex, !enable); } 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 5399ff047..48d10a4ad 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -29,12 +29,14 @@ import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; +import android.net.Uri; import android.os.Build; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.Log; +import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.SurfaceView; @@ -46,17 +48,24 @@ import android.widget.SeekBar; import android.widget.TextView; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; +import com.google.android.exoplayer2.source.SingleSampleMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.Subtitles; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; @@ -64,6 +73,9 @@ import org.schabi.newpipe.util.ListHelper; import java.util.ArrayList; import java.util.List; +import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; +import static com.google.android.exoplayer2.C.TIME_UNSET; +import static com.google.android.exoplayer2.C.TRACK_TYPE_TEXT; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -95,6 +107,8 @@ public abstract class VideoPlayer extends BasePlayer protected String playbackQuality; + private List availableCaptions; + protected boolean wasPlaying = false; /*////////////////////////////////////////////////////////////////////////// @@ -123,6 +137,11 @@ public abstract class VideoPlayer extends BasePlayer private View topControlsRoot; private TextView qualityTextView; + private SubtitleView subtitleView; + + private TextView resizeView; + private TextView captionTextView; + private ValueAnimator controlViewAnimator; private Handler controlsVisibilityHandler = new Handler(); @@ -133,6 +152,9 @@ public abstract class VideoPlayer extends BasePlayer private int playbackSpeedPopupMenuGroupId = 79; private PopupMenu playbackSpeedPopupMenu; + private int captionPopupMenuGroupId = 89; + private PopupMenu captionPopupMenu; + /////////////////////////////////////////////////////////////////////////// public VideoPlayer(String debugTag, Context context) { @@ -163,6 +185,12 @@ public abstract class VideoPlayer extends BasePlayer this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); this.topControlsRoot = rootView.findViewById(R.id.topControls); this.qualityTextView = rootView.findViewById(R.id.qualityTextView); + this.subtitleView = rootView.findViewById(R.id.subtitleView); + + this.resizeView = rootView.findViewById(R.id.resizeTextView); + resizeView.setText(PlayerHelper.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode())); + + this.captionTextView = rootView.findViewById(R.id.captionTextView); //this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f); @@ -172,9 +200,13 @@ public abstract class VideoPlayer extends BasePlayer this.qualityPopupMenu = new PopupMenu(context, qualityTextView); this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeedTextView); + this.captionPopupMenu = new PopupMenu(context, captionTextView); - ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); + ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)) + .getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); + subtitleView.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, + PlayerHelper.getCaptionSizePx(context)); } @Override @@ -183,14 +215,22 @@ public abstract class VideoPlayer extends BasePlayer playbackSeekBar.setOnSeekBarChangeListener(this); playbackSpeedTextView.setOnClickListener(this); qualityTextView.setOnClickListener(this); + captionTextView.setOnClickListener(this); + resizeView.setOnClickListener(this); } @Override public void initPlayer() { super.initPlayer(); + + // Setup video view simpleExoPlayer.setVideoSurfaceView(surfaceView); simpleExoPlayer.addVideoListener(this); + // Setup subtitle view + simpleExoPlayer.addTextOutput(cues -> subtitleView.onCues(cues)); + + // Setup audio session with onboard equalizer if (Build.VERSION.SDK_INT >= 21) { trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)); } @@ -236,6 +276,39 @@ public abstract class VideoPlayer extends BasePlayer playbackSpeedPopupMenu.setOnDismissListener(this); } + private void buildCaptionMenu() { + if (captionPopupMenu == null) return; + captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); + + if (availableCaptions == null || trackSelector == null) return; + MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, + 0, Menu.NONE, "Caption Off"); + captionOffItem.setOnMenuItemClickListener(menuItem -> { + final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); + if (trackSelector != null && textRendererIndex != -1) { + trackSelector.setRendererDisabled(textRendererIndex, true); + } + return true; + }); + + for (int i = 0; i < availableCaptions.size(); i++) { + final Subtitles subtitles = availableCaptions.get(i); + final String captionLanguage = PlayerHelper.captionLanguageOf(subtitles); + MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, + i + 1, Menu.NONE, captionLanguage); + captionItem.setOnMenuItemClickListener(menuItem -> { + final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); + if (trackSelector != null && textRendererIndex != -1) { + trackSelector.setRendererDisabled(textRendererIndex, false); + trackSelector.setParameters(trackSelector.getParameters() + .withPreferredTextLanguage(captionLanguage)); + } + return true; + }); + } + //captionPopupMenu.setOnMenuItemClickListener(this); + captionPopupMenu.setOnDismissListener(this); + } /*////////////////////////////////////////////////////////////////////////// // Playback Listener //////////////////////////////////////////////////////////////////////////*/ @@ -249,9 +322,11 @@ public abstract class VideoPlayer extends BasePlayer super.sync(item, info); qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); + captionTextView.setVisibility(View.GONE); if (info != null) { - final List videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); + final List videos = ListHelper.getSortedStreamVideosList(context, + info.video_streams, info.video_only_streams, false); availableStreams = new ArrayList<>(videos); if (playbackQuality == null) { selectedStreamIndex = getDefaultResolutionIndex(videos); @@ -261,8 +336,12 @@ public abstract class VideoPlayer extends BasePlayer buildQualityMenu(); buildPlaybackSpeedMenu(); + buildCaptionMenu(); qualityTextView.setVisibility(View.VISIBLE); playbackSpeedTextView.setVisibility(View.VISIBLE); + + availableCaptions = info.getSubtitles(); + if (!availableCaptions.isEmpty()) captionTextView.setVisibility(View.VISIBLE); } } @@ -280,13 +359,39 @@ public abstract class VideoPlayer extends BasePlayer if (index < 0 || index >= videos.size()) return null; final VideoStream video = videos.get(index); - final MediaSource streamSource = buildMediaSource(video.getUrl(), MediaFormat.getSuffixById(video.format)); - final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); - if (!video.isVideoOnly || audio == null) return streamSource; + List mediaSources = new ArrayList<>(); + // Create video stream source + final MediaSource streamSource = buildMediaSource(video.getUrl(), + MediaFormat.getSuffixById(video.getFormatId())); + mediaSources.add(streamSource); - // Merge with audio stream in case if video does not contain audio - final MediaSource audioSource = buildMediaSource(audio.getUrl(), MediaFormat.getSuffixById(audio.format)); - return new MergingMediaSource(streamSource, audioSource); + // Create optional audio stream source + final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); + if (video.isVideoOnly && audio != null) { + // Merge with audio stream in case if video does not contain audio + final MediaSource audioSource = buildMediaSource(audio.getUrl(), + MediaFormat.getSuffixById(audio.getFormatId())); + mediaSources.add(audioSource); + } + + // Create subtitle sources + for (final Subtitles subtitle : info.getSubtitles()) { + final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); + if (mimeType == null) continue; + + final Format textFormat = Format.createTextSampleFormat(null, mimeType, + SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(subtitle)); + final MediaSource textSource = new SingleSampleMediaSource( + Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET); + mediaSources.add(textSource); + } + + if (mediaSources.size() == 1) { + return mediaSources.get(0); + } else { + return new MergingMediaSource(mediaSources.toArray( + new MediaSource[mediaSources.size()])); + } } /*////////////////////////////////////////////////////////////////////////// @@ -364,6 +469,20 @@ public abstract class VideoPlayer extends BasePlayer // ExoPlayer Video Listener //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + super.onTracksChanged(trackGroups, trackSelections); + if (trackSelector == null || captionTextView == null) return; + + if (trackSelector.getRendererDisabled(getRendererIndex(C.TRACK_TYPE_TEXT)) || + trackSelector.getParameters().preferredTextLanguage == null) { + captionTextView.setText("No Caption"); + } else { + final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; + captionTextView.setText(preferredLanguage); + } + } + @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { if (DEBUG) { @@ -453,6 +572,10 @@ public abstract class VideoPlayer extends BasePlayer onQualitySelectorClicked(); } else if (v.getId() == playbackSpeedTextView.getId()) { onPlaybackSpeedClicked(); + } else if (v.getId() == resizeView.getId()) { + onResizeClicked(); + } else if (v.getId() == captionTextView.getId()) { + onCaptionClicked(); } } @@ -516,6 +639,27 @@ public abstract class VideoPlayer extends BasePlayer showControls(300); } + private void onCaptionClicked() { + if (DEBUG) Log.d(TAG, "onCaptionClicked() called"); + captionPopupMenu.show(); + isSomePopupMenuVisible = true; + showControls(300); + } + + protected void onResizeClicked() { + if (aspectRatioFrameLayout != null && context != null) { + final int currentResizeMode = aspectRatioFrameLayout.getResizeMode(); + final int newResizeMode; + if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_ZOOM) { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + } else { + newResizeMode = currentResizeMode + 1; + } + + aspectRatioFrameLayout.setResizeMode(newResizeMode); + resizeView.setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + } + } /*////////////////////////////////////////////////////////////////////////// // SeekBar Listener //////////////////////////////////////////////////////////////////////////*/ @@ -557,11 +701,11 @@ public abstract class VideoPlayer extends BasePlayer // Utils //////////////////////////////////////////////////////////////////////////*/ - public int getVideoRendererIndex() { + public int getRendererIndex(final int trackIndex) { if (simpleExoPlayer == null) return -1; for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { - if (simpleExoPlayer.getRendererType(t) == C.TRACK_TYPE_VIDEO) { + if (simpleExoPlayer.getRendererType(t) == trackIndex) { return t; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 40063ba40..75ea3bdc6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -4,8 +4,14 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.util.DisplayMetrics; + +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.Subtitles; +import org.schabi.newpipe.extractor.stream.SubtitlesFormat; import java.text.DecimalFormat; import java.text.NumberFormat; @@ -14,6 +20,12 @@ import java.util.Locale; import javax.annotation.Nonnull; +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL; +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT; +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH; +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + public class PlayerHelper { private PlayerHelper() {} @@ -46,6 +58,39 @@ public class PlayerHelper { return pitchFormatter.format(pitch); } + public static String mimeTypesOf(final SubtitlesFormat format) { + switch (format) { + case VTT: return MimeTypes.TEXT_VTT; + case TTML: return MimeTypes.APPLICATION_TTML; + default: throw new IllegalArgumentException("Unrecognized mime type: " + format.name()); + } + } + + @NonNull + public static String captionLanguageOf(@NonNull final Subtitles subtitles) { + final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale()); + return displayName + (subtitles.isAutoGenerated() ? " (auto-generated)" : ""); + } + + public static String resizeTypeOf(@NonNull final Context context, + @AspectRatioFrameLayout.ResizeMode final int resizeMode) { + switch (resizeMode) { + case RESIZE_MODE_FIT: return "FIT"; + case RESIZE_MODE_FILL: return "FILL"; + case RESIZE_MODE_FIXED_HEIGHT: return "HEIGHT"; + case RESIZE_MODE_FIXED_WIDTH: return "WIDTH"; + case RESIZE_MODE_ZOOM: return "ZOOM"; + default: throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode); + } + } + + public static float getCaptionSizePx(@NonNull final Context context) { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + // todo: expose size control to users + return (float) minimumLength / 20f; + } + public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { return isResumeAfterAudioFocusGain(context, false); } diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 36222f5bc..785505df9 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -30,6 +30,12 @@ + + + + - + @@ -97,6 +101,34 @@ android:src="@drawable/ic_fullscreen_white" tools:ignore="ContentDescription,RtlHardcoded"/> + + + From 6485327b9711f73687b12cb39eb67a991e5fc7f8 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 6 Feb 2018 15:07:49 -0800 Subject: [PATCH 39/52] -Replace main player dropdown menu with expand/collapse custom UI. --- .../newpipe/player/MainVideoPlayer.java | 77 +++++++-------- .../main/res/layout/activity_main_player.xml | 94 ++++++++++++++++--- 2 files changed, 114 insertions(+), 57 deletions(-) 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 8081dcad7..a23290486 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -61,7 +61,6 @@ import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.PopupMenuIconHacker; import org.schabi.newpipe.util.ThemeHelper; import java.util.List; @@ -194,7 +193,6 @@ public final class MainVideoPlayer extends Activity { super.onConfigurationChanged(newConfig); if (playerImpl.isSomePopupMenuVisible()) { - playerImpl.moreOptionsPopupMenu.dismiss(); playerImpl.getQualityPopupMenu().dismiss(); playerImpl.getPlaybackSpeedPopupMenu().dismiss(); } @@ -301,8 +299,11 @@ public final class MainVideoPlayer extends Activity { private boolean queueVisible; private ImageButton moreOptionsButton; - public int moreOptionsPopupMenuGroupId = 89; - public PopupMenu moreOptionsPopupMenu; + private ImageButton toggleOrientationButton; + private ImageButton switchPopupButton; + private ImageButton switchBackgroundButton; + + private View secondaryControls; VideoPlayerImpl(final Context context) { super("VideoPlayerImpl" + MainVideoPlayer.TAG, context); @@ -322,9 +323,12 @@ public final class MainVideoPlayer extends Activity { this.playPauseButton = rootView.findViewById(R.id.playPauseButton); this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton); this.playNextButton = rootView.findViewById(R.id.playNextButton); + this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton); - this.moreOptionsPopupMenu = new PopupMenu(context, moreOptionsButton); - buildMoreOptionsMenu(); + this.secondaryControls = rootView.findViewById(R.id.secondaryControls); + this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation); + this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground); + this.switchPopupButton = rootView.findViewById(R.id.switchPopup); titleTextView.setSelected(true); channelTextView.setSelected(true); @@ -348,7 +352,11 @@ public final class MainVideoPlayer extends Activity { playPauseButton.setOnClickListener(this); playPreviousButton.setOnClickListener(this); playNextButton.setOnClickListener(this); + moreOptionsButton.setOnClickListener(this); + toggleOrientationButton.setOnClickListener(this); + switchBackgroundButton.setOnClickListener(this); + switchPopupButton.setOnClickListener(this); } /*////////////////////////////////////////////////////////////////////////// @@ -464,6 +472,16 @@ public final class MainVideoPlayer extends Activity { return; } else if (v.getId() == moreOptionsButton.getId()) { onMoreOptionsClicked(); + + } else if (v.getId() == toggleOrientationButton.getId()) { + onScreenRotationClicked(); + + } else if (v.getId() == switchPopupButton.getId()) { + onFullScreenButtonClicked(); + + } else if (v.getId() == switchBackgroundButton.getId()) { + onPlayBackgroundButtonClicked(); + } if (getCurrentState() != STATE_COMPLETED) { @@ -497,8 +515,15 @@ public final class MainVideoPlayer extends Activity { private void onMoreOptionsClicked() { if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); - moreOptionsPopupMenu.show(); - isSomePopupMenuVisible = true; + if (secondaryControls.getVisibility() == View.VISIBLE) { + moreOptionsButton.setImageDrawable(getResources().getDrawable( + R.drawable.ic_expand_more_white_24dp)); + animateView(secondaryControls, false, 200); + } else { + moreOptionsButton.setImageDrawable(getResources().getDrawable( + R.drawable.ic_expand_less_white_24dp)); + animateView(secondaryControls, true, 200); + } showControls(300); } @@ -637,42 +662,6 @@ public final class MainVideoPlayer extends Activity { setShuffleButton(shuffleButton, playQueue.isShuffled()); } - private void buildMoreOptionsMenu() { - this.moreOptionsPopupMenu.getMenuInflater().inflate(R.menu.menu_videooptions, - moreOptionsPopupMenu.getMenu()); - - moreOptionsPopupMenu.setOnMenuItemClickListener(menuItem -> { - switch (menuItem.getItemId()) { - case R.id.toggleOrientation: - onScreenRotationClicked(); - break; - case R.id.switchPopup: - onFullScreenButtonClicked(); - break; - case R.id.switchBackground: - onPlayBackgroundButtonClicked(); - break; - } - return false; - }); - - try { - PopupMenuIconHacker.setShowPopupIcon(moreOptionsPopupMenu); - } catch (Exception e) { - e.printStackTrace(); - } - - // fix icon theme - if(ThemeHelper.isLightThemeSelected(MainVideoPlayer.this)) { - moreOptionsPopupMenu.getMenu() - .findItem(R.id.toggleOrientation) - .setIcon(R.drawable.ic_screen_rotation_black_24dp); - moreOptionsPopupMenu.getMenu() - .findItem(R.id.switchPopup) - .setIcon(R.drawable.ic_fullscreen_exit_black_24dp); - } - } - private void buildQueue() { queueLayout = findViewById(R.id.playQueuePanel); diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 785505df9..577a6a8f5 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -145,10 +145,10 @@ android:layout_alignParentTop="true" android:background="@drawable/player_top_controls_bg" android:gravity="top" - android:paddingBottom="70dp" - android:paddingLeft="2dp" - android:paddingRight="10dp" android:paddingTop="10dp" + android:paddingBottom="10dp" + android:paddingLeft="5dp" + android:paddingRight="5dp" tools:ignore="RtlHardcoded"> @@ -232,12 +234,12 @@ android:layout_marginLeft="2dp" android:layout_marginRight="2dp" android:layout_toLeftOf="@+id/moreOptionsButton" - android:background="#00ffffff" android:clickable="true" android:focusable="true" android:padding="5dp" android:scaleType="fitXY" android:src="@drawable/list" + android:background="?attr/selectableItemBackground" tools:ignore="ContentDescription,RtlHardcoded"/> + + + + + + + + + + tools:visibility="visible" /> + tools:visibility="visible" /> + tools:visibility="visible" /> From 880676d67028bc23ad8d8df85be4532e807588e1 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 7 Feb 2018 13:11:19 -0800 Subject: [PATCH 40/52] -Modified popup video player to show extra options only when screen is large enough. -Modified available resize options for different player modes. -Fixed caption menu not working on popup player. -Extracted hardcoded strings. -Added button effects to both main and popup players. --- .../newpipe/player/MainVideoPlayer.java | 29 ++++++++ .../newpipe/player/PopupVideoPlayer.java | 34 ++++++++- .../schabi/newpipe/player/VideoPlayer.java | 51 +++++++------ .../newpipe/player/helper/PlayerHelper.java | 16 +---- .../main/res/layout/activity_main_player.xml | 7 +- app/src/main/res/layout/player_popup.xml | 72 +++++++++++-------- app/src/main/res/values/strings.xml | 8 +++ 7 files changed, 142 insertions(+), 75 deletions(-) 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 a23290486..49ce06a9b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -35,7 +35,9 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; +import android.util.DisplayMetrics; import android.util.Log; +import android.util.TypedValue; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; @@ -48,6 +50,7 @@ import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; @@ -334,6 +337,8 @@ public final class MainVideoPlayer extends Activity { channelTextView.setSelected(true); getRootView().setKeepScreenOn(true); + getSubtitleView().setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, + getCaptionSizePx(context)); } @Override @@ -547,6 +552,24 @@ public final class MainVideoPlayer extends Activity { if (isPlaying()) hideControls(300, 0); } + @Override + protected void onResizeClicked() { + if (getAspectRatioFrameLayout() != null && context != null) { + final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); + final int newResizeMode; + if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FIT) { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; + } else if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + } else { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + } + + getAspectRatioFrameLayout().setResizeMode(newResizeMode); + getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + } + } + @Override protected int getDefaultResolutionIndex(final List sortedVideos) { return ListHelper.getDefaultResolutionIndex(context, sortedVideos); @@ -745,6 +768,12 @@ public final class MainVideoPlayer extends Activity { }; } + private float getCaptionSizePx(@NonNull Context context) { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + // todo: expose size control to users + return (float) minimumLength / 20f; + } /////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index e8ccba7d0..bf6d6f445 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -52,6 +52,7 @@ import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; @@ -89,6 +90,8 @@ public final class PopupVideoPlayer extends Service { private static final String POPUP_SAVED_X = "popup_saved_x"; private static final String POPUP_SAVED_Y = "popup_saved_y"; + private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; + private WindowManager windowManager; private WindowManager.LayoutParams windowLayoutParams; private GestureDetector gestureDetector; @@ -359,10 +362,12 @@ public final class PopupVideoPlayer extends Service { /////////////////////////////////////////////////////////////////////////// - protected class VideoPlayerImpl extends VideoPlayer { + protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener { private TextView resizingIndicator; private ImageButton fullScreenButton; + private View extraOptionsView; + @Override public void handleIntent(Intent intent) { super.handleIntent(intent); @@ -381,6 +386,17 @@ public final class PopupVideoPlayer extends Service { resizingIndicator = rootView.findViewById(R.id.resizing_indicator); fullScreenButton = rootView.findViewById(R.id.fullScreenButton); fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked()); + + extraOptionsView = rootView.findViewById(R.id.extraOptionsView); + rootView.addOnLayoutChangeListener(this); + } + + @Override + public void onLayoutChange(final View view, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + float widthDp = Math.abs(right - left) / getResources().getDisplayMetrics().density; + final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP ? View.VISIBLE : View.GONE; + extraOptionsView.setVisibility(visibility); } @Override @@ -439,6 +455,22 @@ public final class PopupVideoPlayer extends Service { if (isPlaying()) hideControls(500, 0); } + @Override + protected void onResizeClicked() { + if (getAspectRatioFrameLayout() != null && context != null) { + final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); + final int newResizeMode; + if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + } else { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; + } + + getAspectRatioFrameLayout().setResizeMode(newResizeMode); + getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + } + } + @Override public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); 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 48d10a4ad..344175030 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -36,7 +36,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.Log; -import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.SurfaceView; @@ -75,7 +74,6 @@ import java.util.List; import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; import static com.google.android.exoplayer2.C.TIME_UNSET; -import static com.google.android.exoplayer2.C.TRACK_TYPE_TEXT; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -204,9 +202,6 @@ public abstract class VideoPlayer extends BasePlayer ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)) .getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); - - subtitleView.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, - PlayerHelper.getCaptionSizePx(context)); } @Override @@ -281,8 +276,10 @@ public abstract class VideoPlayer extends BasePlayer captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); if (availableCaptions == null || trackSelector == null) return; + + // Add option for turning off caption MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, - 0, Menu.NONE, "Caption Off"); + 0, Menu.NONE, R.string.caption_none); captionOffItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); if (trackSelector != null && textRendererIndex != -1) { @@ -291,6 +288,7 @@ public abstract class VideoPlayer extends BasePlayer return true; }); + // Add all available captions for (int i = 0; i < availableCaptions.size(); i++) { final Subtitles subtitles = availableCaptions.get(i); final String captionLanguage = PlayerHelper.captionLanguageOf(subtitles); @@ -306,7 +304,6 @@ public abstract class VideoPlayer extends BasePlayer return true; }); } - //captionPopupMenu.setOnMenuItemClickListener(this); captionPopupMenu.setOnDismissListener(this); } /*////////////////////////////////////////////////////////////////////////// @@ -334,14 +331,14 @@ public abstract class VideoPlayer extends BasePlayer selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality()); } + availableCaptions = info.getSubtitles(); + buildQualityMenu(); buildPlaybackSpeedMenu(); buildCaptionMenu(); qualityTextView.setVisibility(View.VISIBLE); playbackSpeedTextView.setVisibility(View.VISIBLE); - - availableCaptions = info.getSubtitles(); - if (!availableCaptions.isEmpty()) captionTextView.setVisibility(View.VISIBLE); + captionTextView.setVisibility(availableCaptions.isEmpty() ? View.GONE : View.VISIBLE); } } @@ -472,11 +469,13 @@ public abstract class VideoPlayer extends BasePlayer @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { super.onTracksChanged(trackGroups, trackSelections); - if (trackSelector == null || captionTextView == null) return; + if (captionTextView == null) return; - if (trackSelector.getRendererDisabled(getRendererIndex(C.TRACK_TYPE_TEXT)) || + if (trackSelector == null) { + captionTextView.setVisibility(View.GONE); + } else if (trackSelector.getRendererDisabled(getRendererIndex(C.TRACK_TYPE_TEXT)) || trackSelector.getParameters().preferredTextLanguage == null) { - captionTextView.setText("No Caption"); + captionTextView.setText(R.string.caption_none); } else { final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; captionTextView.setText(preferredLanguage); @@ -646,20 +645,7 @@ public abstract class VideoPlayer extends BasePlayer showControls(300); } - protected void onResizeClicked() { - if (aspectRatioFrameLayout != null && context != null) { - final int currentResizeMode = aspectRatioFrameLayout.getResizeMode(); - final int newResizeMode; - if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_ZOOM) { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - } else { - newResizeMode = currentResizeMode + 1; - } - - aspectRatioFrameLayout.setResizeMode(newResizeMode); - resizeView.setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); - } - } + protected abstract void onResizeClicked(); /*////////////////////////////////////////////////////////////////////////// // SeekBar Listener //////////////////////////////////////////////////////////////////////////*/ @@ -899,4 +885,15 @@ public abstract class VideoPlayer extends BasePlayer return currentDisplaySeek; } + public SubtitleView getSubtitleView() { + return subtitleView; + } + + public TextView getResizeView() { + return resizeView; + } + + public TextView getCaptionTextView() { + return captionTextView; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 75ea3bdc6..476838f13 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -4,7 +4,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.annotation.NonNull; -import android.util.DisplayMetrics; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.util.MimeTypes; @@ -75,22 +74,13 @@ public class PlayerHelper { public static String resizeTypeOf(@NonNull final Context context, @AspectRatioFrameLayout.ResizeMode final int resizeMode) { switch (resizeMode) { - case RESIZE_MODE_FIT: return "FIT"; - case RESIZE_MODE_FILL: return "FILL"; - case RESIZE_MODE_FIXED_HEIGHT: return "HEIGHT"; - case RESIZE_MODE_FIXED_WIDTH: return "WIDTH"; - case RESIZE_MODE_ZOOM: return "ZOOM"; + case RESIZE_MODE_FIT: return context.getResources().getString(R.string.resize_fit); + case RESIZE_MODE_FILL: return context.getResources().getString(R.string.resize_fill); + case RESIZE_MODE_ZOOM: return context.getResources().getString(R.string.resize_zoom); default: throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode); } } - public static float getCaptionSizePx(@NonNull final Context context) { - final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); - // todo: expose size control to users - return (float) minimumLength / 20f; - } - public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { return isResumeAfterAudioFocusGain(context, false); } diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 577a6a8f5..80c67974f 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -286,12 +286,13 @@ @@ -83,52 +84,61 @@ android:layout_height="30dp" android:layout_toRightOf="@+id/qualityTextView" android:gravity="center" - android:padding="6dp" + android:padding="5dp" android:textColor="@android:color/white" android:textStyle="bold" + android:background="?attr/selectableItemBackground" tools:ignore="RelativeOverlap,RtlHardcoded,RtlSymmetry" tools:text="1.75x"/> + + + + + + + - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 767bffa10..b1bd5c3ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -395,4 +395,12 @@ Added to playlist Playlist thumbnail changed Failed to delete playlist + + + No Caption + + FIT + FILL + ZOOM + From f506fc0478cbed1df0387880c9b1513d0f233169 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Fri, 9 Feb 2018 12:43:24 -0800 Subject: [PATCH 41/52] -Moved caption extraction and menu building into exoplayer track changing callback. -Updated extractor dependency. --- .../newpipe/player/PopupVideoPlayer.java | 2 +- .../schabi/newpipe/player/VideoPlayer.java | 89 +++++++++++++------ 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index bf6d6f445..9b5413977 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -676,7 +676,7 @@ public final class PopupVideoPlayer extends Service { /*package-private*/ void enableVideoRenderer(final boolean enable) { final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); - if (trackSelector != null && videoRendererIndex != -1) { + if (trackSelector != null && videoRendererIndex != RENDERER_UNAVAILABLE) { trackSelector.setRendererDisabled(videoRendererIndex, !enable); } } 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 344175030..7f41c028f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -53,6 +53,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; +import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; @@ -98,6 +99,7 @@ public abstract class VideoPlayer extends BasePlayer // Player //////////////////////////////////////////////////////////////////////////*/ + protected static final int RENDERER_UNAVAILABLE = -1; public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds private ArrayList availableStreams; @@ -105,8 +107,6 @@ public abstract class VideoPlayer extends BasePlayer protected String playbackQuality; - private List availableCaptions; - protected boolean wasPlaying = false; /*////////////////////////////////////////////////////////////////////////// @@ -271,35 +271,32 @@ public abstract class VideoPlayer extends BasePlayer playbackSpeedPopupMenu.setOnDismissListener(this); } - private void buildCaptionMenu() { + private void buildCaptionMenu(final List availableLanguages) { if (captionPopupMenu == null) return; captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); - if (availableCaptions == null || trackSelector == null) return; - // Add option for turning off caption MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, 0, Menu.NONE, R.string.caption_none); captionOffItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (trackSelector != null && textRendererIndex != -1) { + if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { trackSelector.setRendererDisabled(textRendererIndex, true); } return true; }); // Add all available captions - for (int i = 0; i < availableCaptions.size(); i++) { - final Subtitles subtitles = availableCaptions.get(i); - final String captionLanguage = PlayerHelper.captionLanguageOf(subtitles); + for (int i = 0; i < availableLanguages.size(); i++) { + final String captionLanguage = availableLanguages.get(i); MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, i + 1, Menu.NONE, captionLanguage); captionItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (trackSelector != null && textRendererIndex != -1) { - trackSelector.setRendererDisabled(textRendererIndex, false); + if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { trackSelector.setParameters(trackSelector.getParameters() .withPreferredTextLanguage(captionLanguage)); + trackSelector.setRendererDisabled(textRendererIndex, false); } return true; }); @@ -319,7 +316,6 @@ public abstract class VideoPlayer extends BasePlayer super.sync(item, info); qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); - captionTextView.setVisibility(View.GONE); if (info != null) { final List videos = ListHelper.getSortedStreamVideosList(context, @@ -331,14 +327,10 @@ public abstract class VideoPlayer extends BasePlayer selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality()); } - availableCaptions = info.getSubtitles(); - buildQualityMenu(); buildPlaybackSpeedMenu(); - buildCaptionMenu(); qualityTextView.setVisibility(View.VISIBLE); playbackSpeedTextView.setVisibility(View.VISIBLE); - captionTextView.setVisibility(availableCaptions.isEmpty() ? View.GONE : View.VISIBLE); } } @@ -469,17 +461,7 @@ public abstract class VideoPlayer extends BasePlayer @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { super.onTracksChanged(trackGroups, trackSelections); - if (captionTextView == null) return; - - if (trackSelector == null) { - captionTextView.setVisibility(View.GONE); - } else if (trackSelector.getRendererDisabled(getRendererIndex(C.TRACK_TYPE_TEXT)) || - trackSelector.getParameters().preferredTextLanguage == null) { - captionTextView.setText(R.string.caption_none); - } else { - final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; - captionTextView.setText(preferredLanguage); - } + onTextTrackUpdate(); } @Override @@ -495,6 +477,55 @@ public abstract class VideoPlayer extends BasePlayer animateView(surfaceForeground, false, 100); } + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Track Updates + //////////////////////////////////////////////////////////////////////////*/ + + private void onTextTrackUpdate() { + final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT); + + if (captionTextView == null) return; + if (trackSelector == null || trackSelector.getCurrentMappedTrackInfo() == null || + textRenderer == RENDERER_UNAVAILABLE) { + captionTextView.setVisibility(View.GONE); + return; + } + + final TrackGroupArray textTracks = trackSelector.getCurrentMappedTrackInfo() + .getTrackGroups(textRenderer); + + // Extract all loaded languages + List availableLanguages = new ArrayList<>(textTracks.length); + for (int i = 0; i < textTracks.length; i++) { + final TrackGroup textTrack = textTracks.get(i); + if (textTrack.length > 0 && textTrack.getFormat(0) != null) { + availableLanguages.add(textTrack.getFormat(0).language); + } + } + + // Normalize mismatching language strings + final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; + // Because ExoPlayer normalizes the preferred language string but not the text track + // language strings, some preferred language string will have the language name in lowercase + String formattedPreferredLanguage = null; + if (preferredLanguage != null) { + for (final String language : availableLanguages) { + if (language.compareToIgnoreCase(preferredLanguage) == 0) + formattedPreferredLanguage = language; + } + } + + // Build UI + buildCaptionMenu(availableLanguages); + if (trackSelector.getRendererDisabled(textRenderer) || formattedPreferredLanguage == null || + !availableLanguages.contains(formattedPreferredLanguage)) { + captionTextView.setText(R.string.caption_none); + } else { + captionTextView.setText(formattedPreferredLanguage); + } + captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); + } + /*////////////////////////////////////////////////////////////////////////// // General Player //////////////////////////////////////////////////////////////////////////*/ @@ -688,7 +719,7 @@ public abstract class VideoPlayer extends BasePlayer //////////////////////////////////////////////////////////////////////////*/ public int getRendererIndex(final int trackIndex) { - if (simpleExoPlayer == null) return -1; + if (simpleExoPlayer == null) return RENDERER_UNAVAILABLE; for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { if (simpleExoPlayer.getRendererType(t) == trackIndex) { @@ -696,7 +727,7 @@ public abstract class VideoPlayer extends BasePlayer } } - return -1; + return RENDERER_UNAVAILABLE; } public boolean isControlsVisible() { From 59f85838950b5dae0c7e154efed817ab14ed2cb2 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Fri, 9 Feb 2018 13:26:03 -0800 Subject: [PATCH 42/52] -Added settings for managing caption font size. --- .../newpipe/player/MainVideoPlayer.java | 27 +++++++++++++------ .../newpipe/player/PopupVideoPlayer.java | 13 +++++++++ .../schabi/newpipe/player/VideoPlayer.java | 9 +++++++ app/src/main/res/values/settings_keys.xml | 19 +++++++++++++ app/src/main/res/values/strings.xml | 5 ++++ app/src/main/res/xml/appearance_settings.xml | 8 ++++++ 6 files changed, 73 insertions(+), 8 deletions(-) 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 49ce06a9b..5e19e8173 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -51,6 +51,7 @@ import android.widget.Toast; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; @@ -337,8 +338,24 @@ public final class MainVideoPlayer extends Activity { channelTextView.setSelected(true); getRootView().setKeepScreenOn(true); - getSubtitleView().setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, - getCaptionSizePx(context)); + } + + @Override + protected void setupSubtitleView(@NonNull SubtitleView view, + @NonNull String captionSizeKey) { + final float captionRatio; + if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) { + captionRatio = 22f; + } else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) { + captionRatio = 18f; + } else { + captionRatio = 20f; + } + + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, + (float) minimumLength / captionRatio); } @Override @@ -768,12 +785,6 @@ public final class MainVideoPlayer extends Activity { }; } - private float getCaptionSizePx(@NonNull Context context) { - final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); - // todo: expose size control to users - return (float) minimumLength / 20f; - } /////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 9b5413977..bda2b7aa5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -53,6 +53,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; @@ -391,6 +392,18 @@ public final class PopupVideoPlayer extends Service { rootView.addOnLayoutChangeListener(this); } + @Override + protected void setupSubtitleView(@NonNull SubtitleView view, + @NonNull String captionSizeKey) { + float captionRatio = SubtitleView.DEFAULT_TEXT_SIZE_FRACTION; + if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) { + captionRatio *= 0.9; + } else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) { + captionRatio *= 1.1; + } + view.setFractionalTextSize(captionRatio); + } + @Override public void onLayoutChange(final View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 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 7f41c028f..a2f5d22ae 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -32,6 +32,7 @@ import android.graphics.PorterDuff; import android.net.Uri; import android.os.Build; import android.os.Handler; +import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; @@ -183,7 +184,12 @@ public abstract class VideoPlayer extends BasePlayer this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); this.topControlsRoot = rootView.findViewById(R.id.topControls); this.qualityTextView = rootView.findViewById(R.id.qualityTextView); + this.subtitleView = rootView.findViewById(R.id.subtitleView); + final String captionSizeKey = PreferenceManager.getDefaultSharedPreferences(context) + .getString(context.getString(R.string.caption_size_key), + context.getString(R.string.caption_size_default)); + setupSubtitleView(subtitleView, captionSizeKey); this.resizeView = rootView.findViewById(R.id.resizeTextView); resizeView.setText(PlayerHelper.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode())); @@ -204,6 +210,9 @@ public abstract class VideoPlayer extends BasePlayer .getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); } + protected abstract void setupSubtitleView(@NonNull SubtitleView view, + @NonNull String captionSizeKey); + @Override public void initListeners() { super.initListeners(); diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..047f03f8b 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -100,6 +100,25 @@ @string/black_theme_title + + caption_size_key + @string/normal_caption_size_key + + smaller_caption_size + normal_caption_size + larger_caption_size + + + @string/smaller_caption_font_size + @string/normal_caption_font_size + @string/larger_caption_font_size + + + @string/smaller_caption_size_key + @string/normal_caption_size_key + @string/larger_caption_size_key + + show_search_suggestions show_play_with_kodi diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b1bd5c3ec..7ceec994b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -403,4 +403,9 @@ FILL ZOOM + Caption Font Size + Smaller Font + Normal Font + Larger Font + diff --git a/app/src/main/res/xml/appearance_settings.xml b/app/src/main/res/xml/appearance_settings.xml index 58b08a284..3ffbd9d81 100644 --- a/app/src/main/res/xml/appearance_settings.xml +++ b/app/src/main/res/xml/appearance_settings.xml @@ -21,4 +21,12 @@ android:key="@string/show_hold_to_append_key" android:title="@string/show_hold_to_append_title" android:summary="@string/show_hold_to_append_summary"/> + + From f09b04dce0c87b7dc4d3039e02101b8e96b460db Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 10 Feb 2018 19:33:48 -0800 Subject: [PATCH 43/52] -Code clean up on resize switching. --- .../newpipe/player/MainVideoPlayer.java | 32 ++++++++----------- .../newpipe/player/PopupVideoPlayer.java | 17 +++------- .../schabi/newpipe/player/VideoPlayer.java | 15 +++++++-- 3 files changed, 31 insertions(+), 33 deletions(-) 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 5e19e8173..d48994d0f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -343,19 +343,19 @@ public final class MainVideoPlayer extends Activity { @Override protected void setupSubtitleView(@NonNull SubtitleView view, @NonNull String captionSizeKey) { - final float captionRatio; + final float captionRatioInverse; if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) { - captionRatio = 22f; + captionRatioInverse = 22f; } else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) { - captionRatio = 18f; + captionRatioInverse = 18f; } else { - captionRatio = 20f; + captionRatioInverse = 20f; } final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, - (float) minimumLength / captionRatio); + (float) minimumLength / captionRatioInverse); } @Override @@ -570,20 +570,14 @@ public final class MainVideoPlayer extends Activity { } @Override - protected void onResizeClicked() { - if (getAspectRatioFrameLayout() != null && context != null) { - final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); - final int newResizeMode; - if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FIT) { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; - } else if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; - } else { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - } - - getAspectRatioFrameLayout().setResizeMode(newResizeMode); - getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + protected int nextResizeMode(int currentResizeMode) { + switch (currentResizeMode) { + case AspectRatioFrameLayout.RESIZE_MODE_FIT: + return AspectRatioFrameLayout.RESIZE_MODE_FILL; + case AspectRatioFrameLayout.RESIZE_MODE_FILL: + return AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + default: + return AspectRatioFrameLayout.RESIZE_MODE_FIT; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index bda2b7aa5..f4e7a0d6a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -469,18 +469,11 @@ public final class PopupVideoPlayer extends Service { } @Override - protected void onResizeClicked() { - if (getAspectRatioFrameLayout() != null && context != null) { - final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); - final int newResizeMode; - if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - } else { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; - } - - getAspectRatioFrameLayout().setResizeMode(newResizeMode); - getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + protected int nextResizeMode(int resizeMode) { + if (resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { + return AspectRatioFrameLayout.RESIZE_MODE_FIT; + } else { + return AspectRatioFrameLayout.RESIZE_MODE_FILL; } } 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 a2f5d22ae..a0bc7223f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -519,8 +519,10 @@ public abstract class VideoPlayer extends BasePlayer String formattedPreferredLanguage = null; if (preferredLanguage != null) { for (final String language : availableLanguages) { - if (language.compareToIgnoreCase(preferredLanguage) == 0) + if (language.compareToIgnoreCase(preferredLanguage) == 0) { formattedPreferredLanguage = language; + break; + } } } @@ -685,7 +687,16 @@ public abstract class VideoPlayer extends BasePlayer showControls(300); } - protected abstract void onResizeClicked(); + private void onResizeClicked() { + if (getAspectRatioFrameLayout() != null && context != null) { + final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); + final int newResizeMode = nextResizeMode(currentResizeMode); + getAspectRatioFrameLayout().setResizeMode(newResizeMode); + getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + } + } + + protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode); /*////////////////////////////////////////////////////////////////////////// // SeekBar Listener //////////////////////////////////////////////////////////////////////////*/ From 622d698ff87e8afde15cb59d0187a0f94296b1dd Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 8 Feb 2018 12:46:54 -0800 Subject: [PATCH 44/52] -Added LeakCanary to debug build for memory detection on activities and fragments. -Added LeakCanary no-op lib to release and beta builds. --- app/build.gradle | 4 ++++ .../java/org/schabi/newpipe/DebugApp.java | 11 +++++++++++ app/src/main/java/org/schabi/newpipe/App.java | 18 ++++++++++++++++++ .../java/org/schabi/newpipe/BaseFragment.java | 8 ++++++++ 4 files changed, 41 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 273616f91..d3084c2d3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,4 +89,8 @@ dependencies { implementation 'frankiesardo:icepick:3.2.0' annotationProcessor 'frankiesardo:icepick-processor:3.2.0' + + debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' + betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' + releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' } diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index 4d37094ba..dba4179c2 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -4,6 +4,10 @@ import android.content.Context; import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; +import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.RefWatcher; + +import java.util.concurrent.TimeUnit; public class DebugApp extends App { private static final String TAG = DebugApp.class.toString(); @@ -41,4 +45,11 @@ public class DebugApp extends App { // Initialize Stetho with the Initializer Stetho.initialize(initializer); } + + @Override + protected RefWatcher installLeakCanary() { + return LeakCanary.refWatcher(this) + .watchDelay(5, TimeUnit.SECONDS) + .buildAndInstall(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 79221db7f..e171c7726 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -10,6 +10,8 @@ import android.util.Log; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; +import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.RefWatcher; import org.acra.ACRA; import org.acra.config.ACRAConfiguration; @@ -54,6 +56,7 @@ import io.reactivex.plugins.RxJavaPlugins; public class App extends Application { protected static final String TAG = App.class.toString(); + private RefWatcher refWatcher; @SuppressWarnings("unchecked") private static final Class[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class}; @@ -69,6 +72,13 @@ public class App extends Application { public void onCreate() { super.onCreate(); + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; + } + refWatcher = installLeakCanary(); + // Initialize settings first because others inits can use its values SettingsActivity.initSettings(this); @@ -157,4 +167,12 @@ public class App extends Application { mNotificationManager.createNotificationChannel(mChannel); } + public static RefWatcher getRefWatcher(Context context) { + final App application = (App) context.getApplicationContext(); + return application.refWatcher; + } + + protected RefWatcher installLeakCanary() { + return RefWatcher.DISABLED; + } } diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index 6cd79e2c9..383e0dc4f 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -13,6 +13,7 @@ import android.view.View; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; +import com.squareup.leakcanary.RefWatcher; import icepick.Icepick; @@ -67,6 +68,13 @@ public abstract class BaseFragment extends Fragment { protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { } + @Override + public void onDestroy() { + super.onDestroy(); + RefWatcher refWatcher = App.getRefWatcher(getActivity()); + refWatcher.watch(this); + } + /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ From 829059ea0101a0ec1346a785d9d82f4231d29974 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 10 Feb 2018 11:07:17 -0800 Subject: [PATCH 45/52] -Added toggle for enabling leak canary heap dump. --- .../java/org/schabi/newpipe/DebugApp.java | 34 ++++++++++++++++++- app/src/main/java/org/schabi/newpipe/App.java | 2 ++ .../java/org/schabi/newpipe/BaseFragment.java | 3 +- .../java/org/schabi/newpipe/MainActivity.java | 24 +++++++++++++ app/src/main/res/menu/debug_menu.xml | 12 +++++++ app/src/main/res/values/settings_keys.xml | 3 ++ app/src/main/res/values/strings.xml | 3 ++ 7 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/menu/debug_menu.xml diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index dba4179c2..ba1fd90cc 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -1,12 +1,20 @@ package org.schabi.newpipe; import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; +import com.squareup.leakcanary.AndroidHeapDumper; +import com.squareup.leakcanary.DefaultLeakDirectoryProvider; +import com.squareup.leakcanary.HeapDumper; import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.LeakDirectoryProvider; import com.squareup.leakcanary.RefWatcher; +import java.io.File; import java.util.concurrent.TimeUnit; public class DebugApp extends App { @@ -49,7 +57,31 @@ public class DebugApp extends App { @Override protected RefWatcher installLeakCanary() { return LeakCanary.refWatcher(this) - .watchDelay(5, TimeUnit.SECONDS) + .heapDumper(new ToggleableHeapDumper(this)) + // give each object 10 seconds to be gc'ed, before leak canary gets nosy on it + .watchDelay(10, TimeUnit.SECONDS) .buildAndInstall(); } + + public static class ToggleableHeapDumper implements HeapDumper { + private final HeapDumper dumper; + private final SharedPreferences preferences; + private final String dumpingAllowanceKey; + + ToggleableHeapDumper(@NonNull final Context context) { + LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context); + this.dumper = new AndroidHeapDumper(context, leakDirectoryProvider); + this.preferences = PreferenceManager.getDefaultSharedPreferences(context); + this.dumpingAllowanceKey = context.getString(R.string.allow_heap_dumping_key); + } + + private boolean isDumpingAllowed() { + return preferences.getBoolean(dumpingAllowanceKey, false); + } + + @Override + public File dumpHeap() { + return isDumpingAllowed() ? dumper.dumpHeap() : HeapDumper.RETRY_LATER; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index e171c7726..b15a38aae 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -5,6 +5,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Build; +import android.support.annotation.Nullable; import android.util.Log; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; @@ -167,6 +168,7 @@ public class App extends Application { mNotificationManager.createNotificationChannel(mChannel); } + @Nullable public static RefWatcher getRefWatcher(Context context) { final App application = (App) context.getApplicationContext(); return application.refWatcher; diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index 383e0dc4f..d3e4a4b28 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -71,8 +71,9 @@ public abstract class BaseFragment extends Fragment { @Override public void onDestroy() { super.onDestroy(); + RefWatcher refWatcher = App.getRefWatcher(getActivity()); - refWatcher.watch(this); + if (refWatcher != null) refWatcher.watch(this); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 9a1ecd07a..329bd4165 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -211,6 +211,12 @@ public class MainActivity extends AppCompatActivity { } } + private void onHeapDumpToggled(@NonNull MenuItem item) { + final boolean newToggleState = !item.isChecked(); + sharedPreferences.edit().putBoolean(getString(R.string.allow_heap_dumping_key), + newToggleState).apply(); + item.setChecked(newToggleState); + } /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @@ -232,6 +238,10 @@ public class MainActivity extends AppCompatActivity { inflater.inflate(R.menu.main_menu, menu); } + if (DEBUG) { + getMenuInflater().inflate(R.menu.debug_menu, menu); + } + ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); @@ -242,6 +252,17 @@ public class MainActivity extends AppCompatActivity { return true; } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem heapDumpToggle = menu.findItem(R.id.action_toggle_heap_dump); + if (heapDumpToggle != null) { + final boolean isToggled = sharedPreferences.getBoolean( + getString(R.string.allow_heap_dumping_key), false); + heapDumpToggle.setChecked(isToggled); + } + return super.onPrepareOptionsMenu(menu); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); @@ -262,6 +283,9 @@ public class MainActivity extends AppCompatActivity { case R.id.action_history: NavigationHelper.openHistory(this); return true; + case R.id.action_toggle_heap_dump: + onHeapDumpToggled(item); + return true; default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/res/menu/debug_menu.xml b/app/src/main/res/menu/debug_menu.xml new file mode 100644 index 000000000..448f9cf23 --- /dev/null +++ b/app/src/main/res/menu/debug_menu.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..5934fdee6 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -83,6 +83,9 @@ last_orientation_landscape_key + + allow_heap_dumping_key + theme light_theme diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 767bffa10..fbdd22564 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -395,4 +395,7 @@ Added to playlist Playlist thumbnail changed Failed to delete playlist + + + Leak Canary From e7d148336b2c88c09835775c6e5e0e9d55c20a43 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 10 Feb 2018 12:24:23 -0800 Subject: [PATCH 46/52] -Changed leak canary toggle text to "monitor leaks". -Added toast when enabling/disabling heap dumping. --- .../java/org/schabi/newpipe/MainActivity.java | 19 +++++++++++++++---- app/src/main/res/values/strings.xml | 4 +++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 329bd4165..b54da2ad0 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -20,6 +20,7 @@ package org.schabi.newpipe; +import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -39,6 +40,7 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.widget.Toast; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.fragments.BackPressable; @@ -211,11 +213,20 @@ public class MainActivity extends AppCompatActivity { } } + @SuppressLint("ShowToast") private void onHeapDumpToggled(@NonNull MenuItem item) { - final boolean newToggleState = !item.isChecked(); - sharedPreferences.edit().putBoolean(getString(R.string.allow_heap_dumping_key), - newToggleState).apply(); - item.setChecked(newToggleState); + final boolean isHeapDumpEnabled = !item.isChecked(); + + sharedPreferences.edit().putBoolean(getString(R.string.allow_heap_dumping_key), isHeapDumpEnabled).apply(); + item.setChecked(isHeapDumpEnabled); + + final String heapDumpNotice; + if (isHeapDumpEnabled) { + heapDumpNotice = getString(R.string.enable_leak_canary_notice); + } else { + heapDumpNotice = getString(R.string.disable_leak_canary_notice); + } + Toast.makeText(getApplicationContext(), heapDumpNotice, Toast.LENGTH_SHORT).show(); } /*////////////////////////////////////////////////////////////////////////// // Menu diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fbdd22564..b1478fed4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -397,5 +397,7 @@ Failed to delete playlist - Leak Canary + Monitor Leaks + Memory leak monitoring enabled, app may become unresponsive when heap dumping + Memory leak monitoring disabled From 263a816c3b9ad1de439a16d1a70964ece6bf74e2 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 11 Feb 2018 11:40:08 -0800 Subject: [PATCH 47/52] -Fixed preferences fetching. --- app/src/main/java/org/schabi/newpipe/MainActivity.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index b54da2ad0..ea6715f16 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -27,6 +27,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; @@ -217,7 +218,8 @@ public class MainActivity extends AppCompatActivity { private void onHeapDumpToggled(@NonNull MenuItem item) { final boolean isHeapDumpEnabled = !item.isChecked(); - sharedPreferences.edit().putBoolean(getString(R.string.allow_heap_dumping_key), isHeapDumpEnabled).apply(); + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putBoolean(getString(R.string.allow_heap_dumping_key), isHeapDumpEnabled).apply(); item.setChecked(isHeapDumpEnabled); final String heapDumpNotice; @@ -267,8 +269,8 @@ public class MainActivity extends AppCompatActivity { public boolean onPrepareOptionsMenu(Menu menu) { MenuItem heapDumpToggle = menu.findItem(R.id.action_toggle_heap_dump); if (heapDumpToggle != null) { - final boolean isToggled = sharedPreferences.getBoolean( - getString(R.string.allow_heap_dumping_key), false); + final boolean isToggled = PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(getString(R.string.allow_heap_dumping_key), false); heapDumpToggle.setChecked(isToggled); } return super.onPrepareOptionsMenu(menu); From 2773f5fbc8318e37c6753a659d0a10ecb14b38a8 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Sun, 11 Feb 2018 21:34:32 +0100 Subject: [PATCH 48/52] move download menu item into detail controls menu --- .../fragments/detail/ActionBarHandler.java | 15 +- .../fragments/detail/VideoDetailFragment.java | 162 +++++++----------- .../main/res/layout/fragment_video_detail.xml | 18 ++ app/src/main/res/menu/video_detail_menu.xml | 6 - app/src/main/res/values/strings.xml | 1 + 5 files changed, 86 insertions(+), 116 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java index 27bffca2d..d928166ab 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java @@ -53,7 +53,6 @@ class ActionBarHandler { // those are edited directly. Typically VideoDetailFragment will implement those callbacks. private OnActionListener onShareListener; private OnActionListener onOpenInBrowserListener; - private OnActionListener onDownloadListener; private OnActionListener onPlayWithKodiListener; // Triggered when a stream related action is triggered. @@ -117,11 +116,6 @@ class ActionBarHandler { } return true; } - case R.id.menu_item_download: - if (onDownloadListener != null) { - onDownloadListener.onActionSelected(selectedVideoStream); - } - return true; case R.id.action_play_with_kodi: if (onPlayWithKodiListener != null) { onPlayWithKodiListener.onActionSelected(selectedVideoStream); @@ -145,19 +139,12 @@ class ActionBarHandler { onOpenInBrowserListener = listener; } - public void setOnDownloadListener(OnActionListener listener) { - onDownloadListener = listener; - } - public void setOnPlayWithKodiListener(OnActionListener listener) { onPlayWithKodiListener = listener; } - public void showDownloadAction(boolean visible) { - menu.findItem(R.id.menu_item_download).setVisible(visible); - } - public void showPlayWithKodiAction(boolean visible) { menu.findItem(R.id.action_play_with_kodi).setVisible(visible); } + } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 89f35c306..daf422516 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -147,6 +147,7 @@ public class VideoDetailFragment extends BaseStateFragment implement private TextView detailControlsBackground; private TextView detailControlsPopup; private TextView detailControlsAddToPlaylist; + private TextView detailControlsDownload; private TextView appendControlsDetail; private LinearLayout videoDescriptionRootLayout; @@ -335,6 +336,24 @@ public class VideoDetailFragment extends BaseStateFragment implement .show(getFragmentManager(), TAG); } break; + case R.id.detail_controls_download: + if (!PermissionHelper.checkStoragePermissions(activity)) { + return; + } + + try { + DownloadDialog downloadDialog = + DownloadDialog.newInstance(currentInfo, + sortedStreamVideosList, + actionBarHandler.getSelectedVideoStream()); + downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); + } catch (Exception e) { + Toast.makeText(activity, + R.string.could_not_setup_download_menu, + Toast.LENGTH_LONG).show(); + e.printStackTrace(); + } + break; case R.id.detail_uploader_root_layout: if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { Log.w(TAG, "Can't open channel because we got no channel URL"); @@ -438,6 +457,7 @@ public class VideoDetailFragment extends BaseStateFragment implement detailControlsBackground = rootView.findViewById(R.id.detail_controls_background); detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup); detailControlsAddToPlaylist = rootView.findViewById(R.id.detail_controls_playlist_append); + detailControlsDownload = rootView.findViewById(R.id.detail_controls_download); appendControlsDetail = rootView.findViewById(R.id.touch_append_detail); videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); @@ -489,6 +509,7 @@ public class VideoDetailFragment extends BaseStateFragment implement detailControlsBackground.setOnClickListener(this); detailControlsPopup.setOnClickListener(this); detailControlsAddToPlaylist.setOnClickListener(this); + detailControlsDownload.setOnClickListener(this); relatedStreamExpandButton.setOnClickListener(this); detailControlsBackground.setLongClickable(true); @@ -508,19 +529,16 @@ public class VideoDetailFragment extends BaseStateFragment implement context.getResources().getString(R.string.enqueue_on_popup) }; - final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - switch (i) { - case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); - break; - case 1: - NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item)); - break; - default: - break; - } + final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item)); + break; + default: + break; } }; @@ -528,21 +546,19 @@ public class VideoDetailFragment extends BaseStateFragment implement } private View.OnTouchListener getOnControlsTouchListener() { - return new View.OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent motionEvent) { - if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false; + return (View view, MotionEvent motionEvent) -> { + view.performClick(); + if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false; - if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { - animateView(appendControlsDetail, true, 250, 0, new Runnable() { - @Override - public void run() { - animateView(appendControlsDetail, false, 1500, 1000); - } - }); - } - return false; + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + animateView(appendControlsDetail, true, 250, 0, new Runnable() { + @Override + public void run() { + animateView(appendControlsDetail, false, 1500, 1000); + } + }); } + return false; }; } @@ -615,18 +631,9 @@ public class VideoDetailFragment extends BaseStateFragment implement private static void showInstallKoreDialog(final Context context) { final AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setMessage(R.string.kore_not_found) - .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - NavigationHelper.installKore(context); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }); + .setPositiveButton(R.string.install, (DialogInterface dialog, int which) -> + NavigationHelper.installKore(context)) + .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {}); builder.create().show(); } @@ -636,41 +643,19 @@ public class VideoDetailFragment extends BaseStateFragment implement actionBarHandler.setupStreamList(sortedStreamVideosList, spinnerToolbar); actionBarHandler.setOnShareListener(selectedStreamId -> shareUrl(info.name, info.url)); - actionBarHandler.setOnOpenInBrowserListener(new ActionBarHandler.OnActionListener() { - @Override - public void onActionSelected(int selectedStreamId) { - openUrlInBrowser(info.getUrl()); + actionBarHandler.setOnOpenInBrowserListener((int selectedStreamId)-> + openUrlInBrowser(info.getUrl())); + + actionBarHandler.setOnPlayWithKodiListener((int selectedStreamId) -> { + try { + NavigationHelper.playWithKore(activity, Uri.parse( + info.getUrl().replace("https", "http"))); + } catch (Exception e) { + if(DEBUG) Log.i(TAG, "Failed to start kore", e); + showInstallKoreDialog(activity); } }); - actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() { - @Override - public void onActionSelected(int selectedStreamId) { - try { - NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http"))); - } catch (Exception e) { - if(DEBUG) Log.i(TAG, "Failed to start kore", e); - showInstallKoreDialog(activity); - } - } - }); - - actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() { - @Override - public void onActionSelected(int selectedStreamId) { - if (!PermissionHelper.checkStoragePermissions(activity)) { - return; - } - - try { - DownloadDialog downloadDialog = DownloadDialog.newInstance(info, sortedStreamVideosList, selectedStreamId); - downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); - } catch (Exception e) { - Toast.makeText(activity, R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show(); - e.printStackTrace(); - } - } - }); } /*////////////////////////////////////////////////////////////////////////// @@ -777,20 +762,14 @@ public class VideoDetailFragment extends BaseStateFragment implement currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(@NonNull StreamInfo result) throws Exception { - isLoading.set(false); - currentInfo = result; - showContentWithAnimation(120, 0, 0); - handleResult(result); - } - }, new Consumer() { - @Override - public void accept(@NonNull Throwable throwable) throws Exception { - isLoading.set(false); - onError(throwable); - } + .subscribe((@NonNull StreamInfo result) -> { + isLoading.set(false); + currentInfo = result; + showContentWithAnimation(120, 0, 0); + handleResult(result); + }, (@NonNull Throwable throwable) -> { + isLoading.set(false); + onError(throwable); }); } @@ -884,9 +863,7 @@ public class VideoDetailFragment extends BaseStateFragment implement } disposables.add(Single.just(descriptionHtml) - .map(new Function() { - @Override - public Spanned apply(@io.reactivex.annotations.NonNull String description) throws Exception { + .map((@io.reactivex.annotations.NonNull String description) -> { Spanned parsedDescription; if (Build.VERSION.SDK_INT >= 24) { parsedDescription = Html.fromHtml(description, 0); @@ -895,16 +872,12 @@ public class VideoDetailFragment extends BaseStateFragment implement parsedDescription = Html.fromHtml(description); } return parsedDescription; - } }) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(@io.reactivex.annotations.NonNull Spanned spanned) throws Exception { + .subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> { videoDescriptionView.setText(spanned); videoDescriptionView.setVisibility(View.VISIBLE); - } })); } @@ -1131,14 +1104,11 @@ public class VideoDetailFragment extends BaseStateFragment implement } public void onBlockedByGemaError() { - thumbnailBackgroundButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { + thumbnailBackgroundButton.setOnClickListener((View v) -> { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setData(Uri.parse(getString(R.string.c3s_url))); startActivity(intent); - } }); showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 2d39d3d70..d47a2215b 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -354,6 +354,24 @@ android:paddingTop="6dp" android:text="@string/controls_popup_title" android:textSize="12sp"/> + + + - - Open in popup mode Share Download + Download stream file. Search Settings Did you mean: %1$s ? From ba0be665ae9b68b4d43fe8f41cb9d07f91cf943e Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 12 Feb 2018 00:43:12 +0100 Subject: [PATCH 49/52] fixed issues from prevoius merge --- .../newpipe/fragments/detail/VideoDetailFragment.java | 9 ++------- app/src/main/res/layout/fragment_video_detail.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index daf422516..206f6edd8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -547,16 +547,11 @@ public class VideoDetailFragment extends BaseStateFragment implement private View.OnTouchListener getOnControlsTouchListener() { return (View view, MotionEvent motionEvent) -> { - view.performClick(); if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false; if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { - animateView(appendControlsDetail, true, 250, 0, new Runnable() { - @Override - public void run() { - animateView(appendControlsDetail, false, 1500, 1000); - } - }); + animateView(appendControlsDetail, true, 250, 0, () -> + animateView(appendControlsDetail, false, 1500, 1000)); } return false; }; diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index d47a2215b..330fb34da 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -364,7 +364,7 @@ android:background="?attr/selectableItemBackground" android:clickable="true" android:focusable="true" - android:contentDescription="@string/controls_download_desg" + android:contentDescription="@string/controls_download_desc" android:drawableTop="?attr/download" android:gravity="center" android:paddingBottom="6dp" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c04cb802..ea8f0fce8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ Open in popup mode Share Download - Download stream file. + Download stream file. Search Settings Did you mean: %1$s ? From 738e2ac3443fa256cbdb7c452cf28c93bf9dfdd5 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 12 Feb 2018 19:44:35 +0100 Subject: [PATCH 50/52] merge RouterActivity and RouterVideoActivity --- app/src/main/AndroidManifest.xml | 113 ++--- .../org/schabi/newpipe/RouterActivity.java | 460 ++++++++++++++++-- .../schabi/newpipe/RouterPlayerActivity.java | 413 ---------------- .../ic_info_outline_black_24dp.png | Bin 0 -> 487 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 485 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 323 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 320 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 640 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 655 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 940 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 953 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 1256 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 1279 bytes app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 2 + 17 files changed, 465 insertions(+), 526 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java create mode 100644 app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bc3dc62e6..f286ee76c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -122,8 +122,12 @@ + + @@ -169,6 +173,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -176,17 +215,8 @@ - - - - - - - - - - + @@ -195,68 +225,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 8aaa248dd..0586a86be 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -1,51 +1,80 @@ package org.schabi.newpipe; +import android.app.IntentService; +import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; +import android.os.PersistableBundle; +import android.preference.PreferenceManager; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; import android.widget.Toast; +import org.schabi.newpipe.extractor.Info; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.StreamingService.LinkType; +import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.playlist.ChannelPlayQueue; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlaylistPlayQueue; +import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ThemeHelper; +import java.io.Serializable; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import icepick.Icepick; import icepick.State; import io.reactivex.Observable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; -/* - * Copyright (C) Christian Schabesberger 2017 - * RouterActivity.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 . - */ +import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; /** - * This Acitivty is designed to route share/open intents to the specified service, and - * to the part of the service which can handle the url. + * Get the url from the intent and open it in the chosen preferred player */ public class RouterActivity extends AppCompatActivity { @State + protected int currentServiceId = -1; + private StreamingService currentService; + @State + protected LinkType currentLinkType; + @State + protected int selectedRadioPosition = -1; + protected int selectedPreviously = -1; + protected String currentUrl; protected CompositeDisposable disposables = new CompositeDisposable(); @@ -62,6 +91,10 @@ public class RouterActivity extends AppCompatActivity { finish(); } } + + setTheme(ThemeHelper.isLightThemeSelected(this) + ? R.style.RouterActivityThemeLight + : R.style.RouterActivityThemeDark); } @Override @@ -73,25 +106,43 @@ public class RouterActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); + handleUrl(currentUrl); } - protected void handleUrl(String url) { - disposables.add(Observable - .fromCallable(() -> NavigationHelper.getIntentByLink(this, url)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(intent -> { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); + @Override + protected void onDestroy() { + super.onDestroy(); - finish(); - }, this::handleError) - ); + disposables.clear(); } - protected void handleError(Throwable error) { + private void handleUrl(String url) { + disposables.add(Observable + .fromCallable(() -> { + if (currentServiceId == -1) { + currentService = NewPipe.getServiceByUrl(url); + currentServiceId = currentService.getServiceId(); + currentLinkType = currentService.getLinkTypeByUrl(url); + currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); + } else { + currentService = NewPipe.getService(currentServiceId); + } + + return currentLinkType != LinkType.NONE; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + if (result) { + onSuccess(); + } else { + onError(); + } + }, this::handleError)); + } + + private void handleError(Throwable error) { error.printStackTrace(); if (error instanceof ExtractionException) { @@ -103,11 +154,339 @@ public class RouterActivity extends AppCompatActivity { finish(); } - @Override - protected void onDestroy() { - super.onDestroy(); + private void onError() { + Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + finish(); + } - disposables.clear(); + protected void onSuccess() { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + + if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { + Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); + finish(); + return; + } + + // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) + if (currentService == ServiceList.SoundCloud.getService()) { + handleChoice(getString(R.string.background_player_key)); + return; + } + + final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); + final String alwaysAskKey = getString(R.string.always_ask_player_key); + + if (playerChoiceKey.equals(alwaysAskKey)) { + showDialog(); + } else { + handleChoice(playerChoiceKey); + } + } + + private void showDialog() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, + ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); + + LayoutInflater inflater = LayoutInflater.from(themeWrapper); + final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); + final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); + + final AdapterChoiceItem[] choices = { + new AdapterChoiceItem(getString(R.string.info_screen_key), getString(R.string.info_screen), + resolveResourceIdFromAttr(themeWrapper, R.attr.info)), + new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.play)), + new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), + new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) + }; + + final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { + final int indexOfChild = radioGroup.indexOfChild( + radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); + final AdapterChoiceItem choice = choices[indexOfChild]; + + handleChoice(choice.key); + + if (which == DialogInterface.BUTTON_POSITIVE) { + preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); + } + }; + + final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) + .setTitle(R.string.preferred_player_share_menu_title) + .setView(radioGroup) + .setCancelable(true) + .setNegativeButton(R.string.just_once, dialogButtonsClickListener) + .setPositiveButton(R.string.always, dialogButtonsClickListener) + .setOnDismissListener((dialog) -> finish()) + .create(); + + alertDialog.setOnShowListener(dialog -> { + setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); + }); + + radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); + final View.OnClickListener radioButtonsClickListener = v -> { + final int indexOfChild = radioGroup.indexOfChild(v); + if (indexOfChild == -1) return; + + selectedPreviously = selectedRadioPosition; + selectedRadioPosition = indexOfChild; + + if (selectedPreviously == selectedRadioPosition) { + handleChoice(choices[selectedRadioPosition].key); + } + }; + + int id = 12345; + for (AdapterChoiceItem item : choices) { + final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); + radioButton.setText(item.description); + radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); + radioButton.setChecked(false); + radioButton.setId(id++); + radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + radioButton.setOnClickListener(radioButtonsClickListener); + radioGroup.addView(radioButton); + } + + if (selectedRadioPosition == -1) { + final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); + if (!TextUtils.isEmpty(lastSelectedPlayer)) { + for (int i = 0; i < choices.length; i++) { + AdapterChoiceItem c = choices[i]; + if (lastSelectedPlayer.equals(c.key)) { + selectedRadioPosition = i; + break; + } + } + } + } + + selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); + if (selectedRadioPosition != -1) { + ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); + } + selectedPreviously = selectedRadioPosition; + + alertDialog.show(); + } + + private void setDialogButtonsState(AlertDialog dialog, boolean state) { + final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); + final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + if (negativeButton == null || positiveButton == null) return; + + negativeButton.setEnabled(state); + positiveButton.setEnabled(state); + } + + private void handleChoice(final String playerChoiceKey) { + if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); + } + + if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { + PermissionHelper.showPopupEnablementToast(this); + finish(); + return; + } + + // stop and bypass FetcherService if InfoScreen was selected since + // StreamDetailFragment can fetch data itself + if(playerChoiceKey.equals(getString(R.string.info_screen_key))) { + disposables.add(Observable + .fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(intent -> { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + + finish(); + }, this::handleError) + ); + return; + } + + final Intent intent = new Intent(this, FetcherService.class); + intent.putExtra(FetcherService.KEY_CHOICE, + new Choice(currentService.getServiceId(), + currentLinkType, + currentUrl, + playerChoiceKey)); + startService(intent); + + finish(); + } + + private static class AdapterChoiceItem { + final String description, key; + @DrawableRes + final int icon; + + AdapterChoiceItem(String key, String description, int icon) { + this.description = description; + this.key = key; + this.icon = icon; + } + } + + private static class Choice implements Serializable { + final int serviceId; + final String url, playerChoice; + final LinkType linkType; + + Choice(int serviceId, LinkType linkType, String url, String playerChoice) { + this.serviceId = serviceId; + this.linkType = linkType; + this.url = url; + this.playerChoice = playerChoice; + } + + @Override + public String toString() { + return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Service Fetcher + //////////////////////////////////////////////////////////////////////////*/ + + public static class FetcherService extends IntentService { + + private static final int ID = 456; + public static final String KEY_CHOICE = "key_choice"; + private Disposable fetcher; + + public FetcherService() { + super(FetcherService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + startForeground(ID, createNotification().build()); + } + + @Override + protected void onHandleIntent(@Nullable Intent intent) { + if (intent == null) return; + + final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); + if (!(serializable instanceof Choice)) return; + Choice playerChoice = (Choice) serializable; + handleChoice(playerChoice); + } + + public void handleChoice(Choice choice) { + Single single = null; + UserAction userAction = UserAction.SOMETHING_ELSE; + + switch (choice.linkType) { + case STREAM: + single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_STREAM; + break; + case CHANNEL: + single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_CHANNEL; + break; + case PLAYLIST: + single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_PLAYLIST; + break; + } + + + if (single != null) { + final UserAction finalUserAction = userAction; + final Consumer resultHandler = getResultHandler(choice); + fetcher = single + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(info -> { + resultHandler.accept(info); + if (fetcher != null) fetcher.dispose(); + }, throwable -> ExtractorHelper.handleGeneralException(this, + choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); + } + } + + public Consumer getResultHandler(Choice choice) { + return info -> { + final String videoPlayerKey = getString(R.string.video_player_key); + final String backgroundPlayerKey = getString(R.string.background_player_key); + final String popupPlayerKey = getString(R.string.popup_player_key); + + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); + + PlayQueue playQueue; + String playerChoice = choice.playerChoice; + + if (info instanceof StreamInfo) { + if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { + NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { + NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { + NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); + + } else { + playQueue = new SinglePlayQueue((StreamInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + NavigationHelper.playOnMainPlayer(this, playQueue); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); + } + } + } + + if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { + playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + NavigationHelper.playOnMainPlayer(this, playQueue); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.playOnBackgroundPlayer(this, playQueue); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.playOnPopupPlayer(this, playQueue); + } + } + }; + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopForeground(true); + if (fetcher != null) fetcher.dispose(); + } + + private NotificationCompat.Builder createNotification() { + return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) + .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); + } } /*////////////////////////////////////////////////////////////////////////// @@ -119,9 +498,9 @@ public class RouterActivity extends AppCompatActivity { * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for * more details. */ - protected final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; + private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; - protected String getUrl(Intent intent) { + private String getUrl(Intent intent) { // first gather data and find service String videoUrl = null; if (intent.getData() != null) { @@ -137,7 +516,7 @@ public class RouterActivity extends AppCompatActivity { return videoUrl; } - protected String removeHeadingGibberish(final String input) { + private String removeHeadingGibberish(final String input) { int start = 0; for (int i = input.indexOf("://") - 1; i >= 0; i--) { if (!input.substring(i, i + 1).matches("\\p{L}")) { @@ -148,7 +527,7 @@ public class RouterActivity extends AppCompatActivity { return input.substring(start, input.length()); } - protected String trim(final String input) { + private String trim(final String input) { if (input == null || input.length() < 1) { return input; } else { @@ -188,5 +567,4 @@ public class RouterActivity extends AppCompatActivity { } return result.toArray(new String[result.size()]); } - } diff --git a/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java deleted file mode 100644 index 7196e413d..000000000 --- a/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java +++ /dev/null @@ -1,413 +0,0 @@ -package org.schabi.newpipe; - -import android.app.IntentService; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.PersistableBundle; -import android.preference.PreferenceManager; -import android.support.annotation.DrawableRes; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; -import android.support.v7.app.AlertDialog; -import android.text.TextUtils; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.RadioButton; -import android.widget.RadioGroup; -import android.widget.Toast; - -import org.schabi.newpipe.extractor.Info; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.ServiceList; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.StreamingService.LinkType; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.playlist.PlaylistInfo; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.playlist.ChannelPlayQueue; -import org.schabi.newpipe.playlist.PlayQueue; -import org.schabi.newpipe.playlist.PlaylistPlayQueue; -import org.schabi.newpipe.playlist.SinglePlayQueue; -import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.ThemeHelper; - -import java.io.Serializable; -import java.util.Arrays; - -import icepick.State; -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; - -import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; - -/** - * Get the url from the intent and open it in the chosen preferred player - */ -public class RouterPlayerActivity extends RouterActivity { - - @State - protected int currentServiceId = -1; - private StreamingService currentService; - @State - protected LinkType currentLinkType; - @State - protected int selectedRadioPosition = -1; - protected int selectedPreviously = -1; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { - super.onCreate(savedInstanceState, persistentState); - setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); - } - - @Override - protected void handleUrl(String url) { - disposables.add(Observable - .fromCallable(() -> { - if (currentServiceId == -1) { - currentService = NewPipe.getServiceByUrl(url); - currentServiceId = currentService.getServiceId(); - currentLinkType = currentService.getLinkTypeByUrl(url); - currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); - } else { - currentService = NewPipe.getService(currentServiceId); - } - - return currentLinkType != LinkType.NONE; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result) { - onSuccess(); - } else { - onError(); - } - }, this::handleError)); - } - - protected void onError() { - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); - finish(); - } - - protected void onSuccess() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - - if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { - Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); - finish(); - return; - } - - // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) - if (currentService == ServiceList.SoundCloud.getService()) { - handleChoice(getString(R.string.background_player_key)); - return; - } - - final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); - final String alwaysAskKey = getString(R.string.always_ask_player_key); - - if (playerChoiceKey.equals(alwaysAskKey)) { - showDialog(); - } else { - handleChoice(playerChoiceKey); - } - } - - private void showDialog() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, - ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); - - LayoutInflater inflater = LayoutInflater.from(themeWrapper); - final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); - final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); - - final AdapterChoiceItem[] choices = { - new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.play)), - new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), - new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) - }; - - final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { - final int indexOfChild = radioGroup.indexOfChild(radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); - final AdapterChoiceItem choice = choices[indexOfChild]; - - handleChoice(choice.key); - - if (which == DialogInterface.BUTTON_POSITIVE) { - preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); - } - }; - - final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) - .setTitle(R.string.preferred_player_share_menu_title) - .setView(radioGroup) - .setCancelable(true) - .setNegativeButton(R.string.just_once, dialogButtonsClickListener) - .setPositiveButton(R.string.always, dialogButtonsClickListener) - .setOnDismissListener((dialog) -> finish()) - .create(); - - alertDialog.setOnShowListener(dialog -> { - setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); - }); - - radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); - final View.OnClickListener radioButtonsClickListener = v -> { - final int indexOfChild = radioGroup.indexOfChild(v); - if (indexOfChild == -1) return; - - selectedPreviously = selectedRadioPosition; - selectedRadioPosition = indexOfChild; - - if (selectedPreviously == selectedRadioPosition) { - handleChoice(choices[selectedRadioPosition].key); - } - }; - - int id = 12345; - for (AdapterChoiceItem item : choices) { - final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); - radioButton.setText(item.description); - radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); - radioButton.setChecked(false); - radioButton.setId(id++); - radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - radioButton.setOnClickListener(radioButtonsClickListener); - radioGroup.addView(radioButton); - } - - if (selectedRadioPosition == -1) { - final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); - if (!TextUtils.isEmpty(lastSelectedPlayer)) { - for (int i = 0; i < choices.length; i++) { - AdapterChoiceItem c = choices[i]; - if (lastSelectedPlayer.equals(c.key)) { - selectedRadioPosition = i; - break; - } - } - } - } - - selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); - if (selectedRadioPosition != -1) { - ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); - } - selectedPreviously = selectedRadioPosition; - - alertDialog.show(); - } - - private void setDialogButtonsState(AlertDialog dialog, boolean state) { - final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); - final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); - if (negativeButton == null || positiveButton == null) return; - - negativeButton.setEnabled(state); - positiveButton.setEnabled(state); - } - - private void handleChoice(final String playerChoiceKey) { - if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { - PreferenceManager.getDefaultSharedPreferences(this).edit() - .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); - } - - if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { - PermissionHelper.showPopupEnablementToast(this); - finish(); - return; - } - - final Intent intent = new Intent(this, FetcherService.class); - intent.putExtra(FetcherService.KEY_CHOICE, new Choice(currentService.getServiceId(), currentLinkType, currentUrl, playerChoiceKey)); - startService(intent); - - finish(); - } - - private static class AdapterChoiceItem { - final String description, key; - @DrawableRes - final int icon; - - AdapterChoiceItem(String key, String description, int icon) { - this.description = description; - this.key = key; - this.icon = icon; - } - } - - private static class Choice implements Serializable { - final int serviceId; - final String url, playerChoice; - final LinkType linkType; - - Choice(int serviceId, LinkType linkType, String url, String playerChoice) { - this.serviceId = serviceId; - this.linkType = linkType; - this.url = url; - this.playerChoice = playerChoice; - } - - @Override - public String toString() { - return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Service Fetcher - //////////////////////////////////////////////////////////////////////////*/ - - public static class FetcherService extends IntentService { - - private static final int ID = 456; - public static final String KEY_CHOICE = "key_choice"; - private Disposable fetcher; - - public FetcherService() { - super(FetcherService.class.getSimpleName()); - } - - @Override - public void onCreate() { - super.onCreate(); - startForeground(ID, createNotification().build()); - } - - @Override - protected void onHandleIntent(@Nullable Intent intent) { - if (intent == null) return; - - final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); - if (!(serializable instanceof Choice)) return; - Choice playerChoice = (Choice) serializable; - handleChoice(playerChoice); - } - - public void handleChoice(Choice choice) { - Single single = null; - UserAction userAction = UserAction.SOMETHING_ELSE; - - switch (choice.linkType) { - case STREAM: - single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_STREAM; - break; - case CHANNEL: - single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_CHANNEL; - break; - case PLAYLIST: - single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_PLAYLIST; - break; - } - - - if (single != null) { - final UserAction finalUserAction = userAction; - final Consumer resultHandler = getResultHandler(choice); - fetcher = single - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(info -> { - resultHandler.accept(info); - if (fetcher != null) fetcher.dispose(); - }, throwable -> ExtractorHelper.handleGeneralException(this, - choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); - } - } - - public Consumer getResultHandler(Choice choice) { - return info -> { - final String videoPlayerKey = getString(R.string.video_player_key); - final String backgroundPlayerKey = getString(R.string.background_player_key); - final String popupPlayerKey = getString(R.string.popup_player_key); - - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); - - PlayQueue playQueue; - String playerChoice = choice.playerChoice; - - if (info instanceof StreamInfo) { - if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { - NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); - - } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { - NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); - - } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { - NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); - - } else { - playQueue = new SinglePlayQueue((StreamInfo) info); - - if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); - } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); - } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); - } - } - } - - if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { - playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); - - if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); - } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.playOnBackgroundPlayer(this, playQueue); - } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.playOnPopupPlayer(this, playQueue); - } - } - }; - } - - @Override - public void onDestroy() { - super.onDestroy(); - stopForeground(true); - if (fetcher != null) fetcher.dispose(); - } - - private NotificationCompat.Builder createNotification() { - return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) - .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); - } - } -} diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4b5ab06e19d61515cf3a7395db43a81bb30a8203 GIT binary patch literal 487 zcmVS-P=?%O3jv@aJa6HGW<0ceyC!`rEoc~?j{U=`ic|b z9N*QPsMv*+MQv@1i8kR%)UIh1`=XoCqqIYBOKErvea{GWZ!y>;D~RpT`=%tUi9cM8 zjMJd-5IZXpx*;AQpQG66NVp4e3Hh{Q=T<@);yl@>u@F125nkF002ovPDHLkV1kFY-~<2w literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c7b1113cfef22bcec86ead7ae67be12326276cab GIT binary patch literal 485 zcmVW+#V#f>U=9~3v zy5k#|%`o-bRp5gvNWBjKU}@RX_o(#cOX<;tO*2A$>f4JBeW-BBCp2Wv4dAi~>H>ZU-{cI? zse=}k;97{XqEJ|gdQ)z}NLv+h2nN-m6riYd=e>Y~D%A1;c7!A-*ac%VF|-AqWJ2?3 zQF6~&s5ci5fY}sT-yed|rRg0)~VyRe(RxP5| ziKSu?+iDTDUGbuL5L2~?+LgFdoQk=|C}Pxj7RQRtyXcJ~MwepHsvL@iJ?s4SCH8$Y z6AQgYt#Ro~-1uQ)CSvZwj!tLSm1hyxCe}LeCL$stVz6(6)`@$smX==JIoA4R{{X3C VfQx`Hg^K_H002ovPDHLkV1oM>kN^Mx literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..353e064951788a29a64eef439284fd97e904f374 GIT binary patch literal 320 zcmV-G0l)rT_h)IX%8`!xx#?O($dW=BI;pgAmAMoJ&FVmpak^_M&10n8om%>Hp0CB z>RbH6BfRjxN9M~)&M4Ia$ETJBLQ^<<<3g$He5hLhidW{F&l zq78;P`5|=ls>YxRGDkGO4)Ha@UK+s{*1}DYDDe@&3c1kJ7>fuhJobWT>>{`#?O7?v zaJd6ic&m6Kxyn*Ci7xy1O7D6yV<~Ad#ZoSbb%oCVPWUuVr%YQDrkfL?gZZ)JBOfu SH&bZ<0000o#88fEO5na|OlO{3G z*yIPEVO{WsIjnKnVT>2DMzkql!ApMPMO=L<#3I9oI}LubOO+CNvScYxqQ)LKcQkDr zB9h}Xp02N~GG)+JIYyNnkc)PDZOnzu*#; zB8R%~1(i4Rl0>e>Eb#WLtxfOV(#vvo;8MrfX8# zOJwj-6bRC^RW#U^^>`Uk%GqUlp=9>%y-#&$qZ(w=qV2=#%SNueJg~}R_Q4VL}5`+@9~aM z91c`Iph*TZkfp^?^`Dp^0y)$Ij}vN48)8+LhbiruA|j3sO47|fb;`I*GKou>Ivq)^ z4u=V1QQ;ad;<{u73-W9;3T-}Qiy5qW$_D4g=4Y%kOJdS2(WFC22NX4g$Gh;r2I3UU zC}4AjSPu(ixIwI-i~@=>;)?%*fd#|^BPhd-@r0NU2I`1i6z1DU)PjK~qBKyTf@lN- zCy2s8fg++640I7Wl<`GYIEcGoz(-^U3JfE>zYypm@_+0U8C+z9*+p(5%4UK1zv(Js zZ=k^ch-xrUL_9KT9C*VMq7V#ZxJHzW0~N$&*uzfo7172v3S>D)tf9gJQ}~FgQJ{|K z@irKcxri5LQU~VfAvRG_fjnJAhY3`QO>%*_Wjs2dSVB8ak(i`~wix%~<)U?%MJ3oA z7ie3kxPZkW+6y&o6w%94r-ydHP_ma_Nr*Gbj78j570v p!{d$?4XPC6k!Bt+*lWOGzX87wDYF(rDzN|n002ovPDHLkV1ir08p{9x literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3847a9fe74774d3593dd694cff48ccca13cdec5a GIT binary patch literal 940 zcmV;d15^BoP)mdcfcOtJM6uO{+z8D@UxkKMBq1+Buz@6?P#P0jG1VJo zr4$sRhK5k2Z=fXRN~6nW@y^{1KD~XI=}gYdoFjX&pWp3sm>a@9yM-m0@&}6isAEB(Qj;>qRst`p9~=6q zAu7LO=u+TQ_K7Dd@dg8*_ZUX>5!&WBSSti4^<9Bi7`TY2+y<+KU=~psWnX%OzQeO# z$Y%%reeR=-{wJ-1^+Iq9eT6F#>jwG=^_&zcppV__#9BjN7F^M%z)NCy=&SoFTsf2ppC<<**2_ZI5@SJo9Db@pv9Dwel>@Q2Y|A^pR+ zXm)h0I4$&8t(hXy6*cz3S)t(J`aO{jqc{F(SB1>4AMjd@E;uWs|KhZ2{;};ky2AQ2 z)rxDxD$`NJH=PukQuA$!?4zc}-&3$&Xn>Du{t5dULsafqFEod!oWp?rpGSuiRtueB z&+LQNE+8tK3|cF6gtuz;3+FNP08y#Yj|~U-15sJR*io8@$}eQs#}cB_phN-_NT5>I zKj|$a=v*bC1q7WMgQRsN@Unzi63g)%L1&Ybq;ZC~2s(dqkQ990An0_M#V3gY=Ga5f z`5TXvN})=^4!0;^!e^R~NT^Pc418W7q3m*(v-Aa=K4XNz?fnc_RXp{J*(OZ^8ac7(2N>$@cY O0000+=u1!Si^=c~o6pU56D8MW!jS z$u@O7JnC$-Nr`E0Z~~QViq1F4UDojk)8rj4KiUtv${KCrYEfp?UeGn(lf*h#%mwiZO?K>;SqC6p{w@`EooS#EOw+c}UBbluHuSO9kD3{Dexd zGgQ&NCewM)0=iXlsD$fh4c%;~LATKTP7f-@j@01>+c-g20r3E3BuF*`h>_@^ zHN+KB7e-Jm@Z>j$y%;(Bn$f*7_b_YN7RCYJVeH7P%ol+c!LiAkL*xA)X9%M!t}A%e=*t&auG}M zUL7k|gPtKKgM)4$K3fdZU#`Cd2c5t}oVOJe{qaNg4l!>nXc19Cg$22YJsc}RIU0!D zQ6KVKhxdR-j0Rnxtv}Fl zg03K-O24t7lYB&c=W=+E>@fn$hQ5wD$_oT6qv8ggqJe-vjQr`djDR|Y=pflR=t-%+ z(kr36+3A;_^XSU-C(b#^3v@H6PJ=SMLRaN%qMQqSME8wcL$0HkK4xj7 z`v)iS)q*bxcDYYB<~wVOFW}3`r@4H|@fyE97P!cfkP~%@MH=|M#7TQSm0^y9)p&}_ z7{lc0-Cv$z#&CH?jf4HhF*6r<<1|RC&P|h7oP+#Da^G2|VEz(#l(!^NVZ`1w`3S4D zh|A+Sm+k)}L4iBGp-Gr;RJbh%++L9EIM5D%liv*A40h bb%Xu~a~|hNXxfgr00000NkvXXu0mjfV)nVh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c1e2a03a4bc540419ce1165d5dedd8d5345fdec2 GIT binary patch literal 1256 zcmVP)W$LQv#I8j8T_{yf~=m$~;IXzu5?eh%l+bI*O5LnIQ3L?S-g zNEKIT<0XBJGshx}%rQL6n2^)65BytPpur(aAq3mzkx5L*!vp z0a@Ola>x)hq%a~$J)clM5drmH23z#q<|xU<0`*qKPGo7Si~n z%wqBU&0hSlj{%E?RDuuU7MJcXB=Jj%b`&x_e(Xp;ExUAT@Y|(jm(qjWZ(B;_Uor&Y z6!R#Nv*chaGsx+GPf(5^TbQOC&z>U}Plh0!MlPf-JUW9CsUc{WQ6klNkmN0L=W$Hb zI`=E``6_{H*O2>uGL!}iknh%)aH)U^vHJHyC=I|j;!5~HKCWcN1R5Z;2B2BgU%-W! zczBp#L#PcDi)Yo>;Taa!Y@Ptv;<}7l4r)8NeQGlwNF& zYb9ky8`z|cGKQ@=tt4)=frR*F&DNq;5;NLBOdDlk)i+CZzQKZh$R zK8n3)UuX^N7uDk=aj8xWGC(Sn1_~Jx)mdCiFf4{?52b-lQTv8C9#o@5E)leAD3OzR z)Fn^Gaf%@QAdedG1kbj}b8XCXgrFSb3rcL@n*sbhFEP*Qfd2Kr_;5}_cz_aF;&K3M ze+ebhM9$XHgOZ`06n-hB6D318YwImEMS(?`<%O>vfHEvXL&vNpLe#!|zDLp9b&_k9T z*x5;~c-Pf0-68_APCn*}yr+}vWZ1(d62yp+WD|SHaD&H;+SWI+mO!0l8Wm6`sU`&F zbfE&s6E+i?YKBohW{8u7EWu?ypnS+US>hOx&jq=;y2>EuNMckT2YAG1l*`QUkZ)PT z3X$L_jr@gjkUknXOq>;|goD)5%rkl!W0nOLm}QJ!o^qco2Ph^Ii9{k1-~0zqnGDmx SqDLYC0000 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3a82cab3b4f2cfe77333cee6121d867353753d5e GIT binary patch literal 1279 zcmVXA;DPbB};p_0dfAo z$vAy9#+6S9#ELYJ@(Lqa#%C_802gIqArYp#f(wN zJ5D-2x}I^0EQMtFA?SeRyu{vHx_o|^)~`kO7MeJbsRD=Aw#N4d<{5Bs2vT z(7+gW9I~nSN2#E6lJC{kK0$L^4urow?@d3Y+FGmL|;O)mE-^F4Bc9HL2 zQm7ENoloT3SxmqprfnYoS5!#bXp=BSqyK>0=yIe`A#Mj2x&gld?d0n~g}ejgYx5J3 z2Egy^UyUJ3s{D( z);XXRX&hy6FOb$b2OLD|GaAs3)ae{>38~9yz)_?g=YYFNn~esfkZw5#3?r>T`I=7o z+A5@n&H)oh4MqdzBfWAC_=prY8jwJmaSq5K#f%2TkiLBX0pHn2D2X)lKlmdaMq2s3 zeU08h+F~>yg>=(7pcm8C~mULv(P2h^i$HWskl&kK!W z1ZkhKfKETJl!_jtL1O{;k-FUlY(=tYHWIKD-3E67NoJ5v83{OpG)>%HfT9m6OI73n zbC^cDf^r+M9^H}115Tj(#eF~-qezp~L>f@f6jFv+?gJDZ=*~qN(2H(2%1b~cZ;&k3 zL>90X-2_Q*0g5hkkEx6#pqf#1X_VK11mozgMH0}DZj8A107V=5_M<}HVe)PBd(>b6 zoyB%ki2Dr--3^rAfQ4kyePSajq;2Lix=9xJ4^Xs&O~Q_l{`EhTudUbvPZ!B&aUg`x zzD2%ssA2=k8N$vrDp3KgnttpIGQ0QyMI9OJJZ23lfURW|J0sKtb?mZ<=h(66p%zud z>bZoyXEX+R@O5c{1jurnYLt)7;RI9Id&-iaj~+Mi1PVr+=0}fjTW6S}kP#ZgIQ(7D z5MMJynq@Bjr=gSke9a&=VI99OrxzzL=;tUYRxzIhF=8Z{&ni+J<2o;Ka+cZQUVzxi z6k(a9Es85BjSLWm8!U+K5>OlC1Zk9QQD4JKaDdkYVS+SqGZ*11*vk`&kzqGU(^nB& zxXOFHn&t{WGwWNq44PmQXZZ&=hUsR5_|TXQ5UXPyo%C>vhrD8jFMMH!S3Kk<=SkB- pEh@+ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..8f33f9297 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -155,6 +155,7 @@ @string/always_ask_player_key preferred_player_last_selected + info_screen video_player background_player popup_player diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea8f0fce8..0e9f6e7ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ Channel unsubscribed Unable to change subscription Unable to update subscription + Info Screen Main Subscriptions diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b16958ae6..dcf8f9268 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,6 +18,7 @@ @drawable/ic_thumb_up_black_24dp @drawable/ic_thumb_down_black_24dp + @drawable/ic_info_outline_black_24dp @drawable/ic_headset_black_24dp @drawable/ic_delete_sweep_white_24dp @drawable/ic_file_download_black_24dp @@ -69,6 +70,7 @@ @drawable/ic_thumb_up_white_24dp @drawable/ic_thumb_down_white_24dp @drawable/ic_headset_white_24dp + @drawable/ic_info_outline_white_24dp @drawable/ic_delete_sweep_black_24dp @drawable/ic_file_download_white_24dp @drawable/ic_share_white_24dp From cbfe91f36f626fad343d4475f7aefd67e8f28901 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 12 Feb 2018 19:44:35 +0100 Subject: [PATCH 51/52] merge RouterActivity and RouterVideoActivity change share title fixed compatiblity issue rename info_screen to show_info --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 113 ++--- .../org/schabi/newpipe/RouterActivity.java | 460 ++++++++++++++++-- .../schabi/newpipe/RouterPlayerActivity.java | 413 ---------------- .../newpipe/fragments/MainFragment.java | 2 +- .../settings/ContentSettingsFragment.java | 4 +- .../newpipe/settings/SelectKioskFragment.java | 4 +- .../schabi/newpipe/util/ServiceHelper.java | 8 +- .../org/schabi/newpipe/util/ThemeHelper.java | 2 +- .../ic_info_outline_black_24dp.png | Bin 0 -> 487 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 485 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 323 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 320 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 640 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 655 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 940 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 953 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 1256 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 1279 bytes app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 3 +- app/src/main/res/values/styles.xml | 2 + app/src/main/res/xml/video_audio_settings.xml | 6 - 24 files changed, 477 insertions(+), 544 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java create mode 100644 app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png diff --git a/app/build.gradle b/app/build.gradle index 273616f91..efc3b69f0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:4fb49d54b5' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:e51bc58a856dcf3' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bc3dc62e6..e15d9abf8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -122,8 +122,12 @@ + + @@ -169,6 +173,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -176,17 +215,8 @@ - - - - - - - - - - + @@ -195,68 +225,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 8aaa248dd..c32b924b0 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -1,51 +1,80 @@ package org.schabi.newpipe; +import android.app.IntentService; +import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; +import android.os.PersistableBundle; +import android.preference.PreferenceManager; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; import android.widget.Toast; +import org.schabi.newpipe.extractor.Info; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.StreamingService.LinkType; +import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.playlist.ChannelPlayQueue; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlaylistPlayQueue; +import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ThemeHelper; +import java.io.Serializable; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import icepick.Icepick; import icepick.State; import io.reactivex.Observable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; -/* - * Copyright (C) Christian Schabesberger 2017 - * RouterActivity.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 . - */ +import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; /** - * This Acitivty is designed to route share/open intents to the specified service, and - * to the part of the service which can handle the url. + * Get the url from the intent and open it in the chosen preferred player */ public class RouterActivity extends AppCompatActivity { @State + protected int currentServiceId = -1; + private StreamingService currentService; + @State + protected LinkType currentLinkType; + @State + protected int selectedRadioPosition = -1; + protected int selectedPreviously = -1; + protected String currentUrl; protected CompositeDisposable disposables = new CompositeDisposable(); @@ -62,6 +91,10 @@ public class RouterActivity extends AppCompatActivity { finish(); } } + + setTheme(ThemeHelper.isLightThemeSelected(this) + ? R.style.RouterActivityThemeLight + : R.style.RouterActivityThemeDark); } @Override @@ -73,25 +106,43 @@ public class RouterActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); + handleUrl(currentUrl); } - protected void handleUrl(String url) { - disposables.add(Observable - .fromCallable(() -> NavigationHelper.getIntentByLink(this, url)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(intent -> { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); + @Override + protected void onDestroy() { + super.onDestroy(); - finish(); - }, this::handleError) - ); + disposables.clear(); } - protected void handleError(Throwable error) { + private void handleUrl(String url) { + disposables.add(Observable + .fromCallable(() -> { + if (currentServiceId == -1) { + currentService = NewPipe.getServiceByUrl(url); + currentServiceId = currentService.getServiceId(); + currentLinkType = currentService.getLinkTypeByUrl(url); + currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); + } else { + currentService = NewPipe.getService(currentServiceId); + } + + return currentLinkType != LinkType.NONE; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + if (result) { + onSuccess(); + } else { + onError(); + } + }, this::handleError)); + } + + private void handleError(Throwable error) { error.printStackTrace(); if (error instanceof ExtractionException) { @@ -103,11 +154,339 @@ public class RouterActivity extends AppCompatActivity { finish(); } - @Override - protected void onDestroy() { - super.onDestroy(); + private void onError() { + Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + finish(); + } - disposables.clear(); + protected void onSuccess() { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + + if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { + Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); + finish(); + return; + } + + // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) + if (currentService == ServiceList.SoundCloud) { + handleChoice(getString(R.string.background_player_key)); + return; + } + + final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); + final String alwaysAskKey = getString(R.string.always_ask_player_key); + + if (playerChoiceKey.equals(alwaysAskKey)) { + showDialog(); + } else { + handleChoice(playerChoiceKey); + } + } + + private void showDialog() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, + ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); + + LayoutInflater inflater = LayoutInflater.from(themeWrapper); + final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); + final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); + + final AdapterChoiceItem[] choices = { + new AdapterChoiceItem(getString(R.string.info_screen_key), getString(R.string.show_info), + resolveResourceIdFromAttr(themeWrapper, R.attr.info)), + new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.play)), + new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), + new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) + }; + + final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { + final int indexOfChild = radioGroup.indexOfChild( + radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); + final AdapterChoiceItem choice = choices[indexOfChild]; + + handleChoice(choice.key); + + if (which == DialogInterface.BUTTON_POSITIVE) { + preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); + } + }; + + final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) + .setTitle(R.string.preferred_player_share_menu_title) + .setView(radioGroup) + .setCancelable(true) + .setNegativeButton(R.string.just_once, dialogButtonsClickListener) + .setPositiveButton(R.string.always, dialogButtonsClickListener) + .setOnDismissListener((dialog) -> finish()) + .create(); + + alertDialog.setOnShowListener(dialog -> { + setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); + }); + + radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); + final View.OnClickListener radioButtonsClickListener = v -> { + final int indexOfChild = radioGroup.indexOfChild(v); + if (indexOfChild == -1) return; + + selectedPreviously = selectedRadioPosition; + selectedRadioPosition = indexOfChild; + + if (selectedPreviously == selectedRadioPosition) { + handleChoice(choices[selectedRadioPosition].key); + } + }; + + int id = 12345; + for (AdapterChoiceItem item : choices) { + final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); + radioButton.setText(item.description); + radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); + radioButton.setChecked(false); + radioButton.setId(id++); + radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + radioButton.setOnClickListener(radioButtonsClickListener); + radioGroup.addView(radioButton); + } + + if (selectedRadioPosition == -1) { + final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); + if (!TextUtils.isEmpty(lastSelectedPlayer)) { + for (int i = 0; i < choices.length; i++) { + AdapterChoiceItem c = choices[i]; + if (lastSelectedPlayer.equals(c.key)) { + selectedRadioPosition = i; + break; + } + } + } + } + + selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); + if (selectedRadioPosition != -1) { + ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); + } + selectedPreviously = selectedRadioPosition; + + alertDialog.show(); + } + + private void setDialogButtonsState(AlertDialog dialog, boolean state) { + final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); + final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + if (negativeButton == null || positiveButton == null) return; + + negativeButton.setEnabled(state); + positiveButton.setEnabled(state); + } + + private void handleChoice(final String playerChoiceKey) { + if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); + } + + if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { + PermissionHelper.showPopupEnablementToast(this); + finish(); + return; + } + + // stop and bypass FetcherService if InfoScreen was selected since + // StreamDetailFragment can fetch data itself + if(playerChoiceKey.equals(getString(R.string.info_screen_key))) { + disposables.add(Observable + .fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(intent -> { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + + finish(); + }, this::handleError) + ); + return; + } + + final Intent intent = new Intent(this, FetcherService.class); + intent.putExtra(FetcherService.KEY_CHOICE, + new Choice(currentService.getServiceId(), + currentLinkType, + currentUrl, + playerChoiceKey)); + startService(intent); + + finish(); + } + + private static class AdapterChoiceItem { + final String description, key; + @DrawableRes + final int icon; + + AdapterChoiceItem(String key, String description, int icon) { + this.description = description; + this.key = key; + this.icon = icon; + } + } + + private static class Choice implements Serializable { + final int serviceId; + final String url, playerChoice; + final LinkType linkType; + + Choice(int serviceId, LinkType linkType, String url, String playerChoice) { + this.serviceId = serviceId; + this.linkType = linkType; + this.url = url; + this.playerChoice = playerChoice; + } + + @Override + public String toString() { + return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Service Fetcher + //////////////////////////////////////////////////////////////////////////*/ + + public static class FetcherService extends IntentService { + + private static final int ID = 456; + public static final String KEY_CHOICE = "key_choice"; + private Disposable fetcher; + + public FetcherService() { + super(FetcherService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + startForeground(ID, createNotification().build()); + } + + @Override + protected void onHandleIntent(@Nullable Intent intent) { + if (intent == null) return; + + final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); + if (!(serializable instanceof Choice)) return; + Choice playerChoice = (Choice) serializable; + handleChoice(playerChoice); + } + + public void handleChoice(Choice choice) { + Single single = null; + UserAction userAction = UserAction.SOMETHING_ELSE; + + switch (choice.linkType) { + case STREAM: + single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_STREAM; + break; + case CHANNEL: + single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_CHANNEL; + break; + case PLAYLIST: + single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_PLAYLIST; + break; + } + + + if (single != null) { + final UserAction finalUserAction = userAction; + final Consumer resultHandler = getResultHandler(choice); + fetcher = single + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(info -> { + resultHandler.accept(info); + if (fetcher != null) fetcher.dispose(); + }, throwable -> ExtractorHelper.handleGeneralException(this, + choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); + } + } + + public Consumer getResultHandler(Choice choice) { + return info -> { + final String videoPlayerKey = getString(R.string.video_player_key); + final String backgroundPlayerKey = getString(R.string.background_player_key); + final String popupPlayerKey = getString(R.string.popup_player_key); + + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); + + PlayQueue playQueue; + String playerChoice = choice.playerChoice; + + if (info instanceof StreamInfo) { + if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { + NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { + NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { + NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); + + } else { + playQueue = new SinglePlayQueue((StreamInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + NavigationHelper.playOnMainPlayer(this, playQueue); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); + } + } + } + + if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { + playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + NavigationHelper.playOnMainPlayer(this, playQueue); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.playOnBackgroundPlayer(this, playQueue); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.playOnPopupPlayer(this, playQueue); + } + } + }; + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopForeground(true); + if (fetcher != null) fetcher.dispose(); + } + + private NotificationCompat.Builder createNotification() { + return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) + .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); + } } /*////////////////////////////////////////////////////////////////////////// @@ -119,9 +498,9 @@ public class RouterActivity extends AppCompatActivity { * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for * more details. */ - protected final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; + private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; - protected String getUrl(Intent intent) { + private String getUrl(Intent intent) { // first gather data and find service String videoUrl = null; if (intent.getData() != null) { @@ -137,7 +516,7 @@ public class RouterActivity extends AppCompatActivity { return videoUrl; } - protected String removeHeadingGibberish(final String input) { + private String removeHeadingGibberish(final String input) { int start = 0; for (int i = input.indexOf("://") - 1; i >= 0; i--) { if (!input.substring(i, i + 1).matches("\\p{L}")) { @@ -148,7 +527,7 @@ public class RouterActivity extends AppCompatActivity { return input.substring(start, input.length()); } - protected String trim(final String input) { + private String trim(final String input) { if (input == null || input.length() < 1) { return input; } else { @@ -188,5 +567,4 @@ public class RouterActivity extends AppCompatActivity { } return result.toArray(new String[result.size()]); } - } diff --git a/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java deleted file mode 100644 index 7196e413d..000000000 --- a/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java +++ /dev/null @@ -1,413 +0,0 @@ -package org.schabi.newpipe; - -import android.app.IntentService; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.PersistableBundle; -import android.preference.PreferenceManager; -import android.support.annotation.DrawableRes; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; -import android.support.v7.app.AlertDialog; -import android.text.TextUtils; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.RadioButton; -import android.widget.RadioGroup; -import android.widget.Toast; - -import org.schabi.newpipe.extractor.Info; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.ServiceList; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.StreamingService.LinkType; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.playlist.PlaylistInfo; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.playlist.ChannelPlayQueue; -import org.schabi.newpipe.playlist.PlayQueue; -import org.schabi.newpipe.playlist.PlaylistPlayQueue; -import org.schabi.newpipe.playlist.SinglePlayQueue; -import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.ThemeHelper; - -import java.io.Serializable; -import java.util.Arrays; - -import icepick.State; -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; - -import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; - -/** - * Get the url from the intent and open it in the chosen preferred player - */ -public class RouterPlayerActivity extends RouterActivity { - - @State - protected int currentServiceId = -1; - private StreamingService currentService; - @State - protected LinkType currentLinkType; - @State - protected int selectedRadioPosition = -1; - protected int selectedPreviously = -1; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { - super.onCreate(savedInstanceState, persistentState); - setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); - } - - @Override - protected void handleUrl(String url) { - disposables.add(Observable - .fromCallable(() -> { - if (currentServiceId == -1) { - currentService = NewPipe.getServiceByUrl(url); - currentServiceId = currentService.getServiceId(); - currentLinkType = currentService.getLinkTypeByUrl(url); - currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); - } else { - currentService = NewPipe.getService(currentServiceId); - } - - return currentLinkType != LinkType.NONE; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result) { - onSuccess(); - } else { - onError(); - } - }, this::handleError)); - } - - protected void onError() { - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); - finish(); - } - - protected void onSuccess() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - - if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { - Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); - finish(); - return; - } - - // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) - if (currentService == ServiceList.SoundCloud.getService()) { - handleChoice(getString(R.string.background_player_key)); - return; - } - - final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); - final String alwaysAskKey = getString(R.string.always_ask_player_key); - - if (playerChoiceKey.equals(alwaysAskKey)) { - showDialog(); - } else { - handleChoice(playerChoiceKey); - } - } - - private void showDialog() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, - ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); - - LayoutInflater inflater = LayoutInflater.from(themeWrapper); - final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); - final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); - - final AdapterChoiceItem[] choices = { - new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.play)), - new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), - new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) - }; - - final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { - final int indexOfChild = radioGroup.indexOfChild(radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); - final AdapterChoiceItem choice = choices[indexOfChild]; - - handleChoice(choice.key); - - if (which == DialogInterface.BUTTON_POSITIVE) { - preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); - } - }; - - final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) - .setTitle(R.string.preferred_player_share_menu_title) - .setView(radioGroup) - .setCancelable(true) - .setNegativeButton(R.string.just_once, dialogButtonsClickListener) - .setPositiveButton(R.string.always, dialogButtonsClickListener) - .setOnDismissListener((dialog) -> finish()) - .create(); - - alertDialog.setOnShowListener(dialog -> { - setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); - }); - - radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); - final View.OnClickListener radioButtonsClickListener = v -> { - final int indexOfChild = radioGroup.indexOfChild(v); - if (indexOfChild == -1) return; - - selectedPreviously = selectedRadioPosition; - selectedRadioPosition = indexOfChild; - - if (selectedPreviously == selectedRadioPosition) { - handleChoice(choices[selectedRadioPosition].key); - } - }; - - int id = 12345; - for (AdapterChoiceItem item : choices) { - final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); - radioButton.setText(item.description); - radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); - radioButton.setChecked(false); - radioButton.setId(id++); - radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - radioButton.setOnClickListener(radioButtonsClickListener); - radioGroup.addView(radioButton); - } - - if (selectedRadioPosition == -1) { - final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); - if (!TextUtils.isEmpty(lastSelectedPlayer)) { - for (int i = 0; i < choices.length; i++) { - AdapterChoiceItem c = choices[i]; - if (lastSelectedPlayer.equals(c.key)) { - selectedRadioPosition = i; - break; - } - } - } - } - - selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); - if (selectedRadioPosition != -1) { - ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); - } - selectedPreviously = selectedRadioPosition; - - alertDialog.show(); - } - - private void setDialogButtonsState(AlertDialog dialog, boolean state) { - final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); - final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); - if (negativeButton == null || positiveButton == null) return; - - negativeButton.setEnabled(state); - positiveButton.setEnabled(state); - } - - private void handleChoice(final String playerChoiceKey) { - if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { - PreferenceManager.getDefaultSharedPreferences(this).edit() - .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); - } - - if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { - PermissionHelper.showPopupEnablementToast(this); - finish(); - return; - } - - final Intent intent = new Intent(this, FetcherService.class); - intent.putExtra(FetcherService.KEY_CHOICE, new Choice(currentService.getServiceId(), currentLinkType, currentUrl, playerChoiceKey)); - startService(intent); - - finish(); - } - - private static class AdapterChoiceItem { - final String description, key; - @DrawableRes - final int icon; - - AdapterChoiceItem(String key, String description, int icon) { - this.description = description; - this.key = key; - this.icon = icon; - } - } - - private static class Choice implements Serializable { - final int serviceId; - final String url, playerChoice; - final LinkType linkType; - - Choice(int serviceId, LinkType linkType, String url, String playerChoice) { - this.serviceId = serviceId; - this.linkType = linkType; - this.url = url; - this.playerChoice = playerChoice; - } - - @Override - public String toString() { - return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Service Fetcher - //////////////////////////////////////////////////////////////////////////*/ - - public static class FetcherService extends IntentService { - - private static final int ID = 456; - public static final String KEY_CHOICE = "key_choice"; - private Disposable fetcher; - - public FetcherService() { - super(FetcherService.class.getSimpleName()); - } - - @Override - public void onCreate() { - super.onCreate(); - startForeground(ID, createNotification().build()); - } - - @Override - protected void onHandleIntent(@Nullable Intent intent) { - if (intent == null) return; - - final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); - if (!(serializable instanceof Choice)) return; - Choice playerChoice = (Choice) serializable; - handleChoice(playerChoice); - } - - public void handleChoice(Choice choice) { - Single single = null; - UserAction userAction = UserAction.SOMETHING_ELSE; - - switch (choice.linkType) { - case STREAM: - single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_STREAM; - break; - case CHANNEL: - single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_CHANNEL; - break; - case PLAYLIST: - single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_PLAYLIST; - break; - } - - - if (single != null) { - final UserAction finalUserAction = userAction; - final Consumer resultHandler = getResultHandler(choice); - fetcher = single - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(info -> { - resultHandler.accept(info); - if (fetcher != null) fetcher.dispose(); - }, throwable -> ExtractorHelper.handleGeneralException(this, - choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); - } - } - - public Consumer getResultHandler(Choice choice) { - return info -> { - final String videoPlayerKey = getString(R.string.video_player_key); - final String backgroundPlayerKey = getString(R.string.background_player_key); - final String popupPlayerKey = getString(R.string.popup_player_key); - - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); - - PlayQueue playQueue; - String playerChoice = choice.playerChoice; - - if (info instanceof StreamInfo) { - if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { - NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); - - } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { - NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); - - } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { - NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); - - } else { - playQueue = new SinglePlayQueue((StreamInfo) info); - - if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); - } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); - } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); - } - } - } - - if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { - playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); - - if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); - } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.playOnBackgroundPlayer(this, playQueue); - } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.playOnPopupPlayer(this, playQueue); - } - } - }; - } - - @Override - public void onDestroy() { - super.onDestroy(); - stopForeground(true); - if (fetcher != null) fetcher.dispose(); - } - - private NotificationCompat.Builder createNotification() { - return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) - .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 4512e316f..abc150e7d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -47,7 +47,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte // Constants //////////////////////////////////////////////////////////////////////////*/ - private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getId(); + private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getServiceId(); private static final String FALLBACK_CHANNEL_URL = "https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ"; private static final String FALLBACK_CHANNEL_NAME = "Music"; private static final String FALLBACK_KIOSK_ID = "Trending"; diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 4161f96c1..855594503 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -73,7 +73,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { .putString(getString(R.string.main_page_selectd_kiosk_id), kioskId).apply(); String serviceName = ""; try { - serviceName = NewPipe.getService(service_id).getServiceInfo().name; + serviceName = NewPipe.getService(service_id).getServiceInfo().getName(); } catch (ExtractionException e) { onError(e); } @@ -245,7 +245,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { String summary = String.format(getString(R.string.service_kiosk_string), - service.getServiceInfo().name, + service.getServiceInfo().getName(), kioskName); mainPagePref.setSummary(summary); diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java index 167b6f31b..5ab1ed1f2 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java @@ -122,11 +122,11 @@ public class SelectKioskFragment extends DialogFragment { for(StreamingService service : NewPipe.getServices()) { //TODO: Multi-service support - if (service.getServiceId() != ServiceList.YouTube.getId()) continue; + if (service.getServiceId() != ServiceList.YouTube.getServiceId()) continue; for(String kioskId : service.getKioskList().getAvailableKiosks()) { String name = String.format(getString(R.string.service_kiosk_string), - service.getServiceInfo().name, + service.getServiceInfo().getName(), KioskTranslator.getTranslatedKioskName(kioskId, getContext())); kioskList.add(new Entry( ServiceHelper.getIcon(service.getServiceId()), diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index ce1491ba4..55c6e68f2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -12,7 +12,7 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; public class ServiceHelper { - private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube.getService(); + private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; @DrawableRes public static int getIcon(int serviceId) { @@ -45,9 +45,9 @@ public class ServiceHelper { public static void setSelectedServiceId(Context context, int serviceId) { String serviceName; try { - serviceName = NewPipe.getService(serviceId).getServiceInfo().name; + serviceName = NewPipe.getService(serviceId).getServiceInfo().getName(); } catch (ExtractionException e) { - serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().name; + serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName(); } setSelectedServicePreferences(context, serviceName); @@ -55,7 +55,7 @@ public class ServiceHelper { public static void setSelectedServiceId(Context context, String serviceName) { int serviceId = NewPipe.getIdOfService(serviceName); - if (serviceId == -1) serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().name; + if (serviceId == -1) serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName(); setSelectedServicePreferences(context, serviceName); } diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index b0e00465a..824ac4a9d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -73,7 +73,7 @@ public class ThemeHelper { else if (selectedTheme.equals(blackTheme)) themeName = "BlackTheme"; else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme"; - themeName += "." + service.getServiceInfo().name; + themeName += "." + service.getServiceInfo().getName(); int resourceId = context.getResources().getIdentifier(themeName, "style", context.getPackageName()); if (resourceId > 0) { diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4b5ab06e19d61515cf3a7395db43a81bb30a8203 GIT binary patch literal 487 zcmVS-P=?%O3jv@aJa6HGW<0ceyC!`rEoc~?j{U=`ic|b z9N*QPsMv*+MQv@1i8kR%)UIh1`=XoCqqIYBOKErvea{GWZ!y>;D~RpT`=%tUi9cM8 zjMJd-5IZXpx*;AQpQG66NVp4e3Hh{Q=T<@);yl@>u@F125nkF002ovPDHLkV1kFY-~<2w literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c7b1113cfef22bcec86ead7ae67be12326276cab GIT binary patch literal 485 zcmVW+#V#f>U=9~3v zy5k#|%`o-bRp5gvNWBjKU}@RX_o(#cOX<;tO*2A$>f4JBeW-BBCp2Wv4dAi~>H>ZU-{cI? zse=}k;97{XqEJ|gdQ)z}NLv+h2nN-m6riYd=e>Y~D%A1;c7!A-*ac%VF|-AqWJ2?3 zQF6~&s5ci5fY}sT-yed|rRg0)~VyRe(RxP5| ziKSu?+iDTDUGbuL5L2~?+LgFdoQk=|C}Pxj7RQRtyXcJ~MwepHsvL@iJ?s4SCH8$Y z6AQgYt#Ro~-1uQ)CSvZwj!tLSm1hyxCe}LeCL$stVz6(6)`@$smX==JIoA4R{{X3C VfQx`Hg^K_H002ovPDHLkV1oM>kN^Mx literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..353e064951788a29a64eef439284fd97e904f374 GIT binary patch literal 320 zcmV-G0l)rT_h)IX%8`!xx#?O($dW=BI;pgAmAMoJ&FVmpak^_M&10n8om%>Hp0CB z>RbH6BfRjxN9M~)&M4Ia$ETJBLQ^<<<3g$He5hLhidW{F&l zq78;P`5|=ls>YxRGDkGO4)Ha@UK+s{*1}DYDDe@&3c1kJ7>fuhJobWT>>{`#?O7?v zaJd6ic&m6Kxyn*Ci7xy1O7D6yV<~Ad#ZoSbb%oCVPWUuVr%YQDrkfL?gZZ)JBOfu SH&bZ<0000o#88fEO5na|OlO{3G z*yIPEVO{WsIjnKnVT>2DMzkql!ApMPMO=L<#3I9oI}LubOO+CNvScYxqQ)LKcQkDr zB9h}Xp02N~GG)+JIYyNnkc)PDZOnzu*#; zB8R%~1(i4Rl0>e>Eb#WLtxfOV(#vvo;8MrfX8# zOJwj-6bRC^RW#U^^>`Uk%GqUlp=9>%y-#&$qZ(w=qV2=#%SNueJg~}R_Q4VL}5`+@9~aM z91c`Iph*TZkfp^?^`Dp^0y)$Ij}vN48)8+LhbiruA|j3sO47|fb;`I*GKou>Ivq)^ z4u=V1QQ;ad;<{u73-W9;3T-}Qiy5qW$_D4g=4Y%kOJdS2(WFC22NX4g$Gh;r2I3UU zC}4AjSPu(ixIwI-i~@=>;)?%*fd#|^BPhd-@r0NU2I`1i6z1DU)PjK~qBKyTf@lN- zCy2s8fg++640I7Wl<`GYIEcGoz(-^U3JfE>zYypm@_+0U8C+z9*+p(5%4UK1zv(Js zZ=k^ch-xrUL_9KT9C*VMq7V#ZxJHzW0~N$&*uzfo7172v3S>D)tf9gJQ}~FgQJ{|K z@irKcxri5LQU~VfAvRG_fjnJAhY3`QO>%*_Wjs2dSVB8ak(i`~wix%~<)U?%MJ3oA z7ie3kxPZkW+6y&o6w%94r-ydHP_ma_Nr*Gbj78j570v p!{d$?4XPC6k!Bt+*lWOGzX87wDYF(rDzN|n002ovPDHLkV1ir08p{9x literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3847a9fe74774d3593dd694cff48ccca13cdec5a GIT binary patch literal 940 zcmV;d15^BoP)mdcfcOtJM6uO{+z8D@UxkKMBq1+Buz@6?P#P0jG1VJo zr4$sRhK5k2Z=fXRN~6nW@y^{1KD~XI=}gYdoFjX&pWp3sm>a@9yM-m0@&}6isAEB(Qj;>qRst`p9~=6q zAu7LO=u+TQ_K7Dd@dg8*_ZUX>5!&WBSSti4^<9Bi7`TY2+y<+KU=~psWnX%OzQeO# z$Y%%reeR=-{wJ-1^+Iq9eT6F#>jwG=^_&zcppV__#9BjN7F^M%z)NCy=&SoFTsf2ppC<<**2_ZI5@SJo9Db@pv9Dwel>@Q2Y|A^pR+ zXm)h0I4$&8t(hXy6*cz3S)t(J`aO{jqc{F(SB1>4AMjd@E;uWs|KhZ2{;};ky2AQ2 z)rxDxD$`NJH=PukQuA$!?4zc}-&3$&Xn>Du{t5dULsafqFEod!oWp?rpGSuiRtueB z&+LQNE+8tK3|cF6gtuz;3+FNP08y#Yj|~U-15sJR*io8@$}eQs#}cB_phN-_NT5>I zKj|$a=v*bC1q7WMgQRsN@Unzi63g)%L1&Ybq;ZC~2s(dqkQ990An0_M#V3gY=Ga5f z`5TXvN})=^4!0;^!e^R~NT^Pc418W7q3m*(v-Aa=K4XNz?fnc_RXp{J*(OZ^8ac7(2N>$@cY O0000+=u1!Si^=c~o6pU56D8MW!jS z$u@O7JnC$-Nr`E0Z~~QViq1F4UDojk)8rj4KiUtv${KCrYEfp?UeGn(lf*h#%mwiZO?K>;SqC6p{w@`EooS#EOw+c}UBbluHuSO9kD3{Dexd zGgQ&NCewM)0=iXlsD$fh4c%;~LATKTP7f-@j@01>+c-g20r3E3BuF*`h>_@^ zHN+KB7e-Jm@Z>j$y%;(Bn$f*7_b_YN7RCYJVeH7P%ol+c!LiAkL*xA)X9%M!t}A%e=*t&auG}M zUL7k|gPtKKgM)4$K3fdZU#`Cd2c5t}oVOJe{qaNg4l!>nXc19Cg$22YJsc}RIU0!D zQ6KVKhxdR-j0Rnxtv}Fl zg03K-O24t7lYB&c=W=+E>@fn$hQ5wD$_oT6qv8ggqJe-vjQr`djDR|Y=pflR=t-%+ z(kr36+3A;_^XSU-C(b#^3v@H6PJ=SMLRaN%qMQqSME8wcL$0HkK4xj7 z`v)iS)q*bxcDYYB<~wVOFW}3`r@4H|@fyE97P!cfkP~%@MH=|M#7TQSm0^y9)p&}_ z7{lc0-Cv$z#&CH?jf4HhF*6r<<1|RC&P|h7oP+#Da^G2|VEz(#l(!^NVZ`1w`3S4D zh|A+Sm+k)}L4iBGp-Gr;RJbh%++L9EIM5D%liv*A40h bb%Xu~a~|hNXxfgr00000NkvXXu0mjfV)nVh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c1e2a03a4bc540419ce1165d5dedd8d5345fdec2 GIT binary patch literal 1256 zcmVP)W$LQv#I8j8T_{yf~=m$~;IXzu5?eh%l+bI*O5LnIQ3L?S-g zNEKIT<0XBJGshx}%rQL6n2^)65BytPpur(aAq3mzkx5L*!vp z0a@Ola>x)hq%a~$J)clM5drmH23z#q<|xU<0`*qKPGo7Si~n z%wqBU&0hSlj{%E?RDuuU7MJcXB=Jj%b`&x_e(Xp;ExUAT@Y|(jm(qjWZ(B;_Uor&Y z6!R#Nv*chaGsx+GPf(5^TbQOC&z>U}Plh0!MlPf-JUW9CsUc{WQ6klNkmN0L=W$Hb zI`=E``6_{H*O2>uGL!}iknh%)aH)U^vHJHyC=I|j;!5~HKCWcN1R5Z;2B2BgU%-W! zczBp#L#PcDi)Yo>;Taa!Y@Ptv;<}7l4r)8NeQGlwNF& zYb9ky8`z|cGKQ@=tt4)=frR*F&DNq;5;NLBOdDlk)i+CZzQKZh$R zK8n3)UuX^N7uDk=aj8xWGC(Sn1_~Jx)mdCiFf4{?52b-lQTv8C9#o@5E)leAD3OzR z)Fn^Gaf%@QAdedG1kbj}b8XCXgrFSb3rcL@n*sbhFEP*Qfd2Kr_;5}_cz_aF;&K3M ze+ebhM9$XHgOZ`06n-hB6D318YwImEMS(?`<%O>vfHEvXL&vNpLe#!|zDLp9b&_k9T z*x5;~c-Pf0-68_APCn*}yr+}vWZ1(d62yp+WD|SHaD&H;+SWI+mO!0l8Wm6`sU`&F zbfE&s6E+i?YKBohW{8u7EWu?ypnS+US>hOx&jq=;y2>EuNMckT2YAG1l*`QUkZ)PT z3X$L_jr@gjkUknXOq>;|goD)5%rkl!W0nOLm}QJ!o^qco2Ph^Ii9{k1-~0zqnGDmx SqDLYC0000 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3a82cab3b4f2cfe77333cee6121d867353753d5e GIT binary patch literal 1279 zcmVXA;DPbB};p_0dfAo z$vAy9#+6S9#ELYJ@(Lqa#%C_802gIqArYp#f(wN zJ5D-2x}I^0EQMtFA?SeRyu{vHx_o|^)~`kO7MeJbsRD=Aw#N4d<{5Bs2vT z(7+gW9I~nSN2#E6lJC{kK0$L^4urow?@d3Y+FGmL|;O)mE-^F4Bc9HL2 zQm7ENoloT3SxmqprfnYoS5!#bXp=BSqyK>0=yIe`A#Mj2x&gld?d0n~g}ejgYx5J3 z2Egy^UyUJ3s{D( z);XXRX&hy6FOb$b2OLD|GaAs3)ae{>38~9yz)_?g=YYFNn~esfkZw5#3?r>T`I=7o z+A5@n&H)oh4MqdzBfWAC_=prY8jwJmaSq5K#f%2TkiLBX0pHn2D2X)lKlmdaMq2s3 zeU08h+F~>yg>=(7pcm8C~mULv(P2h^i$HWskl&kK!W z1ZkhKfKETJl!_jtL1O{;k-FUlY(=tYHWIKD-3E67NoJ5v83{OpG)>%HfT9m6OI73n zbC^cDf^r+M9^H}115Tj(#eF~-qezp~L>f@f6jFv+?gJDZ=*~qN(2H(2%1b~cZ;&k3 zL>90X-2_Q*0g5hkkEx6#pqf#1X_VK11mozgMH0}DZj8A107V=5_M<}HVe)PBd(>b6 zoyB%ki2Dr--3^rAfQ4kyePSajq;2Lix=9xJ4^Xs&O~Q_l{`EhTudUbvPZ!B&aUg`x zzD2%ssA2=k8N$vrDp3KgnttpIGQ0QyMI9OJJZ23lfURW|J0sKtb?mZ<=h(66p%zud z>bZoyXEX+R@O5c{1jurnYLt)7;RI9Id&-iaj~+Mi1PVr+=0}fjTW6S}kP#ZgIQ(7D z5MMJynq@Bjr=gSke9a&=VI99OrxzzL=;tUYRxzIhF=8Z{&ni+J<2o;Ka+cZQUVzxi z6k(a9Es85BjSLWm8!U+K5>OlC1Zk9QQD4JKaDdkYVS+SqGZ*11*vk`&kzqGU(^nB& zxXOFHn&t{WGwWNq44PmQXZZ&=hUsR5_|TXQ5UXPyo%C>vhrD8jFMMH!S3Kk<=SkB- pEh@+ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..8f33f9297 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -155,6 +155,7 @@ @string/always_ask_player_key preferred_player_last_selected + info_screen video_player background_player popup_player diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea8f0fce8..caf792186 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ Channel unsubscribed Unable to change subscription Unable to update subscription + Show info Main Subscriptions @@ -368,7 +369,7 @@ - @string/preferred_player_settings_title + NewPipe Open with preferred player Preferred player diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b16958ae6..dcf8f9268 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,6 +18,7 @@ @drawable/ic_thumb_up_black_24dp @drawable/ic_thumb_down_black_24dp + @drawable/ic_info_outline_black_24dp @drawable/ic_headset_black_24dp @drawable/ic_delete_sweep_white_24dp @drawable/ic_file_download_black_24dp @@ -69,6 +70,7 @@ @drawable/ic_thumb_up_white_24dp @drawable/ic_thumb_down_white_24dp @drawable/ic_headset_white_24dp + @drawable/ic_info_outline_white_24dp @drawable/ic_delete_sweep_black_24dp @drawable/ic_file_download_white_24dp @drawable/ic_share_white_24dp diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index ceacfb142..b32bff6f8 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -74,12 +74,6 @@ android:layout="@layout/settings_category_header_layout" android:title="@string/settings_category_player_behavior_title"> - - Date: Wed, 14 Feb 2018 19:33:43 +0100 Subject: [PATCH 52/52] fixed preferred_player inconsistancy --- .../org/schabi/newpipe/RouterActivity.java | 26 +++++++++++-------- app/src/main/res/values/settings_keys.xml | 22 +++++++++------- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml/video_audio_settings.xml | 8 +++--- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index c32b924b0..ad79c40b4 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -5,7 +5,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import android.os.PersistableBundle; import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; @@ -13,7 +12,6 @@ import android.support.v4.app.NotificationCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; -import android.util.Log; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; @@ -176,8 +174,10 @@ public class RouterActivity extends AppCompatActivity { return; } - final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); - final String alwaysAskKey = getString(R.string.always_ask_player_key); + final String playerChoiceKey = preferences.getString( + getString(R.string.preferred_open_action_key), + getString(R.string.preferred_open_action_default)); + final String alwaysAskKey = getString(R.string.always_ask_open_action_key); if (playerChoiceKey.equals(alwaysAskKey)) { showDialog(); @@ -196,7 +196,7 @@ public class RouterActivity extends AppCompatActivity { final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); final AdapterChoiceItem[] choices = { - new AdapterChoiceItem(getString(R.string.info_screen_key), getString(R.string.show_info), + new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info), resolveResourceIdFromAttr(themeWrapper, R.attr.info)), new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), resolveResourceIdFromAttr(themeWrapper, R.attr.play)), @@ -214,7 +214,7 @@ public class RouterActivity extends AppCompatActivity { handleChoice(choice.key); if (which == DialogInterface.BUTTON_POSITIVE) { - preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); + preferences.edit().putString(getString(R.string.preferred_open_action_key), choice.key).apply(); } }; @@ -257,7 +257,7 @@ public class RouterActivity extends AppCompatActivity { } if (selectedRadioPosition == -1) { - final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); + final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null); if (!TextUtils.isEmpty(lastSelectedPlayer)) { for (int i = 0; i < choices.length; i++) { AdapterChoiceItem c = choices[i]; @@ -288,12 +288,16 @@ public class RouterActivity extends AppCompatActivity { } private void handleChoice(final String playerChoiceKey) { - if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { + if (Arrays.asList(getResources() + .getStringArray(R.array.preferred_open_action_values_list)) + .contains(playerChoiceKey)) { PreferenceManager.getDefaultSharedPreferences(this).edit() - .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); + .putString(getString(R.string.preferred_open_action_last_selected_key), + playerChoiceKey).apply(); } - if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { + if (playerChoiceKey.equals(getString(R.string.popup_player_key)) + && !PermissionHelper.isPopupEnabled(this)) { PermissionHelper.showPopupEnablementToast(this); finish(); return; @@ -301,7 +305,7 @@ public class RouterActivity extends AppCompatActivity { // stop and bypass FetcherService if InfoScreen was selected since // StreamDetailFragment can fetch data itself - if(playerChoiceKey.equals(getString(R.string.info_screen_key))) { + if(playerChoiceKey.equals(getString(R.string.show_info_key))) { disposables.add(Observable .fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl)) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 8f33f9297..1ac8e68cd 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -150,28 +150,30 @@ @string/charset_most_special_characters_value - - preferred_player_key - @string/always_ask_player_key - preferred_player_last_selected + + preferred_open_action_key + @string/always_ask_open_action_key + preferred_open_action_last_selected - info_screen + show_info video_player background_player popup_player - always_ask_player + always_ask_player - + + @string/show_info @string/video_player @string/background_player @string/popup_player - @string/always_ask_player + @string/always_ask_open_action - + + @string/show_info_key @string/video_player_key @string/background_player_key @string/popup_player_key - @string/always_ask_player_key + @string/always_ask_open_action_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index caf792186..23453c89d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -376,7 +376,7 @@ Video player Background player Popup player - Always ask + Always ask Getting info… "The requested content is loading" diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index b32bff6f8..7551834a2 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -75,10 +75,10 @@ android:title="@string/settings_category_player_behavior_title">