diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 9e8f3fa76..9a1ecd07a 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -26,8 +26,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; @@ -42,40 +40,21 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.history.dao.HistoryDAO; -import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; -import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; -import org.schabi.newpipe.database.history.model.HistoryEntry; -import org.schabi.newpipe.database.history.model.SearchHistoryEntry; -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; -import org.schabi.newpipe.history.HistoryListener; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; -import java.util.Date; - -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; -import io.reactivex.subjects.PublishSubject; - -public class MainActivity extends AppCompatActivity implements HistoryListener { +public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); - private SharedPreferences sharedPreferences; private ActionBarDrawerToggle toggle = null; /*////////////////////////////////////////////////////////////////////////// @@ -86,7 +65,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { protected void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); super.onCreate(savedInstanceState); @@ -98,7 +76,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { setSupportActionBar(findViewById(R.id.toolbar)); setupDrawer(); - initHistory(); } private void setupDrawer() { @@ -149,8 +126,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { if (!isChangingConfigurations()) { StateSaver.clearStateFiles(); } - - disposeHistory(); } @Override @@ -357,75 +332,4 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { NavigationHelper.gotoMainFragment(getSupportFragmentManager()); } } - - /*////////////////////////////////////////////////////////////////////////// - // History - //////////////////////////////////////////////////////////////////////////*/ - - private WatchHistoryDAO watchHistoryDAO; - private SearchHistoryDAO searchHistoryDAO; - private PublishSubject historyEntrySubject; - private Disposable disposable; - - private void initHistory() { - final AppDatabase database = NewPipeDatabase.getInstance(); - watchHistoryDAO = database.watchHistoryDAO(); - searchHistoryDAO = database.searchHistoryDAO(); - historyEntrySubject = PublishSubject.create(); - disposable = historyEntrySubject - .observeOn(Schedulers.io()) - .subscribe(getHistoryEntryConsumer()); - } - - private void disposeHistory() { - if (disposable != null) disposable.dispose(); - watchHistoryDAO = null; - searchHistoryDAO = null; - } - - @NonNull - private Consumer getHistoryEntryConsumer() { - return new Consumer() { - @Override - public void accept(HistoryEntry historyEntry) throws Exception { - //noinspection unchecked - HistoryDAO historyDAO = (HistoryDAO) - (historyEntry instanceof SearchHistoryEntry ? searchHistoryDAO : watchHistoryDAO); - - HistoryEntry latestEntry = historyDAO.getLatestEntry(); - if (historyEntry.hasEqualValues(latestEntry)) { - latestEntry.setCreationDate(historyEntry.getCreationDate()); - historyDAO.update(latestEntry); - } else { - historyDAO.insert(historyEntry); - } - } - }; - } - - private void addWatchHistoryEntry(StreamInfo streamInfo) { - if (sharedPreferences.getBoolean(getString(R.string.enable_watch_history_key), true)) { - WatchHistoryEntry entry = new WatchHistoryEntry(streamInfo); - historyEntrySubject.onNext(entry); - } - } - - @Override - public void onVideoPlayed(StreamInfo streamInfo, @Nullable VideoStream videoStream) { - addWatchHistoryEntry(streamInfo); - } - - @Override - public void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream) { - addWatchHistoryEntry(streamInfo); - } - - @Override - public void onSearch(int serviceId, String query) { - // Add search history entry - if (sharedPreferences.getBoolean(getString(R.string.enable_search_history_key), true)) { - SearchHistoryEntry searchHistoryEntry = new SearchHistoryEntry(new Date(), serviceId, query); - historyEntrySubject.onNext(searchHistoryEntry); - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index d5a9164dc..7097dd4a7 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -13,10 +13,10 @@ import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 9200a64b0..b977e43e9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -20,6 +20,7 @@ public class Migrations { // Not much we can do about this, since room doesn't create tables before migration. // It's either this or blasting the entire database anew. + database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java index 70799d971..257c1ec3d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.database.history.dao; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; +import android.support.annotation.Nullable; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; @@ -22,6 +23,7 @@ public interface SearchHistoryDAO extends HistoryDAO { @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") @Override + @Nullable SearchHistoryEntry getLatestEntry(); @Query("DELETE FROM " + TABLE_NAME) diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java similarity index 80% rename from app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java rename to app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 81ee9d912..64003910e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -1,14 +1,13 @@ -package org.schabi.newpipe.database.stream.dao; +package org.schabi.newpipe.database.history.dao; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; -import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.stream.StreamHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import java.util.List; @@ -18,9 +17,9 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LA import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @Dao public abstract class StreamHistoryDAO implements BasicDAO { diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java index d18974089..bba3f4295 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java @@ -3,10 +3,14 @@ package org.schabi.newpipe.database.history.model; import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.Ignore; +import android.arch.persistence.room.Index; import java.util.Date; -@Entity(tableName = SearchHistoryEntry.TABLE_NAME) +import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH; + +@Entity(tableName = SearchHistoryEntry.TABLE_NAME, + indices = {@Index(value = SEARCH)}) public class SearchHistoryEntry extends HistoryEntry { public static final String TABLE_NAME = "search_history"; diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java similarity index 81% rename from app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java rename to app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java index d937a29ed..b238af0a9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.database.stream.model; +package org.schabi.newpipe.database.history.model; import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; @@ -6,12 +6,14 @@ import android.arch.persistence.room.ForeignKey; import android.arch.persistence.room.Index; import android.support.annotation.NonNull; +import org.schabi.newpipe.database.stream.model.StreamEntity; + import java.util.Date; import static android.arch.persistence.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; @Entity(tableName = STREAM_HISTORY_TABLE, primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java similarity index 90% rename from app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java rename to app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java index 3df641372..cdc9cc40a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java @@ -1,9 +1,8 @@ -package org.schabi.newpipe.database.stream; +package org.schabi.newpipe.database.history.model; import android.arch.persistence.room.ColumnInfo; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Date; @@ -44,4 +43,8 @@ public class StreamHistoryEntry { this.streamId = streamId; this.accessDate = accessDate; } + + public StreamHistoryEntity toStreamHistoryEntity() { + return new StreamHistoryEntity(streamId, accessDate); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java index 722cff5cd..1c2a7028e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -2,9 +2,8 @@ package org.schabi.newpipe.database.stream; import android.arch.persistence.room.ColumnInfo; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java index a4955d835..b699e0b6b 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -9,7 +9,7 @@ import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import java.util.ArrayList; @@ -22,7 +22,7 @@ import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @Dao diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 05550a0a5..b134bc98d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -44,6 +44,7 @@ import com.nirhart.parallaxscroll.views.ParallaxScrollView; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; @@ -60,6 +61,7 @@ import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.history.HistoryListener; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.OnInfoItemGesture; @@ -649,9 +651,6 @@ public class VideoDetailFragment extends BaseStateFragment implement public void onActionSelected(int selectedStreamId) { try { NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http"))); - if(activity instanceof HistoryListener) { - ((HistoryListener) activity).onVideoPlayed(info, null); - } } catch (Exception e) { if(DEBUG) Log.i(TAG, "Failed to start kore", e); showInstallKoreDialog(activity); @@ -805,10 +804,6 @@ public class VideoDetailFragment extends BaseStateFragment implement private void openBackgroundPlayer(final boolean append) { AudioStream audioStream = currentInfo.getAudioStreams().get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); - if (activity instanceof HistoryListener) { - ((HistoryListener) activity).onAudioPlayed(currentInfo, audioStream); - } - boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); @@ -825,10 +820,6 @@ public class VideoDetailFragment extends BaseStateFragment implement return; } - if (activity instanceof HistoryListener) { - ((HistoryListener) activity).onVideoPlayed(currentInfo, getSelectedVideoStream()); - } - final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); if (append) { NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue); @@ -844,10 +835,6 @@ public class VideoDetailFragment extends BaseStateFragment implement private void openVideoPlayer() { VideoStream selectedVideoStream = getSelectedVideoStream(); - if (activity instanceof HistoryListener) { - ((HistoryListener) activity).onVideoPlayed(currentInfo, selectedVideoStream); - } - if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream); } else { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index d6ed2a313..0638c06e7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.list.search; import android.app.Activity; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -30,10 +29,8 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; -import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; @@ -44,7 +41,7 @@ import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchResult; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; -import org.schabi.newpipe.history.HistoryListener; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.AnimationUtils; @@ -64,16 +61,11 @@ import java.util.concurrent.TimeUnit; import icepick.State; import io.reactivex.Flowable; -import io.reactivex.Notification; import io.reactivex.Observable; -import io.reactivex.ObservableSource; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.BiFunction; import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; -import io.reactivex.functions.Predicate; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; @@ -121,7 +113,7 @@ public class SearchFragment extends BaseListFragment suggestionPublisher + .onNext(searchEditText.getText().toString()), + + throwable -> showSnackBarError(throwable, + UserAction.SOMETHING_ELSE, "none", + "Deleting item failed", R.string.general_error) + ); + new AlertDialog.Builder(activity) .setTitle(item.query) .setMessage(R.string.delete_item_search_history) .setCancelable(true) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - disposables.add(Observable - .fromCallable(new Callable() { - @Override - public Integer call() throws Exception { - return searchHistoryDAO.deleteAllWhereQuery(item.query); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Integer howManyDeleted) throws Exception { - suggestionPublisher.onNext(searchEditText.getText().toString()); - } - }, new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - showSnackBarError(throwable, UserAction.SOMETHING_ELSE, "none", "Deleting item failed", R.string.general_error); - } - })); - } - }).show(); + .setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete)) + .show(); } @Override @@ -589,83 +569,67 @@ public class SearchFragment extends BaseListFragment observable = suggestionPublisher .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) .startWith(searchQuery != null ? searchQuery : "") - .filter(new Predicate() { - @Override - public boolean test(@io.reactivex.annotations.NonNull String query) throws Exception { - return isSuggestionsEnabled; - } - }); + .filter(query -> isSuggestionsEnabled); suggestionDisposable = observable - .switchMap(new Function>>>() { - @Override - public ObservableSource>> apply(@io.reactivex.annotations.NonNull final String query) throws Exception { - final Flowable> flowable = query.length() > 0 - ? searchHistoryDAO.getSimilarEntries(query, 3) - : searchHistoryDAO.getUniqueEntries(25); - final Observable> local = flowable.toObservable() - .map(new Function, List>() { - @Override - public List apply(@io.reactivex.annotations.NonNull List searchHistoryEntries) throws Exception { - List result = new ArrayList<>(); - for (SearchHistoryEntry entry : searchHistoryEntries) - result.add(new SuggestionItem(true, entry.getSearch())); - return result; - } - }); + .switchMap(query -> { + final Flowable> flowable = historyRecordManager + .getRelatedSearches(query, 3, 25); + final Observable> local = flowable.toObservable() + .map(searchHistoryEntries -> { + List result = new ArrayList<>(); + for (SearchHistoryEntry entry : searchHistoryEntries) + result.add(new SuggestionItem(true, entry.getSearch())); + return result; + }); - if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { - // Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION - return local.materialize(); + if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { + // Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION + return local.materialize(); + } + + final Observable> network = ExtractorHelper + .suggestionsFor(serviceId, query, contentCountry) + .toObservable() + .map(strings -> { + List result = new ArrayList<>(); + for (String entry : strings) { + result.add(new SuggestionItem(false, entry)); + } + return result; + }); + + return Observable.zip(local, network, (localResult, networkResult) -> { + List result = new ArrayList<>(); + if (localResult.size() > 0) result.addAll(localResult); + + // Remove duplicates + final Iterator iterator = networkResult.iterator(); + while (iterator.hasNext() && localResult.size() > 0) { + final SuggestionItem next = iterator.next(); + for (SuggestionItem item : localResult) { + if (item.query.equals(next.query)) { + iterator.remove(); + break; + } + } } - final Observable> network = ExtractorHelper.suggestionsFor(serviceId, query, contentCountry).toObservable() - .map(new Function, List>() { - @Override - public List apply(@io.reactivex.annotations.NonNull List strings) throws Exception { - List result = new ArrayList<>(); - for (String entry : strings) result.add(new SuggestionItem(false, entry)); - return result; - } - }); - - return Observable.zip(local, network, new BiFunction, List, List>() { - @Override - public List apply(@io.reactivex.annotations.NonNull List localResult, @io.reactivex.annotations.NonNull List networkResult) throws Exception { - List result = new ArrayList<>(); - if (localResult.size() > 0) result.addAll(localResult); - - // Remove duplicates - final Iterator iterator = networkResult.iterator(); - while (iterator.hasNext() && localResult.size() > 0) { - final SuggestionItem next = iterator.next(); - for (SuggestionItem item : localResult) { - if (item.query.equals(next.query)) { - iterator.remove(); - break; - } - } - } - - if (networkResult.size() > 0) result.addAll(networkResult); - return result; - } - }).materialize(); - } + if (networkResult.size() > 0) result.addAll(networkResult); + return result; + }).materialize(); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer>>() { - @Override - public void accept(@io.reactivex.annotations.NonNull Notification> listNotification) throws Exception { - if (listNotification.isOnNext()) { - handleSuggestions(listNotification.getValue()); - } else if (listNotification.isOnError()) { - Throwable error = listNotification.getError(); - if (!ExtractorHelper.hasAssignableCauseThrowable(error, - IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class)) { - onSuggestionError(error); - } + .subscribe(listNotification -> { + if (listNotification.isOnNext()) { + handleSuggestions(listNotification.getValue()); + } else if (listNotification.isOnError()) { + Throwable error = listNotification.getError(); + if (!ExtractorHelper.hasAssignableCauseThrowable(error, + IOException.class, SocketException.class, + InterruptedException.class, InterruptedIOException.class)) { + onSuggestionError(error); } } }); @@ -718,11 +682,14 @@ public class SearchFragment extends BaseListFragment {}, + error -> showSnackBarError(error, UserAction.SEARCHED, + NewPipe.getNameOfService(serviceId), query, 0) + ); + suggestionPublisher.onNext(query); startLoading(false); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java index c2181ca8d..6eddc3a5c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java @@ -13,12 +13,12 @@ import android.view.ViewGroup; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; @@ -49,7 +49,7 @@ public abstract class StatisticsPlaylistFragment /* Used for independent events */ private Subscription databaseSubscription; - private StreamRecordManager recordManager; + private HistoryRecordManager recordManager; /////////////////////////////////////////////////////////////////////////// // Abstracts @@ -68,7 +68,7 @@ public abstract class StatisticsPlaylistFragment @Override public void onAttach(Context context) { super.onAttach(context); - recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); + recordManager = new HistoryRecordManager(context); } @Override @@ -205,7 +205,7 @@ public abstract class StatisticsPlaylistFragment super.startLoading(forceLoad); resetFragment(); - recordManager.getStatistics() + recordManager.getStreamStatistics() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getHistoryObserver()); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java deleted file mode 100644 index 993ed58da..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.schabi.newpipe.fragments.local; - -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; -import org.schabi.newpipe.extractor.stream.StreamInfo; - -import java.util.Date; -import java.util.List; - -import io.reactivex.Flowable; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; - -public class StreamRecordManager { - - private final AppDatabase database; - private final StreamDAO streamTable; - private final StreamHistoryDAO historyTable; - - public StreamRecordManager(final AppDatabase db) { - database = db; - streamTable = db.streamDAO(); - historyTable = db.streamHistoryDAO(); - } - - public Single onViewed(final StreamInfo info) { - return Single.fromCallable(() -> database.runInTransaction(() -> { - final long streamId = streamTable.upsert(new StreamEntity(info)); - return historyTable.insert(new StreamHistoryEntity(streamId, new Date())); - })).subscribeOn(Schedulers.io()); - } - - public int removeHistory(final long streamId) { - return historyTable.deleteStreamHistory(streamId); - } - - public Flowable> getStatistics() { - return historyTable.getStatistics(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java index 8d8e4ef16..30589a22c 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java @@ -9,6 +9,7 @@ import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; @@ -50,8 +51,10 @@ public class HistoryActivity extends AppCompatActivity { Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setTitle(R.string.title_activity_history); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setTitle(R.string.title_activity_history); + } // Create the adapter that will return a fragment for each of the three // primary sections of the activity. mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); @@ -66,17 +69,11 @@ public class HistoryActivity extends AppCompatActivity { final FloatingActionButton fab = findViewById(R.id.fab); RxView.clicks(fab) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Object o) { - int currentItem = mViewPager.getCurrentItem(); - HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.instantiateItem(mViewPager, currentItem); - if(fragment != null) { - fragment.onHistoryCleared(); - } else { - Log.w(TAG, "Couldn't find current fragment"); - } - } + .subscribe(ignored -> { + int currentItem = mViewPager.getCurrentItem(); + HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter + .instantiateItem(mViewPager, currentItem); + fragment.onHistoryCleared(); }); } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java index d56469a7e..f61e8eb7d 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java @@ -4,9 +4,8 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; -import android.view.View; -import org.schabi.newpipe.database.history.model.HistoryEntry; +import org.schabi.newpipe.util.Localization; import java.text.DateFormat; import java.util.ArrayList; @@ -19,7 +18,7 @@ import java.util.Date; * @param the type of the entries * @param the type of the view holder */ -public abstract class HistoryEntryAdapter extends RecyclerView.Adapter { +public abstract class HistoryEntryAdapter extends RecyclerView.Adapter { private final ArrayList mEntries; private final DateFormat mDateFormat; @@ -29,9 +28,8 @@ public abstract class HistoryEntryAdapter(); - mDateFormat = android.text.format.DateFormat.getDateFormat(context.getApplicationContext()); - - setHasStableIds(true); + mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, + Localization.getPreferredLocale(context)); } public void setEntries(@NonNull Collection historyEntries) { @@ -53,11 +51,6 @@ public abstract class HistoryEntryAdapter historyItemClickListener = onHistoryItemClickListener; - if(historyItemClickListener != null) { - historyItemClickListener.onHistoryItemClick(entry); - } + holder.itemView.setOnClickListener(v -> { + if(onHistoryItemClickListener != null) { + onHistoryItemClickListener.onHistoryItemClick(entry); } }); + + holder.itemView.setOnLongClickListener(view -> { + if (onHistoryItemClickListener != null) { + onHistoryItemClickListener.onHistoryItemLongClick(entry); + return true; + } + return false; + }); + onBindViewHolder(holder, entry, position); } @@ -94,13 +92,8 @@ public abstract class HistoryEntryAdapter { - void onHistoryItemClick(E historyItem); + public interface OnHistoryItemClickListener { + void onHistoryItemClick(E item); + void onHistoryItemLongClick(E item); } } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java index c64689775..462c12e61 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -2,7 +2,6 @@ package org.schabi.newpipe.history; import android.content.SharedPreferences; -import android.graphics.Color; import android.os.Bundle; import android.os.Parcelable; import android.preference.PreferenceManager; @@ -12,34 +11,31 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.history.dao.HistoryDAO; -import org.schabi.newpipe.database.history.model.HistoryEntry; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import icepick.State; -import io.reactivex.Observer; +import io.reactivex.Flowable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; -import io.reactivex.subjects.PublishSubject; +import io.reactivex.disposables.CompositeDisposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public abstract class HistoryFragment extends BaseFragment +public abstract class HistoryFragment extends BaseFragment implements HistoryEntryAdapter.OnHistoryItemClickListener { private SharedPreferences mSharedPreferences; @@ -54,12 +50,11 @@ public abstract class HistoryFragment extends BaseFragme Parcelable mRecyclerViewState; private RecyclerView mRecyclerView; private HistoryEntryAdapter mHistoryAdapter; - private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback; - // private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; - private HistoryDAO mHistoryDataSource; - private PublishSubject> mHistoryEntryDeleteSubject; - private PublishSubject> mHistoryEntryInsertSubject; + private Subscription historySubscription; + + protected HistoryRecordManager historyRecordManager; + protected CompositeDisposable disposables; @StringRes abstract int getEnabledConfigKey(); @@ -77,88 +72,47 @@ public abstract class HistoryFragment extends BaseFragme // Register history enabled listener mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); - mHistoryDataSource = createHistoryDAO(); - - mHistoryEntryDeleteSubject = PublishSubject.create(); - mHistoryEntryDeleteSubject - .observeOn(Schedulers.io()) - .subscribe(new Consumer>() { - @Override - public void accept(Collection historyEntries) throws Exception { - mHistoryDataSource.delete(historyEntries); - } - }); - - mHistoryEntryInsertSubject = PublishSubject.create(); - mHistoryEntryInsertSubject - .observeOn(Schedulers.io()) - .subscribe(new Consumer>() { - @Override - public void accept(Collection historyEntries) throws Exception { - mHistoryDataSource.insertAll(historyEntries); - } - }); - - - } - - protected void historyItemSwipeCallback(int swipeDirection) { - mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, swipeDirection) { - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { - return false; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { - if (mHistoryAdapter != null) { - final E historyEntry = mHistoryAdapter.removeItemAt(viewHolder.getAdapterPosition()); - mHistoryEntryDeleteSubject.onNext(Collections.singletonList(historyEntry)); - - View view = getActivity().findViewById(R.id.main_content); - if (view == null) view = mRecyclerView.getRootView(); - - Snackbar.make(view, R.string.item_deleted, 5 * 1000) - .setActionTextColor(Color.WHITE) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - mHistoryEntryInsertSubject.onNext(Collections.singletonList(historyEntry)); - } - }).show(); - } - } - }; + historyRecordManager = new HistoryRecordManager(getContext()); + disposables = new CompositeDisposable(); } @NonNull protected abstract HistoryEntryAdapter createAdapter(); + protected abstract Single> insert(final Collection entries); + + protected abstract Single delete(final Collection entries); + + @NonNull + protected abstract Flowable> getAll(); + @Override public void onResume() { super.onResume(); - mHistoryDataSource.getAll() - .toObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getHistoryListConsumer()); - boolean newEnabled = isHistoryEnabled(); + + getAll().observeOn(AndroidSchedulers.mainThread()).subscribe(getHistorySubscriber()); + + final boolean newEnabled = isHistoryEnabled(); if (newEnabled != mHistoryIsEnabled) { onHistoryIsEnabledChanged(newEnabled); } } @NonNull - private Observer> getHistoryListConsumer() { - return new Observer>() { + private Subscriber> getHistorySubscriber() { + return new Subscriber>() { @Override - public void onSubscribe(@NonNull Disposable d) { + public void onSubscribe(Subscription s) { + if (historySubscription != null) historySubscription.cancel(); + historySubscription = s; + historySubscription.request(1); } @Override - public void onNext(@NonNull List historyEntries) { - if (!historyEntries.isEmpty()) { - mHistoryAdapter.setEntries(historyEntries); + public void onNext(List entries) { + if (!entries.isEmpty()) { + mHistoryAdapter.setEntries(entries); animateView(mEmptyHistoryView, false, 200); if (mRecyclerViewState != null) { @@ -169,11 +123,13 @@ public abstract class HistoryFragment extends BaseFragme mHistoryAdapter.clear(); showEmptyHistory(); } + + if (historySubscription != null) historySubscription.request(1); } @Override - public void onError(@NonNull Throwable e) { - // TODO: error handling like in (see e.g. subscription fragment) + public void onError(Throwable t) { + } @Override @@ -192,30 +148,33 @@ public abstract class HistoryFragment extends BaseFragme */ @MainThread public void onHistoryCleared() { - final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState(); - final Collection itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); - mHistoryEntryDeleteSubject.onNext(itemsToDelete); + if (getContext() == null) return; + + new AlertDialog.Builder(getContext()) + .setTitle(R.string.delete_all) + .setMessage(R.string.delete_all_history_prompt) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.delete, (dialog, i) -> clearHistory()) + .show(); + } + + protected void makeSnackbar(@StringRes final int text) { + if (getActivity() == null) return; View view = getActivity().findViewById(R.id.main_content); if (view == null) view = mRecyclerView.getRootView(); + Snackbar.make(view, text, Snackbar.LENGTH_LONG).show(); + } - if (!itemsToDelete.isEmpty()) { - Snackbar.make(view, R.string.history_cleared, 5 * 1000) - .setActionTextColor(Color.WHITE) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - mRecyclerViewState = stateBeforeClear; - mHistoryEntryInsertSubject.onNext(itemsToDelete); - } - }).show(); - } else { - Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show(); - } + private void clearHistory() { + final Collection itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); + disposables.add(delete(itemsToDelete).observeOn(AndroidSchedulers.mainThread()) + .subscribe()); + makeSnackbar(R.string.history_cleared); mHistoryAdapter.clear(); showEmptyHistory(); - } private void showEmptyHistory() { @@ -227,18 +186,18 @@ public abstract class HistoryFragment extends BaseFragme @Nullable @CallSuper @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_history, container, false); mRecyclerView = rootView.findViewById(R.id.history_view); - RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), + LinearLayoutManager.VERTICAL, false); mRecyclerView.setLayoutManager(layoutManager); mHistoryAdapter = createAdapter(); mHistoryAdapter.setOnHistoryItemClickListener(this); mRecyclerView.setAdapter(mHistoryAdapter); - ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mHistoryItemSwipeCallback); - itemTouchHelper.attachToRecyclerView(mRecyclerView); mDisabledView = rootView.findViewById(R.id.history_disabled_view); mEmptyHistoryView = rootView.findViewById(R.id.history_empty); @@ -260,7 +219,7 @@ public abstract class HistoryFragment extends BaseFragme mSharedPreferences = null; mHistoryIsEnabledChangeListener = null; mHistoryIsEnabledKey = null; - mHistoryDataSource = null; + if (disposables != null) disposables.dispose(); } @Override @@ -290,15 +249,8 @@ public abstract class HistoryFragment extends BaseFragme } } - /** - * Creates a new history DAO - * - * @return the history DAO - */ - @NonNull - protected abstract HistoryDAO createHistoryDAO(); - - private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener { + private class HistoryIsEnabledChangeListener + implements SharedPreferences.OnSharedPreferenceChangeListener { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(mHistoryIsEnabledKey)) { diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java new file mode 100644 index 000000000..1a5fe0525 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java @@ -0,0 +1,147 @@ +package org.schabi.newpipe.history; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; +import org.schabi.newpipe.database.history.model.SearchHistoryEntry; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.extractor.stream.StreamInfo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class HistoryRecordManager { + + private final AppDatabase database; + private final StreamDAO streamTable; + private final StreamHistoryDAO streamHistoryTable; + private final SearchHistoryDAO searchHistoryTable; + private final SharedPreferences sharedPreferences; + private final String searchHistoryKey; + private final String streamHistoryKey; + + public HistoryRecordManager(final Context context) { + database = NewPipeDatabase.getInstance(context); + streamTable = database.streamDAO(); + streamHistoryTable = database.streamHistoryDAO(); + searchHistoryTable = database.searchHistoryDAO(); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + searchHistoryKey = context.getString(R.string.enable_search_history_key); + streamHistoryKey = context.getString(R.string.enable_watch_history_key); + } + + public Maybe onViewed(final StreamInfo info) { + if (!isStreamHistoryEnabled()) return Maybe.empty(); + + final Date currentTime = new Date(); + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); + })).subscribeOn(Schedulers.io()); + } + + public Single deleteStreamHistory(final long streamId) { + return Single.fromCallable(() -> streamHistoryTable.deleteStreamHistory(streamId)) + .subscribeOn(Schedulers.io()); + } + + public Flowable> getStreamHistory() { + return streamHistoryTable.getHistory().subscribeOn(Schedulers.io()); + } + + public Flowable> getStreamStatistics() { + return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); + } + + public Single> insertStreamHistory(final Collection entries) { + List entities = new ArrayList<>(entries.size()); + for (final StreamHistoryEntry entry : entries) { + entities.add(entry.toStreamHistoryEntity()); + } + return Single.fromCallable(() -> streamHistoryTable.insertAll(entities)) + .subscribeOn(Schedulers.io()); + } + + public Single deleteStreamHistory(final Collection entries) { + List entities = new ArrayList<>(entries.size()); + for (final StreamHistoryEntry entry : entries) { + entities.add(entry.toStreamHistoryEntity()); + } + return Single.fromCallable(() -> streamHistoryTable.delete(entities)) + .subscribeOn(Schedulers.io()); + } + + private boolean isStreamHistoryEnabled() { + return sharedPreferences.getBoolean(streamHistoryKey, false); + } + + /////////////////////////////////////////////////////// + // Search History + /////////////////////////////////////////////////////// + + public Single> insertSearches(final Collection entries) { + return Single.fromCallable(() -> searchHistoryTable.insertAll(entries)) + .subscribeOn(Schedulers.io()); + } + + public Single deleteSearches(final Collection entries) { + return Single.fromCallable(() -> searchHistoryTable.delete(entries)) + .subscribeOn(Schedulers.io()); + } + + public Flowable> getSearchHistory() { + return searchHistoryTable.getAll(); + } + + public Maybe onSearched(final int serviceId, final String search) { + if (!isSearchHistoryEnabled()) return Maybe.empty(); + + final Date currentTime = new Date(); + final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search); + + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry(); + if (latestEntry != null && latestEntry.hasEqualValues(newEntry)) { + latestEntry.setCreationDate(currentTime); + return (long) searchHistoryTable.update(latestEntry); + } else { + return searchHistoryTable.insert(newEntry); + } + })).subscribeOn(Schedulers.io()); + } + + public Single deleteSearchHistory(final String search) { + return Single.fromCallable(() -> searchHistoryTable.deleteAllWhereQuery(search)) + .subscribeOn(Schedulers.io()); + } + + public Flowable> getRelatedSearches(final String query, + final int similarQueryLimit, + final int uniqueQueryLimit) { + return query.length() > 0 + ? searchHistoryTable.getSimilarEntries(query, similarQueryLimit) + : searchHistoryTable.getUniqueEntries(uniqueQueryLimit); + } + + private boolean isSearchHistoryEnabled() { + return sharedPreferences.getBoolean(searchHistoryKey, false); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java index 91e2cecff..a8bba0573 100644 --- a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java @@ -5,22 +5,27 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.history.dao.HistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.util.NavigationHelper; -public class SearchHistoryFragment extends HistoryFragment { +import java.util.Collection; +import java.util.Collections; +import java.util.List; - private static int allowedSwipeToDeleteDirections = ItemTouchHelper.RIGHT; +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; + +public class SearchHistoryFragment extends HistoryFragment { @NonNull public static SearchHistoryFragment newInstance() { @@ -30,7 +35,6 @@ public class SearchHistoryFragment extends HistoryFragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - historyItemSwipeCallback(allowedSwipeToDeleteDirections); } @NonNull @@ -39,21 +43,58 @@ public class SearchHistoryFragment extends HistoryFragment { return new SearchHistoryAdapter(getContext()); } + @Override + protected Single> insert(Collection entries) { + return historyRecordManager.insertSearches(entries); + } + + @Override + protected Single delete(Collection entries) { + return historyRecordManager.deleteSearches(entries); + } + + @NonNull + @Override + protected Flowable> getAll() { + return historyRecordManager.getSearchHistory(); + } + @StringRes @Override int getEnabledConfigKey() { return R.string.enable_search_history_key; } - @NonNull @Override - protected HistoryDAO createHistoryDAO() { - return NewPipeDatabase.getInstance().searchHistoryDAO(); + public void onHistoryItemClick(final SearchHistoryEntry historyItem) { + NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), + historyItem.getSearch()); } @Override - public void onHistoryItemClick(SearchHistoryEntry historyItem) { - NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), historyItem.getSearch()); + public void onHistoryItemLongClick(final SearchHistoryEntry item) { + if (activity == null) return; + + new AlertDialog.Builder(activity) + .setTitle(item.getSearch()) + .setMessage(R.string.delete_item_search_history) + .setCancelable(true) + .setNeutralButton(R.string.cancel, null) + .setPositiveButton(R.string.delete_one, (dialog, i) -> { + final Single onDelete = historyRecordManager + .deleteSearches(Collections.singleton(item)) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDelete.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .setNegativeButton(R.string.delete_all, (dialog, i) -> { + final Single onDeleteAll = historyRecordManager + .deleteSearchHistory(item.getSearch()) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDeleteAll.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .show(); } private static class ViewHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java index d898bf353..026d5ee16 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java @@ -6,8 +6,8 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -16,18 +16,22 @@ import android.widget.TextView; import com.nostra13.universalimageloader.core.ImageLoader; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.history.dao.HistoryDAO; -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import java.util.Collection; +import java.util.Collections; +import java.util.List; -public class WatchedHistoryFragment extends HistoryFragment { +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; - private static int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT; + +public class WatchedHistoryFragment extends HistoryFragment { @NonNull public static WatchedHistoryFragment newInstance() { @@ -37,7 +41,6 @@ public class WatchedHistoryFragment extends HistoryFragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - historyItemSwipeCallback(allowedSwipeToDeleteDirections); } @StringRes @@ -48,27 +51,59 @@ public class WatchedHistoryFragment extends HistoryFragment { @NonNull @Override - protected WatchedHistoryAdapter createAdapter() { - return new WatchedHistoryAdapter(getContext()); + protected StreamHistoryAdapter createAdapter() { + return new StreamHistoryAdapter(getContext()); + } + + @Override + protected Single> insert(Collection entries) { + return historyRecordManager.insertStreamHistory(entries); + } + + @Override + protected Single delete(Collection entries) { + return historyRecordManager.deleteStreamHistory(entries); } @NonNull @Override - protected HistoryDAO createHistoryDAO() { - return NewPipeDatabase.getInstance().watchHistoryDAO(); + protected Flowable> getAll() { + return historyRecordManager.getStreamHistory(); } @Override - public void onHistoryItemClick(WatchHistoryEntry historyItem) { - NavigationHelper.openVideoDetail(getContext(), - historyItem.getServiceId(), - historyItem.getUrl(), - historyItem.getTitle()); + public void onHistoryItemClick(StreamHistoryEntry historyItem) { + NavigationHelper.openVideoDetail(getContext(), historyItem.serviceId, historyItem.url, + historyItem.title); } - private static class WatchedHistoryAdapter extends HistoryEntryAdapter { + @Override + public void onHistoryItemLongClick(StreamHistoryEntry item) { + new AlertDialog.Builder(activity) + .setTitle(item.title) + .setMessage(R.string.delete_stream_history_prompt) + .setCancelable(true) + .setNeutralButton(R.string.cancel, null) + .setPositiveButton(R.string.delete_one, (dialog, i) -> { + final Single onDelete = historyRecordManager + .deleteStreamHistory(Collections.singleton(item)) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDelete.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .setNegativeButton(R.string.delete_all, (dialog, i) -> { + final Single onDeleteAll = historyRecordManager + .deleteStreamHistory(item.streamId) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDeleteAll.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .show(); + } - public WatchedHistoryAdapter(Context context) { + private static class StreamHistoryAdapter extends HistoryEntryAdapter { + + StreamHistoryAdapter(Context context) { super(context); } @@ -87,13 +122,13 @@ public class WatchedHistoryFragment extends HistoryFragment { } @Override - void onBindViewHolder(ViewHolder holder, WatchHistoryEntry entry, int position) { - holder.date.setText(getFormattedDate(entry.getCreationDate())); - holder.streamTitle.setText(entry.getTitle()); - holder.uploader.setText(entry.getUploader()); - holder.duration.setText(Localization.getDurationString(entry.getDuration())); - ImageLoader.getInstance() - .displayImage(entry.getThumbnailURL(), holder.thumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + void onBindViewHolder(ViewHolder holder, StreamHistoryEntry entry, int position) { + holder.date.setText(getFormattedDate(entry.accessDate)); + holder.streamTitle.setText(entry.title); + holder.uploader.setText(entry.uploader); + holder.duration.setText(Localization.getDurationString(entry.duration)); + ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView, + StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index a481b3335..8260adc6e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -61,10 +61,9 @@ import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.fragments.local.StreamRecordManager; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; @@ -150,7 +149,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected Disposable progressUpdateReactor; protected CompositeDisposable databaseUpdateReactor; - protected StreamRecordManager recordManager; + protected HistoryRecordManager recordManager; //////////////////////////////////////////////////////////////////////////*/ @@ -176,9 +175,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void initPlayer() { if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); - if (recordManager == null) { - recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); - } + if (recordManager == null) recordManager = new HistoryRecordManager(context); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); databaseUpdateReactor = new CompositeDisposable(); @@ -614,7 +611,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen // If the user selects a new track, then the discontinuity occurs after the index is changed. // Therefore, the only source that causes a discrepancy would be gapless transition, // which can only offset the current track by +1. - if (newWindowIndex == playQueue.getIndex() + 1) { + if (newWindowIndex == playQueue.getIndex() + 1 || + (newWindowIndex == 0 && playQueue.getIndex() == playQueue.size() - 1)) { playQueue.offsetIndex(+1); } playbackManager.load(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 161cf1735..fcfe5c4a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -229,6 +229,8 @@ Play Create Delete + Delete One + Delete All Checksum @@ -307,6 +309,8 @@ History cleared Item deleted Do you want to delete this item from search history? + Do you want to delete this item from watch history? + Are you sure you want to delete all items from history? Watch History Most Played