1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-12-23 16:40:32 +00:00

Add search and watch history (#626)

Add search and watch history

* Make MainActicity a single task
* Remove some casting
* SearchFragment: start searching when created with query
* Handle settings change in onResume
* History: Log pop up and background playback
* History: Add swipe to remove functionallity
* Enable history by default
* Use stream item
* Store more information about the stream
* Integrate history database into AppDatabase
* Remove redundant casts
* Re-enable date converters
* History: Use Rx Java and run DB in background
 * Also make HistoryDAO extend BasicDAO
* History: RX-ify swipe to remove
* Sort history entries by creation date
* History: Set toolbar title
* Don't repeat history entries
  * Introduced setters so we can update entries in the database
  * If the latest entry has the same (main) values, just update it
This commit is contained in:
Cyril Müller 2017-08-12 06:50:25 +02:00 committed by Mauricio Colli
parent 09159ec245
commit c0515de6b7
37 changed files with 1470 additions and 33 deletions

View File

@ -12,6 +12,7 @@ android {
versionName "0.10.0" versionName "0.10.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
} }
buildTypes { buildTypes {
release { release {
@ -43,6 +44,9 @@ dependencies {
exclude module: 'support-annotations' 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 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19' testCompile 'org.mockito:mockito-core:1.10.19'

View File

@ -20,7 +20,8 @@
tools:ignore="AllowBackup"> tools:ignore="AllowBackup">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name"> android:label="@string/app_name"
android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
@ -212,8 +213,11 @@
<activity <activity
android:name=".about.AboutActivity" android:name=".about.AboutActivity"
android:label="@string/title_activity_about" android:label="@string/title_activity_about"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme" />
</activity> <activity
android:name=".history.HistoryActivity"
android:label="@string/title_activity_history"
android:theme="@style/AppTheme" />
</application> </application>
</manifest> </manifest>

View File

@ -24,11 +24,8 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; 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.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.ActionBar;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
@ -38,22 +35,39 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; 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.extractor.StreamingService;
import org.schabi.newpipe.fragments.FeedFragment; import org.schabi.newpipe.extractor.stream_info.AudioStream;
import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.fragments.SubscriptionFragment; import org.schabi.newpipe.extractor.stream_info.VideoStream;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.search.SearchFragment; 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.Constants;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
public class MainActivity extends AppCompatActivity { import java.util.Date;
private static final String TAG = "MainActivity";
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; public static final boolean DEBUG = false;
private static final String TAG = "MainActivity";
private WatchHistoryDAO watchHistoryDAO;
private SearchHistoryDAO searchHistoryDAO;
private SharedPreferences sharedPreferences;
private PublishSubject<HistoryEntry> historyEntrySubject;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Activity's LifeCycle // Activity's LifeCycle
@ -61,7 +75,8 @@ public class MainActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); if (DEBUG)
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
@ -70,15 +85,50 @@ public class MainActivity extends AppCompatActivity {
initFragments(); initFragments();
} }
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(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<HistoryEntry> createHistoryEntryConsumer() {
return new Consumer<HistoryEntry>() {
@Override
public void accept(HistoryEntry historyEntry) throws Exception {
//noinspection unchecked
HistoryDAO<HistoryEntry> historyDAO = (HistoryDAO<HistoryEntry>)
(historyEntry instanceof SearchHistoryEntry ? searchHistoryDAO : watchHistoryDAO);
HistoryEntry latestEntry = historyDAO.getLatestEntry();
if (historyEntry.hasEqualValues(latestEntry)) {
latestEntry.setCreationDate(historyEntry.getCreationDate());
historyDAO.update(latestEntry);
} else {
historyDAO.insert(historyEntry);
}
}
};
}
@Override
protected void onDestroy() {
super.onDestroy();
watchHistoryDAO = null;
searchHistoryDAO = null;
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) { if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity..."); if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity...");
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply(); 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 ...) // Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...)
// to not destroy the already created backstack // to not destroy the already created backstack
String action = intent.getAction(); 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); super.onNewIntent(intent);
@ -107,7 +158,8 @@ public class MainActivity extends AppCompatActivity {
if (DEBUG) Log.d(TAG, "onBackPressed() called"); if (DEBUG) Log.d(TAG, "onBackPressed() called");
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); 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) { if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
@ -164,6 +216,10 @@ public class MainActivity extends AppCompatActivity {
case R.id.action_about: case R.id.action_about:
NavigationHelper.openAbout(this); NavigationHelper.openAbout(this);
return true; return true;
case R.id.action_history:
Intent intent = new Intent(this, HistoryActivity.class);
startActivity(intent);
return true;
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -208,4 +264,31 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.gotoMainFragment(getSupportFragmentManager()); 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);
}
}
} }

View File

@ -2,14 +2,25 @@ package org.schabi.newpipe.database;
import android.arch.persistence.room.Database; import android.arch.persistence.room.Database;
import android.arch.persistence.room.RoomDatabase; 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.SubscriptionDAO;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; 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 abstract class AppDatabase extends RoomDatabase{
public static final String DATABASE_NAME = "newpipe.db"; public static final String DATABASE_NAME = "newpipe.db";
public abstract SubscriptionDAO subscriptionDAO(); public abstract SubscriptionDAO subscriptionDAO();
public abstract WatchHistoryDAO watchHistoryDAO();
public abstract SearchHistoryDAO searchHistoryDAO();
} }

View File

@ -9,7 +9,6 @@ import android.arch.persistence.room.Update;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Flowable; import io.reactivex.Flowable;
@Dao @Dao
@ -39,6 +38,8 @@ public interface BasicDAO<Entity> {
@Delete @Delete
int delete(final Collection<Entity> entities); int delete(final Collection<Entity> entities);
int deleteAll();
/* Updates */ /* Updates */
@Update @Update
int update(final Entity entity); int update(final Entity entity);

View File

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

View File

@ -0,0 +1,7 @@
package org.schabi.newpipe.database.history.dao;
import org.schabi.newpipe.database.BasicDAO;
public interface HistoryDAO<T> extends BasicDAO<T> {
T getLatestEntry();
}

View File

@ -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<SearchHistoryEntry> {
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<List<SearchHistoryEntry>> findAll();
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
}

View File

@ -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<WatchHistoryEntry> {
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<List<WatchHistoryEntry>> findAll();
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<WatchHistoryEntry>> listByService(int serviceId);
}

View File

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

View File

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

View File

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

View File

@ -19,6 +19,10 @@ public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> {
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE) @Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
Flowable<List<SubscriptionEntity>> findAll(); Flowable<List<SubscriptionEntity>> findAll();
@Override
@Query("DELETE FROM " + SUBSCRIPTION_TABLE)
int deleteAll();
@Override @Override
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId") @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
Flowable<List<SubscriptionEntity>> listByService(int serviceId); Flowable<List<SubscriptionEntity>> listByService(int serviceId);

View File

@ -188,7 +188,7 @@ private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode());
outState.putString(Constants.KEY_TITLE, channelName); outState.putString(Constants.KEY_TITLE, channelName);
outState.putInt(Constants.KEY_SERVICE_ID, serviceId); outState.putInt(Constants.KEY_SERVICE_ID, serviceId);
outState.putSerializable(INFO_LIST_KEY, ((ArrayList<InfoItem>) infoListAdapter.getItemsList())); outState.putSerializable(INFO_LIST_KEY, infoListAdapter.getItemsList());
outState.putSerializable(CHANNEL_INFO_KEY, currentChannelInfo); outState.putSerializable(CHANNEL_INFO_KEY, currentChannelInfo);
outState.putInt(PAGE_NUMBER_KEY, pageNumber); outState.putInt(PAGE_NUMBER_KEY, pageNumber);
} }

View File

@ -156,6 +156,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor
private LinearLayout relatedStreamRootLayout; private LinearLayout relatedStreamRootLayout;
private LinearLayout relatedStreamsView; private LinearLayout relatedStreamsView;
private ImageButton relatedStreamExpandButton; private ImageButton relatedStreamExpandButton;
private OnVideoPlayListener onVideoPlayedListener;
/*////////////////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////////////////*/
@ -230,6 +231,18 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor
else prepareAndLoad(currentStreamInfo, false); 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 @Override
public void onResume() { public void onResume() {
super.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); .getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
Intent intent; Intent intent;
AudioStream audioStream = currentStreamInfo.audio_streams.get(Utils.getPreferredAudioFormat(activity, currentStreamInfo.audio_streams)); 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) { if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) {
activity.startService(NavigationHelper.getOpenBackgroundPlayerIntent(activity, currentStreamInfo, audioStream)); activity.startService(NavigationHelper.getOpenBackgroundPlayerIntent(activity, currentStreamInfo, audioStream));
Toast.makeText(activity, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); 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; return;
} }
onVideoPlayedListener.onVideoPlayed(getSelectedVideoStream(), currentStreamInfo);
Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
Intent mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, PopupVideoPlayer.class, currentStreamInfo, actionBarHandler.getSelectedVideoStream()); Intent mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, PopupVideoPlayer.class, currentStreamInfo, actionBarHandler.getSelectedVideoStream());
activity.startService(mIntent); 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) { public void playVideo(StreamInfo info) {
// ----------- THE MAGIC MOMENT --------------- // ----------- 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)) { 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; 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);
}
} }

View File

@ -3,6 +3,7 @@ package org.schabi.newpipe.fragments.search;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
@ -85,12 +86,16 @@ public class SearchFragment extends BaseFragment implements SuggestionWorker.OnS
private View searchClear; private View searchClear;
private RecyclerView resultRecyclerView; private RecyclerView resultRecyclerView;
private OnSearchListener onSearchListener;
/*////////////////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////////////////*/
public static SearchFragment getInstance(int serviceId, String query) { public static SearchFragment getInstance(int serviceId, String query) {
SearchFragment searchFragment = new SearchFragment(); SearchFragment searchFragment = new SearchFragment();
searchFragment.setQuery(serviceId, query); searchFragment.setQuery(serviceId, query);
if(!TextUtils.isEmpty(query)) {
searchFragment.wasLoading.set(true);
}
return searchFragment; return searchFragment;
} }
@ -120,13 +125,26 @@ public class SearchFragment extends BaseFragment implements SuggestionWorker.OnS
@Override @Override
public void onViewCreated(View rootView, @Nullable Bundle savedInstanceState) { public void onViewCreated(View rootView, @Nullable Bundle savedInstanceState) {
final boolean wasLoadingPreserved = wasLoading.get();
super.onViewCreated(rootView, savedInstanceState); super.onViewCreated(rootView, savedInstanceState);
wasLoading.set(wasLoadingPreserved);
if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]"); if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
if (savedInstanceState != null && savedInstanceState.getBoolean(ERROR_KEY, false)) { if (savedInstanceState != null && savedInstanceState.getBoolean(ERROR_KEY, false)) {
search(searchQuery, 0, true); 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 @Override
@ -179,10 +197,12 @@ public class SearchFragment extends BaseFragment implements SuggestionWorker.OnS
outState.putString(QUERY_KEY, query); outState.putString(QUERY_KEY, query);
outState.putInt(Constants.KEY_SERVICE_ID, serviceId); outState.putInt(Constants.KEY_SERVICE_ID, serviceId);
outState.putInt(PAGE_NUMBER_KEY, pageNumber); outState.putInt(PAGE_NUMBER_KEY, pageNumber);
outState.putSerializable(INFO_LIST_KEY, ((ArrayList<InfoItem>) infoListAdapter.getItemsList())); outState.putSerializable(INFO_LIST_KEY, infoListAdapter.getItemsList());
outState.putBoolean(WAS_LOADING_KEY, curSearchWorker != null && curSearchWorker.isRunning()); 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); 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 + "]"); if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "], pageNumber = [" + pageNumber + "], clearList = [" + clearList + "]");
isLoading.set(true); isLoading.set(true);
hideSoftKeyboard(searchEditText); hideSoftKeyboard(searchEditText);
if(pageNumber == 0) {
if(onSearchListener != null) {
onSearchListener.onSearch(serviceId, query);
}
}
searchQuery = query; searchQuery = query;
this.pageNumber = pageNumber; this.pageNumber = pageNumber;
@ -612,4 +636,7 @@ public class SearchFragment extends BaseFragment implements SuggestionWorker.OnS
startActivityForResult(new Intent(getActivity(), ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST); startActivityForResult(new Intent(getActivity(), ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST);
} }
public interface OnSearchListener {
void onSearch(int serviceId, String query);
}
} }

View File

@ -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<Object, Observable<HistoryFragment>>() {
@Override
public Observable<HistoryFragment> 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<HistoryFragment>() {
@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<Fragment> 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;
}
}
}

View File

@ -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 <E> the type of the entries
* @param <VH> the type of the view holder
*/
public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
private final ArrayList<E> mEntries;
private final DateFormat mDateFormat;
private OnHistoryItemClickListener<E> 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<E> 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<E> 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<E> 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<E extends HistoryEntry> {
void onHistoryItemClick(E historyItem);
}
}

View File

@ -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<E extends HistoryEntry> extends Fragment
implements HistoryEntryAdapter.OnHistoryItemClickListener<E> {
private boolean mHistoryIsEnabled;
private HistoryIsEnabledChangeListener mHistoryIsEnabledChangeListener;
private String mHistoryIsEnabledKey;
private SharedPreferences mSharedPreferences;
private RecyclerView mRecyclerView;
private View mDisabledView;
private HistoryDAO<E> mHistoryDataSource;
private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter;
private View mEmptyHistoryView;
private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback;
private PublishSubject<E> 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<E>() {
@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<E, ? extends RecyclerView.ViewHolder> 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<List<E>> getHistoryListConsumer() {
return new Observer<List<E>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull List<E> 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<E> 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);
}
}
}
}
}

View File

@ -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<SearchHistoryEntry> {
@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<SearchHistoryEntry> 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<SearchHistoryEntry, ViewHolder> {
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()));
}
}
}

View File

@ -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<WatchHistoryEntry> {
@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<WatchHistoryEntry> 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<WatchHistoryEntry, ViewHolder> {
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);
}
}
}

View File

@ -39,7 +39,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private static final String TAG = InfoListAdapter.class.toString(); private static final String TAG = InfoListAdapter.class.toString();
private final InfoItemBuilder infoItemBuilder; private final InfoItemBuilder infoItemBuilder;
private final List<InfoItem> infoItemList; private final ArrayList<InfoItem> infoItemList;
private boolean showFooter = false; private boolean showFooter = false;
private View header = null; private View header = null;
private View footer = null; private View footer = null;
@ -104,7 +104,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
notifyDataSetChanged(); notifyDataSetChanged();
} }
public List<InfoItem> getItemsList() { public ArrayList<InfoItem> getItemsList() {
return infoItemList; return infoItemList;
} }

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15,16h4v2h-4zM15,8h7v2h-7zM15,12h6v2h-6zM3,18c0,1.1 0.9,2 2,2h6c1.1,0 2,-0.9 2,-2L13,8L3,8v10zM14,5h-3l-1,-1L6,4L5,5L2,5v2h12z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M15,16h4v2h-4zM15,8h7v2h-7zM15,12h6v2h-6zM3,18c0,1.1 0.9,2 2,2h6c1.1,0 2,-0.9 2,-2L13,8L3,8v10zM14,5h-3l-1,-1L6,4L5,5L2,5v2h12z"/>
</vector>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="org.schabi.newpipe.history.HistoryActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_weight="1"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="@string/app_name">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</android.support.design.widget.TabLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="?attr/clear_history" />
</android.support.design.widget.CoordinatorLayout>

View File

@ -0,0 +1,33 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="org.schabi.newpipe.history.HistoryFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/history_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_search_history" />
<TextView
android:id="@+id/history_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/history_empty"
android:textAppearance="?android:attr/textAppearanceMedium"
android:visibility="gone"
tools:visibility="visible" />
<include
android:id="@+id/history_disabled_view"
layout="@layout/history_disabled_view"
android:visibility="gone" />
</FrameLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<TextView
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="¯\\_(ツ)_/¯"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:text="@string/history_disabled"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:orientation="vertical"
android:paddingBottom="8dp"
android:paddingTop="8dp">
<TextView
android:id="@+id/time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="10/11/2017 11:32" />
<TextView
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Search query" />
</LinearLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:orientation="vertical"
android:paddingBottom="8dp"
android:paddingTop="8dp">
<TextView
android:id="@+id/history_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="11/10/2017 at 10:12" />
<TextView
android:id="@+id/stream_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="How to Watch Youtube Videos? [5h loop]" />
</LinearLayout>

View File

@ -7,12 +7,19 @@
android:title="@string/downloads" android:title="@string/downloads"
app:showAsAction="never"/> app:showAsAction="never"/>
<item android:id="@+id/action_history"
android:orderInCategory="981"
android:title="@string/action_history"
app:showAsAction="never"/>
<item android:id="@+id/action_settings" <item android:id="@+id/action_settings"
android:orderInCategory="990" android:orderInCategory="990"
android:title="@string/settings" android:title="@string/settings"
app:showAsAction="never"/> app:showAsAction="never"/>
<item android:id="@+id/action_about" <item android:id="@+id/action_about"
android:orderInCategory="1000" android:orderInCategory="1000"
android:title="@string/action_about" /> android:title="@string/action_about" />
</menu> </menu>

View File

@ -0,0 +1,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.schabi.newpipe.history.HistoryActivity">
<item android:id="@+id/action_settings"
android:orderInCategory="990"
android:title="@string/settings"
app:showAsAction="never"/>
</menu>

View File

@ -15,4 +15,5 @@
<attr name="collapse" format="reference"/> <attr name="collapse" format="reference"/>
<attr name="volume_off" format="reference"/> <attr name="volume_off" format="reference"/>
<attr name="separatorColor" format="color"/> <attr name="separatorColor" format="color"/>
<attr name="clear_history" format="reference" />
</resources> </resources>

View File

@ -50,4 +50,7 @@
<dimen name="activity_vertical_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="appbar_padding_top">8dp</dimen> <dimen name="appbar_padding_top">8dp</dimen>
<dimen name="app_bar_height">180dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="text_margin">16dp</dimen>
</resources> </resources>

View File

@ -232,6 +232,8 @@
</string-array> </string-array>
<string name="show_age_restricted_content" translatable="false">show_age_restricted_content</string> <string name="show_age_restricted_content" translatable="false">show_age_restricted_content</string>
<string name="use_tor_key" translatable="false">use_tor</string> <string name="use_tor_key" translatable="false">use_tor</string>
<string name="enable_search_history_key" translatable="false">enable_search_history</string>
<string name="enable_watch_history_key" translatable="false">enable_watch_history</string>
<string name="settings_file_charset_key" translatable="false">file_rename</string> <string name="settings_file_charset_key" translatable="false">file_rename</string>
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>

View File

@ -71,9 +71,14 @@
<string name="player_gesture_controls_summary">Use gestures to control the brightness and volume of the player</string> <string name="player_gesture_controls_summary">Use gestures to control the brightness and volume of the player</string>
<string name="show_search_suggestions_title">Search suggestions</string> <string name="show_search_suggestions_title">Search suggestions</string>
<string name="show_search_suggestions_summary">Show suggestions when searching</string> <string name="show_search_suggestions_summary">Show suggestions when searching</string>
<string name="enable_search_history_title">Search history</string>
<string name="enable_search_history_summary">Store search queries locally</string>
<string name="enable_watch_history_title">Watch history</string>
<string name="enable_watch_history_summary">Store watch history</string>
<string name="resume_on_audio_focus_gain_title">Resume on focus gain</string> <string name="resume_on_audio_focus_gain_title">Resume on focus gain</string>
<string name="resume_on_audio_focus_gain_summary">Continue playing after interruptions (e.g. phone calls)</string> <string name="resume_on_audio_focus_gain_summary">Continue playing after interruptions (e.g. phone calls)</string>
<string name="download_dialog_title">Download</string> <string name="download_dialog_title">Download</string>
<string-array name="theme_description_list"> <string-array name="theme_description_list">
@ -233,4 +238,14 @@
<string name="contribution_encouragement">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!</string> <string name="contribution_encouragement">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!</string>
<string name="read_full_license">Read license</string> <string name="read_full_license">Read license</string>
<string name="contribution_title">Contribution</string> <string name="contribution_title">Contribution</string>
<!-- History -->
<string name="title_activity_history">History</string>
<string name="title_history_search">Searched</string>
<string name="title_history_view">Watched</string>
<string name="history_disabled">History is disabled</string>
<string name="action_history">History</string>
<string name="history_empty">The History is empty.</string>
<string name="history_cleared">History cleared</string>
</resources> </resources>

View File

@ -1,6 +1,6 @@
<resources> <resources>
<style name="RootTheme" parent="android:Theme.Holo"/> <style name="RootTheme" parent="android:Theme.Holo" />
<style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar"> <style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
@ -16,6 +16,7 @@
<item name="thumbs_up">@drawable/ic_thumb_up_black_24dp</item> <item name="thumbs_up">@drawable/ic_thumb_up_black_24dp</item>
<item name="thumbs_down">@drawable/ic_thumb_down_black_24dp</item> <item name="thumbs_down">@drawable/ic_thumb_down_black_24dp</item>
<item name="audio">@drawable/ic_headset_black_24dp</item> <item name="audio">@drawable/ic_headset_black_24dp</item>
<item name="clear_history">@drawable/ic_delete_sweep_white_24dp</item>
<item name="download">@drawable/ic_file_download_black_24dp</item> <item name="download">@drawable/ic_file_download_black_24dp</item>
<item name="share">@drawable/ic_share_black_24dp</item> <item name="share">@drawable/ic_share_black_24dp</item>
<item name="cast">@drawable/ic_cast_black_24dp</item> <item name="cast">@drawable/ic_cast_black_24dp</item>
@ -41,6 +42,7 @@
<item name="thumbs_up">@drawable/ic_thumb_up_white_24dp</item> <item name="thumbs_up">@drawable/ic_thumb_up_white_24dp</item>
<item name="thumbs_down">@drawable/ic_thumb_down_white_24dp</item> <item name="thumbs_down">@drawable/ic_thumb_down_white_24dp</item>
<item name="audio">@drawable/ic_headset_white_24dp</item> <item name="audio">@drawable/ic_headset_white_24dp</item>
<item name="clear_history">@drawable/ic_delete_sweep_black_24dp</item>
<item name="download">@drawable/ic_file_download_white_24dp</item> <item name="download">@drawable/ic_file_download_white_24dp</item>
<item name="share">@drawable/ic_share_white_24dp</item> <item name="share">@drawable/ic_share_white_24dp</item>
<item name="cast">@drawable/ic_cast_white_24dp</item> <item name="cast">@drawable/ic_cast_white_24dp</item>
@ -107,8 +109,8 @@
<item name="colorAccent">@color/light_youtube_accent_color</item> <item name="colorAccent">@color/light_youtube_accent_color</item>
</style> </style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/> <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/> <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources> </resources>

View File

@ -188,6 +188,17 @@
android:summary="@string/show_search_suggestions_summary" android:summary="@string/show_search_suggestions_summary"
android:title="@string/show_search_suggestions_title"/> android:title="@string/show_search_suggestions_title"/>
<CheckBoxPreference
android:defaultValue="true"
android:key="@string/enable_search_history_key"
android:title="@string/enable_search_history_title"
android:summary="@string/enable_search_history_summary"/>
<CheckBoxPreference
android:defaultValue="true"
android:key="@string/enable_watch_history_key"
android:title="@string/enable_watch_history_title"
android:summary="@string/enable_watch_history_summary"/>
<!-- <!--
<CheckBoxPreference <CheckBoxPreference
android:key="@string/use_tor_key" android:key="@string/use_tor_key"
@ -195,6 +206,5 @@
android:summary="@string/use_tor_summary" android:summary="@string/use_tor_summary"
android:defaultValue="false"/> android:defaultValue="false"/>
--> -->
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>