diff --git a/app/build.gradle b/app/build.gradle index fab2cd541..a110e36e2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,6 +12,7 @@ android { versionName "0.10.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true } buildTypes { release { @@ -43,6 +44,9 @@ dependencies { exclude module: 'support-annotations' } + compile "android.arch.persistence.room:runtime:1.0.0-alpha3" + annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha3" + testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 66d08ded1..c21fd6043 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,7 +20,8 @@ tools:ignore="AllowBackup"> + android:label="@string/app_name" + android:launchMode="singleTask"> @@ -212,8 +213,11 @@ - + android:theme="@style/AppTheme" /> + diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index c3782965e..fb381fe3c 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -24,11 +24,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; -import android.support.design.widget.TabLayout; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentStatePagerAdapter; -import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; @@ -38,22 +35,39 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import org.schabi.newpipe.download.DownloadActivity; +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.fragments.FeedFragment; -import org.schabi.newpipe.fragments.MainFragment; -import org.schabi.newpipe.fragments.SubscriptionFragment; +import org.schabi.newpipe.extractor.stream_info.AudioStream; +import org.schabi.newpipe.extractor.stream_info.StreamInfo; +import org.schabi.newpipe.extractor.stream_info.VideoStream; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.search.SearchFragment; -import org.schabi.newpipe.settings.SettingsActivity; +import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; -public class MainActivity extends AppCompatActivity { - private static final String TAG = "MainActivity"; +import java.util.Date; + +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.PublishSubject; + +public class MainActivity extends AppCompatActivity implements + VideoDetailFragment.OnVideoPlayListener, + SearchFragment.OnSearchListener { public static final boolean DEBUG = false; + private static final String TAG = "MainActivity"; + private WatchHistoryDAO watchHistoryDAO; + private SearchHistoryDAO searchHistoryDAO; + private SharedPreferences sharedPreferences; + private PublishSubject historyEntrySubject; /*////////////////////////////////////////////////////////////////////////// // Activity's LifeCycle @@ -61,7 +75,8 @@ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + if (DEBUG) + Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); ThemeHelper.setTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); @@ -70,15 +85,50 @@ public class MainActivity extends AppCompatActivity { initFragments(); } - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); + + AppDatabase database = NewPipeDatabase.getInstance(this); + watchHistoryDAO = database.watchHistoryDAO(); + searchHistoryDAO = database.searchHistoryDAO(); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + historyEntrySubject = PublishSubject.create(); + historyEntrySubject + .observeOn(Schedulers.io()) + .subscribe(createHistoryEntryConsumer()); + } + + @NonNull + private Consumer createHistoryEntryConsumer() { + 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); + } + } + }; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + watchHistoryDAO = null; + searchHistoryDAO = null; } @Override protected void onResume() { super.onResume(); - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) { if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity..."); sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply(); @@ -94,7 +144,8 @@ public class MainActivity extends AppCompatActivity { // Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...) // to not destroy the already created backstack String action = intent.getAction(); - if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) return; + if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) + return; } super.onNewIntent(intent); @@ -107,7 +158,8 @@ public class MainActivity extends AppCompatActivity { if (DEBUG) Log.d(TAG, "onBackPressed() called"); Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); - if (fragment instanceof VideoDetailFragment) if (((VideoDetailFragment) fragment).onActivityBackPressed()) return; + if (fragment instanceof VideoDetailFragment) + if (((VideoDetailFragment) fragment).onActivityBackPressed()) return; if (getSupportFragmentManager().getBackStackEntryCount() == 1) { @@ -164,6 +216,10 @@ public class MainActivity extends AppCompatActivity { case R.id.action_about: NavigationHelper.openAbout(this); return true; + case R.id.action_history: + Intent intent = new Intent(this, HistoryActivity.class); + startActivity(intent); + return true; default: return super.onOptionsItemSelected(item); } @@ -208,4 +264,31 @@ public class MainActivity extends AppCompatActivity { NavigationHelper.gotoMainFragment(getSupportFragmentManager()); } } + + + 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(VideoStream videoStream, StreamInfo streamInfo) { + addWatchHistoryEntry(streamInfo); + } + + @Override + public void onBackgroundPlayed(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 8ce33d32d..6b11de8c2 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -2,14 +2,25 @@ package org.schabi.newpipe.database; import android.arch.persistence.room.Database; import android.arch.persistence.room.RoomDatabase; +import android.arch.persistence.room.TypeConverters; +import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; +import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; +import org.schabi.newpipe.database.history.model.SearchHistoryEntry; +import org.schabi.newpipe.database.history.model.WatchHistoryEntry; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; +import org.schabi.newpipe.database.history.Converters; -@Database(entities = {SubscriptionEntity.class}, version = 1, exportSchema = false) +@TypeConverters({Converters.class}) +@Database(entities = {SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class}, version = 1, exportSchema = false) public abstract class AppDatabase extends RoomDatabase{ public static final String DATABASE_NAME = "newpipe.db"; public abstract SubscriptionDAO subscriptionDAO(); + + public abstract WatchHistoryDAO watchHistoryDAO(); + + public abstract SearchHistoryDAO searchHistoryDAO(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java index beb5f4b77..25ee47842 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java @@ -9,7 +9,6 @@ import android.arch.persistence.room.Update; import java.util.Collection; import java.util.List; -import io.reactivex.Completable; import io.reactivex.Flowable; @Dao @@ -39,6 +38,8 @@ public interface BasicDAO { @Delete int delete(final Collection entities); + int deleteAll(); + /* Updates */ @Update int update(final Entity entity); diff --git a/app/src/main/java/org/schabi/newpipe/database/history/Converters.java b/app/src/main/java/org/schabi/newpipe/database/history/Converters.java new file mode 100644 index 000000000..093c741f1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/Converters.java @@ -0,0 +1,28 @@ +package org.schabi.newpipe.database.history; + +import android.arch.persistence.room.TypeConverter; + +import java.util.Date; + +public class Converters { + + /** + * Convert a long value to a date + * @param value the long value + * @return the date + */ + @TypeConverter + public static Date fromTimestamp(Long value) { + return value == null ? null : new Date(value); + } + + /** + * Convert a date to a long value + * @param date the date + * @return the long value + */ + @TypeConverter + public static Long dateToTimestamp(Date date) { + return date == null ? null : date.getTime(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java new file mode 100644 index 000000000..1ade08122 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.database.history.dao; + +import org.schabi.newpipe.database.BasicDAO; + +public interface HistoryDAO extends BasicDAO { + T getLatestEntry(); +} 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 new file mode 100644 index 000000000..c7d5b0ae6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java @@ -0,0 +1,37 @@ +package org.schabi.newpipe.database.history.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; + +import org.schabi.newpipe.database.history.model.SearchHistoryEntry; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE; +import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID; +import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID; +import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME; + +@Dao +public interface SearchHistoryDAO extends HistoryDAO { + + String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; + + @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") + @Override + SearchHistoryEntry getLatestEntry(); + + @Query("DELETE FROM " + TABLE_NAME) + @Override + int deleteAll(); + + @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) + @Override + Flowable> findAll(); + + @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) + @Override + Flowable> listByService(int serviceId); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java new file mode 100644 index 000000000..b81cc2e35 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java @@ -0,0 +1,37 @@ +package org.schabi.newpipe.database.history.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; + +import org.schabi.newpipe.database.history.model.WatchHistoryEntry; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.CREATION_DATE; +import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.ID; +import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.SERVICE_ID; +import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.TABLE_NAME; + +@Dao +public interface WatchHistoryDAO extends HistoryDAO { + + String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; + + @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") + @Override + WatchHistoryEntry getLatestEntry(); + + @Query("DELETE FROM " + TABLE_NAME) + @Override + int deleteAll(); + + @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) + @Override + Flowable> findAll(); + + @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) + @Override + Flowable> listByService(int serviceId); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/HistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/HistoryEntry.java new file mode 100644 index 000000000..cd9ac259e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/HistoryEntry.java @@ -0,0 +1,60 @@ +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.PrimaryKey; + +import java.util.Date; + +@Entity +public abstract class HistoryEntry { + + public static final String ID = "id"; + public static final String SERVICE_ID = "service_id"; + public static final String CREATION_DATE = "creation_date"; + + @ColumnInfo(name = CREATION_DATE) + private Date creationDate; + + @ColumnInfo(name = SERVICE_ID) + private int serviceId; + + @ColumnInfo(name = ID) + @PrimaryKey(autoGenerate = true) + private long id; + + public HistoryEntry(Date creationDate, int serviceId) { + this.serviceId = serviceId; + this.creationDate = creationDate; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public int getServiceId() { + return serviceId; + } + + public void setServiceId(int serviceId) { + this.serviceId = serviceId; + } + + @Ignore + public boolean hasEqualValues(HistoryEntry otherEntry) { + return otherEntry != null && getServiceId() == otherEntry.getServiceId(); + } +} 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 new file mode 100644 index 000000000..d18974089 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java @@ -0,0 +1,37 @@ +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 java.util.Date; + +@Entity(tableName = SearchHistoryEntry.TABLE_NAME) +public class SearchHistoryEntry extends HistoryEntry { + + public static final String TABLE_NAME = "search_history"; + public static final String SEARCH = "search"; + + @ColumnInfo(name = SEARCH) + private String search; + + public SearchHistoryEntry(Date creationDate, int serviceId, String search) { + super(creationDate, serviceId); + this.search = search; + } + + public String getSearch() { + return search; + } + + public void setSearch(String search) { + this.search = search; + } + + @Ignore + @Override + public boolean hasEqualValues(HistoryEntry otherEntry) { + return otherEntry instanceof SearchHistoryEntry && super.hasEqualValues(otherEntry) + && getSearch().equals(((SearchHistoryEntry) otherEntry).getSearch()); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java new file mode 100644 index 000000000..1ed9fda39 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java @@ -0,0 +1,109 @@ +package org.schabi.newpipe.database.history.model; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Ignore; + +import org.schabi.newpipe.extractor.stream_info.StreamInfo; + +import java.util.Date; + +@Entity(tableName = WatchHistoryEntry.TABLE_NAME) +public class WatchHistoryEntry extends HistoryEntry { + + public static final String TABLE_NAME = "watch_history"; + public static final String TITLE = "title"; + public static final String URL = "url"; + public static final String STREAM_ID = "stream_id"; + public static final String THUMBNAIL_URL = "thumbnail_url"; + public static final String UPLOADER = "uploader"; + public static final String DURATION = "duration"; + + @ColumnInfo(name = TITLE) + private String title; + + @ColumnInfo(name = URL) + private String url; + + @ColumnInfo(name = STREAM_ID) + private String streamId; + + @ColumnInfo(name = THUMBNAIL_URL) + private String thumbnailURL; + + @ColumnInfo(name = UPLOADER) + private String uploader; + + @ColumnInfo(name = DURATION) + private int duration; + + public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, int duration) { + super(creationDate, serviceId); + this.title = title; + this.url = url; + this.streamId = streamId; + this.thumbnailURL = thumbnailURL; + this.uploader = uploader; + this.duration = duration; + } + + public WatchHistoryEntry(StreamInfo streamInfo) { + this(new Date(), streamInfo.service_id, streamInfo.title, streamInfo.webpage_url, + streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader, streamInfo.duration); + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getStreamId() { + return streamId; + } + + public void setStreamId(String streamId) { + this.streamId = streamId; + } + + public String getThumbnailURL() { + return thumbnailURL; + } + + public void setThumbnailURL(String thumbnailURL) { + this.thumbnailURL = thumbnailURL; + } + + public String getUploader() { + return uploader; + } + + public void setUploader(String uploader) { + this.uploader = uploader; + } + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } + + @Ignore + @Override + public boolean hasEqualValues(HistoryEntry otherEntry) { + return otherEntry instanceof WatchHistoryEntry && super.hasEqualValues(otherEntry) + && getUrl().equals(((WatchHistoryEntry) otherEntry).getUrl()); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java index c34048a3e..95eeb3fcf 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java @@ -19,6 +19,10 @@ public interface SubscriptionDAO extends BasicDAO { @Query("SELECT * FROM " + SUBSCRIPTION_TABLE) Flowable> findAll(); + @Override + @Query("DELETE FROM " + SUBSCRIPTION_TABLE) + int deleteAll(); + @Override @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId") Flowable> listByService(int serviceId); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java index 583f00f0c..d5964b7ac 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java @@ -188,7 +188,7 @@ private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode()); outState.putString(Constants.KEY_TITLE, channelName); outState.putInt(Constants.KEY_SERVICE_ID, serviceId); - outState.putSerializable(INFO_LIST_KEY, ((ArrayList) infoListAdapter.getItemsList())); + outState.putSerializable(INFO_LIST_KEY, infoListAdapter.getItemsList()); outState.putSerializable(CHANNEL_INFO_KEY, currentChannelInfo); outState.putInt(PAGE_NUMBER_KEY, pageNumber); } 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 3aeb62f32..2f5a3fc5c 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 @@ -156,6 +156,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor private LinearLayout relatedStreamRootLayout; private LinearLayout relatedStreamsView; private ImageButton relatedStreamExpandButton; + private OnVideoPlayListener onVideoPlayedListener; /*////////////////////////////////////////////////////////////////////////*/ @@ -230,6 +231,18 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor else prepareAndLoad(currentStreamInfo, false); } + @Override + public void onAttach(Context context) { + super.onAttach(context); + onVideoPlayedListener = (OnVideoPlayListener) context; + } + + @Override + public void onDetach() { + super.onDetach(); + onVideoPlayedListener = null; + } + @Override public void onResume() { super.onResume(); @@ -414,6 +427,8 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); Intent intent; AudioStream audioStream = currentStreamInfo.audio_streams.get(Utils.getPreferredAudioFormat(activity, currentStreamInfo.audio_streams)); + onVideoPlayedListener.onBackgroundPlayed(currentStreamInfo, audioStream); + if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { activity.startService(NavigationHelper.getOpenBackgroundPlayerIntent(activity, currentStreamInfo, audioStream)); Toast.makeText(activity, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); @@ -464,6 +479,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor return; } + onVideoPlayedListener.onVideoPlayed(getSelectedVideoStream(), currentStreamInfo); Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); Intent mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, PopupVideoPlayer.class, currentStreamInfo, actionBarHandler.getSelectedVideoStream()); activity.startService(mIntent); @@ -968,9 +984,21 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor } } + /** + * Get the currently selected video stream + * @return the selected video stream + */ + private VideoStream getSelectedVideoStream() { + return sortedStreamVideosList.get(actionBarHandler.getSelectedVideoStream()); + } + public void playVideo(StreamInfo info) { // ----------- THE MAGIC MOMENT --------------- - VideoStream selectedVideoStream = sortedStreamVideosList.get(actionBarHandler.getSelectedVideoStream()); + VideoStream selectedVideoStream = getSelectedVideoStream(); + + if(onVideoPlayedListener != null) { + onVideoPlayedListener.onVideoPlayed(selectedVideoStream, info); + } if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { @@ -1242,4 +1270,20 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor return false; } } + + public interface OnVideoPlayListener { + /** + * Called when a video is played + * @param videoStream the video stream that is played + * @param streamInfo the stream info + */ + void onVideoPlayed(VideoStream videoStream, StreamInfo streamInfo); + + /** + * Called when the audio is played in the background + * @param streamInfo the stream info + * @param audioStream the audio stream that is played + */ + void onBackgroundPlayed(StreamInfo streamInfo, AudioStream audioStream); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java index 628e1e055..8ec3e7709 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.fragments.search; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; @@ -85,12 +86,16 @@ public class SearchFragment extends BaseFragment implements SuggestionWorker.OnS private View searchClear; private RecyclerView resultRecyclerView; + private OnSearchListener onSearchListener; /*////////////////////////////////////////////////////////////////////////*/ public static SearchFragment getInstance(int serviceId, String query) { SearchFragment searchFragment = new SearchFragment(); searchFragment.setQuery(serviceId, query); + if(!TextUtils.isEmpty(query)) { + searchFragment.wasLoading.set(true); + } return searchFragment; } @@ -120,13 +125,26 @@ public class SearchFragment extends BaseFragment implements SuggestionWorker.OnS @Override public void onViewCreated(View rootView, @Nullable Bundle savedInstanceState) { + final boolean wasLoadingPreserved = wasLoading.get(); super.onViewCreated(rootView, savedInstanceState); + wasLoading.set(wasLoadingPreserved); if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]"); if (savedInstanceState != null && savedInstanceState.getBoolean(ERROR_KEY, false)) { search(searchQuery, 0, true); } + } + @Override + public void onAttach(Context context) { + super.onAttach(context); + onSearchListener = (OnSearchListener) context; + } + + @Override + public void onDetach() { + super.onDetach(); + onSearchListener = null; } @Override @@ -179,10 +197,12 @@ public class SearchFragment extends BaseFragment implements SuggestionWorker.OnS outState.putString(QUERY_KEY, query); outState.putInt(Constants.KEY_SERVICE_ID, serviceId); outState.putInt(PAGE_NUMBER_KEY, pageNumber); - outState.putSerializable(INFO_LIST_KEY, ((ArrayList) infoListAdapter.getItemsList())); + outState.putSerializable(INFO_LIST_KEY, infoListAdapter.getItemsList()); outState.putBoolean(WAS_LOADING_KEY, curSearchWorker != null && curSearchWorker.isRunning()); - if (errorPanel != null && errorPanel.getVisibility() == View.VISIBLE) outState.putBoolean(ERROR_KEY, true); + if (errorPanel != null && errorPanel.getVisibility() == View.VISIBLE) { + outState.putBoolean(ERROR_KEY, true); + } if (filterItemCheckedId != -1) outState.putInt(FILTER_CHECKED_ID_KEY, filterItemCheckedId); } @@ -537,7 +557,11 @@ public class SearchFragment extends BaseFragment implements SuggestionWorker.OnS if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "], pageNumber = [" + pageNumber + "], clearList = [" + clearList + "]"); isLoading.set(true); hideSoftKeyboard(searchEditText); - + if(pageNumber == 0) { + if(onSearchListener != null) { + onSearchListener.onSearch(serviceId, query); + } + } searchQuery = query; this.pageNumber = pageNumber; @@ -612,4 +636,7 @@ public class SearchFragment extends BaseFragment implements SuggestionWorker.OnS startActivityForResult(new Intent(getActivity(), ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST); } + public interface OnSearchListener { + void onSearch(int serviceId, String query); + } } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java new file mode 100644 index 000000000..26f91a06b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java @@ -0,0 +1,182 @@ +package org.schabi.newpipe.history; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.util.SparseArray; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import com.jakewharton.rxbinding2.view.RxView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.settings.SettingsActivity; +import org.schabi.newpipe.util.ThemeHelper; + +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; +import io.reactivex.schedulers.Schedulers; + +public class HistoryActivity extends AppCompatActivity { + + private static final String TAG = "HistoryActivity"; + /** + * The {@link android.support.v4.view.PagerAdapter} that will provide + * fragments for each of the sections. We use a + * {@link FragmentPagerAdapter} derivative, which will keep every + * loaded fragment in memory. If this becomes too memory intensive, it + * may be best to switch to a + * {@link android.support.v4.app.FragmentStatePagerAdapter}. + */ + private SectionsPagerAdapter mSectionsPagerAdapter; + + /** + * The {@link ViewPager} that will host the section contents. + */ + private ViewPager mViewPager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.setTheme(this); + setContentView(R.layout.activity_history); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + 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()); + + // Set up the ViewPager with the sections adapter. + mViewPager = findViewById(R.id.container); + mViewPager.setAdapter(mSectionsPagerAdapter); + + TabLayout tabLayout = findViewById(R.id.tabs); + tabLayout.setupWithViewPager(mViewPager); + + final FloatingActionButton fab = findViewById(R.id.fab); + RxView.clicks(fab) + .observeOn(Schedulers.io()) + .flatMap(new Function>() { + @Override + public Observable apply(Object o) { + int currentItem = mViewPager.getCurrentItem(); + HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.getFragment(currentItem); + if(fragment == null) { + Log.w(TAG, "Couldn't find current fragment"); + return Observable.empty(); + } else { + fragment.onClearHistory(); + return Observable.just(fragment); + } + } + }) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(HistoryFragment historyFragment) { + View view = historyFragment.getView(); + if(view != null) { + Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show(); + } + historyFragment.onHistoryCleared(); + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_history, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + case R.id.action_settings: + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** + * A {@link FragmentPagerAdapter} that returns a fragment corresponding to + * one of the sections/tabs/pages. + */ + public class SectionsPagerAdapter extends FragmentPagerAdapter { + + private SparseArray fragments = new SparseArray<>(); + + public SectionsPagerAdapter(FragmentManager fm) { + super(fm); + } + + + @Override + public Fragment getItem(int position) { + Fragment fragment; + switch (position) { + case 0: + fragment = SearchHistoryFragment.newInstance(); + break; + case 1: + fragment = WatchedHistoryFragment.newInstance(); + break; + default: + throw new IllegalArgumentException("position: " + position); + } + fragments.put(position, fragment); + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + super.destroyItem(container, position, object); + fragments.remove(position); + } + + @Nullable + public Fragment getFragment(int position) { + return fragments.get(position); + } + + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case 0: + return getString(R.string.title_history_search); + case 1: + return getString(R.string.title_history_view); + } + throw new IllegalArgumentException("position: " + position); + } + + @Override + public int getCount() { + // Show 3 total pages. + return 2; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java new file mode 100644 index 000000000..c0ee464e5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java @@ -0,0 +1,104 @@ +package org.schabi.newpipe.history; + +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 java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; + + +/** + * Adapter for history entries + * @param the type of the entries + * @param the type of the view holder + */ +public abstract class HistoryEntryAdapter extends RecyclerView.Adapter { + + private final ArrayList mEntries; + private final DateFormat mDateFormat; + private OnHistoryItemClickListener onHistoryItemClickListener = null; + + + public HistoryEntryAdapter(Context context) { + super(); + mEntries = new ArrayList<>(); + mDateFormat = android.text.format.DateFormat.getDateFormat(context.getApplicationContext()); + + setHasStableIds(true); + } + + public void setEntries(@NonNull Collection historyEntries) { + mEntries.clear(); + mEntries.addAll(historyEntries); + notifyDataSetChanged(); + } + + public void clear() { + mEntries.clear(); + notifyDataSetChanged(); + } + + protected String getFormattedDate(Date date) { + return mDateFormat.format(date); + } + + @Override + public long getItemId(int position) { + return mEntries.get(position).getId(); + } + + @Override + public int getItemCount() { + return mEntries.size(); + } + + @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 historyItemClickListener = onHistoryItemClickListener; + if(historyItemClickListener != null) { + historyItemClickListener.onHistoryItemClick(entry); + } + } + }); + onBindViewHolder(holder, entry, position); + } + + @Override + public void onViewRecycled(VH holder) { + super.onViewRecycled(holder); + holder.itemView.setOnClickListener(null); + } + + abstract void onBindViewHolder(VH holder, E entry, int position); + + public void setOnHistoryItemClickListener(@Nullable OnHistoryItemClickListener onHistoryItemClickListener) { + this.onHistoryItemClickListener = onHistoryItemClickListener; + } + + public boolean isEmpty() { + return mEntries.isEmpty(); + } + + public E removeItemAt(int position) { + E entry = mEntries.remove(position); + notifyItemRemoved(position); + return entry; + } + + public interface OnHistoryItemClickListener { + void onHistoryItemClick(E historyItem); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java new file mode 100644 index 000000000..3e22f603e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -0,0 +1,248 @@ +package org.schabi.newpipe.history; + + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.CallSuper; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.app.Fragment; +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.schabi.newpipe.R; +import org.schabi.newpipe.database.history.dao.HistoryDAO; +import org.schabi.newpipe.database.history.model.HistoryEntry; + +import java.util.List; + +import io.reactivex.Observer; +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 static org.schabi.newpipe.util.AnimationUtils.animateView; + +public abstract class HistoryFragment extends Fragment + implements HistoryEntryAdapter.OnHistoryItemClickListener { + + private boolean mHistoryIsEnabled; + private HistoryIsEnabledChangeListener mHistoryIsEnabledChangeListener; + private String mHistoryIsEnabledKey; + private SharedPreferences mSharedPreferences; + private RecyclerView mRecyclerView; + private View mDisabledView; + private HistoryDAO mHistoryDataSource; + private HistoryEntryAdapter mHistoryAdapter; + private View mEmptyHistoryView; + private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback; + private PublishSubject mHistoryEntryDeleteSubject; + + @StringRes + abstract int getEnabledConfigKey(); + + @CallSuper + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mHistoryIsEnabledKey = getString(getEnabledConfigKey()); + + mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + // Read history enabled from preferences + mHistoryIsEnabled = isHistoryEnabled(); + // Register history enabled listener + mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); + + mHistoryDataSource = createHistoryDAO(getContext()); + + mHistoryEntryDeleteSubject = PublishSubject.create(); + mHistoryEntryDeleteSubject + .observeOn(Schedulers.io()) + .subscribe(new Consumer() { + @Override + public void accept(E historyEntry) throws Exception { + mHistoryDataSource.delete(historyEntry); + } + }); + + mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { + @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) { + E historyEntry = mHistoryAdapter.removeItemAt(viewHolder.getAdapterPosition()); + mHistoryEntryDeleteSubject.onNext(historyEntry); + } + } + }; + } + + @NonNull + protected abstract HistoryEntryAdapter createAdapter(); + + @Override + public void onResume() { + super.onResume(); + mHistoryDataSource.findAll() + .toObservable() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getHistoryListConsumer()); + boolean newEnabled = isHistoryEnabled(); + if (newEnabled != mHistoryIsEnabled) { + onHistoryIsEnabledChanged(newEnabled); + } + } + + @NonNull + private Observer> getHistoryListConsumer() { + return new Observer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + + } + + @Override + public void onNext(@NonNull List historyEntries) { + if (!historyEntries.isEmpty()) { + mHistoryAdapter.setEntries(historyEntries); + animateView(mEmptyHistoryView, false, 200); + } else { + mHistoryAdapter.clear(); + onEmptyHistory(); + } + } + + @Override + public void onError(@NonNull Throwable e) { + // TODO: error handling like in (see e.g. subscription fragment) + } + + @Override + public void onComplete() { + + } + }; + } + + private boolean isHistoryEnabled() { + return mSharedPreferences.getBoolean(mHistoryIsEnabledKey, false); + } + + /** + * Called when the history is cleared to update the views + */ + @MainThread + public void onHistoryCleared() { + mHistoryAdapter.clear(); + onEmptyHistory(); + } + + private void onEmptyHistory() { + if (mHistoryIsEnabled) { + animateView(mEmptyHistoryView, true, 200); + } + } + + @Nullable + @CallSuper + @Override + public View onCreateView(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); + 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); + + if (mHistoryIsEnabled) { + mRecyclerView.setVisibility(View.VISIBLE); + } else { + mDisabledView.setVisibility(View.VISIBLE); + } + + return rootView; + } + + @CallSuper + @Override + public void onDestroy() { + super.onDestroy(); + mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); + mSharedPreferences = null; + mHistoryIsEnabledChangeListener = null; + mHistoryIsEnabledKey = null; + mHistoryDataSource = null; + } + + /** + * Called when the history is cleared + */ + @CallSuper + public void onClearHistory() { + mHistoryDataSource.deleteAll(); + } + + /** + * Called when history enabled flag is changed. + * + * @param historyIsEnabled the new value + */ + @CallSuper + public void onHistoryIsEnabledChanged(boolean historyIsEnabled) { + mHistoryIsEnabled = historyIsEnabled; + if (historyIsEnabled) { + animateView(mRecyclerView, true, 300); + animateView(mDisabledView, false, 300); + if (mHistoryAdapter.isEmpty()) { + animateView(mEmptyHistoryView, true, 300); + } + } else { + animateView(mRecyclerView, false, 300); + animateView(mDisabledView, true, 300); + animateView(mEmptyHistoryView, false, 300); + } + } + + /** + * Creates a new history DAO + * + * @param context the fragments context + * @return the history DAO + */ + @NonNull + protected abstract HistoryDAO createHistoryDAO(Context context); + + private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals(mHistoryIsEnabledKey)) { + boolean enabled = sharedPreferences.getBoolean(key, false); + if (mHistoryIsEnabled != enabled) { + onHistoryIsEnabledChanged(enabled); + } + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java new file mode 100644 index 000000000..5904dd986 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java @@ -0,0 +1,80 @@ +package org.schabi.newpipe.history; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.support.v7.widget.RecyclerView; +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.AppDatabase; +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 { + + @NonNull + public static SearchHistoryFragment newInstance() { + return new SearchHistoryFragment(); + } + + @NonNull + @Override + protected SearchHistoryAdapter createAdapter() { + return new SearchHistoryAdapter(getContext()); + } + + @StringRes + @Override + int getEnabledConfigKey() { + return R.string.enable_search_history_key; + } + + @NonNull + @Override + protected HistoryDAO createHistoryDAO(Context context) { + return NewPipeDatabase.getInstance(context).searchHistoryDAO(); + } + + @Override + public void onHistoryItemClick(SearchHistoryEntry historyItem) { + NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), historyItem.getSearch()); + } + + private static class ViewHolder extends RecyclerView.ViewHolder { + private final TextView search; + private final TextView time; + + public ViewHolder(View itemView) { + super(itemView); + search = itemView.findViewById(R.id.search); + time = itemView.findViewById(R.id.time); + } + } + + protected class SearchHistoryAdapter extends HistoryEntryAdapter { + + + public SearchHistoryAdapter(Context context) { + super(context); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View rootView = inflater.inflate(R.layout.item_search_history, parent, false); + return new ViewHolder(rootView); + } + + @Override + void onBindViewHolder(ViewHolder holder, SearchHistoryEntry entry, int position) { + holder.search.setText(entry.getSearch()); + holder.time.setText(getFormattedDate(entry.getCreationDate())); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java new file mode 100644 index 000000000..f096d7c63 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java @@ -0,0 +1,112 @@ +package org.schabi.newpipe.history; + + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +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.util.NavigationHelper; + +import static org.schabi.newpipe.info_list.InfoItemBuilder.getDurationString; + +public class WatchedHistoryFragment extends HistoryFragment { + + @NonNull + public static WatchedHistoryFragment newInstance() { + return new WatchedHistoryFragment(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + } + + @StringRes + @Override + int getEnabledConfigKey() { + return R.string.enable_watch_history_key; + } + + @NonNull + @Override + protected WatchedHistoryAdapter createAdapter() { + return new WatchedHistoryAdapter(getContext()); + } + + @NonNull + @Override + protected HistoryDAO createHistoryDAO(Context context) { + return NewPipeDatabase.getInstance(context).watchHistoryDAO(); + } + + @Override + public void onHistoryItemClick(WatchHistoryEntry historyItem) { + NavigationHelper.openVideoDetail(getContext(), + historyItem.getServiceId(), + historyItem.getUrl(), + historyItem.getTitle()); + } + + private static class WatchedHistoryAdapter extends HistoryEntryAdapter { + + public WatchedHistoryAdapter(Context context) { + super(context); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View itemView = inflater.inflate(R.layout.stream_item, parent, false); + return new ViewHolder(itemView); + } + + @Override + public void onViewRecycled(ViewHolder holder) { + holder.itemView.setOnClickListener(null); + ImageLoader.getInstance() + .cancelDisplayTask(holder.thumbnailView); + } + + @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(getDurationString(entry.getDuration())); + ImageLoader.getInstance() + .displayImage(entry.getThumbnailURL(), holder.thumbnailView); + } + } + + private static class ViewHolder extends RecyclerView.ViewHolder { + private final TextView date; + private final TextView streamTitle; + private final ImageView thumbnailView; + private final TextView uploader; + private final TextView duration; + + public ViewHolder(View itemView) { + super(itemView); + thumbnailView = itemView.findViewById(R.id.itemThumbnailView); + date = itemView.findViewById(R.id.itemAdditionalDetails); + streamTitle = itemView.findViewById(R.id.itemVideoTitleView); + uploader = itemView.findViewById(R.id.itemUploaderView); + duration = itemView.findViewById(R.id.itemDurationView); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 0881801f5..5230b2772 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -39,7 +39,7 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; + private final ArrayList infoItemList; private boolean showFooter = false; private View header = null; private View footer = null; @@ -104,7 +104,7 @@ public class InfoListAdapter extends RecyclerView.Adapter getItemsList() { + public ArrayList getItemsList() { return infoItemList; } diff --git a/app/src/main/res/drawable/ic_delete_sweep_black_24dp.xml b/app/src/main/res/drawable/ic_delete_sweep_black_24dp.xml new file mode 100644 index 000000000..bfa31fc9d --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_sweep_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_sweep_white_24dp.xml b/app/src/main/res/drawable/ic_delete_sweep_white_24dp.xml new file mode 100644 index 000000000..121b7ed8d --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_sweep_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_history.xml b/app/src/main/res/layout/activity_history.xml new file mode 100644 index 000000000..dd4d284ad --- /dev/null +++ b/app/src/main/res/layout/activity_history.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_history.xml b/app/src/main/res/layout/fragment_history.xml new file mode 100644 index 000000000..825cd395d --- /dev/null +++ b/app/src/main/res/layout/fragment_history.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/history_disabled_view.xml b/app/src/main/res/layout/history_disabled_view.xml new file mode 100644 index 000000000..abfbd072c --- /dev/null +++ b/app/src/main/res/layout/history_disabled_view.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_history.xml b/app/src/main/res/layout/item_search_history.xml new file mode 100644 index 000000000..f35406b16 --- /dev/null +++ b/app/src/main/res/layout/item_search_history.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_watch_history.xml b/app/src/main/res/layout/item_watch_history.xml new file mode 100644 index 000000000..245f242f0 --- /dev/null +++ b/app/src/main/res/layout/item_watch_history.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml index 4798c99f6..be3548532 100644 --- a/app/src/main/res/menu/main_menu.xml +++ b/app/src/main/res/menu/main_menu.xml @@ -7,12 +7,19 @@ android:title="@string/downloads" app:showAsAction="never"/> + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_history.xml b/app/src/main/res/menu/menu_history.xml new file mode 100644 index 000000000..526f500f6 --- /dev/null +++ b/app/src/main/res/menu/menu_history.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index b27ef510a..0bd65f172 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -15,4 +15,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index ae78279da..c29a7e876 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -50,4 +50,7 @@ 16dp 8dp + 180dp + 16dp + 16dp diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 2a1398b09..c35e58d97 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -232,6 +232,8 @@ show_age_restricted_content use_tor + enable_search_history + enable_watch_history file_rename file_replacement_character diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91dc973ef..c26f5a9dd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -71,9 +71,14 @@ Use gestures to control the brightness and volume of the player Search suggestions Show suggestions when searching + Search history + Store search queries locally + Watch history + Store watch history Resume on focus gain Continue playing after interruptions (e.g. phone calls) + Download @@ -233,4 +238,14 @@ Whether you have ideas, translation, design changes, code cleaning, or real heavy code changes, help is always welcome. The more is done the better it gets! Read license Contribution + + + History + Searched + Watched + History is disabled + History + The History is empty. + History cleared + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index aeae8d655..2cf55a099 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,6 +1,6 @@ - -