-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.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<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.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;

View File

@ -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 )");

View File

@ -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<SearchHistoryEntry> {
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
@Override
@Nullable
SearchHistoryEntry getLatestEntry();
@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.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<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.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";

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.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},

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 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);
}
}

View File

@ -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;

View File

@ -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

View File

@ -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<StreamInfo> 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<StreamInfo> 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<StreamInfo> 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<StreamInfo> 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 {

View File

@ -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<SearchResult, ListExtractor
private CompositeDisposable disposables = new CompositeDisposable();
private SuggestionListAdapter suggestionListAdapter;
private SearchHistoryDAO searchHistoryDAO;
private HistoryRecordManager historyRecordManager;
/*//////////////////////////////////////////////////////////////////////////
// Views
@ -166,8 +158,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true);
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
searchHistoryDAO = NewPipeDatabase.getInstance().searchHistoryDAO();
historyRecordManager = new HistoryRecordManager(context);
}
@Override
@ -535,36 +527,24 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
}
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)
.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<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();
.setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete))
.show();
}
@Override
@ -589,83 +569,67 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
final Observable<String> observable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWith(searchQuery != null ? searchQuery : "")
.filter(new Predicate<String>() {
@Override
public boolean test(@io.reactivex.annotations.NonNull String query) throws Exception {
return isSuggestionsEnabled;
}
});
.filter(query -> isSuggestionsEnabled);
suggestionDisposable = observable
.switchMap(new Function<String, ObservableSource<Notification<List<SuggestionItem>>>>() {
@Override
public ObservableSource<Notification<List<SuggestionItem>>> apply(@io.reactivex.annotations.NonNull final String query) throws Exception {
final Flowable<List<SearchHistoryEntry>> flowable = query.length() > 0
? searchHistoryDAO.getSimilarEntries(query, 3)
: searchHistoryDAO.getUniqueEntries(25);
final Observable<List<SuggestionItem>> local = flowable.toObservable()
.map(new Function<List<SearchHistoryEntry>, List<SuggestionItem>>() {
@Override
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;
}
});
.switchMap(query -> {
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
.getRelatedSearches(query, 3, 25);
final Observable<List<SuggestionItem>> local = flowable.toObservable()
.map(searchHistoryEntries -> {
List<SuggestionItem> 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<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()
.map(new Function<List<String>, List<SuggestionItem>>() {
@Override
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();
}
if (networkResult.size() > 0) result.addAll(networkResult);
return result;
}).materialize();
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Notification<List<SuggestionItem>>>() {
@Override
public void accept(@io.reactivex.annotations.NonNull Notification<List<SuggestionItem>> 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<SearchResult, ListExtractor
hideSuggestionsPanel();
hideKeyboardSearch();
if (activity instanceof HistoryListener) {
((HistoryListener) activity).onSearch(serviceId, query);
suggestionPublisher.onNext(query);
}
historyRecordManager.onSearched(serviceId, query)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
ignored -> {},
error -> showSnackBarError(error, UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), query, 0)
);
suggestionPublisher.onNext(query);
startLoading(false);
}

View File

@ -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());
}

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.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<Object>() {
@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();
});
}

View File

@ -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 <E> the type of the entries
* @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 DateFormat mDateFormat;
@ -29,9 +28,8 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec
public HistoryEntryAdapter(Context context) {
super();
mEntries = new ArrayList<>();
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<E> historyEntries) {
@ -53,11 +51,6 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec
return mDateFormat.format(date);
}
@Override
public long getItemId(int position) {
return mEntries.get(position).getId();
}
@Override
public int getItemCount() {
return mEntries.size();
@ -66,15 +59,20 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec
@Override
public void onBindViewHolder(VH holder, int position) {
final E entry = mEntries.get(position);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final OnHistoryItemClickListener<E> 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<E extends HistoryEntry, VH extends Rec
return mEntries.isEmpty();
}
public E removeItemAt(int position) {
E entry = mEntries.remove(position);
notifyItemRemoved(position);
return entry;
}
public interface OnHistoryItemClickListener<E extends HistoryEntry> {
void onHistoryItemClick(E historyItem);
public interface OnHistoryItemClickListener<E> {
void onHistoryItemClick(E item);
void onHistoryItemLongClick(E item);
}
}

View File

@ -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<E extends HistoryEntry> extends BaseFragment
public abstract class HistoryFragment<E> extends BaseFragment
implements HistoryEntryAdapter.OnHistoryItemClickListener<E> {
private SharedPreferences mSharedPreferences;
@ -54,12 +50,11 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
Parcelable mRecyclerViewState;
private RecyclerView mRecyclerView;
private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter;
private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback;
// private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
private HistoryDAO<E> mHistoryDataSource;
private PublishSubject<Collection<E>> mHistoryEntryDeleteSubject;
private PublishSubject<Collection<E>> mHistoryEntryInsertSubject;
private Subscription historySubscription;
protected HistoryRecordManager historyRecordManager;
protected CompositeDisposable disposables;
@StringRes
abstract int getEnabledConfigKey();
@ -77,88 +72,47 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
// Register history enabled listener
mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
mHistoryDataSource = createHistoryDAO();
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();
}
}
};
historyRecordManager = new HistoryRecordManager(getContext());
disposables = new CompositeDisposable();
}
@NonNull
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
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<List<E>> getHistoryListConsumer() {
return new Observer<List<E>>() {
private Subscriber<List<E>> getHistorySubscriber() {
return new Subscriber<List<E>>() {
@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<E> historyEntries) {
if (!historyEntries.isEmpty()) {
mHistoryAdapter.setEntries(historyEntries);
public void onNext(List<E> entries) {
if (!entries.isEmpty()) {
mHistoryAdapter.setEntries(entries);
animateView(mEmptyHistoryView, false, 200);
if (mRecyclerViewState != null) {
@ -169,11 +123,13 @@ public abstract class HistoryFragment<E extends HistoryEntry> 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<E extends HistoryEntry> extends BaseFragme
*/
@MainThread
public void onHistoryCleared() {
final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState();
final Collection<E> 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<E> 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<E extends HistoryEntry> 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<E extends HistoryEntry> extends BaseFragme
mSharedPreferences = null;
mHistoryIsEnabledChangeListener = null;
mHistoryIsEnabledKey = null;
mHistoryDataSource = null;
if (disposables != null) disposables.dispose();
}
@Override
@ -290,15 +249,8 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
}
}
/**
* Creates a new history DAO
*
* @return the history DAO
*/
@NonNull
protected abstract HistoryDAO<E> 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)) {

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.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<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
public static SearchHistoryFragment newInstance() {
@ -30,7 +35,6 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
historyItemSwipeCallback(allowedSwipeToDeleteDirections);
}
@NonNull
@ -39,21 +43,58 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
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
@Override
int getEnabledConfigKey() {
return R.string.enable_search_history_key;
}
@NonNull
@Override
protected HistoryDAO<SearchHistoryEntry> 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<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 {

View File

@ -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<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
public static WatchedHistoryFragment newInstance() {
@ -37,7 +41,6 @@ public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
historyItemSwipeCallback(allowedSwipeToDeleteDirections);
}
@StringRes
@ -48,27 +51,59 @@ public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
@NonNull
@Override
protected WatchedHistoryAdapter createAdapter() {
return new WatchedHistoryAdapter(getContext());
protected StreamHistoryAdapter createAdapter() {
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
@Override
protected HistoryDAO<WatchHistoryEntry> createHistoryDAO() {
return NewPipeDatabase.getInstance().watchHistoryDAO();
protected Flowable<List<StreamHistoryEntry>> 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<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);
}
@ -87,13 +122,13 @@ public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
}
@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);
}
}

View File

@ -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();

View File

@ -229,6 +229,8 @@
<string name="view">Play</string>
<string name="create">Create</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>
<!-- Fragment -->
@ -307,6 +309,8 @@
<string name="history_cleared">History cleared</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_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_most_played">Most Played</string>