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 //////////////////////////////////////////////////////////////////////////*/