mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-30 14:52:59 +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
					Cyril Müller
				
			
				
					committed by
					
						 Mauricio Colli
						Mauricio Colli
					
				
			
			
				
	
			
			
			 Mauricio Colli
						Mauricio Colli
					
				
			
						parent
						
							09159ec245
						
					
				
				
					commit
					c0515de6b7
				
			| @@ -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<HistoryEntry> 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<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 | ||||
|     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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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(); | ||||
| } | ||||
|   | ||||
| @@ -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<Entity> { | ||||
|     @Delete | ||||
|     int delete(final Collection<Entity> entities); | ||||
|  | ||||
|     int deleteAll(); | ||||
|  | ||||
|     /* Updates */ | ||||
|     @Update | ||||
|     int update(final Entity entity); | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
| } | ||||
| @@ -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); | ||||
| } | ||||
| @@ -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); | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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()); | ||||
|     } | ||||
| } | ||||
| @@ -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()); | ||||
|     } | ||||
| } | ||||
| @@ -19,6 +19,10 @@ public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> { | ||||
|     @Query("SELECT * FROM " + SUBSCRIPTION_TABLE) | ||||
|     Flowable<List<SubscriptionEntity>> findAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("DELETE FROM " + SUBSCRIPTION_TABLE) | ||||
|     int deleteAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId") | ||||
|     Flowable<List<SubscriptionEntity>> listByService(int serviceId); | ||||
|   | ||||
| @@ -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<InfoItem>) infoListAdapter.getItemsList())); | ||||
|         outState.putSerializable(INFO_LIST_KEY, infoListAdapter.getItemsList()); | ||||
|         outState.putSerializable(CHANNEL_INFO_KEY, currentChannelInfo); | ||||
|         outState.putInt(PAGE_NUMBER_KEY, pageNumber); | ||||
|     } | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<InfoItem>) 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); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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())); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -39,7 +39,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | ||||
|     private static final String TAG = InfoListAdapter.class.toString(); | ||||
|  | ||||
|     private final InfoItemBuilder infoItemBuilder; | ||||
|     private final List<InfoItem> infoItemList; | ||||
|     private final ArrayList<InfoItem> infoItemList; | ||||
|     private boolean showFooter = false; | ||||
|     private View header = null; | ||||
|     private View footer = null; | ||||
| @@ -104,7 +104,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public List<InfoItem> getItemsList() { | ||||
|     public ArrayList<InfoItem> getItemsList() { | ||||
|         return infoItemList; | ||||
|     } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user