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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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/36] -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 d0808ce1590b6d43f15ef1984a1a55807d3c73c5 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 8 Feb 2018 18:48:36 -0800 Subject: [PATCH 34/36] -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 35/36] -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 36/36] -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); }