-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.
This commit is contained in:
John Zhen Mo 2018-01-26 21:34:17 -08:00
parent f0829f9ef3
commit 388ec3e3d3
22 changed files with 476 additions and 485 deletions

View file

@ -26,8 +26,6 @@ import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.NavigationView; import android.support.design.widget.NavigationView;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.view.GravityCompat; import android.support.v4.view.GravityCompat;
@ -42,40 +40,21 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; 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.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.BackPressable;
import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment; 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.Constants;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.util.Date; public class MainActivity extends AppCompatActivity {
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 {
private static final String TAG = "MainActivity"; private static final String TAG = "MainActivity";
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
private SharedPreferences sharedPreferences;
private ActionBarDrawerToggle toggle = null; private ActionBarDrawerToggle toggle = null;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -86,7 +65,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -98,7 +76,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
setSupportActionBar(findViewById(R.id.toolbar)); setSupportActionBar(findViewById(R.id.toolbar));
setupDrawer(); setupDrawer();
initHistory();
} }
private void setupDrawer() { private void setupDrawer() {
@ -149,8 +126,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
if (!isChangingConfigurations()) { if (!isChangingConfigurations()) {
StateSaver.clearStateFiles(); StateSaver.clearStateFiles();
} }
disposeHistory();
} }
@Override @Override
@ -357,75 +332,4 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
NavigationHelper.gotoMainFragment(getSupportFragmentManager()); NavigationHelper.gotoMainFragment(getSupportFragmentManager());
} }
} }
/*//////////////////////////////////////////////////////////////////////////
// History
//////////////////////////////////////////////////////////////////////////*/
private WatchHistoryDAO watchHistoryDAO;
private SearchHistoryDAO searchHistoryDAO;
private PublishSubject<HistoryEntry> 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<HistoryEntry> getHistoryEntryConsumer() {
return new Consumer<HistoryEntry>() {
@Override
public void accept(HistoryEntry historyEntry) throws Exception {
//noinspection unchecked
HistoryDAO<HistoryEntry> historyDAO = (HistoryDAO<HistoryEntry>)
(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);
}
}
} }

View file

@ -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.PlaylistEntity;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.dao.StreamDAO; 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.dao.StreamStateDAO;
import org.schabi.newpipe.database.stream.model.StreamEntity; 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.stream.model.StreamStateEntity;
import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionDAO;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;

View file

@ -20,6 +20,7 @@ public class Migrations {
// Not much we can do about this, since room doesn't create tables before migration. // 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. // 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 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 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, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");

View file

@ -2,6 +2,7 @@ package org.schabi.newpipe.database.history.dao;
import android.arch.persistence.room.Dao; import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query; import android.arch.persistence.room.Query;
import android.support.annotation.Nullable;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
@ -22,6 +23,7 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
@Override @Override
@Nullable
SearchHistoryEntry getLatestEntry(); SearchHistoryEntry getLatestEntry();
@Query("DELETE FROM " + TABLE_NAME) @Query("DELETE FROM " + TABLE_NAME)

View file

@ -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.Dao;
import android.arch.persistence.room.Query; import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
import org.schabi.newpipe.database.BasicDAO; 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.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
import java.util.List; 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.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_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; 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.history.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_ACCESS_DATE;
import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
@Dao @Dao
public abstract class StreamHistoryDAO implements BasicDAO<StreamHistoryEntity> { public abstract class StreamHistoryDAO implements BasicDAO<StreamHistoryEntity> {

View file

@ -3,10 +3,14 @@ package org.schabi.newpipe.database.history.model;
import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity; import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore; import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.Index;
import java.util.Date; 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 class SearchHistoryEntry extends HistoryEntry {
public static final String TABLE_NAME = "search_history"; public static final String TABLE_NAME = "search_history";

View file

@ -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.ColumnInfo;
import android.arch.persistence.room.Entity; import android.arch.persistence.room.Entity;
@ -6,12 +6,14 @@ import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Index; import android.arch.persistence.room.Index;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import java.util.Date; import java.util.Date;
import static android.arch.persistence.room.ForeignKey.CASCADE; 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.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; import static org.schabi.newpipe.database.history.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_ACCESS_DATE;
@Entity(tableName = STREAM_HISTORY_TABLE, @Entity(tableName = STREAM_HISTORY_TABLE,
primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE},

View file

@ -1,9 +1,8 @@
package org.schabi.newpipe.database.stream; package org.schabi.newpipe.database.history.model;
import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.ColumnInfo;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.Date; import java.util.Date;
@ -44,4 +43,8 @@ public class StreamHistoryEntry {
this.streamId = streamId; this.streamId = streamId;
this.accessDate = accessDate; this.accessDate = accessDate;
} }
public StreamHistoryEntity toStreamHistoryEntity() {
return new StreamHistoryEntity(streamId, accessDate);
}
} }

View file

@ -2,9 +2,8 @@ package org.schabi.newpipe.database.stream;
import android.arch.persistence.room.ColumnInfo; 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.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem;

View file

@ -9,7 +9,7 @@ import android.arch.persistence.room.Transaction;
import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity; 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.stream.model.StreamStateEntity;
import java.util.ArrayList; 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_SERVICE_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; 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.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; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao @Dao

View file

@ -44,6 +44,7 @@ import com.nirhart.parallaxscroll.views.ParallaxScrollView;
import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.download.DownloadDialog; 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.BaseStateFragment;
import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.fragments.local.PlaylistAppendDialog;
import org.schabi.newpipe.history.HistoryListener; 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.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.OnInfoItemGesture;
@ -649,9 +651,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
public void onActionSelected(int selectedStreamId) { public void onActionSelected(int selectedStreamId) {
try { try {
NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http"))); NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http")));
if(activity instanceof HistoryListener) {
((HistoryListener) activity).onVideoPlayed(info, null);
}
} catch (Exception e) { } catch (Exception e) {
if(DEBUG) Log.i(TAG, "Failed to start kore", e); if(DEBUG) Log.i(TAG, "Failed to start kore", e);
showInstallKoreDialog(activity); showInstallKoreDialog(activity);
@ -805,10 +804,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
private void openBackgroundPlayer(final boolean append) { private void openBackgroundPlayer(final boolean append) {
AudioStream audioStream = currentInfo.getAudioStreams().get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); AudioStream audioStream = currentInfo.getAudioStreams().get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
if (activity instanceof HistoryListener) {
((HistoryListener) activity).onAudioPlayed(currentInfo, audioStream);
}
boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false); .getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
@ -825,10 +820,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
return; return;
} }
if (activity instanceof HistoryListener) {
((HistoryListener) activity).onVideoPlayed(currentInfo, getSelectedVideoStream());
}
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
if (append) { if (append) {
NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue); NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue);
@ -844,10 +835,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
private void openVideoPlayer() { private void openVideoPlayer() {
VideoStream selectedVideoStream = getSelectedVideoStream(); 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)) { if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream); NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream);
} else { } else {

View file

@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.list.search;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
@ -30,10 +29,8 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity; 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.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; 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.extractor.search.SearchResult;
import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment; 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.report.UserAction;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
@ -64,16 +61,11 @@ import java.util.concurrent.TimeUnit;
import icepick.State; import icepick.State;
import io.reactivex.Flowable; import io.reactivex.Flowable;
import io.reactivex.Notification;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.functions.BiFunction;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.functions.Predicate;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.PublishSubject;
@ -121,7 +113,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
private CompositeDisposable disposables = new CompositeDisposable(); private CompositeDisposable disposables = new CompositeDisposable();
private SuggestionListAdapter suggestionListAdapter; private SuggestionListAdapter suggestionListAdapter;
private SearchHistoryDAO searchHistoryDAO; private HistoryRecordManager historyRecordManager;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Views // Views
@ -167,7 +159,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true); isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true);
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled); suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
searchHistoryDAO = NewPipeDatabase.getInstance().searchHistoryDAO(); historyRecordManager = new HistoryRecordManager(context);
} }
@Override @Override
@ -535,36 +527,24 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
} }
private void showDeleteSuggestionDialog(final SuggestionItem item) { private void showDeleteSuggestionDialog(final SuggestionItem item) {
final Disposable onDelete = historyRecordManager.deleteSearchHistory(item.query)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable,
UserAction.SOMETHING_ELSE, "none",
"Deleting item failed", R.string.general_error)
);
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)
.setTitle(item.query) .setTitle(item.query)
.setMessage(R.string.delete_item_search_history) .setMessage(R.string.delete_item_search_history)
.setCancelable(true) .setCancelable(true)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { .setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete))
@Override .show();
public void onClick(DialogInterface dialog, int which) {
disposables.add(Observable
.fromCallable(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return searchHistoryDAO.deleteAllWhereQuery(item.query);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer howManyDeleted) throws Exception {
suggestionPublisher.onNext(searchEditText.getText().toString());
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
showSnackBarError(throwable, UserAction.SOMETHING_ELSE, "none", "Deleting item failed", R.string.general_error);
}
}));
}
}).show();
} }
@Override @Override
@ -589,83 +569,67 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
final Observable<String> observable = suggestionPublisher final Observable<String> observable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWith(searchQuery != null ? searchQuery : "") .startWith(searchQuery != null ? searchQuery : "")
.filter(new Predicate<String>() { .filter(query -> isSuggestionsEnabled);
@Override
public boolean test(@io.reactivex.annotations.NonNull String query) throws Exception {
return isSuggestionsEnabled;
}
});
suggestionDisposable = observable suggestionDisposable = observable
.switchMap(new Function<String, ObservableSource<Notification<List<SuggestionItem>>>>() { .switchMap(query -> {
@Override final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
public ObservableSource<Notification<List<SuggestionItem>>> apply(@io.reactivex.annotations.NonNull final String query) throws Exception { .getRelatedSearches(query, 3, 25);
final Flowable<List<SearchHistoryEntry>> flowable = query.length() > 0 final Observable<List<SuggestionItem>> local = flowable.toObservable()
? searchHistoryDAO.getSimilarEntries(query, 3) .map(searchHistoryEntries -> {
: searchHistoryDAO.getUniqueEntries(25); List<SuggestionItem> result = new ArrayList<>();
final Observable<List<SuggestionItem>> local = flowable.toObservable() for (SearchHistoryEntry entry : searchHistoryEntries)
.map(new Function<List<SearchHistoryEntry>, List<SuggestionItem>>() { result.add(new SuggestionItem(true, entry.getSearch()));
@Override return result;
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<SearchHistoryEntry> searchHistoryEntries) throws Exception { });
List<SuggestionItem> result = new ArrayList<>();
for (SearchHistoryEntry entry : searchHistoryEntries)
result.add(new SuggestionItem(true, entry.getSearch()));
return result;
}
});
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
// Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION // Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION
return local.materialize(); return local.materialize();
}
final Observable<List<SuggestionItem>> network = ExtractorHelper
.suggestionsFor(serviceId, query, contentCountry)
.toObservable()
.map(strings -> {
List<SuggestionItem> result = new ArrayList<>();
for (String entry : strings) {
result.add(new SuggestionItem(false, entry));
}
return result;
});
return Observable.zip(local, network, (localResult, networkResult) -> {
List<SuggestionItem> result = new ArrayList<>();
if (localResult.size() > 0) result.addAll(localResult);
// Remove duplicates
final Iterator<SuggestionItem> 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<List<SuggestionItem>> network = ExtractorHelper.suggestionsFor(serviceId, query, contentCountry).toObservable() if (networkResult.size() > 0) result.addAll(networkResult);
.map(new Function<List<String>, List<SuggestionItem>>() { return result;
@Override }).materialize();
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<String> strings) throws Exception {
List<SuggestionItem> result = new ArrayList<>();
for (String entry : strings) result.add(new SuggestionItem(false, entry));
return result;
}
});
return Observable.zip(local, network, new BiFunction<List<SuggestionItem>, List<SuggestionItem>, List<SuggestionItem>>() {
@Override
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<SuggestionItem> localResult, @io.reactivex.annotations.NonNull List<SuggestionItem> networkResult) throws Exception {
List<SuggestionItem> result = new ArrayList<>();
if (localResult.size() > 0) result.addAll(localResult);
// Remove duplicates
final Iterator<SuggestionItem> 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();
}
}) })
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Notification<List<SuggestionItem>>>() { .subscribe(listNotification -> {
@Override if (listNotification.isOnNext()) {
public void accept(@io.reactivex.annotations.NonNull Notification<List<SuggestionItem>> listNotification) throws Exception { handleSuggestions(listNotification.getValue());
if (listNotification.isOnNext()) { } else if (listNotification.isOnError()) {
handleSuggestions(listNotification.getValue()); Throwable error = listNotification.getError();
} else if (listNotification.isOnError()) { if (!ExtractorHelper.hasAssignableCauseThrowable(error,
Throwable error = listNotification.getError(); IOException.class, SocketException.class,
if (!ExtractorHelper.hasAssignableCauseThrowable(error, InterruptedException.class, InterruptedIOException.class)) {
IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class)) { onSuggestionError(error);
onSuggestionError(error);
}
} }
} }
}); });
@ -718,11 +682,14 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
hideSuggestionsPanel(); hideSuggestionsPanel();
hideKeyboardSearch(); hideKeyboardSearch();
if (activity instanceof HistoryListener) { historyRecordManager.onSearched(serviceId, query)
((HistoryListener) activity).onSearch(serviceId, query); .observeOn(AndroidSchedulers.mainThread())
suggestionPublisher.onNext(query); .subscribe(
} ignored -> {},
error -> showSnackBarError(error, UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), query, 0)
);
suggestionPublisher.onNext(query);
startLoading(false); startLoading(false);
} }

View file

@ -13,12 +13,12 @@ import android.view.ViewGroup;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListFragment; 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.InfoItemDialog;
import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.OnInfoItemGesture;
import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem;
@ -49,7 +49,7 @@ public abstract class StatisticsPlaylistFragment
/* Used for independent events */ /* Used for independent events */
private Subscription databaseSubscription; private Subscription databaseSubscription;
private StreamRecordManager recordManager; private HistoryRecordManager recordManager;
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Abstracts // Abstracts
@ -68,7 +68,7 @@ public abstract class StatisticsPlaylistFragment
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); recordManager = new HistoryRecordManager(context);
} }
@Override @Override
@ -205,7 +205,7 @@ public abstract class StatisticsPlaylistFragment
super.startLoading(forceLoad); super.startLoading(forceLoad);
resetFragment(); resetFragment();
recordManager.getStatistics() recordManager.getStreamStatistics()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(getHistoryObserver()); .subscribe(getHistoryObserver());
} }

View file

@ -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<Long> 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<List<StreamStatisticsEntry>> getStatistics() {
return historyTable.getStatistics();
}
}

View file

@ -9,6 +9,7 @@ import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.Log; import android.util.Log;
@ -50,8 +51,10 @@ public class HistoryActivity extends AppCompatActivity {
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(R.string.title_activity_history); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.title_activity_history);
}
// Create the adapter that will return a fragment for each of the three // Create the adapter that will return a fragment for each of the three
// primary sections of the activity. // primary sections of the activity.
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
@ -66,17 +69,11 @@ public class HistoryActivity extends AppCompatActivity {
final FloatingActionButton fab = findViewById(R.id.fab); final FloatingActionButton fab = findViewById(R.id.fab);
RxView.clicks(fab) RxView.clicks(fab)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Object>() { .subscribe(ignored -> {
@Override int currentItem = mViewPager.getCurrentItem();
public void accept(Object o) { HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter
int currentItem = mViewPager.getCurrentItem(); .instantiateItem(mViewPager, currentItem);
HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.instantiateItem(mViewPager, currentItem); fragment.onHistoryCleared();
if(fragment != null) {
fragment.onHistoryCleared();
} else {
Log.w(TAG, "Couldn't find current fragment");
}
}
}); });
} }

View file

@ -4,9 +4,8 @@ import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView; 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.text.DateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -19,7 +18,7 @@ import java.util.Date;
* @param <E> the type of the entries * @param <E> the type of the entries
* @param <VH> the type of the view holder * @param <VH> the type of the view holder
*/ */
public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> { public abstract class HistoryEntryAdapter<E, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
private final ArrayList<E> mEntries; private final ArrayList<E> mEntries;
private final DateFormat mDateFormat; private final DateFormat mDateFormat;
@ -29,9 +28,8 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec
public HistoryEntryAdapter(Context context) { public HistoryEntryAdapter(Context context) {
super(); super();
mEntries = new ArrayList<>(); mEntries = new ArrayList<>();
mDateFormat = android.text.format.DateFormat.getDateFormat(context.getApplicationContext()); mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM,
Localization.getPreferredLocale(context));
setHasStableIds(true);
} }
public void setEntries(@NonNull Collection<E> historyEntries) { public void setEntries(@NonNull Collection<E> historyEntries) {
@ -53,11 +51,6 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec
return mDateFormat.format(date); return mDateFormat.format(date);
} }
@Override
public long getItemId(int position) {
return mEntries.get(position).getId();
}
@Override @Override
public int getItemCount() { public int getItemCount() {
return mEntries.size(); return mEntries.size();
@ -66,15 +59,20 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec
@Override @Override
public void onBindViewHolder(VH holder, int position) { public void onBindViewHolder(VH holder, int position) {
final E entry = mEntries.get(position); final E entry = mEntries.get(position);
holder.itemView.setOnClickListener(new View.OnClickListener() { holder.itemView.setOnClickListener(v -> {
@Override if(onHistoryItemClickListener != null) {
public void onClick(View v) { onHistoryItemClickListener.onHistoryItemClick(entry);
final OnHistoryItemClickListener<E> historyItemClickListener = onHistoryItemClickListener;
if(historyItemClickListener != null) {
historyItemClickListener.onHistoryItemClick(entry);
}
} }
}); });
holder.itemView.setOnLongClickListener(view -> {
if (onHistoryItemClickListener != null) {
onHistoryItemClickListener.onHistoryItemLongClick(entry);
return true;
}
return false;
});
onBindViewHolder(holder, entry, position); onBindViewHolder(holder, entry, position);
} }
@ -94,13 +92,8 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec
return mEntries.isEmpty(); return mEntries.isEmpty();
} }
public E removeItemAt(int position) { public interface OnHistoryItemClickListener<E> {
E entry = mEntries.remove(position); void onHistoryItemClick(E item);
notifyItemRemoved(position); void onHistoryItemLongClick(E item);
return entry;
}
public interface OnHistoryItemClickListener<E extends HistoryEntry> {
void onHistoryItemClick(E historyItem);
} }
} }

View file

@ -2,7 +2,6 @@ package org.schabi.newpipe.history;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
@ -12,34 +11,31 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; 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.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import icepick.State; import icepick.State;
import io.reactivex.Observer; import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragment public abstract class HistoryFragment<E> extends BaseFragment
implements HistoryEntryAdapter.OnHistoryItemClickListener<E> { implements HistoryEntryAdapter.OnHistoryItemClickListener<E> {
private SharedPreferences mSharedPreferences; private SharedPreferences mSharedPreferences;
@ -54,12 +50,11 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
Parcelable mRecyclerViewState; Parcelable mRecyclerViewState;
private RecyclerView mRecyclerView; private RecyclerView mRecyclerView;
private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter; private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter;
private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback;
// private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
private HistoryDAO<E> mHistoryDataSource; private Subscription historySubscription;
private PublishSubject<Collection<E>> mHistoryEntryDeleteSubject;
private PublishSubject<Collection<E>> mHistoryEntryInsertSubject; protected HistoryRecordManager historyRecordManager;
protected CompositeDisposable disposables;
@StringRes @StringRes
abstract int getEnabledConfigKey(); abstract int getEnabledConfigKey();
@ -77,88 +72,47 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
// Register history enabled listener // Register history enabled listener
mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
mHistoryDataSource = createHistoryDAO(); historyRecordManager = new HistoryRecordManager(getContext());
disposables = new CompositeDisposable();
mHistoryEntryDeleteSubject = PublishSubject.create();
mHistoryEntryDeleteSubject
.observeOn(Schedulers.io())
.subscribe(new Consumer<Collection<E>>() {
@Override
public void accept(Collection<E> historyEntries) throws Exception {
mHistoryDataSource.delete(historyEntries);
}
});
mHistoryEntryInsertSubject = PublishSubject.create();
mHistoryEntryInsertSubject
.observeOn(Schedulers.io())
.subscribe(new Consumer<Collection<E>>() {
@Override
public void accept(Collection<E> 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();
}
}
};
} }
@NonNull @NonNull
protected abstract HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> createAdapter(); protected abstract HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> createAdapter();
protected abstract Single<List<Long>> insert(final Collection<E> entries);
protected abstract Single<Integer> delete(final Collection<E> entries);
@NonNull
protected abstract Flowable<List<E>> getAll();
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
mHistoryDataSource.getAll()
.toObservable() getAll().observeOn(AndroidSchedulers.mainThread()).subscribe(getHistorySubscriber());
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHistoryListConsumer()); final boolean newEnabled = isHistoryEnabled();
boolean newEnabled = isHistoryEnabled();
if (newEnabled != mHistoryIsEnabled) { if (newEnabled != mHistoryIsEnabled) {
onHistoryIsEnabledChanged(newEnabled); onHistoryIsEnabledChanged(newEnabled);
} }
} }
@NonNull @NonNull
private Observer<List<E>> getHistoryListConsumer() { private Subscriber<List<E>> getHistorySubscriber() {
return new Observer<List<E>>() { return new Subscriber<List<E>>() {
@Override @Override
public void onSubscribe(@NonNull Disposable d) { public void onSubscribe(Subscription s) {
if (historySubscription != null) historySubscription.cancel();
historySubscription = s;
historySubscription.request(1);
} }
@Override @Override
public void onNext(@NonNull List<E> historyEntries) { public void onNext(List<E> entries) {
if (!historyEntries.isEmpty()) { if (!entries.isEmpty()) {
mHistoryAdapter.setEntries(historyEntries); mHistoryAdapter.setEntries(entries);
animateView(mEmptyHistoryView, false, 200); animateView(mEmptyHistoryView, false, 200);
if (mRecyclerViewState != null) { if (mRecyclerViewState != null) {
@ -169,11 +123,13 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
mHistoryAdapter.clear(); mHistoryAdapter.clear();
showEmptyHistory(); showEmptyHistory();
} }
if (historySubscription != null) historySubscription.request(1);
} }
@Override @Override
public void onError(@NonNull Throwable e) { public void onError(Throwable t) {
// TODO: error handling like in (see e.g. subscription fragment)
} }
@Override @Override
@ -192,30 +148,33 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
*/ */
@MainThread @MainThread
public void onHistoryCleared() { public void onHistoryCleared() {
final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState(); if (getContext() == null) return;
final Collection<E> itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems());
mHistoryEntryDeleteSubject.onNext(itemsToDelete); 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); View view = getActivity().findViewById(R.id.main_content);
if (view == null) view = mRecyclerView.getRootView(); if (view == null) view = mRecyclerView.getRootView();
Snackbar.make(view, text, Snackbar.LENGTH_LONG).show();
}
if (!itemsToDelete.isEmpty()) { private void clearHistory() {
Snackbar.make(view, R.string.history_cleared, 5 * 1000) final Collection<E> itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems());
.setActionTextColor(Color.WHITE) disposables.add(delete(itemsToDelete).observeOn(AndroidSchedulers.mainThread())
.setAction(R.string.undo, new View.OnClickListener() { .subscribe());
@Override
public void onClick(View v) {
mRecyclerViewState = stateBeforeClear;
mHistoryEntryInsertSubject.onNext(itemsToDelete);
}
}).show();
} else {
Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show();
}
makeSnackbar(R.string.history_cleared);
mHistoryAdapter.clear(); mHistoryAdapter.clear();
showEmptyHistory(); showEmptyHistory();
} }
private void showEmptyHistory() { private void showEmptyHistory() {
@ -227,18 +186,18 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
@Nullable @Nullable
@CallSuper @CallSuper
@Override @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); View rootView = inflater.inflate(R.layout.fragment_history, container, false);
mRecyclerView = rootView.findViewById(R.id.history_view); 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); mRecyclerView.setLayoutManager(layoutManager);
mHistoryAdapter = createAdapter(); mHistoryAdapter = createAdapter();
mHistoryAdapter.setOnHistoryItemClickListener(this); mHistoryAdapter.setOnHistoryItemClickListener(this);
mRecyclerView.setAdapter(mHistoryAdapter); mRecyclerView.setAdapter(mHistoryAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mHistoryItemSwipeCallback);
itemTouchHelper.attachToRecyclerView(mRecyclerView);
mDisabledView = rootView.findViewById(R.id.history_disabled_view); mDisabledView = rootView.findViewById(R.id.history_disabled_view);
mEmptyHistoryView = rootView.findViewById(R.id.history_empty); mEmptyHistoryView = rootView.findViewById(R.id.history_empty);
@ -260,7 +219,7 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
mSharedPreferences = null; mSharedPreferences = null;
mHistoryIsEnabledChangeListener = null; mHistoryIsEnabledChangeListener = null;
mHistoryIsEnabledKey = null; mHistoryIsEnabledKey = null;
mHistoryDataSource = null; if (disposables != null) disposables.dispose();
} }
@Override @Override
@ -290,15 +249,8 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
} }
} }
/** private class HistoryIsEnabledChangeListener
* Creates a new history DAO implements SharedPreferences.OnSharedPreferenceChangeListener {
*
* @return the history DAO
*/
@NonNull
protected abstract HistoryDAO<E> createHistoryDAO();
private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(mHistoryIsEnabledKey)) { if (key.equals(mHistoryIsEnabledKey)) {

View file

@ -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<Long> 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<Integer> deleteStreamHistory(final long streamId) {
return Single.fromCallable(() -> streamHistoryTable.deleteStreamHistory(streamId))
.subscribeOn(Schedulers.io());
}
public Flowable<List<StreamHistoryEntry>> getStreamHistory() {
return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
}
public Flowable<List<StreamStatisticsEntry>> getStreamStatistics() {
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io());
}
public Single<List<Long>> insertStreamHistory(final Collection<StreamHistoryEntry> entries) {
List<StreamHistoryEntity> 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<Integer> deleteStreamHistory(final Collection<StreamHistoryEntry> entries) {
List<StreamHistoryEntity> 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<List<Long>> insertSearches(final Collection<SearchHistoryEntry> entries) {
return Single.fromCallable(() -> searchHistoryTable.insertAll(entries))
.subscribeOn(Schedulers.io());
}
public Single<Integer> deleteSearches(final Collection<SearchHistoryEntry> entries) {
return Single.fromCallable(() -> searchHistoryTable.delete(entries))
.subscribeOn(Schedulers.io());
}
public Flowable<List<SearchHistoryEntry>> getSearchHistory() {
return searchHistoryTable.getAll();
}
public Maybe<Long> 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<Integer> deleteSearchHistory(final String search) {
return Single.fromCallable(() -> searchHistoryTable.deleteAllWhereQuery(search))
.subscribeOn(Schedulers.io());
}
public Flowable<List<SearchHistoryEntry>> 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);
}
}

View file

@ -5,22 +5,27 @@ import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; 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.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> { 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<SearchHistoryEntry> {
@NonNull @NonNull
public static SearchHistoryFragment newInstance() { public static SearchHistoryFragment newInstance() {
@ -30,7 +35,6 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
historyItemSwipeCallback(allowedSwipeToDeleteDirections);
} }
@NonNull @NonNull
@ -39,21 +43,58 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
return new SearchHistoryAdapter(getContext()); return new SearchHistoryAdapter(getContext());
} }
@Override
protected Single<List<Long>> insert(Collection<SearchHistoryEntry> entries) {
return historyRecordManager.insertSearches(entries);
}
@Override
protected Single<Integer> delete(Collection<SearchHistoryEntry> entries) {
return historyRecordManager.deleteSearches(entries);
}
@NonNull
@Override
protected Flowable<List<SearchHistoryEntry>> getAll() {
return historyRecordManager.getSearchHistory();
}
@StringRes @StringRes
@Override @Override
int getEnabledConfigKey() { int getEnabledConfigKey() {
return R.string.enable_search_history_key; return R.string.enable_search_history_key;
} }
@NonNull
@Override @Override
protected HistoryDAO<SearchHistoryEntry> createHistoryDAO() { public void onHistoryItemClick(final SearchHistoryEntry historyItem) {
return NewPipeDatabase.getInstance().searchHistoryDAO(); NavigationHelper.openSearch(getContext(), historyItem.getServiceId(),
historyItem.getSearch());
} }
@Override @Override
public void onHistoryItemClick(SearchHistoryEntry historyItem) { public void onHistoryItemLongClick(final SearchHistoryEntry item) {
NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), historyItem.getSearch()); 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<Integer> 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<Integer> 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 { private static class ViewHolder extends RecyclerView.ViewHolder {

View file

@ -6,8 +6,8 @@ import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -16,18 +16,22 @@ import android.widget.TextView;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.history.dao.HistoryDAO; import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> { 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<StreamHistoryEntry> {
@NonNull @NonNull
public static WatchedHistoryFragment newInstance() { public static WatchedHistoryFragment newInstance() {
@ -37,7 +41,6 @@ public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
historyItemSwipeCallback(allowedSwipeToDeleteDirections);
} }
@StringRes @StringRes
@ -48,27 +51,59 @@ public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
@NonNull @NonNull
@Override @Override
protected WatchedHistoryAdapter createAdapter() { protected StreamHistoryAdapter createAdapter() {
return new WatchedHistoryAdapter(getContext()); return new StreamHistoryAdapter(getContext());
}
@Override
protected Single<List<Long>> insert(Collection<StreamHistoryEntry> entries) {
return historyRecordManager.insertStreamHistory(entries);
}
@Override
protected Single<Integer> delete(Collection<StreamHistoryEntry> entries) {
return historyRecordManager.deleteStreamHistory(entries);
} }
@NonNull @NonNull
@Override @Override
protected HistoryDAO<WatchHistoryEntry> createHistoryDAO() { protected Flowable<List<StreamHistoryEntry>> getAll() {
return NewPipeDatabase.getInstance().watchHistoryDAO(); return historyRecordManager.getStreamHistory();
} }
@Override @Override
public void onHistoryItemClick(WatchHistoryEntry historyItem) { public void onHistoryItemClick(StreamHistoryEntry historyItem) {
NavigationHelper.openVideoDetail(getContext(), NavigationHelper.openVideoDetail(getContext(), historyItem.serviceId, historyItem.url,
historyItem.getServiceId(), historyItem.title);
historyItem.getUrl(),
historyItem.getTitle());
} }
private static class WatchedHistoryAdapter extends HistoryEntryAdapter<WatchHistoryEntry, ViewHolder> { @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<Integer> 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<Integer> 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<StreamHistoryEntry, ViewHolder> {
StreamHistoryAdapter(Context context) {
super(context); super(context);
} }
@ -87,13 +122,13 @@ public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
} }
@Override @Override
void onBindViewHolder(ViewHolder holder, WatchHistoryEntry entry, int position) { void onBindViewHolder(ViewHolder holder, StreamHistoryEntry entry, int position) {
holder.date.setText(getFormattedDate(entry.getCreationDate())); holder.date.setText(getFormattedDate(entry.accessDate));
holder.streamTitle.setText(entry.getTitle()); holder.streamTitle.setText(entry.title);
holder.uploader.setText(entry.getUploader()); holder.uploader.setText(entry.uploader);
holder.duration.setText(Localization.getDurationString(entry.getDuration())); holder.duration.setText(Localization.getDurationString(entry.duration));
ImageLoader.getInstance() ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView,
.displayImage(entry.getThumbnailURL(), holder.thumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
} }
} }

View file

@ -61,10 +61,9 @@ import com.google.android.exoplayer2.util.Util;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo; 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.AudioReactor;
import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.CacheFactory;
import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.LoadController;
@ -150,7 +149,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
protected Disposable progressUpdateReactor; protected Disposable progressUpdateReactor;
protected CompositeDisposable databaseUpdateReactor; protected CompositeDisposable databaseUpdateReactor;
protected StreamRecordManager recordManager; protected HistoryRecordManager recordManager;
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -176,9 +175,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
public void initPlayer() { public void initPlayer() {
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
if (recordManager == null) { if (recordManager == null) recordManager = new HistoryRecordManager(context);
recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context));
}
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
databaseUpdateReactor = new CompositeDisposable(); 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. // 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, // Therefore, the only source that causes a discrepancy would be gapless transition,
// which can only offset the current track by +1. // 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); playQueue.offsetIndex(+1);
} }
playbackManager.load(); playbackManager.load();

View file

@ -229,6 +229,8 @@
<string name="view">Play</string> <string name="view">Play</string>
<string name="create">Create</string> <string name="create">Create</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="delete_one">Delete One</string>
<string name="delete_all">Delete All</string>
<string name="checksum">Checksum</string> <string name="checksum">Checksum</string>
<!-- Fragment --> <!-- Fragment -->
@ -307,6 +309,8 @@
<string name="history_cleared">History cleared</string> <string name="history_cleared">History cleared</string>
<string name="item_deleted">Item deleted</string> <string name="item_deleted">Item deleted</string>
<string name="delete_item_search_history">Do you want to delete this item from search history?</string> <string name="delete_item_search_history">Do you want to delete this item from search history?</string>
<string name="delete_stream_history_prompt">Do you want to delete this item from watch history?</string>
<string name="delete_all_history_prompt">Are you sure you want to delete all items from history?</string>
<string name="title_watch_history">Watch History</string> <string name="title_watch_history">Watch History</string>
<string name="title_most_played">Most Played</string> <string name="title_most_played">Most Played</string>