Merge remote-tracking branch 'origin/dev' into dev
| @@ -1,9 +1,21 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.multidex.MultiDex; | ||||
|  | ||||
| import com.facebook.stetho.Stetho; | ||||
| import com.squareup.leakcanary.AndroidHeapDumper; | ||||
| import com.squareup.leakcanary.DefaultLeakDirectoryProvider; | ||||
| import com.squareup.leakcanary.HeapDumper; | ||||
| import com.squareup.leakcanary.LeakCanary; | ||||
| import com.squareup.leakcanary.LeakDirectoryProvider; | ||||
| import com.squareup.leakcanary.RefWatcher; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.util.concurrent.TimeUnit; | ||||
|  | ||||
| public class DebugApp extends App { | ||||
|     private static final String TAG = DebugApp.class.toString(); | ||||
| @@ -17,7 +29,6 @@ public class DebugApp extends App { | ||||
|     @Override | ||||
|     public void onCreate() { | ||||
|         super.onCreate(); | ||||
|  | ||||
|         initStetho(); | ||||
|     } | ||||
|  | ||||
| @@ -42,4 +53,35 @@ public class DebugApp extends App { | ||||
|         // Initialize Stetho with the Initializer | ||||
|         Stetho.initialize(initializer); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected RefWatcher installLeakCanary() { | ||||
|         return LeakCanary.refWatcher(this) | ||||
|                 .heapDumper(new ToggleableHeapDumper(this)) | ||||
|                 // give each object 10 seconds to be gc'ed, before leak canary gets nosy on it | ||||
|                 .watchDelay(10, TimeUnit.SECONDS) | ||||
|                 .buildAndInstall(); | ||||
|     } | ||||
|  | ||||
|     public static class ToggleableHeapDumper implements HeapDumper { | ||||
|         private final HeapDumper dumper; | ||||
|         private final SharedPreferences preferences; | ||||
|         private final String dumpingAllowanceKey; | ||||
|  | ||||
|         ToggleableHeapDumper(@NonNull final Context context) { | ||||
|             LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context); | ||||
|             this.dumper = new AndroidHeapDumper(context, leakDirectoryProvider); | ||||
|             this.preferences = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|             this.dumpingAllowanceKey = context.getString(R.string.allow_heap_dumping_key); | ||||
|         } | ||||
|  | ||||
|         private boolean isDumpingAllowed() { | ||||
|             return preferences.getBoolean(dumpingAllowanceKey, false); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public File dumpHeap() { | ||||
|             return isDumpingAllowed() ? dumper.dumpHeap() : HeapDumper.RETRY_LATER; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -122,8 +122,12 @@ | ||||
|  | ||||
|         <activity | ||||
|             android:name=".RouterActivity" | ||||
|             android:excludeFromRecents="true" | ||||
|             android:label="@string/preferred_player_share_menu_title" | ||||
|             android:taskAffinity="" | ||||
|             android:theme="@style/RouterActivityThemeDark"> | ||||
|  | ||||
|             <!-- Youtube filter --> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW"/> | ||||
|                 <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/> | ||||
| @@ -169,6 +173,41 @@ | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|                 <category android:name="android.intent.category.BROWSABLE"/> | ||||
|  | ||||
|                 <data android:scheme="vnd.youtube"/> | ||||
|                 <data android:scheme="vnd.youtube.launch"/> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <!-- Hooktube filter --> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW"/> | ||||
|                 <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/> | ||||
|                 <action android:name="android.nfc.action.NDEF_DISCOVERED"/> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|                 <category android:name="android.intent.category.BROWSABLE"/> | ||||
|  | ||||
|                 <data android:scheme="http"/> | ||||
|                 <data android:scheme="https"/> | ||||
|                 <data android:host="hooktube.com"/> | ||||
|                 <data android:host="*.hooktube.com"/> | ||||
|                 <!-- video prefix --> | ||||
|                 <data android:pathPrefix="/v/"/> | ||||
|                 <data android:pathPrefix="/embed/"/> | ||||
|                 <data android:pathPrefix="/watch"/> | ||||
|                 <!-- channel prefix --> | ||||
|                 <data android:pathPrefix="/channel/"/> | ||||
|                 <data android:pathPrefix="/user/"/> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <!-- Soundcloud filter --> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW"/> | ||||
|                 <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/> | ||||
|                 <action android:name="android.nfc.action.NDEF_DISCOVERED"/> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|                 <category android:name="android.intent.category.BROWSABLE"/> | ||||
|  | ||||
|                 <data android:scheme="http"/> | ||||
|                 <data android:scheme="https"/> | ||||
|                 <data android:host="soundcloud.com"/> | ||||
| @@ -176,17 +215,8 @@ | ||||
|                 <data android:host="www.soundcloud.com"/> | ||||
|                 <data android:pathPrefix="/"/> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW"/> | ||||
|                 <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/> | ||||
|                 <action android:name="android.nfc.action.NDEF_DISCOVERED"/> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|                 <category android:name="android.intent.category.BROWSABLE"/> | ||||
|  | ||||
|                 <data android:scheme="vnd.youtube"/> | ||||
|                 <data android:scheme="vnd.youtube.launch"/> | ||||
|             </intent-filter> | ||||
|             <!-- Share filter --> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND"/> | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
| @@ -195,68 +225,7 @@ | ||||
|         </activity> | ||||
|  | ||||
|         <service | ||||
|             android:name=".RouterPlayerActivity$FetcherService" | ||||
|             android:name=".RouterActivity$FetcherService" | ||||
|             android:exported="false"/> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".RouterPlayerActivity" | ||||
|             android:excludeFromRecents="true" | ||||
|             android:label="@string/preferred_player_share_menu_title" | ||||
|             android:taskAffinity="" | ||||
|             android:theme="@style/RouterActivityThemeDark"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW"/> | ||||
|                 <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/> | ||||
|                 <action android:name="android.nfc.action.NDEF_DISCOVERED"/> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|                 <category android:name="android.intent.category.BROWSABLE"/> | ||||
|  | ||||
|                 <data android:scheme="http"/> | ||||
|                 <data android:scheme="https"/> | ||||
|                 <data android:host="youtube.com"/> | ||||
|                 <data android:host="m.youtube.com"/> | ||||
|                 <data android:host="www.youtube.com"/> | ||||
|                 <!-- video prefix --> | ||||
|                 <data android:pathPrefix="/v/"/> | ||||
|                 <data android:pathPrefix="/embed/"/> | ||||
|                 <data android:pathPrefix="/watch"/> | ||||
|                 <data android:pathPrefix="/attribution_link"/> | ||||
|                 <!-- channel prefix --> | ||||
|                 <data android:pathPrefix="/channel/"/> | ||||
|                 <data android:pathPrefix="/user/"/> | ||||
|                 <!-- playlist prefix --> | ||||
|                 <data android:pathPrefix="/playlist"/> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW"/> | ||||
|                 <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/> | ||||
|                 <action android:name="android.nfc.action.NDEF_DISCOVERED"/> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|                 <category android:name="android.intent.category.BROWSABLE"/> | ||||
|  | ||||
|                 <data android:scheme="http"/> | ||||
|                 <data android:scheme="https"/> | ||||
|                 <data android:host="youtu.be"/> | ||||
|                 <data android:pathPrefix="/"/> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW"/> | ||||
|                 <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/> | ||||
|                 <action android:name="android.nfc.action.NDEF_DISCOVERED"/> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|                 <category android:name="android.intent.category.BROWSABLE"/> | ||||
|  | ||||
|                 <data android:scheme="vnd.youtube"/> | ||||
|                 <data android:scheme="vnd.youtube.launch"/> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND"/> | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|                 <data android:mimeType="text/plain"/> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|     </application> | ||||
| </manifest> | ||||
| @@ -1,17 +1,18 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.app.AlarmManager; | ||||
| import android.app.Application; | ||||
| import android.app.NotificationChannel; | ||||
| import android.app.NotificationManager; | ||||
| import android.app.PendingIntent; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.os.Build; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.util.Log; | ||||
|  | ||||
| import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; | ||||
| import com.nostra13.universalimageloader.core.ImageLoader; | ||||
| import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; | ||||
| import com.squareup.leakcanary.LeakCanary; | ||||
| import com.squareup.leakcanary.RefWatcher; | ||||
|  | ||||
| import org.acra.ACRA; | ||||
| import org.acra.config.ACRAConfiguration; | ||||
| @@ -56,6 +57,7 @@ import io.reactivex.plugins.RxJavaPlugins; | ||||
|  | ||||
| public class App extends Application { | ||||
|     protected static final String TAG = App.class.toString(); | ||||
|     private RefWatcher refWatcher; | ||||
|  | ||||
|     @SuppressWarnings("unchecked") | ||||
|     private static final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class}; | ||||
| @@ -71,6 +73,13 @@ public class App extends Application { | ||||
|     public void onCreate() { | ||||
|         super.onCreate(); | ||||
|  | ||||
|         if (LeakCanary.isInAnalyzerProcess(this)) { | ||||
|             // This process is dedicated to LeakCanary for heap analysis. | ||||
|             // You should not init your app in this process. | ||||
|             return; | ||||
|         } | ||||
|         refWatcher = installLeakCanary(); | ||||
|  | ||||
|         // Initialize settings first because others inits can use its values | ||||
|         SettingsActivity.initSettings(this); | ||||
|  | ||||
| @@ -80,8 +89,7 @@ public class App extends Application { | ||||
|         initNotificationChannel(); | ||||
|  | ||||
|         // Initialize image loader | ||||
|         ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build(); | ||||
|         ImageLoader.getInstance().init(config); | ||||
|         ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50)); | ||||
|  | ||||
|         configureRxJavaErrorHandler(); | ||||
|     } | ||||
| @@ -119,6 +127,14 @@ public class App extends Application { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb, | ||||
|                                                                   final int diskCacheSizeMb) { | ||||
|         return new ImageLoaderConfiguration.Builder(this) | ||||
|                 .memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024)) | ||||
|                 .diskCacheSize(diskCacheSizeMb * 1024 * 1024) | ||||
|                 .build(); | ||||
|     } | ||||
|  | ||||
|     private void initACRA() { | ||||
|         try { | ||||
|             final ACRAConfiguration acraConfig = new ConfigurationBuilder(this) | ||||
| @@ -152,4 +168,13 @@ public class App extends Application { | ||||
|         mNotificationManager.createNotificationChannel(mChannel); | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public static RefWatcher getRefWatcher(Context context) { | ||||
|         final App application = (App) context.getApplicationContext(); | ||||
|         return application.refWatcher; | ||||
|     } | ||||
|  | ||||
|     protected RefWatcher installLeakCanary() { | ||||
|         return RefWatcher.DISABLED; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import android.view.View; | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
| import com.nostra13.universalimageloader.core.ImageLoader; | ||||
| import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; | ||||
| import com.squareup.leakcanary.RefWatcher; | ||||
|  | ||||
| import icepick.Icepick; | ||||
|  | ||||
| @@ -67,6 +68,14 @@ public abstract class BaseFragment extends Fragment { | ||||
|     protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|  | ||||
|         RefWatcher refWatcher = App.getRefWatcher(getActivity()); | ||||
|         if (refWatcher != null) refWatcher.watch(this); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Init | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
|  | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| @@ -27,7 +28,6 @@ import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.design.widget.NavigationView; | ||||
| import android.support.v4.app.Fragment; | ||||
| import android.support.v4.view.GravityCompat; | ||||
| @@ -41,41 +41,23 @@ import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.database.AppDatabase; | ||||
| import org.schabi.newpipe.database.history.dao.HistoryDAO; | ||||
| import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; | ||||
| import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; | ||||
| import org.schabi.newpipe.database.history.model.HistoryEntry; | ||||
| import org.schabi.newpipe.database.history.model.SearchHistoryEntry; | ||||
| import org.schabi.newpipe.database.history.model.WatchHistoryEntry; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.fragments.BackPressable; | ||||
| import org.schabi.newpipe.fragments.MainFragment; | ||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||
| import org.schabi.newpipe.fragments.list.search.SearchFragment; | ||||
| import org.schabi.newpipe.history.HistoryListener; | ||||
| import org.schabi.newpipe.util.Constants; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.ServiceHelper; | ||||
| import org.schabi.newpipe.util.StateSaver; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import io.reactivex.functions.Consumer; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import io.reactivex.subjects.PublishSubject; | ||||
|  | ||||
| public class MainActivity extends AppCompatActivity implements HistoryListener { | ||||
| public class MainActivity extends AppCompatActivity { | ||||
|     private static final String TAG = "MainActivity"; | ||||
|     public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); | ||||
|  | ||||
|     private SharedPreferences sharedPreferences; | ||||
|     private ActionBarDrawerToggle toggle = null; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -86,7 +68,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); | ||||
|  | ||||
|         sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); | ||||
|  | ||||
|         super.onCreate(savedInstanceState); | ||||
| @@ -98,7 +79,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { | ||||
|  | ||||
|         setSupportActionBar(findViewById(R.id.toolbar)); | ||||
|         setupDrawer(); | ||||
|         initHistory(); | ||||
|     } | ||||
|  | ||||
|     private void setupDrawer() { | ||||
| @@ -149,8 +129,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { | ||||
|         if (!isChangingConfigurations()) { | ||||
|             StateSaver.clearStateFiles(); | ||||
|         } | ||||
|  | ||||
|         disposeHistory(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -236,6 +214,22 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("ShowToast") | ||||
|     private void onHeapDumpToggled(@NonNull MenuItem item) { | ||||
|         final boolean isHeapDumpEnabled = !item.isChecked(); | ||||
|  | ||||
|         PreferenceManager.getDefaultSharedPreferences(this).edit() | ||||
|                 .putBoolean(getString(R.string.allow_heap_dumping_key), isHeapDumpEnabled).apply(); | ||||
|         item.setChecked(isHeapDumpEnabled); | ||||
|  | ||||
|         final String heapDumpNotice; | ||||
|         if (isHeapDumpEnabled) { | ||||
|             heapDumpNotice = getString(R.string.enable_leak_canary_notice); | ||||
|         } else { | ||||
|             heapDumpNotice = getString(R.string.disable_leak_canary_notice); | ||||
|         } | ||||
|         Toast.makeText(getApplicationContext(), heapDumpNotice, Toast.LENGTH_SHORT).show(); | ||||
|     } | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Menu | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -257,6 +251,10 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { | ||||
|             inflater.inflate(R.menu.main_menu, menu); | ||||
|         } | ||||
|  | ||||
|         if (DEBUG) { | ||||
|             getMenuInflater().inflate(R.menu.debug_menu, menu); | ||||
|         } | ||||
|  | ||||
|         ActionBar actionBar = getSupportActionBar(); | ||||
|         if (actionBar != null) { | ||||
|             actionBar.setDisplayHomeAsUpEnabled(false); | ||||
| @@ -267,6 +265,17 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onPrepareOptionsMenu(Menu menu) { | ||||
|         MenuItem heapDumpToggle = menu.findItem(R.id.action_toggle_heap_dump); | ||||
|         if (heapDumpToggle != null) { | ||||
|             final boolean isToggled = PreferenceManager.getDefaultSharedPreferences(this) | ||||
|                     .getBoolean(getString(R.string.allow_heap_dumping_key), false); | ||||
|             heapDumpToggle.setChecked(isToggled); | ||||
|         } | ||||
|         return super.onPrepareOptionsMenu(menu); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); | ||||
| @@ -287,6 +296,9 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { | ||||
|             case R.id.action_history: | ||||
|                 NavigationHelper.openHistory(this); | ||||
|                 return true; | ||||
|             case R.id.action_toggle_heap_dump: | ||||
|                 onHeapDumpToggled(item); | ||||
|                 return true; | ||||
|             default: | ||||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
| @@ -357,75 +369,4 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { | ||||
|             NavigationHelper.gotoMainFragment(getSupportFragmentManager()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // History | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private WatchHistoryDAO watchHistoryDAO; | ||||
|     private SearchHistoryDAO searchHistoryDAO; | ||||
|     private PublishSubject<HistoryEntry> historyEntrySubject; | ||||
|     private Disposable disposable; | ||||
|  | ||||
|     private void initHistory() { | ||||
|         final AppDatabase database = NewPipeDatabase.getInstance(); | ||||
|         watchHistoryDAO = database.watchHistoryDAO(); | ||||
|         searchHistoryDAO = database.searchHistoryDAO(); | ||||
|         historyEntrySubject = PublishSubject.create(); | ||||
|         disposable = historyEntrySubject | ||||
|                 .observeOn(Schedulers.io()) | ||||
|                 .subscribe(getHistoryEntryConsumer()); | ||||
|     } | ||||
|  | ||||
|     private void disposeHistory() { | ||||
|         if (disposable != null) disposable.dispose(); | ||||
|         watchHistoryDAO = null; | ||||
|         searchHistoryDAO = null; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     private Consumer<HistoryEntry> getHistoryEntryConsumer() { | ||||
|         return new Consumer<HistoryEntry>() { | ||||
|             @Override | ||||
|             public void accept(HistoryEntry historyEntry) throws Exception { | ||||
|                 //noinspection unchecked | ||||
|                 HistoryDAO<HistoryEntry> historyDAO = (HistoryDAO<HistoryEntry>) | ||||
|                         (historyEntry instanceof SearchHistoryEntry ? searchHistoryDAO : watchHistoryDAO); | ||||
|  | ||||
|                 HistoryEntry latestEntry = historyDAO.getLatestEntry(); | ||||
|                 if (historyEntry.hasEqualValues(latestEntry)) { | ||||
|                     latestEntry.setCreationDate(historyEntry.getCreationDate()); | ||||
|                     historyDAO.update(latestEntry); | ||||
|                 } else { | ||||
|                     historyDAO.insert(historyEntry); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private void addWatchHistoryEntry(StreamInfo streamInfo) { | ||||
|         if (sharedPreferences.getBoolean(getString(R.string.enable_watch_history_key), true)) { | ||||
|             WatchHistoryEntry entry = new WatchHistoryEntry(streamInfo); | ||||
|             historyEntrySubject.onNext(entry); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onVideoPlayed(StreamInfo streamInfo, @Nullable VideoStream videoStream) { | ||||
|         addWatchHistoryEntry(streamInfo); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream) { | ||||
|         addWatchHistoryEntry(streamInfo); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSearch(int serviceId, String query) { | ||||
|         // Add search history entry | ||||
|         if (sharedPreferences.getBoolean(getString(R.string.enable_search_history_key), true)) { | ||||
|             SearchHistoryEntry searchHistoryEntry = new SearchHistoryEntry(new Date(), serviceId, query); | ||||
|             historyEntrySubject.onNext(searchHistoryEntry); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import android.support.annotation.NonNull; | ||||
| import org.schabi.newpipe.database.AppDatabase; | ||||
|  | ||||
| import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; | ||||
| import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12; | ||||
|  | ||||
| public final class NewPipeDatabase { | ||||
|  | ||||
| @@ -17,15 +18,24 @@ public final class NewPipeDatabase { | ||||
|     } | ||||
|  | ||||
|     public static void init(Context context) { | ||||
|         databaseInstance = Room.databaseBuilder(context.getApplicationContext(), | ||||
|                 AppDatabase.class, DATABASE_NAME | ||||
|         ).build(); | ||||
|         databaseInstance = Room | ||||
|                 .databaseBuilder(context, AppDatabase.class, DATABASE_NAME) | ||||
|                 .addMigrations(MIGRATION_11_12) | ||||
|                 .fallbackToDestructiveMigration() | ||||
|                 .build(); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     @Deprecated | ||||
|     public static AppDatabase getInstance() { | ||||
|         if (databaseInstance == null) throw new RuntimeException("Database not initialized"); | ||||
|  | ||||
|         return databaseInstance; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public static AppDatabase getInstance(Context context) { | ||||
|         if (databaseInstance == null) init(context); | ||||
|         return databaseInstance; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,51 +1,78 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.app.IntentService; | ||||
| import android.content.DialogInterface; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.DrawableRes; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.NotificationCompat; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.text.TextUtils; | ||||
| import android.view.ContextThemeWrapper; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.Button; | ||||
| import android.widget.LinearLayout; | ||||
| import android.widget.RadioButton; | ||||
| import android.widget.RadioGroup; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.Info; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.StreamingService.LinkType; | ||||
| import org.schabi.newpipe.extractor.channel.ChannelInfo; | ||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.playlist.ChannelPlayQueue; | ||||
| import org.schabi.newpipe.playlist.PlayQueue; | ||||
| import org.schabi.newpipe.playlist.PlaylistPlayQueue; | ||||
| import org.schabi.newpipe.playlist.SinglePlayQueue; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.PermissionHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collection; | ||||
| import java.util.HashSet; | ||||
|  | ||||
| import icepick.Icepick; | ||||
| import icepick.State; | ||||
| import io.reactivex.Observable; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import io.reactivex.functions.Consumer; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
|  | ||||
| /* | ||||
|  * Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org> | ||||
|  * RouterActivity.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; | ||||
|  | ||||
| /** | ||||
|  * This Acitivty is designed to route share/open intents to the specified service, and | ||||
|  * to the part of the service which can handle the url. | ||||
|  * Get the url from the intent and open it in the chosen preferred player | ||||
|  */ | ||||
| public class RouterActivity extends AppCompatActivity { | ||||
|  | ||||
|     @State | ||||
|     protected int currentServiceId = -1; | ||||
|     private StreamingService currentService; | ||||
|     @State | ||||
|     protected LinkType currentLinkType; | ||||
|     @State | ||||
|     protected int selectedRadioPosition = -1; | ||||
|     protected int selectedPreviously = -1; | ||||
|  | ||||
|     protected String currentUrl; | ||||
|     protected CompositeDisposable disposables = new CompositeDisposable(); | ||||
|  | ||||
| @@ -62,6 +89,10 @@ public class RouterActivity extends AppCompatActivity { | ||||
|                 finish(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         setTheme(ThemeHelper.isLightThemeSelected(this) | ||||
|                 ? R.style.RouterActivityThemeLight | ||||
|                 : R.style.RouterActivityThemeDark); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -73,25 +104,43 @@ public class RouterActivity extends AppCompatActivity { | ||||
|     @Override | ||||
|     protected void onStart() { | ||||
|         super.onStart(); | ||||
|  | ||||
|         handleUrl(currentUrl); | ||||
|     } | ||||
|  | ||||
|     protected void handleUrl(String url) { | ||||
|         disposables.add(Observable | ||||
|                 .fromCallable(() -> NavigationHelper.getIntentByLink(this, url)) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(intent -> { | ||||
|                     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|                     intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); | ||||
|                     startActivity(intent); | ||||
|     @Override | ||||
|     protected void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|  | ||||
|                     finish(); | ||||
|                 }, this::handleError) | ||||
|         ); | ||||
|         disposables.clear(); | ||||
|     } | ||||
|  | ||||
|     protected void handleError(Throwable error) { | ||||
|     private void handleUrl(String url) { | ||||
|         disposables.add(Observable | ||||
|                 .fromCallable(() -> { | ||||
|                     if (currentServiceId == -1) { | ||||
|                         currentService = NewPipe.getServiceByUrl(url); | ||||
|                         currentServiceId = currentService.getServiceId(); | ||||
|                         currentLinkType = currentService.getLinkTypeByUrl(url); | ||||
|                         currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); | ||||
|                     } else { | ||||
|                         currentService = NewPipe.getService(currentServiceId); | ||||
|                     } | ||||
|  | ||||
|                     return currentLinkType != LinkType.NONE; | ||||
|                 }) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(result -> { | ||||
|                     if (result) { | ||||
|                         onSuccess(); | ||||
|                     } else { | ||||
|                         onError(); | ||||
|                     } | ||||
|                 }, this::handleError)); | ||||
|     } | ||||
|  | ||||
|     private void handleError(Throwable error) { | ||||
|         error.printStackTrace(); | ||||
|  | ||||
|         if (error instanceof ExtractionException) { | ||||
| @@ -103,11 +152,345 @@ public class RouterActivity extends AppCompatActivity { | ||||
|         finish(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|     private void onError() { | ||||
|         Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); | ||||
|         finish(); | ||||
|     } | ||||
|  | ||||
|         disposables.clear(); | ||||
|     protected void onSuccess() { | ||||
|         final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); | ||||
|         boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); | ||||
|  | ||||
|         if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { | ||||
|             Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); | ||||
|             finish(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) | ||||
|         if (currentService == ServiceList.SoundCloud) { | ||||
|             handleChoice(getString(R.string.background_player_key)); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final String playerChoiceKey = preferences.getString( | ||||
|                 getString(R.string.preferred_open_action_key), | ||||
|                 getString(R.string.preferred_open_action_default)); | ||||
|         final String alwaysAskKey = getString(R.string.always_ask_open_action_key); | ||||
|  | ||||
|         if (playerChoiceKey.equals(alwaysAskKey)) { | ||||
|             showDialog(); | ||||
|         } else { | ||||
|             handleChoice(playerChoiceKey); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void showDialog() { | ||||
|         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, | ||||
|                 ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); | ||||
|  | ||||
|         LayoutInflater inflater = LayoutInflater.from(themeWrapper); | ||||
|         final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); | ||||
|         final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); | ||||
|  | ||||
|         final AdapterChoiceItem[] choices = { | ||||
|                 new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info), | ||||
|                         resolveResourceIdFromAttr(themeWrapper, R.attr.info)), | ||||
|                 new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), | ||||
|                         resolveResourceIdFromAttr(themeWrapper, R.attr.play)), | ||||
|                 new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), | ||||
|                         resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), | ||||
|                 new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), | ||||
|                         resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { | ||||
|             final int indexOfChild = radioGroup.indexOfChild( | ||||
|                     radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); | ||||
|             final AdapterChoiceItem choice = choices[indexOfChild]; | ||||
|  | ||||
|             handleChoice(choice.key); | ||||
|  | ||||
|             if (which == DialogInterface.BUTTON_POSITIVE) { | ||||
|                 preferences.edit().putString(getString(R.string.preferred_open_action_key), choice.key).apply(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) | ||||
|                 .setTitle(R.string.preferred_player_share_menu_title) | ||||
|                 .setView(radioGroup) | ||||
|                 .setCancelable(true) | ||||
|                 .setNegativeButton(R.string.just_once, dialogButtonsClickListener) | ||||
|                 .setPositiveButton(R.string.always, dialogButtonsClickListener) | ||||
|                 .setOnDismissListener((dialog) -> finish()) | ||||
|                 .create(); | ||||
|  | ||||
|         alertDialog.setOnShowListener(dialog -> { | ||||
|             setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); | ||||
|         }); | ||||
|  | ||||
|         radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); | ||||
|         final View.OnClickListener radioButtonsClickListener = v -> { | ||||
|             final int indexOfChild = radioGroup.indexOfChild(v); | ||||
|             if (indexOfChild == -1) return; | ||||
|  | ||||
|             selectedPreviously = selectedRadioPosition; | ||||
|             selectedRadioPosition = indexOfChild; | ||||
|  | ||||
|             if (selectedPreviously == selectedRadioPosition) { | ||||
|                 handleChoice(choices[selectedRadioPosition].key); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         int id = 12345; | ||||
|         for (AdapterChoiceItem item : choices) { | ||||
|             final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); | ||||
|             radioButton.setText(item.description); | ||||
|             radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); | ||||
|             radioButton.setChecked(false); | ||||
|             radioButton.setId(id++); | ||||
|             radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); | ||||
|             radioButton.setOnClickListener(radioButtonsClickListener); | ||||
|             radioGroup.addView(radioButton); | ||||
|         } | ||||
|  | ||||
|         if (selectedRadioPosition == -1) { | ||||
|             final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null); | ||||
|             if (!TextUtils.isEmpty(lastSelectedPlayer)) { | ||||
|                 for (int i = 0; i < choices.length; i++) { | ||||
|                     AdapterChoiceItem c = choices[i]; | ||||
|                     if (lastSelectedPlayer.equals(c.key)) { | ||||
|                         selectedRadioPosition = i; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); | ||||
|         if (selectedRadioPosition != -1) { | ||||
|             ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); | ||||
|         } | ||||
|         selectedPreviously = selectedRadioPosition; | ||||
|  | ||||
|         alertDialog.show(); | ||||
|     } | ||||
|  | ||||
|     private void setDialogButtonsState(AlertDialog dialog, boolean state) { | ||||
|         final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); | ||||
|         final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); | ||||
|         if (negativeButton == null || positiveButton == null) return; | ||||
|  | ||||
|         negativeButton.setEnabled(state); | ||||
|         positiveButton.setEnabled(state); | ||||
|     } | ||||
|  | ||||
|     private void handleChoice(final String playerChoiceKey) { | ||||
|         if (Arrays.asList(getResources() | ||||
|                 .getStringArray(R.array.preferred_open_action_values_list)) | ||||
|                 .contains(playerChoiceKey)) { | ||||
|             PreferenceManager.getDefaultSharedPreferences(this).edit() | ||||
|                     .putString(getString(R.string.preferred_open_action_last_selected_key), | ||||
|                             playerChoiceKey).apply(); | ||||
|         } | ||||
|  | ||||
|         if (playerChoiceKey.equals(getString(R.string.popup_player_key)) | ||||
|                 && !PermissionHelper.isPopupEnabled(this)) { | ||||
|             PermissionHelper.showPopupEnablementToast(this); | ||||
|             finish(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // stop and bypass FetcherService if InfoScreen was selected since | ||||
|         // StreamDetailFragment can fetch data itself | ||||
|         if(playerChoiceKey.equals(getString(R.string.show_info_key))) { | ||||
|             disposables.add(Observable | ||||
|                     .fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl)) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe(intent -> { | ||||
|                         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|                         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); | ||||
|                         startActivity(intent); | ||||
|  | ||||
|                         finish(); | ||||
|                     }, this::handleError) | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final Intent intent = new Intent(this, FetcherService.class); | ||||
|         intent.putExtra(FetcherService.KEY_CHOICE, | ||||
|                 new Choice(currentService.getServiceId(), | ||||
|                         currentLinkType, | ||||
|                         currentUrl, | ||||
|                         playerChoiceKey)); | ||||
|         startService(intent); | ||||
|  | ||||
|         finish(); | ||||
|     } | ||||
|  | ||||
|     private static class AdapterChoiceItem { | ||||
|         final String description, key; | ||||
|         @DrawableRes | ||||
|         final int icon; | ||||
|  | ||||
|         AdapterChoiceItem(String key, String description, int icon) { | ||||
|             this.description = description; | ||||
|             this.key = key; | ||||
|             this.icon = icon; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class Choice implements Serializable { | ||||
|         final int serviceId; | ||||
|         final String url, playerChoice; | ||||
|         final LinkType linkType; | ||||
|  | ||||
|         Choice(int serviceId, LinkType linkType, String url, String playerChoice) { | ||||
|             this.serviceId = serviceId; | ||||
|             this.linkType = linkType; | ||||
|             this.url = url; | ||||
|             this.playerChoice = playerChoice; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String toString() { | ||||
|             return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Service Fetcher | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public static class FetcherService extends IntentService { | ||||
|  | ||||
|         private static final int ID = 456; | ||||
|         public static final String KEY_CHOICE = "key_choice"; | ||||
|         private Disposable fetcher; | ||||
|  | ||||
|         public FetcherService() { | ||||
|             super(FetcherService.class.getSimpleName()); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onCreate() { | ||||
|             super.onCreate(); | ||||
|             startForeground(ID, createNotification().build()); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void onHandleIntent(@Nullable Intent intent) { | ||||
|             if (intent == null) return; | ||||
|  | ||||
|             final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); | ||||
|             if (!(serializable instanceof Choice)) return; | ||||
|             Choice playerChoice = (Choice) serializable; | ||||
|             handleChoice(playerChoice); | ||||
|         } | ||||
|  | ||||
|         public void handleChoice(Choice choice) { | ||||
|             Single<? extends Info> single = null; | ||||
|             UserAction userAction = UserAction.SOMETHING_ELSE; | ||||
|  | ||||
|             switch (choice.linkType) { | ||||
|                 case STREAM: | ||||
|                     single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); | ||||
|                     userAction = UserAction.REQUESTED_STREAM; | ||||
|                     break; | ||||
|                 case CHANNEL: | ||||
|                     single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); | ||||
|                     userAction = UserAction.REQUESTED_CHANNEL; | ||||
|                     break; | ||||
|                 case PLAYLIST: | ||||
|                     single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); | ||||
|                     userAction = UserAction.REQUESTED_PLAYLIST; | ||||
|                     break; | ||||
|             } | ||||
|  | ||||
|  | ||||
|             if (single != null) { | ||||
|                 final UserAction finalUserAction = userAction; | ||||
|                 final Consumer<Info> resultHandler = getResultHandler(choice); | ||||
|                 fetcher = single | ||||
|                         .observeOn(AndroidSchedulers.mainThread()) | ||||
|                         .subscribe(info -> { | ||||
|                             resultHandler.accept(info); | ||||
|                             if (fetcher != null) fetcher.dispose(); | ||||
|                         }, throwable -> ExtractorHelper.handleGeneralException(this, | ||||
|                                 choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public Consumer<Info> getResultHandler(Choice choice) { | ||||
|             return info -> { | ||||
|                 final String videoPlayerKey = getString(R.string.video_player_key); | ||||
|                 final String backgroundPlayerKey = getString(R.string.background_player_key); | ||||
|                 final String popupPlayerKey = getString(R.string.popup_player_key); | ||||
|  | ||||
|                 final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|                 boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); | ||||
|                 boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); | ||||
|                 boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); | ||||
|  | ||||
|                 PlayQueue playQueue; | ||||
|                 String playerChoice = choice.playerChoice; | ||||
|  | ||||
|                 if (info instanceof StreamInfo) { | ||||
|                     if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { | ||||
|                         NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); | ||||
|  | ||||
|                     } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { | ||||
|                         NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); | ||||
|  | ||||
|                     } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { | ||||
|                         NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); | ||||
|  | ||||
|                     } else { | ||||
|                         playQueue = new SinglePlayQueue((StreamInfo) info); | ||||
|  | ||||
|                         if (playerChoice.equals(videoPlayerKey)) { | ||||
|                             NavigationHelper.playOnMainPlayer(this, playQueue); | ||||
|                         } else if (playerChoice.equals(backgroundPlayerKey)) { | ||||
|                             NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); | ||||
|                         } else if (playerChoice.equals(popupPlayerKey)) { | ||||
|                             NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { | ||||
|                     playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); | ||||
|  | ||||
|                     if (playerChoice.equals(videoPlayerKey)) { | ||||
|                         NavigationHelper.playOnMainPlayer(this, playQueue); | ||||
|                     } else if (playerChoice.equals(backgroundPlayerKey)) { | ||||
|                         NavigationHelper.playOnBackgroundPlayer(this, playQueue); | ||||
|                     } else if (playerChoice.equals(popupPlayerKey)) { | ||||
|                         NavigationHelper.playOnPopupPlayer(this, playQueue); | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onDestroy() { | ||||
|             super.onDestroy(); | ||||
|             stopForeground(true); | ||||
|             if (fetcher != null) fetcher.dispose(); | ||||
|         } | ||||
|  | ||||
|         private NotificationCompat.Builder createNotification() { | ||||
|             return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) | ||||
|                     .setOngoing(true) | ||||
|                     .setSmallIcon(R.drawable.ic_newpipe_triangle_white) | ||||
|                     .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||
|                     .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) | ||||
|                     .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -119,9 +502,9 @@ public class RouterActivity extends AppCompatActivity { | ||||
|      * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for | ||||
|      * more details. | ||||
|      */ | ||||
|     protected final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; | ||||
|     private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; | ||||
|  | ||||
|     protected String getUrl(Intent intent) { | ||||
|     private String getUrl(Intent intent) { | ||||
|         // first gather data and find service | ||||
|         String videoUrl = null; | ||||
|         if (intent.getData() != null) { | ||||
| @@ -137,7 +520,7 @@ public class RouterActivity extends AppCompatActivity { | ||||
|         return videoUrl; | ||||
|     } | ||||
|  | ||||
|     protected String removeHeadingGibberish(final String input) { | ||||
|     private String removeHeadingGibberish(final String input) { | ||||
|         int start = 0; | ||||
|         for (int i = input.indexOf("://") - 1; i >= 0; i--) { | ||||
|             if (!input.substring(i, i + 1).matches("\\p{L}")) { | ||||
| @@ -148,7 +531,7 @@ public class RouterActivity extends AppCompatActivity { | ||||
|         return input.substring(start, input.length()); | ||||
|     } | ||||
|  | ||||
|     protected String trim(final String input) { | ||||
|     private String trim(final String input) { | ||||
|         if (input == null || input.length() < 1) { | ||||
|             return input; | ||||
|         } else { | ||||
| @@ -188,5 +571,4 @@ public class RouterActivity extends AppCompatActivity { | ||||
|         } | ||||
|         return result.toArray(new String[result.size()]); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,413 +0,0 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.app.IntentService; | ||||
| import android.content.DialogInterface; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| import android.os.PersistableBundle; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.DrawableRes; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.NotificationCompat; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.text.TextUtils; | ||||
| import android.view.ContextThemeWrapper; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.Button; | ||||
| import android.widget.LinearLayout; | ||||
| import android.widget.RadioButton; | ||||
| import android.widget.RadioGroup; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.Info; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.StreamingService.LinkType; | ||||
| import org.schabi.newpipe.extractor.channel.ChannelInfo; | ||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.playlist.ChannelPlayQueue; | ||||
| import org.schabi.newpipe.playlist.PlayQueue; | ||||
| import org.schabi.newpipe.playlist.PlaylistPlayQueue; | ||||
| import org.schabi.newpipe.playlist.SinglePlayQueue; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.PermissionHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| import java.util.Arrays; | ||||
|  | ||||
| import icepick.State; | ||||
| import io.reactivex.Observable; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import io.reactivex.functions.Consumer; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
|  | ||||
| import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; | ||||
|  | ||||
| /** | ||||
|  * Get the url from the intent and open it in the chosen preferred player | ||||
|  */ | ||||
| public class RouterPlayerActivity extends RouterActivity { | ||||
|  | ||||
|     @State | ||||
|     protected int currentServiceId = -1; | ||||
|     private StreamingService currentService; | ||||
|     @State | ||||
|     protected LinkType currentLinkType; | ||||
|     @State | ||||
|     protected int selectedRadioPosition = -1; | ||||
|     protected int selectedPreviously = -1; | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { | ||||
|         super.onCreate(savedInstanceState, persistentState); | ||||
|         setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void handleUrl(String url) { | ||||
|         disposables.add(Observable | ||||
|                 .fromCallable(() -> { | ||||
|                     if (currentServiceId == -1) { | ||||
|                         currentService = NewPipe.getServiceByUrl(url); | ||||
|                         currentServiceId = currentService.getServiceId(); | ||||
|                         currentLinkType = currentService.getLinkTypeByUrl(url); | ||||
|                         currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); | ||||
|                     } else { | ||||
|                         currentService = NewPipe.getService(currentServiceId); | ||||
|                     } | ||||
|  | ||||
|                     return currentLinkType != LinkType.NONE; | ||||
|                 }) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(result -> { | ||||
|                     if (result) { | ||||
|                         onSuccess(); | ||||
|                     } else { | ||||
|                         onError(); | ||||
|                     } | ||||
|                 }, this::handleError)); | ||||
|     } | ||||
|  | ||||
|     protected void onError() { | ||||
|         Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); | ||||
|         finish(); | ||||
|     } | ||||
|  | ||||
|     protected void onSuccess() { | ||||
|         final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); | ||||
|         boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); | ||||
|  | ||||
|         if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { | ||||
|             Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); | ||||
|             finish(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) | ||||
|         if (currentService == ServiceList.SoundCloud.getService()) { | ||||
|             handleChoice(getString(R.string.background_player_key)); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); | ||||
|         final String alwaysAskKey = getString(R.string.always_ask_player_key); | ||||
|  | ||||
|         if (playerChoiceKey.equals(alwaysAskKey)) { | ||||
|             showDialog(); | ||||
|         } else { | ||||
|             handleChoice(playerChoiceKey); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void showDialog() { | ||||
|         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, | ||||
|                 ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); | ||||
|  | ||||
|         LayoutInflater inflater = LayoutInflater.from(themeWrapper); | ||||
|         final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); | ||||
|         final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); | ||||
|  | ||||
|         final AdapterChoiceItem[] choices = { | ||||
|                 new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), | ||||
|                         resolveResourceIdFromAttr(themeWrapper, R.attr.play)), | ||||
|                 new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), | ||||
|                         resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), | ||||
|                 new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), | ||||
|                         resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { | ||||
|             final int indexOfChild = radioGroup.indexOfChild(radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); | ||||
|             final AdapterChoiceItem choice = choices[indexOfChild]; | ||||
|  | ||||
|             handleChoice(choice.key); | ||||
|  | ||||
|             if (which == DialogInterface.BUTTON_POSITIVE) { | ||||
|                 preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) | ||||
|                 .setTitle(R.string.preferred_player_share_menu_title) | ||||
|                 .setView(radioGroup) | ||||
|                 .setCancelable(true) | ||||
|                 .setNegativeButton(R.string.just_once, dialogButtonsClickListener) | ||||
|                 .setPositiveButton(R.string.always, dialogButtonsClickListener) | ||||
|                 .setOnDismissListener((dialog) -> finish()) | ||||
|                 .create(); | ||||
|  | ||||
|         alertDialog.setOnShowListener(dialog -> { | ||||
|             setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); | ||||
|         }); | ||||
|  | ||||
|         radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); | ||||
|         final View.OnClickListener radioButtonsClickListener = v -> { | ||||
|             final int indexOfChild = radioGroup.indexOfChild(v); | ||||
|             if (indexOfChild == -1) return; | ||||
|  | ||||
|             selectedPreviously = selectedRadioPosition; | ||||
|             selectedRadioPosition = indexOfChild; | ||||
|  | ||||
|             if (selectedPreviously == selectedRadioPosition) { | ||||
|                 handleChoice(choices[selectedRadioPosition].key); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         int id = 12345; | ||||
|         for (AdapterChoiceItem item : choices) { | ||||
|             final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); | ||||
|             radioButton.setText(item.description); | ||||
|             radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); | ||||
|             radioButton.setChecked(false); | ||||
|             radioButton.setId(id++); | ||||
|             radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); | ||||
|             radioButton.setOnClickListener(radioButtonsClickListener); | ||||
|             radioGroup.addView(radioButton); | ||||
|         } | ||||
|  | ||||
|         if (selectedRadioPosition == -1) { | ||||
|             final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); | ||||
|             if (!TextUtils.isEmpty(lastSelectedPlayer)) { | ||||
|                 for (int i = 0; i < choices.length; i++) { | ||||
|                     AdapterChoiceItem c = choices[i]; | ||||
|                     if (lastSelectedPlayer.equals(c.key)) { | ||||
|                         selectedRadioPosition = i; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); | ||||
|         if (selectedRadioPosition != -1) { | ||||
|             ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); | ||||
|         } | ||||
|         selectedPreviously = selectedRadioPosition; | ||||
|  | ||||
|         alertDialog.show(); | ||||
|     } | ||||
|  | ||||
|     private void setDialogButtonsState(AlertDialog dialog, boolean state) { | ||||
|         final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); | ||||
|         final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); | ||||
|         if (negativeButton == null || positiveButton == null) return; | ||||
|  | ||||
|         negativeButton.setEnabled(state); | ||||
|         positiveButton.setEnabled(state); | ||||
|     } | ||||
|  | ||||
|     private void handleChoice(final String playerChoiceKey) { | ||||
|         if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { | ||||
|             PreferenceManager.getDefaultSharedPreferences(this).edit() | ||||
|                     .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); | ||||
|         } | ||||
|  | ||||
|         if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { | ||||
|             PermissionHelper.showPopupEnablementToast(this); | ||||
|             finish(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final Intent intent = new Intent(this, FetcherService.class); | ||||
|         intent.putExtra(FetcherService.KEY_CHOICE, new Choice(currentService.getServiceId(), currentLinkType, currentUrl, playerChoiceKey)); | ||||
|         startService(intent); | ||||
|  | ||||
|         finish(); | ||||
|     } | ||||
|  | ||||
|     private static class AdapterChoiceItem { | ||||
|         final String description, key; | ||||
|         @DrawableRes | ||||
|         final int icon; | ||||
|  | ||||
|         AdapterChoiceItem(String key, String description, int icon) { | ||||
|             this.description = description; | ||||
|             this.key = key; | ||||
|             this.icon = icon; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class Choice implements Serializable { | ||||
|         final int serviceId; | ||||
|         final String url, playerChoice; | ||||
|         final LinkType linkType; | ||||
|  | ||||
|         Choice(int serviceId, LinkType linkType, String url, String playerChoice) { | ||||
|             this.serviceId = serviceId; | ||||
|             this.linkType = linkType; | ||||
|             this.url = url; | ||||
|             this.playerChoice = playerChoice; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String toString() { | ||||
|             return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Service Fetcher | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public static class FetcherService extends IntentService { | ||||
|  | ||||
|         private static final int ID = 456; | ||||
|         public static final String KEY_CHOICE = "key_choice"; | ||||
|         private Disposable fetcher; | ||||
|  | ||||
|         public FetcherService() { | ||||
|             super(FetcherService.class.getSimpleName()); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onCreate() { | ||||
|             super.onCreate(); | ||||
|             startForeground(ID, createNotification().build()); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void onHandleIntent(@Nullable Intent intent) { | ||||
|             if (intent == null) return; | ||||
|  | ||||
|             final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); | ||||
|             if (!(serializable instanceof Choice)) return; | ||||
|             Choice playerChoice = (Choice) serializable; | ||||
|             handleChoice(playerChoice); | ||||
|         } | ||||
|  | ||||
|         public void handleChoice(Choice choice) { | ||||
|             Single<? extends Info> single = null; | ||||
|             UserAction userAction = UserAction.SOMETHING_ELSE; | ||||
|  | ||||
|             switch (choice.linkType) { | ||||
|                 case STREAM: | ||||
|                     single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); | ||||
|                     userAction = UserAction.REQUESTED_STREAM; | ||||
|                     break; | ||||
|                 case CHANNEL: | ||||
|                     single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); | ||||
|                     userAction = UserAction.REQUESTED_CHANNEL; | ||||
|                     break; | ||||
|                 case PLAYLIST: | ||||
|                     single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); | ||||
|                     userAction = UserAction.REQUESTED_PLAYLIST; | ||||
|                     break; | ||||
|             } | ||||
|  | ||||
|  | ||||
|             if (single != null) { | ||||
|                 final UserAction finalUserAction = userAction; | ||||
|                 final Consumer<Info> resultHandler = getResultHandler(choice); | ||||
|                 fetcher = single | ||||
|                         .observeOn(AndroidSchedulers.mainThread()) | ||||
|                         .subscribe(info -> { | ||||
|                             resultHandler.accept(info); | ||||
|                             if (fetcher != null) fetcher.dispose(); | ||||
|                         }, throwable -> ExtractorHelper.handleGeneralException(this, | ||||
|                                 choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public Consumer<Info> getResultHandler(Choice choice) { | ||||
|             return info -> { | ||||
|                 final String videoPlayerKey = getString(R.string.video_player_key); | ||||
|                 final String backgroundPlayerKey = getString(R.string.background_player_key); | ||||
|                 final String popupPlayerKey = getString(R.string.popup_player_key); | ||||
|  | ||||
|                 final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|                 boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); | ||||
|                 boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); | ||||
|                 boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); | ||||
|  | ||||
|                 PlayQueue playQueue; | ||||
|                 String playerChoice = choice.playerChoice; | ||||
|  | ||||
|                 if (info instanceof StreamInfo) { | ||||
|                     if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { | ||||
|                         NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); | ||||
|  | ||||
|                     } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { | ||||
|                         NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); | ||||
|  | ||||
|                     } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { | ||||
|                         NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); | ||||
|  | ||||
|                     } else { | ||||
|                         playQueue = new SinglePlayQueue((StreamInfo) info); | ||||
|  | ||||
|                         if (playerChoice.equals(videoPlayerKey)) { | ||||
|                             NavigationHelper.playOnMainPlayer(this, playQueue); | ||||
|                         } else if (playerChoice.equals(backgroundPlayerKey)) { | ||||
|                             NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); | ||||
|                         } else if (playerChoice.equals(popupPlayerKey)) { | ||||
|                             NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { | ||||
|                     playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); | ||||
|  | ||||
|                     if (playerChoice.equals(videoPlayerKey)) { | ||||
|                         NavigationHelper.playOnMainPlayer(this, playQueue); | ||||
|                     } else if (playerChoice.equals(backgroundPlayerKey)) { | ||||
|                         NavigationHelper.playOnBackgroundPlayer(this, playQueue); | ||||
|                     } else if (playerChoice.equals(popupPlayerKey)) { | ||||
|                         NavigationHelper.playOnPopupPlayer(this, playQueue); | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onDestroy() { | ||||
|             super.onDestroy(); | ||||
|             stopForeground(true); | ||||
|             if (fetcher != null) fetcher.dispose(); | ||||
|         } | ||||
|  | ||||
|         private NotificationCompat.Builder createNotification() { | ||||
|             return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) | ||||
|                     .setOngoing(true) | ||||
|                     .setSmallIcon(R.drawable.ic_newpipe_triangle_white) | ||||
|                     .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||
|                     .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) | ||||
|                     .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -4,23 +4,52 @@ import android.arch.persistence.room.Database; | ||||
| import android.arch.persistence.room.RoomDatabase; | ||||
| import android.arch.persistence.room.TypeConverters; | ||||
|  | ||||
| import org.schabi.newpipe.database.history.Converters; | ||||
| import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; | ||||
| import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; | ||||
| import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; | ||||
| import org.schabi.newpipe.database.history.model.SearchHistoryEntry; | ||||
| import org.schabi.newpipe.database.history.model.WatchHistoryEntry; | ||||
| import org.schabi.newpipe.database.history.model.StreamHistoryEntity; | ||||
| import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; | ||||
| import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; | ||||
| import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistEntity; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; | ||||
| import org.schabi.newpipe.database.stream.dao.StreamDAO; | ||||
| import org.schabi.newpipe.database.stream.dao.StreamStateDAO; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.database.stream.model.StreamStateEntity; | ||||
| import org.schabi.newpipe.database.subscription.SubscriptionDAO; | ||||
| import org.schabi.newpipe.database.subscription.SubscriptionEntity; | ||||
|  | ||||
| import static org.schabi.newpipe.database.Migrations.DB_VER_12_0; | ||||
|  | ||||
| @TypeConverters({Converters.class}) | ||||
| @Database(entities = {SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class}, version = 1, exportSchema = false) | ||||
| @Database( | ||||
|         entities = { | ||||
|                 SubscriptionEntity.class, SearchHistoryEntry.class, | ||||
|                 StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, | ||||
|                 PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class | ||||
|         }, | ||||
|         version = DB_VER_12_0, | ||||
|         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(); | ||||
|  | ||||
|     public abstract StreamDAO streamDAO(); | ||||
|  | ||||
|     public abstract StreamHistoryDAO streamHistoryDAO(); | ||||
|  | ||||
|     public abstract StreamStateDAO streamStateDAO(); | ||||
|  | ||||
|     public abstract PlaylistDAO playlistDAO(); | ||||
|  | ||||
|     public abstract PlaylistStreamDAO playlistStreamDAO(); | ||||
|  | ||||
|     public abstract PlaylistRemoteDAO playlistRemoteDAO(); | ||||
| } | ||||
|   | ||||
| @@ -23,9 +23,6 @@ public interface BasicDAO<Entity> { | ||||
|     @Insert(onConflict = OnConflictStrategy.FAIL) | ||||
|     List<Long> insertAll(final Collection<Entity> entities); | ||||
|  | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     long upsert(final Entity entity); | ||||
|  | ||||
|     /* Searches */ | ||||
|     Flowable<List<Entity>> getAll(); | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| package org.schabi.newpipe.database.history; | ||||
| package org.schabi.newpipe.database; | ||||
| 
 | ||||
| import android.arch.persistence.room.TypeConverter; | ||||
| 
 | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
| 
 | ||||
| import java.util.Date; | ||||
| 
 | ||||
| public class Converters { | ||||
| @@ -25,4 +27,14 @@ public class Converters { | ||||
|     public static Long dateToTimestamp(Date date) { | ||||
|         return date == null ? null : date.getTime(); | ||||
|     } | ||||
| 
 | ||||
|     @TypeConverter | ||||
|     public static StreamType streamTypeOf(String value) { | ||||
|         return StreamType.valueOf(value); | ||||
|     } | ||||
| 
 | ||||
|     @TypeConverter | ||||
|     public static String stringOf(StreamType streamType) { | ||||
|         return streamType.name(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										13
									
								
								app/src/main/java/org/schabi/newpipe/database/LocalItem.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| package org.schabi.newpipe.database; | ||||
|  | ||||
| public interface LocalItem { | ||||
|     enum LocalItemType { | ||||
|         PLAYLIST_LOCAL_ITEM, | ||||
|         PLAYLIST_REMOTE_ITEM, | ||||
|  | ||||
|         PLAYLIST_STREAM_ITEM, | ||||
|         STATISTIC_STREAM_ITEM, | ||||
|     } | ||||
|  | ||||
|     LocalItemType getLocalItemType(); | ||||
| } | ||||
| @@ -0,0 +1,61 @@ | ||||
| package org.schabi.newpipe.database; | ||||
|  | ||||
| import android.arch.persistence.db.SupportSQLiteDatabase; | ||||
| import android.arch.persistence.room.migration.Migration; | ||||
| import android.support.annotation.NonNull; | ||||
|  | ||||
| public class Migrations { | ||||
|  | ||||
|     public static final int DB_VER_11_0 = 1; | ||||
|     public static final int DB_VER_12_0 = 2; | ||||
|  | ||||
|     public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) { | ||||
|         @Override | ||||
|         public void migrate(@NonNull SupportSQLiteDatabase database) { | ||||
|             /* | ||||
|             * Unfortunately these queries must be hardcoded due to the possibility of | ||||
|             * schema and names changing at a later date, thus invalidating the older migration | ||||
|             * scripts if they are not hardcoded. | ||||
|             * */ | ||||
|  | ||||
|             // Not much we can do about this, since room doesn't create tables before migration. | ||||
|             // It's either this or blasting the entire database anew. | ||||
|             database.execSQL("CREATE  INDEX `index_search_history_search` ON `search_history` (`search`)"); | ||||
|             database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); | ||||
|             database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); | ||||
|             database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); | ||||
|             database.execSQL("CREATE  INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)"); | ||||
|             database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); | ||||
|             database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)"); | ||||
|             database.execSQL("CREATE  INDEX `index_playlists_name` ON `playlists` (`name`)"); | ||||
|             database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); | ||||
|             database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)"); | ||||
|             database.execSQL("CREATE  INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)"); | ||||
|             database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)"); | ||||
|             database.execSQL("CREATE  INDEX `index_remote_playlists_name` ON `remote_playlists` (`name`)"); | ||||
|             database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `remote_playlists` (`service_id`, `url`)"); | ||||
|  | ||||
|             // Populate streams table with existing entries in watch history | ||||
|             // Latest data first, thus ignoring older entries with the same indices | ||||
|             database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " + | ||||
|                     "stream_type, duration, uploader, thumbnail_url) " + | ||||
|  | ||||
|                     "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + | ||||
|                     "uploader, thumbnail_url " + | ||||
|  | ||||
|                     "FROM watch_history " + | ||||
|                     "ORDER BY creation_date DESC"); | ||||
|  | ||||
|             // Once the streams have PKs, join them with the normalized history table | ||||
|             // and populate it with the remaining data from watch history | ||||
|             database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" + | ||||
|                     "SELECT uid, creation_date, 1 " + | ||||
|                     "FROM watch_history INNER JOIN streams " + | ||||
|                     "ON watch_history.service_id == streams.service_id " + | ||||
|                     "AND watch_history.url == streams.url " + | ||||
|                     "ORDER BY creation_date DESC"); | ||||
|  | ||||
|             database.execSQL("DROP TABLE IF EXISTS watch_history"); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -2,7 +2,9 @@ package org.schabi.newpipe.database.history.dao; | ||||
|  | ||||
| import android.arch.persistence.room.Dao; | ||||
| import android.arch.persistence.room.Query; | ||||
| import android.support.annotation.Nullable; | ||||
|  | ||||
| import org.schabi.newpipe.database.BasicDAO; | ||||
| import org.schabi.newpipe.database.history.model.SearchHistoryEntry; | ||||
|  | ||||
| import java.util.List; | ||||
| @@ -20,8 +22,9 @@ 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 | ||||
|     @Query("SELECT * FROM " + TABLE_NAME + | ||||
|             " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") | ||||
|     @Nullable | ||||
|     SearchHistoryEntry getLatestEntry(); | ||||
|  | ||||
|     @Query("DELETE FROM " + TABLE_NAME) | ||||
|   | ||||
| @@ -0,0 +1,68 @@ | ||||
| package org.schabi.newpipe.database.history.dao; | ||||
|  | ||||
|  | ||||
| import android.arch.persistence.room.Dao; | ||||
| import android.arch.persistence.room.Query; | ||||
| import android.support.annotation.Nullable; | ||||
|  | ||||
| import org.schabi.newpipe.database.BasicDAO; | ||||
| import org.schabi.newpipe.database.history.model.StreamHistoryEntry; | ||||
| import org.schabi.newpipe.database.stream.StreamStatisticsEntry; | ||||
| import org.schabi.newpipe.database.history.model.StreamHistoryEntity; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.Flowable; | ||||
|  | ||||
| import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT; | ||||
| import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; | ||||
| import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; | ||||
| import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; | ||||
| import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; | ||||
| import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; | ||||
|  | ||||
| @Dao | ||||
| public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> { | ||||
|     @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + | ||||
|             " WHERE " + STREAM_ACCESS_DATE + " = " + | ||||
|             "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")") | ||||
|     @Override | ||||
|     @Nullable | ||||
|     public abstract StreamHistoryEntity getLatestEntry(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("SELECT * FROM " + STREAM_HISTORY_TABLE) | ||||
|     public abstract Flowable<List<StreamHistoryEntity>> getAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("DELETE FROM " + STREAM_HISTORY_TABLE) | ||||
|     public abstract int deleteAll(); | ||||
|  | ||||
|     @Override | ||||
|     public Flowable<List<StreamHistoryEntity>> listByService(int serviceId) { | ||||
|         throw new UnsupportedOperationException(); | ||||
|     } | ||||
|  | ||||
|     @Query("SELECT * FROM " + STREAM_TABLE + | ||||
|             " INNER JOIN " + STREAM_HISTORY_TABLE + | ||||
|             " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + | ||||
|             " ORDER BY " + STREAM_ACCESS_DATE + " DESC") | ||||
|     public abstract Flowable<List<StreamHistoryEntry>> getHistory(); | ||||
|  | ||||
|     @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") | ||||
|     public abstract int deleteStreamHistory(final long streamId); | ||||
|  | ||||
|     @Query("SELECT * FROM " + STREAM_TABLE + | ||||
|  | ||||
|             // Select the latest entry and watch count for each stream id on history table | ||||
|             " INNER JOIN " + | ||||
|             "(SELECT " + JOIN_STREAM_ID + ", " + | ||||
|             "  MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " + | ||||
|             "  SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT + | ||||
|             " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" + | ||||
|  | ||||
|             " ON " + STREAM_ID + " = " + JOIN_STREAM_ID) | ||||
|     public abstract Flowable<List<StreamStatisticsEntry>> getStatistics(); | ||||
| } | ||||
| @@ -1,37 +0,0 @@ | ||||
| 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>> getAll(); | ||||
|  | ||||
|     @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) | ||||
|     @Override | ||||
|     Flowable<List<WatchHistoryEntry>> listByService(int serviceId); | ||||
| } | ||||
| @@ -1,60 +0,0 @@ | ||||
| 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(); | ||||
|     } | ||||
| } | ||||
| @@ -3,23 +3,66 @@ package org.schabi.newpipe.database.history.model; | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
| import android.arch.persistence.room.Entity; | ||||
| import android.arch.persistence.room.Ignore; | ||||
| import android.arch.persistence.room.Index; | ||||
| import android.arch.persistence.room.PrimaryKey; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| @Entity(tableName = SearchHistoryEntry.TABLE_NAME) | ||||
| public class SearchHistoryEntry extends HistoryEntry { | ||||
| import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH; | ||||
|  | ||||
| @Entity(tableName = SearchHistoryEntry.TABLE_NAME, | ||||
|         indices = {@Index(value = SEARCH)}) | ||||
| public class SearchHistoryEntry { | ||||
|  | ||||
|     public static final String ID = "id"; | ||||
|     public static final String TABLE_NAME = "search_history"; | ||||
|     public static final String SERVICE_ID = "service_id"; | ||||
|     public static final String CREATION_DATE = "creation_date"; | ||||
|     public static final String SEARCH = "search"; | ||||
|  | ||||
|     @ColumnInfo(name = ID) | ||||
|     @PrimaryKey(autoGenerate = true) | ||||
|     private long id; | ||||
|  | ||||
|     @ColumnInfo(name = CREATION_DATE) | ||||
|     private Date creationDate; | ||||
|  | ||||
|     @ColumnInfo(name = SERVICE_ID) | ||||
|     private int serviceId; | ||||
|  | ||||
|     @ColumnInfo(name = SEARCH) | ||||
|     private String search; | ||||
|  | ||||
|     public SearchHistoryEntry(Date creationDate, int serviceId, String search) { | ||||
|         super(creationDate, serviceId); | ||||
|         this.serviceId = serviceId; | ||||
|         this.creationDate = creationDate; | ||||
|         this.search = search; | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|     } | ||||
|  | ||||
|     public String getSearch() { | ||||
|         return search; | ||||
|     } | ||||
| @@ -29,9 +72,8 @@ public class SearchHistoryEntry extends HistoryEntry { | ||||
|     } | ||||
|  | ||||
|     @Ignore | ||||
|     @Override | ||||
|     public boolean hasEqualValues(HistoryEntry otherEntry) { | ||||
|         return otherEntry instanceof SearchHistoryEntry && super.hasEqualValues(otherEntry) | ||||
|                 && getSearch().equals(((SearchHistoryEntry) otherEntry).getSearch()); | ||||
|     public boolean hasEqualValues(SearchHistoryEntry otherEntry) { | ||||
|         return getServiceId() == otherEntry.getServiceId() && | ||||
|                 getSearch().equals(otherEntry.getSearch()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,79 @@ | ||||
| package org.schabi.newpipe.database.history.model; | ||||
|  | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
| import android.arch.persistence.room.Entity; | ||||
| import android.arch.persistence.room.ForeignKey; | ||||
| import android.arch.persistence.room.Ignore; | ||||
| import android.arch.persistence.room.Index; | ||||
| import android.support.annotation.NonNull; | ||||
|  | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| import static android.arch.persistence.room.ForeignKey.CASCADE; | ||||
| import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; | ||||
| import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; | ||||
| import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; | ||||
|  | ||||
| @Entity(tableName = STREAM_HISTORY_TABLE, | ||||
|         primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, | ||||
|         // No need to index for timestamp as they will almost always be unique | ||||
|         indices = {@Index(value = {JOIN_STREAM_ID})}, | ||||
|         foreignKeys = { | ||||
|                 @ForeignKey(entity = StreamEntity.class, | ||||
|                         parentColumns = StreamEntity.STREAM_ID, | ||||
|                         childColumns = JOIN_STREAM_ID, | ||||
|                         onDelete = CASCADE, onUpdate = CASCADE) | ||||
|         }) | ||||
| public class StreamHistoryEntity { | ||||
|     final public static String STREAM_HISTORY_TABLE = "stream_history"; | ||||
|     final public static String JOIN_STREAM_ID       = "stream_id"; | ||||
|     final public static String STREAM_ACCESS_DATE   = "access_date"; | ||||
|     final public static String STREAM_REPEAT_COUNT  = "repeat_count"; | ||||
|  | ||||
|     @ColumnInfo(name = JOIN_STREAM_ID) | ||||
|     private long streamUid; | ||||
|  | ||||
|     @NonNull | ||||
|     @ColumnInfo(name = STREAM_ACCESS_DATE) | ||||
|     private Date accessDate; | ||||
|  | ||||
|     @ColumnInfo(name = STREAM_REPEAT_COUNT) | ||||
|     private long repeatCount; | ||||
|  | ||||
|     public StreamHistoryEntity(long streamUid, @NonNull Date accessDate, long repeatCount) { | ||||
|         this.streamUid = streamUid; | ||||
|         this.accessDate = accessDate; | ||||
|         this.repeatCount = repeatCount; | ||||
|     } | ||||
|  | ||||
|     @Ignore | ||||
|     public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) { | ||||
|         this(streamUid, accessDate, 1); | ||||
|     } | ||||
|  | ||||
|     public long getStreamUid() { | ||||
|         return streamUid; | ||||
|     } | ||||
|  | ||||
|     public void setStreamUid(long streamUid) { | ||||
|         this.streamUid = streamUid; | ||||
|     } | ||||
|  | ||||
|     public Date getAccessDate() { | ||||
|         return accessDate; | ||||
|     } | ||||
|  | ||||
|     public void setAccessDate(@NonNull Date accessDate) { | ||||
|         this.accessDate = accessDate; | ||||
|     } | ||||
|  | ||||
|     public long getRepeatCount() { | ||||
|         return repeatCount; | ||||
|     } | ||||
|  | ||||
|     public void setRepeatCount(long repeatCount) { | ||||
|         this.repeatCount = repeatCount; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,59 @@ | ||||
| package org.schabi.newpipe.database.history.model; | ||||
|  | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
|  | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| public class StreamHistoryEntry { | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_ID) | ||||
|     final public long uid; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) | ||||
|     final public int serviceId; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_URL) | ||||
|     final public String url; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_TITLE) | ||||
|     final public String title; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_TYPE) | ||||
|     final public StreamType streamType; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_DURATION) | ||||
|     final public long duration; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) | ||||
|     final public String uploader; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) | ||||
|     final public String thumbnailUrl; | ||||
|     @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) | ||||
|     final public long streamId; | ||||
|     @ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE) | ||||
|     final public Date accessDate; | ||||
|     @ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT) | ||||
|     final public long repeatCount; | ||||
|  | ||||
|     public StreamHistoryEntry(long uid, int serviceId, String url, String title, | ||||
|                               StreamType streamType, long duration, String uploader, | ||||
|                               String thumbnailUrl, long streamId, Date accessDate, | ||||
|                               long repeatCount) { | ||||
|         this.uid = uid; | ||||
|         this.serviceId = serviceId; | ||||
|         this.url = url; | ||||
|         this.title = title; | ||||
|         this.streamType = streamType; | ||||
|         this.duration = duration; | ||||
|         this.uploader = uploader; | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|         this.streamId = streamId; | ||||
|         this.accessDate = accessDate; | ||||
|         this.repeatCount = repeatCount; | ||||
|     } | ||||
|  | ||||
|     public StreamHistoryEntity toStreamHistoryEntity() { | ||||
|         return new StreamHistoryEntity(streamId, accessDate, repeatCount); | ||||
|     } | ||||
|  | ||||
|     public boolean hasEqualValues(StreamHistoryEntry other) { | ||||
|         return this.uid == other.uid && streamId == other.streamId && | ||||
|                 accessDate.compareTo(other.accessDate) == 0; | ||||
|     } | ||||
| } | ||||
| @@ -1,109 +0,0 @@ | ||||
| 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.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 long duration; | ||||
|  | ||||
|     public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, long 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.getServiceId(), streamInfo.getName(), streamInfo.getUrl(), | ||||
|                 streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader_name, 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 long 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()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| package org.schabi.newpipe.database.playlist; | ||||
|  | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
|  | ||||
| public interface PlaylistLocalItem extends LocalItem { | ||||
|     String getOrderingName(); | ||||
| } | ||||
| @@ -0,0 +1,37 @@ | ||||
| package org.schabi.newpipe.database.playlist; | ||||
|  | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
|  | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; | ||||
|  | ||||
| public class PlaylistMetadataEntry implements PlaylistLocalItem { | ||||
|     final public static String PLAYLIST_STREAM_COUNT = "streamCount"; | ||||
|  | ||||
|     @ColumnInfo(name = PLAYLIST_ID) | ||||
|     final public long uid; | ||||
|     @ColumnInfo(name = PLAYLIST_NAME) | ||||
|     final public String name; | ||||
|     @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) | ||||
|     final public String thumbnailUrl; | ||||
|     @ColumnInfo(name = PLAYLIST_STREAM_COUNT) | ||||
|     final public long streamCount; | ||||
|  | ||||
|     public PlaylistMetadataEntry(long uid, String name, String thumbnailUrl, long streamCount) { | ||||
|         this.uid = uid; | ||||
|         this.name = name; | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|         this.streamCount = streamCount; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public LocalItemType getLocalItemType() { | ||||
|         return LocalItemType.PLAYLIST_LOCAL_ITEM; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getOrderingName() { | ||||
|         return name; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,60 @@ | ||||
| package org.schabi.newpipe.database.playlist; | ||||
|  | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
|  | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
|  | ||||
| public class PlaylistStreamEntry implements LocalItem { | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_ID) | ||||
|     final public long uid; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) | ||||
|     final public int serviceId; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_URL) | ||||
|     final public String url; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_TITLE) | ||||
|     final public String title; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_TYPE) | ||||
|     final public StreamType streamType; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_DURATION) | ||||
|     final public long duration; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) | ||||
|     final public String uploader; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) | ||||
|     final public String thumbnailUrl; | ||||
|     @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) | ||||
|     final public long streamId; | ||||
|     @ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX) | ||||
|     final public int joinIndex; | ||||
|  | ||||
|     public PlaylistStreamEntry(long uid, int serviceId, String url, String title, | ||||
|                                StreamType streamType, long duration, String uploader, | ||||
|                                String thumbnailUrl, long streamId, int joinIndex) { | ||||
|         this.uid = uid; | ||||
|         this.serviceId = serviceId; | ||||
|         this.url = url; | ||||
|         this.title = title; | ||||
|         this.streamType = streamType; | ||||
|         this.duration = duration; | ||||
|         this.uploader = uploader; | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|         this.streamId = streamId; | ||||
|         this.joinIndex = joinIndex; | ||||
|     } | ||||
|  | ||||
|     public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { | ||||
|         StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); | ||||
|         item.setThumbnailUrl(thumbnailUrl); | ||||
|         item.setUploaderName(uploader); | ||||
|         item.setDuration(duration); | ||||
|         return item; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public LocalItemType getLocalItemType() { | ||||
|         return LocalItemType.PLAYLIST_STREAM_ITEM; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,38 @@ | ||||
| package org.schabi.newpipe.database.playlist.dao; | ||||
|  | ||||
| import android.arch.persistence.room.Dao; | ||||
| import android.arch.persistence.room.Query; | ||||
| import android.arch.persistence.room.Transaction; | ||||
|  | ||||
| import org.schabi.newpipe.database.BasicDAO; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistEntity; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.Flowable; | ||||
|  | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; | ||||
|  | ||||
| @Dao | ||||
| public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> { | ||||
|     @Override | ||||
|     @Query("SELECT * FROM " + PLAYLIST_TABLE) | ||||
|     public abstract Flowable<List<PlaylistEntity>> getAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("DELETE FROM " + PLAYLIST_TABLE) | ||||
|     public abstract int deleteAll(); | ||||
|  | ||||
|     @Override | ||||
|     public Flowable<List<PlaylistEntity>> listByService(int serviceId) { | ||||
|         throw new UnsupportedOperationException(); | ||||
|     } | ||||
|  | ||||
|     @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") | ||||
|     public abstract Flowable<List<PlaylistEntity>> getPlaylist(final long playlistId); | ||||
|  | ||||
|     @Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") | ||||
|     public abstract int deletePlaylist(final long playlistId); | ||||
| } | ||||
| @@ -0,0 +1,60 @@ | ||||
| package org.schabi.newpipe.database.playlist.dao; | ||||
|  | ||||
| import android.arch.persistence.room.Dao; | ||||
| import android.arch.persistence.room.Query; | ||||
| import android.arch.persistence.room.Transaction; | ||||
|  | ||||
| import org.schabi.newpipe.database.BasicDAO; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.Flowable; | ||||
|  | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL; | ||||
|  | ||||
| @Dao | ||||
| public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity> { | ||||
|     @Override | ||||
|     @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE) | ||||
|     public abstract Flowable<List<PlaylistRemoteEntity>> getAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE) | ||||
|     public abstract int deleteAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + | ||||
|             " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") | ||||
|     public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId); | ||||
|  | ||||
|     @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " + | ||||
|             REMOTE_PLAYLIST_URL + " = :url AND " + | ||||
|             REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") | ||||
|     public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url); | ||||
|  | ||||
|     @Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE + | ||||
|             " WHERE " + | ||||
|             REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") | ||||
|     abstract Long getPlaylistIdInternal(long serviceId, String url); | ||||
|  | ||||
|     @Transaction | ||||
|     public long upsert(PlaylistRemoteEntity playlist) { | ||||
|         final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl()); | ||||
|  | ||||
|         if (playlistId == null) { | ||||
|             return insert(playlist); | ||||
|         } else { | ||||
|             playlist.setUid(playlistId); | ||||
|             update(playlist); | ||||
|             return playlistId; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE + | ||||
|             " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId") | ||||
|     public abstract int deletePlaylist(final long playlistId); | ||||
| } | ||||
| @@ -0,0 +1,69 @@ | ||||
| package org.schabi.newpipe.database.playlist.dao; | ||||
|  | ||||
| import android.arch.persistence.room.Dao; | ||||
| import android.arch.persistence.room.Query; | ||||
| import android.arch.persistence.room.Transaction; | ||||
|  | ||||
| import org.schabi.newpipe.database.BasicDAO; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.Flowable; | ||||
|  | ||||
| import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.*; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.*; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamEntity.*; | ||||
|  | ||||
| @Dao | ||||
| public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> { | ||||
|     @Override | ||||
|     @Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE) | ||||
|     public abstract Flowable<List<PlaylistStreamEntity>> getAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE) | ||||
|     public abstract int deleteAll(); | ||||
|  | ||||
|     @Override | ||||
|     public Flowable<List<PlaylistStreamEntity>> listByService(int serviceId) { | ||||
|         throw new UnsupportedOperationException(); | ||||
|     } | ||||
|  | ||||
|     @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE + | ||||
|             " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") | ||||
|     public abstract void deleteBatch(final long playlistId); | ||||
|  | ||||
|     @Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" + | ||||
|             " FROM " + PLAYLIST_STREAM_JOIN_TABLE + | ||||
|             " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") | ||||
|     public abstract Flowable<Integer> getMaximumIndexOf(final long playlistId); | ||||
|  | ||||
|     @Transaction | ||||
|     @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " + | ||||
|             // get ids of streams of the given playlist | ||||
|             "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX + | ||||
|             " FROM " + PLAYLIST_STREAM_JOIN_TABLE + | ||||
|             " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" + | ||||
|  | ||||
|             // then merge with the stream metadata | ||||
|             " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + | ||||
|             " ORDER BY " + JOIN_INDEX + " ASC") | ||||
|     public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId); | ||||
|  | ||||
|     @Transaction | ||||
|     @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + | ||||
|             PLAYLIST_THUMBNAIL_URL + ", " + | ||||
|             "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + | ||||
|  | ||||
|             " FROM " + PLAYLIST_TABLE + | ||||
|             " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + | ||||
|             " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + | ||||
|             " GROUP BY " + JOIN_PLAYLIST_ID + | ||||
|             " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC") | ||||
|     public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata(); | ||||
| } | ||||
| @@ -0,0 +1,59 @@ | ||||
| package org.schabi.newpipe.database.playlist.model; | ||||
|  | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
| import android.arch.persistence.room.Entity; | ||||
| import android.arch.persistence.room.Index; | ||||
| import android.arch.persistence.room.PrimaryKey; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; | ||||
|  | ||||
| @Entity(tableName = PLAYLIST_TABLE, | ||||
|         indices = {@Index(value = {PLAYLIST_NAME})}) | ||||
| public class PlaylistEntity { | ||||
|     final public static String PLAYLIST_TABLE           = "playlists"; | ||||
|     final public static String PLAYLIST_ID              = "uid"; | ||||
|     final public static String PLAYLIST_NAME            = "name"; | ||||
|     final public static String PLAYLIST_THUMBNAIL_URL   = "thumbnail_url"; | ||||
|  | ||||
|     @PrimaryKey(autoGenerate = true) | ||||
|     @ColumnInfo(name = PLAYLIST_ID) | ||||
|     private long uid = 0; | ||||
|  | ||||
|     @ColumnInfo(name = PLAYLIST_NAME) | ||||
|     private String name; | ||||
|  | ||||
|     @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) | ||||
|     private String thumbnailUrl; | ||||
|  | ||||
|     public PlaylistEntity(String name, String thumbnailUrl) { | ||||
|         this.name = name; | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|     } | ||||
|  | ||||
|     public long getUid() { | ||||
|         return uid; | ||||
|     } | ||||
|  | ||||
|     public void setUid(long uid) { | ||||
|         this.uid = uid; | ||||
|     } | ||||
|  | ||||
|     public String getName() { | ||||
|         return name; | ||||
|     } | ||||
|  | ||||
|     public void setName(String name) { | ||||
|         this.name = name; | ||||
|     } | ||||
|  | ||||
|     public String getThumbnailUrl() { | ||||
|         return thumbnailUrl; | ||||
|     } | ||||
|  | ||||
|     public void setThumbnailUrl(String thumbnailUrl) { | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,139 @@ | ||||
| package org.schabi.newpipe.database.playlist.model; | ||||
|  | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
| import android.arch.persistence.room.Entity; | ||||
| import android.arch.persistence.room.Ignore; | ||||
| import android.arch.persistence.room.Index; | ||||
| import android.arch.persistence.room.PrimaryKey; | ||||
|  | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistLocalItem; | ||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | ||||
| import org.schabi.newpipe.util.Constants; | ||||
|  | ||||
| import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL; | ||||
|  | ||||
| @Entity(tableName = REMOTE_PLAYLIST_TABLE, | ||||
|         indices = { | ||||
|                 @Index(value = {REMOTE_PLAYLIST_NAME}), | ||||
|                 @Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true) | ||||
|         }) | ||||
| public class PlaylistRemoteEntity implements PlaylistLocalItem { | ||||
|     final public static String REMOTE_PLAYLIST_TABLE         = "remote_playlists"; | ||||
|     final public static String REMOTE_PLAYLIST_ID            = "uid"; | ||||
|     final public static String REMOTE_PLAYLIST_SERVICE_ID    = "service_id"; | ||||
|     final public static String REMOTE_PLAYLIST_NAME          = "name"; | ||||
|     final public static String REMOTE_PLAYLIST_URL           = "url"; | ||||
|     final public static String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; | ||||
|     final public static String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader"; | ||||
|     final public static String REMOTE_PLAYLIST_STREAM_COUNT  = "stream_count"; | ||||
|  | ||||
|     @PrimaryKey(autoGenerate = true) | ||||
|     @ColumnInfo(name = REMOTE_PLAYLIST_ID) | ||||
|     private long uid = 0; | ||||
|  | ||||
|     @ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID) | ||||
|     private int serviceId = Constants.NO_SERVICE_ID; | ||||
|  | ||||
|     @ColumnInfo(name = REMOTE_PLAYLIST_NAME) | ||||
|     private String name; | ||||
|  | ||||
|     @ColumnInfo(name = REMOTE_PLAYLIST_URL) | ||||
|     private String url; | ||||
|  | ||||
|     @ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL) | ||||
|     private String thumbnailUrl; | ||||
|  | ||||
|     @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME) | ||||
|     private String uploader; | ||||
|  | ||||
|     @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) | ||||
|     private Long streamCount; | ||||
|  | ||||
|     public PlaylistRemoteEntity(int serviceId, String name, String url, String thumbnailUrl, | ||||
|                                 String uploader, Long streamCount) { | ||||
|         this.serviceId = serviceId; | ||||
|         this.name = name; | ||||
|         this.url = url; | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|         this.uploader = uploader; | ||||
|         this.streamCount = streamCount; | ||||
|     } | ||||
|  | ||||
|     @Ignore | ||||
|     public PlaylistRemoteEntity(final PlaylistInfo info) { | ||||
|         this(info.getServiceId(), info.getName(), info.getUrl(), | ||||
|                 info.getThumbnailUrl() == null ? info.getUploaderAvatarUrl() : info.getThumbnailUrl(), | ||||
|                 info.getUploaderName(), info.getStreamCount()); | ||||
|     } | ||||
|  | ||||
|     public long getUid() { | ||||
|         return uid; | ||||
|     } | ||||
|  | ||||
|     public void setUid(long uid) { | ||||
|         this.uid = uid; | ||||
|     } | ||||
|  | ||||
|     public int getServiceId() { | ||||
|         return serviceId; | ||||
|     } | ||||
|  | ||||
|     public void setServiceId(int serviceId) { | ||||
|         this.serviceId = serviceId; | ||||
|     } | ||||
|  | ||||
|     public String getName() { | ||||
|         return name; | ||||
|     } | ||||
|  | ||||
|     public void setName(String name) { | ||||
|         this.name = name; | ||||
|     } | ||||
|  | ||||
|     public String getThumbnailUrl() { | ||||
|         return thumbnailUrl; | ||||
|     } | ||||
|  | ||||
|     public void setThumbnailUrl(String thumbnailUrl) { | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|     } | ||||
|  | ||||
|     public String getUrl() { | ||||
|         return url; | ||||
|     } | ||||
|  | ||||
|     public void setUrl(String url) { | ||||
|         this.url = url; | ||||
|     } | ||||
|  | ||||
|     public String getUploader() { | ||||
|         return uploader; | ||||
|     } | ||||
|  | ||||
|     public void setUploader(String uploader) { | ||||
|         this.uploader = uploader; | ||||
|     } | ||||
|  | ||||
|     public Long getStreamCount() { | ||||
|         return streamCount; | ||||
|     } | ||||
|  | ||||
|     public void setStreamCount(Long streamCount) { | ||||
|         this.streamCount = streamCount; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public LocalItemType getLocalItemType() { | ||||
|         return PLAYLIST_REMOTE_ITEM; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getOrderingName() { | ||||
|         return name; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,77 @@ | ||||
| package org.schabi.newpipe.database.playlist.model; | ||||
|  | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
| import android.arch.persistence.room.Entity; | ||||
| import android.arch.persistence.room.ForeignKey; | ||||
| import android.arch.persistence.room.Index; | ||||
|  | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
|  | ||||
| import static android.arch.persistence.room.ForeignKey.CASCADE; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID; | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; | ||||
|  | ||||
| @Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE, | ||||
|         primaryKeys = {JOIN_PLAYLIST_ID, JOIN_INDEX}, | ||||
|         indices = { | ||||
|                 @Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true), | ||||
|                 @Index(value = {JOIN_STREAM_ID}) | ||||
|         }, | ||||
|         foreignKeys = { | ||||
|                 @ForeignKey(entity = PlaylistEntity.class, | ||||
|                         parentColumns = PlaylistEntity.PLAYLIST_ID, | ||||
|                         childColumns = JOIN_PLAYLIST_ID, | ||||
|                         onDelete = CASCADE, onUpdate = CASCADE, deferred = true), | ||||
|                 @ForeignKey(entity = StreamEntity.class, | ||||
|                         parentColumns = StreamEntity.STREAM_ID, | ||||
|                         childColumns = JOIN_STREAM_ID, | ||||
|                         onDelete = CASCADE, onUpdate = CASCADE, deferred = true) | ||||
|         }) | ||||
| public class PlaylistStreamEntity { | ||||
|  | ||||
|     final public static String PLAYLIST_STREAM_JOIN_TABLE   = "playlist_stream_join"; | ||||
|     final public static String JOIN_PLAYLIST_ID             = "playlist_id"; | ||||
|     final public static String JOIN_STREAM_ID               = "stream_id"; | ||||
|     final public static String JOIN_INDEX                   = "join_index"; | ||||
|  | ||||
|     @ColumnInfo(name = JOIN_PLAYLIST_ID) | ||||
|     private long playlistUid; | ||||
|  | ||||
|     @ColumnInfo(name = JOIN_STREAM_ID) | ||||
|     private long streamUid; | ||||
|  | ||||
|     @ColumnInfo(name = JOIN_INDEX) | ||||
|     private int index; | ||||
|  | ||||
|     public PlaylistStreamEntity(final long playlistUid, final long streamUid, final int index) { | ||||
|         this.playlistUid = playlistUid; | ||||
|         this.streamUid = streamUid; | ||||
|         this.index = index; | ||||
|     } | ||||
|  | ||||
|     public long getPlaylistUid() { | ||||
|         return playlistUid; | ||||
|     } | ||||
|  | ||||
|     public long getStreamUid() { | ||||
|         return streamUid; | ||||
|     } | ||||
|  | ||||
|     public int getIndex() { | ||||
|         return index; | ||||
|     } | ||||
|  | ||||
|     public void setPlaylistUid(long playlistUid) { | ||||
|         this.playlistUid = playlistUid; | ||||
|     } | ||||
|  | ||||
|     public void setStreamUid(long streamUid) { | ||||
|         this.streamUid = streamUid; | ||||
|     } | ||||
|  | ||||
|     public void setIndex(int index) { | ||||
|         this.index = index; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,69 @@ | ||||
| package org.schabi.newpipe.database.stream; | ||||
|  | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
|  | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.history.model.StreamHistoryEntity; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| public class StreamStatisticsEntry implements LocalItem { | ||||
|     final public static String STREAM_LATEST_DATE = "latestAccess"; | ||||
|     final public static String STREAM_WATCH_COUNT = "watchCount"; | ||||
|  | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_ID) | ||||
|     final public long uid; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) | ||||
|     final public int serviceId; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_URL) | ||||
|     final public String url; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_TITLE) | ||||
|     final public String title; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_TYPE) | ||||
|     final public StreamType streamType; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_DURATION) | ||||
|     final public long duration; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) | ||||
|     final public String uploader; | ||||
|     @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) | ||||
|     final public String thumbnailUrl; | ||||
|     @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) | ||||
|     final public long streamId; | ||||
|     @ColumnInfo(name = StreamStatisticsEntry.STREAM_LATEST_DATE) | ||||
|     final public Date latestAccessDate; | ||||
|     @ColumnInfo(name = StreamStatisticsEntry.STREAM_WATCH_COUNT) | ||||
|     final public long watchCount; | ||||
|  | ||||
|     public StreamStatisticsEntry(long uid, int serviceId, String url, String title, | ||||
|                                  StreamType streamType, long duration, String uploader, | ||||
|                                  String thumbnailUrl, long streamId, Date latestAccessDate, | ||||
|                                  long watchCount) { | ||||
|         this.uid = uid; | ||||
|         this.serviceId = serviceId; | ||||
|         this.url = url; | ||||
|         this.title = title; | ||||
|         this.streamType = streamType; | ||||
|         this.duration = duration; | ||||
|         this.uploader = uploader; | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|         this.streamId = streamId; | ||||
|         this.latestAccessDate = latestAccessDate; | ||||
|         this.watchCount = watchCount; | ||||
|     } | ||||
|  | ||||
|     public StreamInfoItem toStreamInfoItem() { | ||||
|         StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); | ||||
|         item.setDuration(duration); | ||||
|         item.setUploaderName(uploader); | ||||
|         item.setThumbnailUrl(thumbnailUrl); | ||||
|         return item; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public LocalItemType getLocalItemType() { | ||||
|         return LocalItemType.STATISTIC_STREAM_ITEM; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,100 @@ | ||||
| package org.schabi.newpipe.database.stream.dao; | ||||
|  | ||||
| import android.arch.persistence.room.Dao; | ||||
| import android.arch.persistence.room.Insert; | ||||
| import android.arch.persistence.room.OnConflictStrategy; | ||||
| import android.arch.persistence.room.Query; | ||||
| import android.arch.persistence.room.Transaction; | ||||
|  | ||||
| import org.schabi.newpipe.database.BasicDAO; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.database.history.model.StreamHistoryEntity; | ||||
| import org.schabi.newpipe.database.stream.model.StreamStateEntity; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.Flowable; | ||||
|  | ||||
| import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; | ||||
| import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; | ||||
|  | ||||
| @Dao | ||||
| public abstract class StreamDAO implements BasicDAO<StreamEntity> { | ||||
|     @Override | ||||
|     @Query("SELECT * FROM " + STREAM_TABLE) | ||||
|     public abstract Flowable<List<StreamEntity>> getAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("DELETE FROM " + STREAM_TABLE) | ||||
|     public abstract int deleteAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId") | ||||
|     public abstract Flowable<List<StreamEntity>> listByService(int serviceId); | ||||
|  | ||||
|     @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + | ||||
|             STREAM_URL + " = :url AND " + | ||||
|             STREAM_SERVICE_ID + " = :serviceId") | ||||
|     public abstract Flowable<List<StreamEntity>> getStream(long serviceId, String url); | ||||
|  | ||||
|     @Insert(onConflict = OnConflictStrategy.IGNORE) | ||||
|     abstract void silentInsertAllInternal(final List<StreamEntity> streams); | ||||
|  | ||||
|     @Query("SELECT " + STREAM_ID + " FROM " + STREAM_TABLE + " WHERE " + | ||||
|             STREAM_URL + " = :url AND " + | ||||
|             STREAM_SERVICE_ID + " = :serviceId") | ||||
|     abstract Long getStreamIdInternal(long serviceId, String url); | ||||
|  | ||||
|     @Transaction | ||||
|     public long upsert(StreamEntity stream) { | ||||
|         final Long streamIdCandidate = getStreamIdInternal(stream.getServiceId(), stream.getUrl()); | ||||
|  | ||||
|         if (streamIdCandidate == null) { | ||||
|             return insert(stream); | ||||
|         } else { | ||||
|             stream.setUid(streamIdCandidate); | ||||
|             update(stream); | ||||
|             return streamIdCandidate; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Transaction | ||||
|     public List<Long> upsertAll(List<StreamEntity> streams) { | ||||
|         silentInsertAllInternal(streams); | ||||
|  | ||||
|         final List<Long> streamIds = new ArrayList<>(streams.size()); | ||||
|         for (StreamEntity stream : streams) { | ||||
|             final Long streamId = getStreamIdInternal(stream.getServiceId(), stream.getUrl()); | ||||
|             if (streamId == null) { | ||||
|                 throw new IllegalStateException("StreamID cannot be null just after insertion."); | ||||
|             } | ||||
|  | ||||
|             streamIds.add(streamId); | ||||
|             stream.setUid(streamId); | ||||
|         } | ||||
|  | ||||
|         update(streams); | ||||
|         return streamIds; | ||||
|     } | ||||
|  | ||||
|     @Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID + | ||||
|             " NOT IN " + | ||||
|             "(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE + | ||||
|  | ||||
|             " LEFT JOIN " + STREAM_HISTORY_TABLE + | ||||
|             " ON " + STREAM_ID + " = " + | ||||
|             StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID + | ||||
|  | ||||
|             " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + | ||||
|             " ON " + STREAM_ID + " = " + | ||||
|             PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID + | ||||
|             ")") | ||||
|     public abstract int deleteOrphans(); | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| package org.schabi.newpipe.database.stream.dao; | ||||
|  | ||||
| import android.arch.persistence.room.Dao; | ||||
| import android.arch.persistence.room.Insert; | ||||
| import android.arch.persistence.room.OnConflictStrategy; | ||||
| import android.arch.persistence.room.Query; | ||||
| import android.arch.persistence.room.Transaction; | ||||
|  | ||||
| import org.schabi.newpipe.database.BasicDAO; | ||||
| import org.schabi.newpipe.database.stream.model.StreamStateEntity; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.Flowable; | ||||
|  | ||||
| import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; | ||||
|  | ||||
| @Dao | ||||
| public abstract class StreamStateDAO implements BasicDAO<StreamStateEntity> { | ||||
|     @Override | ||||
|     @Query("SELECT * FROM " + STREAM_STATE_TABLE) | ||||
|     public abstract Flowable<List<StreamStateEntity>> getAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("DELETE FROM " + STREAM_STATE_TABLE) | ||||
|     public abstract int deleteAll(); | ||||
|  | ||||
|     @Override | ||||
|     public Flowable<List<StreamStateEntity>> listByService(int serviceId) { | ||||
|         throw new UnsupportedOperationException(); | ||||
|     } | ||||
|  | ||||
|     @Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") | ||||
|     public abstract Flowable<List<StreamStateEntity>> getState(final long streamId); | ||||
|  | ||||
|     @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") | ||||
|     public abstract int deleteState(final long streamId); | ||||
|  | ||||
|     @Insert(onConflict = OnConflictStrategy.IGNORE) | ||||
|     abstract void silentInsertInternal(final StreamStateEntity streamState); | ||||
|  | ||||
|     @Transaction | ||||
|     public long upsert(StreamStateEntity stream) { | ||||
|         silentInsertInternal(stream); | ||||
|         return update(stream); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,153 @@ | ||||
| package org.schabi.newpipe.database.stream.model; | ||||
|  | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
| import android.arch.persistence.room.Entity; | ||||
| import android.arch.persistence.room.Ignore; | ||||
| import android.arch.persistence.room.Index; | ||||
| import android.arch.persistence.room.PrimaryKey; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | ||||
| import org.schabi.newpipe.util.Constants; | ||||
|  | ||||
| import java.io.Serializable; | ||||
|  | ||||
| import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; | ||||
|  | ||||
| @Entity(tableName = STREAM_TABLE, | ||||
|         indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)}) | ||||
| public class StreamEntity implements Serializable { | ||||
|  | ||||
|     final public static String STREAM_TABLE             = "streams"; | ||||
|     final public static String STREAM_ID                = "uid"; | ||||
|     final public static String STREAM_SERVICE_ID        = "service_id"; | ||||
|     final public static String STREAM_URL               = "url"; | ||||
|     final public static String STREAM_TITLE             = "title"; | ||||
|     final public static String STREAM_TYPE              = "stream_type"; | ||||
|     final public static String STREAM_DURATION          = "duration"; | ||||
|     final public static String STREAM_UPLOADER          = "uploader"; | ||||
|     final public static String STREAM_THUMBNAIL_URL     = "thumbnail_url"; | ||||
|  | ||||
|     @PrimaryKey(autoGenerate = true) | ||||
|     @ColumnInfo(name = STREAM_ID) | ||||
|     private long uid = 0; | ||||
|  | ||||
|     @ColumnInfo(name = STREAM_SERVICE_ID) | ||||
|     private int serviceId = Constants.NO_SERVICE_ID; | ||||
|  | ||||
|     @ColumnInfo(name = STREAM_URL) | ||||
|     private String url; | ||||
|  | ||||
|     @ColumnInfo(name = STREAM_TITLE) | ||||
|     private String title; | ||||
|  | ||||
|     @ColumnInfo(name = STREAM_TYPE) | ||||
|     private StreamType streamType; | ||||
|  | ||||
|     @ColumnInfo(name = STREAM_DURATION) | ||||
|     private Long duration; | ||||
|  | ||||
|     @ColumnInfo(name = STREAM_UPLOADER) | ||||
|     private String uploader; | ||||
|  | ||||
|     @ColumnInfo(name = STREAM_THUMBNAIL_URL) | ||||
|     private String thumbnailUrl; | ||||
|  | ||||
|     public StreamEntity(final int serviceId, final String title, final String url, | ||||
|                         final StreamType streamType, final String thumbnailUrl, final String uploader, | ||||
|                         final long duration) { | ||||
|         this.serviceId = serviceId; | ||||
|         this.title = title; | ||||
|         this.url = url; | ||||
|         this.streamType = streamType; | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|         this.uploader = uploader; | ||||
|         this.duration = duration; | ||||
|     } | ||||
|  | ||||
|     @Ignore | ||||
|     public StreamEntity(final StreamInfoItem item) { | ||||
|         this(item.service_id, item.name, item.url, item.stream_type, item.thumbnail_url, | ||||
|                 item.uploader_name, item.duration); | ||||
|     } | ||||
|  | ||||
|     @Ignore | ||||
|     public StreamEntity(final StreamInfo info) { | ||||
|         this(info.service_id, info.name, info.url, info.stream_type, info.thumbnail_url, | ||||
|                 info.uploader_name, info.duration); | ||||
|     } | ||||
|  | ||||
|     @Ignore | ||||
|     public StreamEntity(final PlayQueueItem item) { | ||||
|         this(item.getServiceId(), item.getTitle(), item.getUrl(), item.getStreamType(), | ||||
|                 item.getThumbnailUrl(), item.getUploader(), item.getDuration()); | ||||
|     } | ||||
|  | ||||
|     public long getUid() { | ||||
|         return uid; | ||||
|     } | ||||
|  | ||||
|     public void setUid(long uid) { | ||||
|         this.uid = uid; | ||||
|     } | ||||
|  | ||||
|     public int getServiceId() { | ||||
|         return serviceId; | ||||
|     } | ||||
|  | ||||
|     public void setServiceId(int serviceId) { | ||||
|         this.serviceId = serviceId; | ||||
|     } | ||||
|  | ||||
|     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 StreamType getStreamType() { | ||||
|         return streamType; | ||||
|     } | ||||
|  | ||||
|     public void setStreamType(StreamType type) { | ||||
|         this.streamType = type; | ||||
|     } | ||||
|  | ||||
|     public Long getDuration() { | ||||
|         return duration; | ||||
|     } | ||||
|  | ||||
|     public void setDuration(Long duration) { | ||||
|         this.duration = duration; | ||||
|     } | ||||
|  | ||||
|     public String getUploader() { | ||||
|         return uploader; | ||||
|     } | ||||
|  | ||||
|     public void setUploader(String uploader) { | ||||
|         this.uploader = uploader; | ||||
|     } | ||||
|  | ||||
|     public String getThumbnailUrl() { | ||||
|         return thumbnailUrl; | ||||
|     } | ||||
|  | ||||
|     public void setThumbnailUrl(String thumbnailUrl) { | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,51 @@ | ||||
| package org.schabi.newpipe.database.stream.model; | ||||
|  | ||||
|  | ||||
| import android.arch.persistence.room.ColumnInfo; | ||||
| import android.arch.persistence.room.Entity; | ||||
| import android.arch.persistence.room.ForeignKey; | ||||
|  | ||||
| import static android.arch.persistence.room.ForeignKey.CASCADE; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; | ||||
| import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; | ||||
|  | ||||
| @Entity(tableName = STREAM_STATE_TABLE, | ||||
|         primaryKeys = {JOIN_STREAM_ID}, | ||||
|         foreignKeys = { | ||||
|                 @ForeignKey(entity = StreamEntity.class, | ||||
|                         parentColumns = StreamEntity.STREAM_ID, | ||||
|                         childColumns = JOIN_STREAM_ID, | ||||
|                         onDelete = CASCADE, onUpdate = CASCADE) | ||||
|         }) | ||||
| public class StreamStateEntity { | ||||
|     final public static String STREAM_STATE_TABLE   = "stream_state"; | ||||
|     final public static String JOIN_STREAM_ID       = "stream_id"; | ||||
|     final public static String STREAM_PROGRESS_TIME = "progress_time"; | ||||
|  | ||||
|     @ColumnInfo(name = JOIN_STREAM_ID) | ||||
|     private long streamUid; | ||||
|  | ||||
|     @ColumnInfo(name = STREAM_PROGRESS_TIME) | ||||
|     private long progressTime; | ||||
|  | ||||
|     public StreamStateEntity(long streamUid, long progressTime) { | ||||
|         this.streamUid = streamUid; | ||||
|         this.progressTime = progressTime; | ||||
|     } | ||||
|  | ||||
|     public long getStreamUid() { | ||||
|         return streamUid; | ||||
|     } | ||||
|  | ||||
|     public void setStreamUid(long streamUid) { | ||||
|         this.streamUid = streamUid; | ||||
|     } | ||||
|  | ||||
|     public long getProgressTime() { | ||||
|         return progressTime; | ||||
|     } | ||||
|  | ||||
|     public void setProgressTime(long progressTime) { | ||||
|         this.progressTime = progressTime; | ||||
|     } | ||||
| } | ||||
| @@ -50,8 +50,7 @@ public class SubscriptionEntity { | ||||
|         return uid; | ||||
|     } | ||||
|  | ||||
|     /* Keep this package-private since UID should always be auto generated by Room impl */ | ||||
|     void setUid(long uid) { | ||||
|     public void setUid(long uid) { | ||||
|         this.uid = uid; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskList; | ||||
| import org.schabi.newpipe.fragments.list.channel.ChannelFragment; | ||||
| import org.schabi.newpipe.fragments.list.feed.FeedFragment; | ||||
| import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; | ||||
| import org.schabi.newpipe.fragments.local.bookmark.BookmarkFragment; | ||||
| import org.schabi.newpipe.fragments.subscription.SubscriptionFragment; | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| @@ -46,7 +47,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|     // Constants | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getId(); | ||||
|     private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getServiceId(); | ||||
|     private static final String FALLBACK_CHANNEL_URL = "https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ"; | ||||
|     private static final String FALLBACK_CHANNEL_NAME = "Music"; | ||||
|     private static final String FALLBACK_KIOSK_ID = "Trending"; | ||||
| @@ -84,12 +85,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|  | ||||
|         int channelIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_channel); | ||||
|         int whatsHotIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_hot); | ||||
|         int bookmarkIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_bookmark); | ||||
|  | ||||
|         if (isSubscriptionsPageOnlySelected()) { | ||||
|             tabLayout.getTabAt(0).setIcon(channelIcon); | ||||
|             tabLayout.getTabAt(1).setIcon(bookmarkIcon); | ||||
|         } else { | ||||
|             tabLayout.getTabAt(0).setIcon(whatsHotIcon); | ||||
|             tabLayout.getTabAt(1).setIcon(channelIcon); | ||||
|             tabLayout.getTabAt(2).setIcon(bookmarkIcon); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -147,7 +151,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|     } | ||||
|  | ||||
|     private class PagerAdapter extends FragmentPagerAdapter { | ||||
|  | ||||
|         PagerAdapter(FragmentManager fm) { | ||||
|             super(fm); | ||||
|         } | ||||
| @@ -158,7 +161,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|                 case 0: | ||||
|                     return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment(); | ||||
|                 case 1: | ||||
|                     return new SubscriptionFragment(); | ||||
|                     if(PreferenceManager.getDefaultSharedPreferences(getActivity()) | ||||
|                             .getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key)) | ||||
|                             .equals(getString(R.string.subscription_page_key))) { | ||||
|                         return new BookmarkFragment(); | ||||
|                     } else { | ||||
|                         return new SubscriptionFragment(); | ||||
|                     } | ||||
|                 case 2: | ||||
|                     return new BookmarkFragment(); | ||||
|                 default: | ||||
|                     return new BlankFragment(); | ||||
|             } | ||||
| @@ -172,7 +183,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|  | ||||
|         @Override | ||||
|         public int getCount() { | ||||
|             return isSubscriptionsPageOnlySelected() ? 1 : 2; | ||||
|             return isSubscriptionsPageOnlySelected() ? 2 : 3; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -187,6 +198,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|     } | ||||
|  | ||||
|     private Fragment getMainPageFragment() { | ||||
|         if (getActivity() == null) return new BlankFragment(); | ||||
|  | ||||
|         try { | ||||
|             SharedPreferences preferences = | ||||
|                     PreferenceManager.getDefaultSharedPreferences(getActivity()); | ||||
|   | ||||
| @@ -53,7 +53,6 @@ class ActionBarHandler { | ||||
|     // those are edited directly. Typically VideoDetailFragment will implement those callbacks. | ||||
|     private OnActionListener onShareListener; | ||||
|     private OnActionListener onOpenInBrowserListener; | ||||
|     private OnActionListener onDownloadListener; | ||||
|     private OnActionListener onPlayWithKodiListener; | ||||
|  | ||||
|     // Triggered when a stream related action is triggered. | ||||
| @@ -117,11 +116,6 @@ class ActionBarHandler { | ||||
|                 } | ||||
|                 return true; | ||||
|             } | ||||
|             case R.id.menu_item_download: | ||||
|                 if (onDownloadListener != null) { | ||||
|                     onDownloadListener.onActionSelected(selectedVideoStream); | ||||
|                 } | ||||
|                 return true; | ||||
|             case R.id.action_play_with_kodi: | ||||
|                 if (onPlayWithKodiListener != null) { | ||||
|                     onPlayWithKodiListener.onActionSelected(selectedVideoStream); | ||||
| @@ -145,19 +139,12 @@ class ActionBarHandler { | ||||
|         onOpenInBrowserListener = listener; | ||||
|     } | ||||
|  | ||||
|     public void setOnDownloadListener(OnActionListener listener) { | ||||
|         onDownloadListener = listener; | ||||
|     } | ||||
|  | ||||
|     public void setOnPlayWithKodiListener(OnActionListener listener) { | ||||
|         onPlayWithKodiListener = listener; | ||||
|     } | ||||
|  | ||||
|     public void showDownloadAction(boolean visible) { | ||||
|         menu.findItem(R.id.menu_item_download).setVisible(visible); | ||||
|     } | ||||
|  | ||||
|     public void showPlayWithKodiAction(boolean visible) { | ||||
|         menu.findItem(R.id.action_play_with_kodi).setVisible(visible); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -58,7 +58,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.fragments.BackPressable; | ||||
| import org.schabi.newpipe.fragments.BaseStateFragment; | ||||
| import org.schabi.newpipe.history.HistoryListener; | ||||
| import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; | ||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | ||||
| import org.schabi.newpipe.player.MainVideoPlayer; | ||||
| @@ -75,6 +75,7 @@ import org.schabi.newpipe.util.InfoCache; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
| import org.schabi.newpipe.util.PermissionHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| @@ -145,6 +146,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|  | ||||
|     private TextView detailControlsBackground; | ||||
|     private TextView detailControlsPopup; | ||||
|     private TextView detailControlsAddToPlaylist; | ||||
|     private TextView detailControlsDownload; | ||||
|     private TextView appendControlsDetail; | ||||
|  | ||||
|     private LinearLayout videoDescriptionRootLayout; | ||||
| @@ -327,6 +330,30 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|             case R.id.detail_controls_popup: | ||||
|                 openPopupPlayer(false); | ||||
|                 break; | ||||
|             case R.id.detail_controls_playlist_append: | ||||
|                 if (getFragmentManager() != null && currentInfo != null) { | ||||
|                     PlaylistAppendDialog.fromStreamInfo(currentInfo) | ||||
|                             .show(getFragmentManager(), TAG); | ||||
|                 } | ||||
|                 break; | ||||
|             case R.id.detail_controls_download: | ||||
|                 if (!PermissionHelper.checkStoragePermissions(activity)) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 try { | ||||
|                     DownloadDialog downloadDialog = | ||||
|                             DownloadDialog.newInstance(currentInfo, | ||||
|                                     sortedStreamVideosList, | ||||
|                                     actionBarHandler.getSelectedVideoStream()); | ||||
|                     downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); | ||||
|                 } catch (Exception e) { | ||||
|                     Toast.makeText(activity, | ||||
|                             R.string.could_not_setup_download_menu, | ||||
|                             Toast.LENGTH_LONG).show(); | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|                 break; | ||||
|             case R.id.detail_uploader_root_layout: | ||||
|                 if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { | ||||
|                     Log.w(TAG, "Can't open channel because we got no channel URL"); | ||||
| @@ -429,6 +456,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|  | ||||
|         detailControlsBackground = rootView.findViewById(R.id.detail_controls_background); | ||||
|         detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup); | ||||
|         detailControlsAddToPlaylist = rootView.findViewById(R.id.detail_controls_playlist_append); | ||||
|         detailControlsDownload = rootView.findViewById(R.id.detail_controls_download); | ||||
|         appendControlsDetail = rootView.findViewById(R.id.touch_append_detail); | ||||
|  | ||||
|         videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); | ||||
| @@ -462,7 +491,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|     @Override | ||||
|     protected void initListeners() { | ||||
|         super.initListeners(); | ||||
|         infoItemBuilder.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() { | ||||
|         infoItemBuilder.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() { | ||||
|             @Override | ||||
|             public void selected(StreamInfoItem selectedItem) { | ||||
|                 selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); | ||||
| @@ -479,6 +508,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|         thumbnailBackgroundButton.setOnClickListener(this); | ||||
|         detailControlsBackground.setOnClickListener(this); | ||||
|         detailControlsPopup.setOnClickListener(this); | ||||
|         detailControlsAddToPlaylist.setOnClickListener(this); | ||||
|         detailControlsDownload.setOnClickListener(this); | ||||
|         relatedStreamExpandButton.setOnClickListener(this); | ||||
|  | ||||
|         detailControlsBackground.setLongClickable(true); | ||||
| @@ -498,19 +529,16 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|                 context.getResources().getString(R.string.enqueue_on_popup) | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(DialogInterface dialogInterface, int i) { | ||||
|                 switch (i) { | ||||
|                     case 0: | ||||
|                         NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); | ||||
|                         break; | ||||
|                     case 1: | ||||
|                         NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item)); | ||||
|                         break; | ||||
|                     default: | ||||
|                         break; | ||||
|                 } | ||||
|         final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { | ||||
|             switch (i) { | ||||
|                 case 0: | ||||
|                     NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); | ||||
|                     break; | ||||
|                 case 1: | ||||
|                     NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item)); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @@ -518,21 +546,14 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|     } | ||||
|  | ||||
|     private View.OnTouchListener getOnControlsTouchListener() { | ||||
|         return new View.OnTouchListener() { | ||||
|             @Override | ||||
|             public boolean onTouch(View view, MotionEvent motionEvent) { | ||||
|                 if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false; | ||||
|         return (View view, MotionEvent motionEvent) -> { | ||||
|             if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false; | ||||
|  | ||||
|                 if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { | ||||
|                     animateView(appendControlsDetail, true, 250, 0, new Runnable() { | ||||
|                         @Override | ||||
|                         public void run() { | ||||
|                             animateView(appendControlsDetail, false, 1500, 1000); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|                 return false; | ||||
|             if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { | ||||
|                 animateView(appendControlsDetail, true, 250, 0, () -> | ||||
|                         animateView(appendControlsDetail, false, 1500, 1000)); | ||||
|             } | ||||
|             return false; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @@ -605,18 +626,9 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|     private static void showInstallKoreDialog(final Context context) { | ||||
|         final AlertDialog.Builder builder = new AlertDialog.Builder(context); | ||||
|         builder.setMessage(R.string.kore_not_found) | ||||
|                 .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { | ||||
|                     @Override | ||||
|                     public void onClick(DialogInterface dialog, int which) { | ||||
|                         NavigationHelper.installKore(context); | ||||
|                     } | ||||
|                 }) | ||||
|                 .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { | ||||
|                     @Override | ||||
|                     public void onClick(DialogInterface dialog, int which) { | ||||
|  | ||||
|                     } | ||||
|                 }); | ||||
|                 .setPositiveButton(R.string.install, (DialogInterface dialog, int which) -> | ||||
|                         NavigationHelper.installKore(context)) | ||||
|                 .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {}); | ||||
|         builder.create().show(); | ||||
|     } | ||||
|  | ||||
| @@ -626,44 +638,19 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|         actionBarHandler.setupStreamList(sortedStreamVideosList, spinnerToolbar); | ||||
|         actionBarHandler.setOnShareListener(selectedStreamId -> shareUrl(info.name, info.url)); | ||||
|  | ||||
|         actionBarHandler.setOnOpenInBrowserListener(new ActionBarHandler.OnActionListener() { | ||||
|             @Override | ||||
|             public void onActionSelected(int selectedStreamId) { | ||||
|                 openUrlInBrowser(info.getUrl()); | ||||
|         actionBarHandler.setOnOpenInBrowserListener((int selectedStreamId)-> | ||||
|                 openUrlInBrowser(info.getUrl())); | ||||
|  | ||||
|         actionBarHandler.setOnPlayWithKodiListener((int selectedStreamId) -> { | ||||
|             try { | ||||
|                 NavigationHelper.playWithKore(activity, Uri.parse( | ||||
|                         info.getUrl().replace("https", "http"))); | ||||
|             } catch (Exception e) { | ||||
|                 if(DEBUG) Log.i(TAG, "Failed to start kore", e); | ||||
|                 showInstallKoreDialog(activity); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() { | ||||
|             @Override | ||||
|             public void onActionSelected(int selectedStreamId) { | ||||
|                 try { | ||||
|                     NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http"))); | ||||
|                     if(activity instanceof HistoryListener) { | ||||
|                         ((HistoryListener) activity).onVideoPlayed(info, null); | ||||
|                     } | ||||
|                 } catch (Exception e) { | ||||
|                     if(DEBUG) Log.i(TAG, "Failed to start kore", e); | ||||
|                     showInstallKoreDialog(activity); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() { | ||||
|             @Override | ||||
|             public void onActionSelected(int selectedStreamId) { | ||||
|                 if (!PermissionHelper.checkStoragePermissions(activity)) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 try { | ||||
|                     DownloadDialog downloadDialog = DownloadDialog.newInstance(info, sortedStreamVideosList, selectedStreamId); | ||||
|                     downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); | ||||
|                 } catch (Exception e) { | ||||
|                     Toast.makeText(activity, R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show(); | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -770,20 +757,14 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|         currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(new Consumer<StreamInfo>() { | ||||
|                     @Override | ||||
|                     public void accept(@NonNull StreamInfo result) throws Exception { | ||||
|                         isLoading.set(false); | ||||
|                         currentInfo = result; | ||||
|                         showContentWithAnimation(120, 0, 0); | ||||
|                         handleResult(result); | ||||
|                     } | ||||
|                 }, new Consumer<Throwable>() { | ||||
|                     @Override | ||||
|                     public void accept(@NonNull Throwable throwable) throws Exception { | ||||
|                         isLoading.set(false); | ||||
|                         onError(throwable); | ||||
|                     } | ||||
|                 .subscribe((@NonNull StreamInfo result) -> { | ||||
|                     isLoading.set(false); | ||||
|                     currentInfo = result; | ||||
|                     showContentWithAnimation(120, 0, 0); | ||||
|                     handleResult(result); | ||||
|                 }, (@NonNull Throwable throwable) -> { | ||||
|                     isLoading.set(false); | ||||
|                     onError(throwable); | ||||
|                 }); | ||||
|     } | ||||
|  | ||||
| @@ -794,10 +775,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|     private void openBackgroundPlayer(final boolean append) { | ||||
|         AudioStream audioStream = currentInfo.getAudioStreams().get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); | ||||
|  | ||||
|         if (activity instanceof HistoryListener) { | ||||
|             ((HistoryListener) activity).onAudioPlayed(currentInfo, audioStream); | ||||
|         } | ||||
|  | ||||
|         boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) | ||||
|                 .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); | ||||
|  | ||||
| @@ -814,10 +791,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (activity instanceof HistoryListener) { | ||||
|             ((HistoryListener) activity).onVideoPlayed(currentInfo, getSelectedVideoStream()); | ||||
|         } | ||||
|  | ||||
|         final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); | ||||
|         if (append) { | ||||
|             NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue); | ||||
| @@ -833,10 +806,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|     private void openVideoPlayer() { | ||||
|         VideoStream selectedVideoStream = getSelectedVideoStream(); | ||||
|  | ||||
|         if (activity instanceof HistoryListener) { | ||||
|             ((HistoryListener) activity).onVideoPlayed(currentInfo, selectedVideoStream); | ||||
|         } | ||||
|  | ||||
|         if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { | ||||
|             NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream); | ||||
|         } else { | ||||
| @@ -889,9 +858,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|         } | ||||
|  | ||||
|         disposables.add(Single.just(descriptionHtml) | ||||
|                 .map(new Function<String, Spanned>() { | ||||
|                     @Override | ||||
|                     public Spanned apply(@io.reactivex.annotations.NonNull String description) throws Exception { | ||||
|                 .map((@io.reactivex.annotations.NonNull String description) -> { | ||||
|                         Spanned parsedDescription; | ||||
|                         if (Build.VERSION.SDK_INT >= 24) { | ||||
|                             parsedDescription = Html.fromHtml(description, 0); | ||||
| @@ -900,16 +867,12 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|                             parsedDescription = Html.fromHtml(description); | ||||
|                         } | ||||
|                         return parsedDescription; | ||||
|                     } | ||||
|                 }) | ||||
|                 .subscribeOn(Schedulers.computation()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(new Consumer<Spanned>() { | ||||
|                     @Override | ||||
|                     public void accept(@io.reactivex.annotations.NonNull Spanned spanned) throws Exception { | ||||
|                 .subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> { | ||||
|                         videoDescriptionView.setText(spanned); | ||||
|                         videoDescriptionView.setVisibility(View.VISIBLE); | ||||
|                     } | ||||
|                 })); | ||||
|     } | ||||
|  | ||||
| @@ -1033,6 +996,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|         if (!TextUtils.isEmpty(info.getUploaderName())) { | ||||
|             uploaderTextView.setText(info.getUploaderName()); | ||||
|             uploaderTextView.setVisibility(View.VISIBLE); | ||||
|             uploaderTextView.setSelected(true); | ||||
|         } else { | ||||
|             uploaderTextView.setVisibility(View.GONE); | ||||
|         } | ||||
| @@ -1135,14 +1099,11 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement | ||||
|     } | ||||
|  | ||||
|     public void onBlockedByGemaError() { | ||||
|         thumbnailBackgroundButton.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View v) { | ||||
|         thumbnailBackgroundButton.setOnClickListener((View v) -> { | ||||
|                 Intent intent = new Intent(); | ||||
|                 intent.setAction(Intent.ACTION_VIEW); | ||||
|                 intent.setData(Uri.parse(getString(R.string.c3s_url))); | ||||
|                 startActivity(intent); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); | ||||
|   | ||||
| @@ -3,19 +3,15 @@ package org.schabi.newpipe.fragments.list; | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.DialogInterface; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.v7.app.ActionBar; | ||||
| import android.support.v7.widget.LinearLayoutManager; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.util.Log; | ||||
| import android.view.Gravity; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.InfoItem; | ||||
| @@ -24,14 +20,15 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.fragments.BaseStateFragment; | ||||
| import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | ||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||
| import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; | ||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | ||||
| import org.schabi.newpipe.info_list.InfoListAdapter; | ||||
| import org.schabi.newpipe.playlist.SinglePlayQueue; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.PermissionHelper; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
| import org.schabi.newpipe.util.StateSaver; | ||||
|  | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Queue; | ||||
|  | ||||
| @@ -140,7 +137,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem | ||||
|     @Override | ||||
|     protected void initListeners() { | ||||
|         super.initListeners(); | ||||
|         infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() { | ||||
|         infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() { | ||||
|             @Override | ||||
|             public void selected(StreamInfoItem selectedItem) { | ||||
|                 onItemSelected(selectedItem); | ||||
| @@ -155,7 +152,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() { | ||||
|         infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() { | ||||
|             @Override | ||||
|             public void selected(ChannelInfoItem selectedItem) { | ||||
|                 onItemSelected(selectedItem); | ||||
| @@ -163,12 +160,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem | ||||
|                         useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), | ||||
|                         selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void held(ChannelInfoItem selectedItem) {} | ||||
|         }); | ||||
|  | ||||
|         infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<PlaylistInfoItem>() { | ||||
|         infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() { | ||||
|             @Override | ||||
|             public void selected(PlaylistInfoItem selectedItem) { | ||||
|                 onItemSelected(selectedItem); | ||||
| @@ -176,9 +170,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem | ||||
|                         useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), | ||||
|                         selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void held(PlaylistInfoItem selectedItem) {} | ||||
|         }); | ||||
|  | ||||
|         itemsList.clearOnScrollListeners(); | ||||
| @@ -203,22 +194,26 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem | ||||
|  | ||||
|         final String[] commands = new String[]{ | ||||
|                 context.getResources().getString(R.string.enqueue_on_background), | ||||
|                 context.getResources().getString(R.string.enqueue_on_popup) | ||||
|                 context.getResources().getString(R.string.enqueue_on_popup), | ||||
|                 context.getResources().getString(R.string.append_playlist) | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(DialogInterface dialogInterface, int i) { | ||||
|                 switch (i) { | ||||
|                     case 0: | ||||
|                         NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); | ||||
|                         break; | ||||
|                     case 1: | ||||
|                         NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); | ||||
|                         break; | ||||
|                     default: | ||||
|                         break; | ||||
|                 } | ||||
|         final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { | ||||
|             switch (i) { | ||||
|                 case 0: | ||||
|                     NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); | ||||
|                     break; | ||||
|                 case 1: | ||||
|                     NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); | ||||
|                     break; | ||||
|                 case 2: | ||||
|                     if (getFragmentManager() != null) { | ||||
|                         PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) | ||||
|                                 .show(getFragmentManager(), TAG); | ||||
|                     } | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|   | ||||
| @@ -194,17 +194,14 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||
|         super.onCreateOptionsMenu(menu, inflater); | ||||
|         ActionBar supportActionBar = activity.getSupportActionBar(); | ||||
|         if(useAsFrontPage) { | ||||
|         if(useAsFrontPage && supportActionBar != null) { | ||||
|             supportActionBar.setDisplayHomeAsUpEnabled(false); | ||||
|         } else { | ||||
|             inflater.inflate(R.menu.menu_channel, menu); | ||||
|  | ||||
|             if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); | ||||
|             if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + | ||||
|                     "], inflater = [" + inflater + "]"); | ||||
|             menuRssButton = menu.findItem(R.id.menu_item_rss); | ||||
|             if (currentInfo != null) { | ||||
|                 menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl())); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -225,10 +222,9 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|             case R.id.menu_item_openInBrowser: | ||||
|                 openUrlInBrowser(url); | ||||
|                 break; | ||||
|             case R.id.menu_item_share: { | ||||
|             case R.id.menu_item_share: | ||||
|                 shareUrl(name, url); | ||||
|                 break; | ||||
|             } | ||||
|             default: | ||||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
| @@ -428,6 +424,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|         } else headerSubscribersTextView.setVisibility(View.GONE); | ||||
|  | ||||
|         if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); | ||||
|  | ||||
|         playlistCtrl.setVisibility(View.VISIBLE); | ||||
|  | ||||
|         if (!result.errors.isEmpty()) { | ||||
|   | ||||
| @@ -17,13 +17,18 @@ import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import org.reactivestreams.Subscriber; | ||||
| import org.reactivestreams.Subscription; | ||||
| import org.schabi.newpipe.NewPipeDatabase; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||
| import org.schabi.newpipe.extractor.ListExtractor; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | ||||
| import org.schabi.newpipe.fragments.local.RemotePlaylistManager; | ||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | ||||
| import org.schabi.newpipe.playlist.PlayQueue; | ||||
| import org.schabi.newpipe.playlist.PlaylistPlayQueue; | ||||
| @@ -31,13 +36,27 @@ import org.schabi.newpipe.playlist.SinglePlayQueue; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.concurrent.atomic.AtomicBoolean; | ||||
|  | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import io.reactivex.disposables.Disposables; | ||||
|  | ||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||
|  | ||||
| public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|  | ||||
|     private CompositeDisposable disposables; | ||||
|     private Subscription bookmarkReactor; | ||||
|     private AtomicBoolean isBookmarkButtonReady; | ||||
|  | ||||
|     private RemotePlaylistManager remotePlaylistManager; | ||||
|     private PlaylistRemoteEntity playlistEntity; | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Views | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -54,6 +73,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|     private View headerPopupButton; | ||||
|     private View headerBackgroundButton; | ||||
|  | ||||
|     private MenuItem playlistBookmarkButton; | ||||
|  | ||||
|     public static PlaylistFragment getInstance(int serviceId, String url, String name) { | ||||
|         PlaylistFragment instance = new PlaylistFragment(); | ||||
|         instance.setInitialData(serviceId, url, name); | ||||
| @@ -65,7 +86,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         disposables = new CompositeDisposable(); | ||||
|         isBookmarkButtonReady = new AtomicBoolean(false); | ||||
|         remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext())); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, | ||||
|                              @Nullable Bundle savedInstanceState) { | ||||
|         return inflater.inflate(R.layout.fragment_playlist, container, false); | ||||
|     } | ||||
|  | ||||
| @@ -86,6 +116,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|         headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); | ||||
|         headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); | ||||
|  | ||||
|  | ||||
|         return headerRootLayout; | ||||
|     } | ||||
|  | ||||
| @@ -110,29 +141,26 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|                 context.getResources().getString(R.string.start_here_on_popup), | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(DialogInterface dialogInterface, int i) { | ||||
|                 final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); | ||||
|                 switch (i) { | ||||
|                     case 0: | ||||
|                         NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); | ||||
|                         break; | ||||
|                     case 1: | ||||
|                         NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); | ||||
|                         break; | ||||
|                     case 2: | ||||
|                         NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); | ||||
|                         break; | ||||
|                     case 3: | ||||
|                         NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); | ||||
|                         break; | ||||
|                     case 4: | ||||
|                         NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); | ||||
|                         break; | ||||
|                     default: | ||||
|                         break; | ||||
|                 } | ||||
|         final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { | ||||
|             final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); | ||||
|             switch (i) { | ||||
|                 case 0: | ||||
|                     NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); | ||||
|                     break; | ||||
|                 case 1: | ||||
|                     NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); | ||||
|                     break; | ||||
|                 case 2: | ||||
|                     NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); | ||||
|                     break; | ||||
|                 case 3: | ||||
|                     NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); | ||||
|                     break; | ||||
|                 case 4: | ||||
|                     NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @@ -141,9 +169,36 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|  | ||||
|     @Override | ||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||
|         if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); | ||||
|         if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + | ||||
|                 "], inflater = [" + inflater + "]"); | ||||
|         super.onCreateOptionsMenu(menu, inflater); | ||||
|         inflater.inflate(R.menu.menu_playlist, menu); | ||||
|  | ||||
|         playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark); | ||||
|         updateBookmarkButtons(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         if (isBookmarkButtonReady != null) isBookmarkButtonReady.set(false); | ||||
|  | ||||
|         if (disposables != null) disposables.clear(); | ||||
|         if (bookmarkReactor != null) bookmarkReactor.cancel(); | ||||
|  | ||||
|         bookmarkReactor = null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|  | ||||
|         if (disposables != null) disposables.dispose(); | ||||
|  | ||||
|         disposables = null; | ||||
|         remotePlaylistManager = null; | ||||
|         playlistEntity = null; | ||||
|         isBookmarkButtonReady = null; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -166,10 +221,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|             case R.id.menu_item_openInBrowser: | ||||
|                 openUrlInBrowser(url); | ||||
|                 break; | ||||
|             case R.id.menu_item_share: { | ||||
|             case R.id.menu_item_share: | ||||
|                 shareUrl(name, url); | ||||
|                 break; | ||||
|             } | ||||
|             case R.id.menu_item_bookmark: | ||||
|                 onBookmarkClicked(); | ||||
|                 break; | ||||
|             default: | ||||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
| @@ -201,12 +258,11 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|         if (!TextUtils.isEmpty(result.getUploaderName())) { | ||||
|             headerUploaderName.setText(result.getUploaderName()); | ||||
|             if (!TextUtils.isEmpty(result.getUploaderUrl())) { | ||||
|                 headerUploaderLayout.setOnClickListener(new View.OnClickListener() { | ||||
|                     @Override | ||||
|                     public void onClick(View v) { | ||||
|                         NavigationHelper.openChannelFragment(getFragmentManager(), result.getServiceId(), result.getUploaderUrl(), result.getUploaderName()); | ||||
|                     } | ||||
|                 }); | ||||
|                 headerUploaderLayout.setOnClickListener(v -> | ||||
|                         NavigationHelper.openChannelFragment(getFragmentManager(), | ||||
|                                 result.getServiceId(), result.getUploaderUrl(), | ||||
|                                 result.getUploaderName()) | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -219,24 +275,21 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|             showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); | ||||
|         } | ||||
|  | ||||
|         headerPlayAllButton.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View view) { | ||||
|                 NavigationHelper.playOnMainPlayer(activity, getPlayQueue()); | ||||
|             } | ||||
|         }); | ||||
|         headerPopupButton.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View view) { | ||||
|                 NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()); | ||||
|             } | ||||
|         }); | ||||
|         headerBackgroundButton.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View view) { | ||||
|                 NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()); | ||||
|             } | ||||
|         }); | ||||
|         remotePlaylistManager.getPlaylist(result) | ||||
|                 .onBackpressureLatest() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(getPlaylistBookmarkSubscriber()); | ||||
|  | ||||
|         remotePlaylistManager.onUpdate(result) | ||||
|                 .subscribeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(integer -> {/* Do nothing*/}, this::onError); | ||||
|  | ||||
|         headerPlayAllButton.setOnClickListener(view -> | ||||
|                 NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); | ||||
|         headerPopupButton.setOnClickListener(view -> | ||||
|                 NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); | ||||
|         headerBackgroundButton.setOnClickListener(view -> | ||||
|                 NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); | ||||
|     } | ||||
|  | ||||
|     private PlayQueue getPlayQueue() { | ||||
| @@ -280,9 +333,76 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() { | ||||
|         return new Subscriber<List<PlaylistRemoteEntity>>() { | ||||
|             @Override | ||||
|             public void onSubscribe(Subscription s) { | ||||
|                 if (bookmarkReactor != null) bookmarkReactor.cancel(); | ||||
|                 bookmarkReactor = s; | ||||
|                 bookmarkReactor.request(1); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onNext(List<PlaylistRemoteEntity> playlist) { | ||||
|                 playlistEntity = playlist.isEmpty() ? null : playlist.get(0); | ||||
|  | ||||
|                 updateBookmarkButtons(); | ||||
|                 isBookmarkButtonReady.set(true); | ||||
|  | ||||
|                 if (bookmarkReactor != null) bookmarkReactor.request(1); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onError(Throwable t) { | ||||
|                 PlaylistFragment.this.onError(t); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onComplete() { | ||||
|  | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void setTitle(String title) { | ||||
|         super.setTitle(title); | ||||
|         headerTitleView.setText(title); | ||||
|     } | ||||
|  | ||||
|     private void onBookmarkClicked() { | ||||
|         if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get() || | ||||
|                 remotePlaylistManager == null) | ||||
|             return; | ||||
|  | ||||
|         final Disposable action; | ||||
|  | ||||
|         if (currentInfo != null && playlistEntity == null) { | ||||
|             action = remotePlaylistManager.onBookmark(currentInfo) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe(ignored -> {/* Do nothing */}, this::onError); | ||||
|         } else if (playlistEntity != null) { | ||||
|             action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .doFinally(() -> playlistEntity = null) | ||||
|                     .subscribe(ignored -> {/* Do nothing */}, this::onError); | ||||
|         } else { | ||||
|             action = Disposables.empty(); | ||||
|         } | ||||
|  | ||||
|         disposables.add(action); | ||||
|     } | ||||
|  | ||||
|     private void updateBookmarkButtons() { | ||||
|         if (playlistBookmarkButton == null || activity == null) return; | ||||
|  | ||||
|         final int iconAttr = playlistEntity == null ? | ||||
|                 R.attr.ic_playlist_add : R.attr.ic_playlist_check; | ||||
|  | ||||
|         final int titleRes = playlistEntity == null ? | ||||
|                 R.string.bookmark_playlist : R.string.unbookmark_playlist; | ||||
|  | ||||
|         playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); | ||||
|         playlistBookmarkButton.setTitle(titleRes); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.list.search; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.DialogInterface; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| @@ -30,10 +29,8 @@ import android.view.inputmethod.InputMethodManager; | ||||
| import android.widget.EditText; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import org.schabi.newpipe.NewPipeDatabase; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.ReCaptchaActivity; | ||||
| import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; | ||||
| import org.schabi.newpipe.database.history.model.SearchHistoryEntry; | ||||
| import org.schabi.newpipe.extractor.InfoItem; | ||||
| import org.schabi.newpipe.extractor.ListExtractor; | ||||
| @@ -44,7 +41,7 @@ import org.schabi.newpipe.extractor.search.SearchEngine; | ||||
| import org.schabi.newpipe.extractor.search.SearchResult; | ||||
| import org.schabi.newpipe.fragments.BackPressable; | ||||
| import org.schabi.newpipe.fragments.list.BaseListFragment; | ||||
| import org.schabi.newpipe.history.HistoryListener; | ||||
| import org.schabi.newpipe.history.HistoryRecordManager; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.Constants; | ||||
| import org.schabi.newpipe.util.AnimationUtils; | ||||
| @@ -64,16 +61,11 @@ import java.util.concurrent.TimeUnit; | ||||
|  | ||||
| import icepick.State; | ||||
| import io.reactivex.Flowable; | ||||
| import io.reactivex.Notification; | ||||
| import io.reactivex.Observable; | ||||
| import io.reactivex.ObservableSource; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import io.reactivex.functions.BiFunction; | ||||
| import io.reactivex.functions.Consumer; | ||||
| import io.reactivex.functions.Function; | ||||
| import io.reactivex.functions.Predicate; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import io.reactivex.subjects.PublishSubject; | ||||
|  | ||||
| @@ -121,7 +113,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor | ||||
|     private CompositeDisposable disposables = new CompositeDisposable(); | ||||
|  | ||||
|     private SuggestionListAdapter suggestionListAdapter; | ||||
|     private SearchHistoryDAO searchHistoryDAO; | ||||
|     private HistoryRecordManager historyRecordManager; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Views | ||||
| @@ -166,8 +158,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor | ||||
|         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); | ||||
|         isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true); | ||||
|         suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled); | ||||
|          | ||||
|         searchHistoryDAO = NewPipeDatabase.getInstance().searchHistoryDAO(); | ||||
|  | ||||
|         historyRecordManager = new HistoryRecordManager(context); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -535,36 +527,24 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor | ||||
|     } | ||||
|  | ||||
|     private void showDeleteSuggestionDialog(final SuggestionItem item) { | ||||
|         final Disposable onDelete = historyRecordManager.deleteSearchHistory(item.query) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe( | ||||
|                         howManyDeleted -> suggestionPublisher | ||||
|                                 .onNext(searchEditText.getText().toString()), | ||||
|  | ||||
|                         throwable -> showSnackBarError(throwable, | ||||
|                                 UserAction.SOMETHING_ELSE, "none", | ||||
|                                 "Deleting item failed", R.string.general_error) | ||||
|                 ); | ||||
|  | ||||
|         new AlertDialog.Builder(activity) | ||||
|                 .setTitle(item.query) | ||||
|                 .setMessage(R.string.delete_item_search_history) | ||||
|                 .setCancelable(true) | ||||
|                 .setNegativeButton(R.string.cancel, null) | ||||
|                 .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { | ||||
|                     @Override | ||||
|                     public void onClick(DialogInterface dialog, int which) { | ||||
|                         disposables.add(Observable | ||||
|                                 .fromCallable(new Callable<Integer>() { | ||||
|                                     @Override | ||||
|                                     public Integer call() throws Exception { | ||||
|                                         return searchHistoryDAO.deleteAllWhereQuery(item.query); | ||||
|                                     } | ||||
|                                 }) | ||||
|                                 .subscribeOn(Schedulers.io()) | ||||
|                                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                                 .subscribe(new Consumer<Integer>() { | ||||
|                                     @Override | ||||
|                                     public void accept(Integer howManyDeleted) throws Exception { | ||||
|                                         suggestionPublisher.onNext(searchEditText.getText().toString()); | ||||
|                                     } | ||||
|                                 }, new Consumer<Throwable>() { | ||||
|                                     @Override | ||||
|                                     public void accept(Throwable throwable) throws Exception { | ||||
|                                         showSnackBarError(throwable, UserAction.SOMETHING_ELSE, "none", "Deleting item failed", R.string.general_error); | ||||
|                                     } | ||||
|                                 })); | ||||
|                     } | ||||
|                 }).show(); | ||||
|                 .setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete)) | ||||
|                 .show(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -589,83 +569,67 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor | ||||
|         final Observable<String> observable = suggestionPublisher | ||||
|                 .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) | ||||
|                 .startWith(searchQuery != null ? searchQuery : "") | ||||
|                 .filter(new Predicate<String>() { | ||||
|                     @Override | ||||
|                     public boolean test(@io.reactivex.annotations.NonNull String query) throws Exception { | ||||
|                         return isSuggestionsEnabled; | ||||
|                     } | ||||
|                 }); | ||||
|                 .filter(query -> isSuggestionsEnabled); | ||||
|  | ||||
|         suggestionDisposable = observable | ||||
|                 .switchMap(new Function<String, ObservableSource<Notification<List<SuggestionItem>>>>() { | ||||
|                     @Override | ||||
|                     public ObservableSource<Notification<List<SuggestionItem>>> apply(@io.reactivex.annotations.NonNull final String query) throws Exception { | ||||
|                         final Flowable<List<SearchHistoryEntry>> flowable = query.length() > 0 | ||||
|                                 ? searchHistoryDAO.getSimilarEntries(query, 3) | ||||
|                                 : searchHistoryDAO.getUniqueEntries(25); | ||||
|                         final Observable<List<SuggestionItem>> local = flowable.toObservable() | ||||
|                                 .map(new Function<List<SearchHistoryEntry>, List<SuggestionItem>>() { | ||||
|                                     @Override | ||||
|                                     public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<SearchHistoryEntry> searchHistoryEntries) throws Exception { | ||||
|                                         List<SuggestionItem> result = new ArrayList<>(); | ||||
|                                         for (SearchHistoryEntry entry : searchHistoryEntries) | ||||
|                                             result.add(new SuggestionItem(true, entry.getSearch())); | ||||
|                                         return result; | ||||
|                                     } | ||||
|                                 }); | ||||
|                 .switchMap(query -> { | ||||
|                     final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager | ||||
|                             .getRelatedSearches(query, 3, 25); | ||||
|                     final Observable<List<SuggestionItem>> local = flowable.toObservable() | ||||
|                             .map(searchHistoryEntries -> { | ||||
|                                 List<SuggestionItem> result = new ArrayList<>(); | ||||
|                                 for (SearchHistoryEntry entry : searchHistoryEntries) | ||||
|                                     result.add(new SuggestionItem(true, entry.getSearch())); | ||||
|                                 return result; | ||||
|                             }); | ||||
|  | ||||
|                         if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { | ||||
|                             // Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION | ||||
|                             return local.materialize(); | ||||
|                     if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { | ||||
|                         // Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION | ||||
|                         return local.materialize(); | ||||
|                     } | ||||
|  | ||||
|                     final Observable<List<SuggestionItem>> network = ExtractorHelper | ||||
|                             .suggestionsFor(serviceId, query, contentCountry) | ||||
|                             .toObservable() | ||||
|                             .map(strings -> { | ||||
|                                 List<SuggestionItem> result = new ArrayList<>(); | ||||
|                                 for (String entry : strings) { | ||||
|                                     result.add(new SuggestionItem(false, entry)); | ||||
|                                 } | ||||
|                                 return result; | ||||
|                             }); | ||||
|  | ||||
|                     return Observable.zip(local, network, (localResult, networkResult) -> { | ||||
|                         List<SuggestionItem> result = new ArrayList<>(); | ||||
|                         if (localResult.size() > 0) result.addAll(localResult); | ||||
|  | ||||
|                         // Remove duplicates | ||||
|                         final Iterator<SuggestionItem> iterator = networkResult.iterator(); | ||||
|                         while (iterator.hasNext() && localResult.size() > 0) { | ||||
|                             final SuggestionItem next = iterator.next(); | ||||
|                             for (SuggestionItem item : localResult) { | ||||
|                                 if (item.query.equals(next.query)) { | ||||
|                                     iterator.remove(); | ||||
|                                     break; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         final Observable<List<SuggestionItem>> network = ExtractorHelper.suggestionsFor(serviceId, query, contentCountry).toObservable() | ||||
|                                 .map(new Function<List<String>, List<SuggestionItem>>() { | ||||
|                                     @Override | ||||
|                                     public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<String> strings) throws Exception { | ||||
|                                         List<SuggestionItem> result = new ArrayList<>(); | ||||
|                                         for (String entry : strings) result.add(new SuggestionItem(false, entry)); | ||||
|                                         return result; | ||||
|                                     } | ||||
|                                 }); | ||||
|  | ||||
|                         return Observable.zip(local, network, new BiFunction<List<SuggestionItem>, List<SuggestionItem>, List<SuggestionItem>>() { | ||||
|                             @Override | ||||
|                             public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<SuggestionItem> localResult, @io.reactivex.annotations.NonNull List<SuggestionItem> networkResult) throws Exception { | ||||
|                                 List<SuggestionItem> result = new ArrayList<>(); | ||||
|                                 if (localResult.size() > 0) result.addAll(localResult); | ||||
|  | ||||
|                                 // Remove duplicates | ||||
|                                 final Iterator<SuggestionItem> iterator = networkResult.iterator(); | ||||
|                                 while (iterator.hasNext() && localResult.size() > 0) { | ||||
|                                     final SuggestionItem next = iterator.next(); | ||||
|                                     for (SuggestionItem item : localResult) { | ||||
|                                         if (item.query.equals(next.query)) { | ||||
|                                             iterator.remove(); | ||||
|                                             break; | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|  | ||||
|                                 if (networkResult.size() > 0) result.addAll(networkResult); | ||||
|                                 return result; | ||||
|                             } | ||||
|                         }).materialize(); | ||||
|                     } | ||||
|                         if (networkResult.size() > 0) result.addAll(networkResult); | ||||
|                         return result; | ||||
|                     }).materialize(); | ||||
|                 }) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(new Consumer<Notification<List<SuggestionItem>>>() { | ||||
|                     @Override | ||||
|                     public void accept(@io.reactivex.annotations.NonNull Notification<List<SuggestionItem>> listNotification) throws Exception { | ||||
|                         if (listNotification.isOnNext()) { | ||||
|                             handleSuggestions(listNotification.getValue()); | ||||
|                         } else if (listNotification.isOnError()) { | ||||
|                             Throwable error = listNotification.getError(); | ||||
|                             if (!ExtractorHelper.hasAssignableCauseThrowable(error, | ||||
|                                     IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class)) { | ||||
|                                 onSuggestionError(error); | ||||
|                             } | ||||
|                 .subscribe(listNotification -> { | ||||
|                     if (listNotification.isOnNext()) { | ||||
|                         handleSuggestions(listNotification.getValue()); | ||||
|                     } else if (listNotification.isOnError()) { | ||||
|                         Throwable error = listNotification.getError(); | ||||
|                         if (!ExtractorHelper.hasAssignableCauseThrowable(error, | ||||
|                                 IOException.class, SocketException.class, | ||||
|                                 InterruptedException.class, InterruptedIOException.class)) { | ||||
|                             onSuggestionError(error); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
| @@ -718,11 +682,14 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor | ||||
|         hideSuggestionsPanel(); | ||||
|         hideKeyboardSearch(); | ||||
|  | ||||
|         if (activity instanceof HistoryListener) { | ||||
|             ((HistoryListener) activity).onSearch(serviceId, query); | ||||
|             suggestionPublisher.onNext(query); | ||||
|         } | ||||
|  | ||||
|         historyRecordManager.onSearched(serviceId, query) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe( | ||||
|                         ignored -> {}, | ||||
|                         error -> showSnackBarError(error, UserAction.SEARCHED, | ||||
|                                 NewPipe.getNameOfService(serviceId), query, 0) | ||||
|                 ); | ||||
|         suggestionPublisher.onNext(query); | ||||
|         startLoading(false); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,13 @@ | ||||
| package org.schabi.newpipe.fragments.local; | ||||
|  | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.view.View; | ||||
|  | ||||
| public class HeaderFooterHolder extends RecyclerView.ViewHolder { | ||||
|     public View view; | ||||
|  | ||||
|     public HeaderFooterHolder(View v) { | ||||
|         super(v); | ||||
|         view = v; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,62 @@ | ||||
| package org.schabi.newpipe.fragments.local; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.graphics.Bitmap; | ||||
| import android.widget.ImageView; | ||||
|  | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
| import com.nostra13.universalimageloader.core.ImageLoader; | ||||
| import com.nostra13.universalimageloader.core.process.BitmapProcessor; | ||||
|  | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
|  | ||||
| /* | ||||
|  * Created by Christian Schabesberger on 26.09.16. | ||||
|  * <p> | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * InfoItemBuilder.java is part of NewPipe. | ||||
|  * <p> | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * <p> | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * <p> | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class LocalItemBuilder { | ||||
|     private static final String TAG = LocalItemBuilder.class.toString(); | ||||
|  | ||||
|     private final Context context; | ||||
|     private ImageLoader imageLoader = ImageLoader.getInstance(); | ||||
|  | ||||
|     private OnClickGesture<LocalItem> onSelectedListener; | ||||
|  | ||||
|     public LocalItemBuilder(Context context) { | ||||
|         this.context = context; | ||||
|     } | ||||
|  | ||||
|     public Context getContext() { | ||||
|         return context; | ||||
|     } | ||||
|  | ||||
|     public void displayImage(final String url, final ImageView view, | ||||
|                              final DisplayImageOptions options) { | ||||
|         imageLoader.displayImage(url, view, options); | ||||
|     } | ||||
|  | ||||
|     public OnClickGesture<LocalItem> getOnItemSelectedListener() { | ||||
|         return onSelectedListener; | ||||
|     } | ||||
|  | ||||
|     public void setOnItemSelectedListener(OnClickGesture<LocalItem> listener) { | ||||
|         this.onSelectedListener = listener; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,247 @@ | ||||
| package org.schabi.newpipe.fragments.local; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.util.Log; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
|  | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.fragments.local.holder.LocalItemHolder; | ||||
| import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder; | ||||
| import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder; | ||||
| import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder; | ||||
| import org.schabi.newpipe.fragments.local.holder.RemotePlaylistItemHolder; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
|  | ||||
| import java.text.DateFormat; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| /* | ||||
|  * Created by Christian Schabesberger on 01.08.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * InfoListAdapter.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { | ||||
|      | ||||
|     private static final String TAG = LocalItemListAdapter.class.getSimpleName(); | ||||
|     private static final boolean DEBUG = false; | ||||
|  | ||||
|     private static final int HEADER_TYPE = 0; | ||||
|     private static final int FOOTER_TYPE = 1; | ||||
|  | ||||
|     private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000; | ||||
|     private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001; | ||||
|     private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000; | ||||
|     private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x2001; | ||||
|  | ||||
|     private final LocalItemBuilder localItemBuilder; | ||||
|     private final ArrayList<LocalItem> localItems; | ||||
|     private final DateFormat dateFormat; | ||||
|  | ||||
|     private boolean showFooter = false; | ||||
|     private View header = null; | ||||
|     private View footer = null; | ||||
|  | ||||
|     public LocalItemListAdapter(Activity activity) { | ||||
|         localItemBuilder = new LocalItemBuilder(activity); | ||||
|         localItems = new ArrayList<>(); | ||||
|         dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, | ||||
|                 Localization.getPreferredLocale(activity)); | ||||
|     } | ||||
|  | ||||
|     public void setSelectedListener(OnClickGesture<LocalItem> listener) { | ||||
|         localItemBuilder.setOnItemSelectedListener(listener); | ||||
|     } | ||||
|  | ||||
|     public void unsetSelectedListener() { | ||||
|         localItemBuilder.setOnItemSelectedListener(null); | ||||
|     } | ||||
|  | ||||
|     public void addItems(List<? extends LocalItem> data) { | ||||
|         if (data != null) { | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "addItems() before > localItems.size() = " + | ||||
|                         localItems.size() + ", data.size() = " + data.size()); | ||||
|             } | ||||
|  | ||||
|             int offsetStart = sizeConsideringHeader(); | ||||
|             localItems.addAll(data); | ||||
|  | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + | ||||
|                         ", localItems.size() = " + localItems.size() + | ||||
|                         ", header = " + header + ", footer = " + footer + | ||||
|                         ", showFooter = " + showFooter); | ||||
|             } | ||||
|  | ||||
|             notifyItemRangeInserted(offsetStart, data.size()); | ||||
|  | ||||
|             if (footer != null && showFooter) { | ||||
|                 int footerNow = sizeConsideringHeader(); | ||||
|                 notifyItemMoved(offsetStart, footerNow); | ||||
|  | ||||
|                 if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart + | ||||
|                         " to " + footerNow); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void removeItem(final LocalItem data) { | ||||
|         final int index = localItems.indexOf(data); | ||||
|  | ||||
|         localItems.remove(index); | ||||
|         notifyItemRemoved(index + (header != null ? 1 : 0)); | ||||
|     } | ||||
|  | ||||
|     public boolean swapItems(int fromAdapterPosition, int toAdapterPosition) { | ||||
|         final int actualFrom = adapterOffsetWithoutHeader(fromAdapterPosition); | ||||
|         final int actualTo = adapterOffsetWithoutHeader(toAdapterPosition); | ||||
|  | ||||
|         if (actualFrom < 0 || actualTo < 0) return false; | ||||
|         if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false; | ||||
|  | ||||
|         localItems.add(actualTo, localItems.remove(actualFrom)); | ||||
|         notifyItemMoved(fromAdapterPosition, toAdapterPosition); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public void clearStreamItemList() { | ||||
|         if (localItems.isEmpty()) { | ||||
|             return; | ||||
|         } | ||||
|         localItems.clear(); | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public void setHeader(View header) { | ||||
|         boolean changed = header != this.header; | ||||
|         this.header = header; | ||||
|         if (changed) notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public void setFooter(View view) { | ||||
|         this.footer = view; | ||||
|     } | ||||
|  | ||||
|     public void showFooter(boolean show) { | ||||
|         if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]"); | ||||
|         if (show == showFooter) return; | ||||
|  | ||||
|         showFooter = show; | ||||
|         if (show) notifyItemInserted(sizeConsideringHeader()); | ||||
|         else notifyItemRemoved(sizeConsideringHeader()); | ||||
|     } | ||||
|  | ||||
|     private int adapterOffsetWithoutHeader(final int offset) { | ||||
|         return offset - (header != null ? 1 : 0); | ||||
|     } | ||||
|  | ||||
|     private int sizeConsideringHeader() { | ||||
|         return localItems.size() + (header != null ? 1 : 0); | ||||
|     } | ||||
|  | ||||
|     public ArrayList<LocalItem> getItemsList() { | ||||
|         return localItems; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int getItemCount() { | ||||
|         int count = localItems.size(); | ||||
|         if (header != null) count++; | ||||
|         if (footer != null && showFooter) count++; | ||||
|  | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "getItemCount() called, count = " + count + | ||||
|                     ", localItems.size() = " + localItems.size() + | ||||
|                     ", header = " + header + ", footer = " + footer + | ||||
|                     ", showFooter = " + showFooter); | ||||
|         } | ||||
|         return count; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int getItemViewType(int position) { | ||||
|         if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]"); | ||||
|  | ||||
|         if (header != null && position == 0) { | ||||
|             return HEADER_TYPE; | ||||
|         } else if (header != null) { | ||||
|             position--; | ||||
|         } | ||||
|         if (footer != null && position == localItems.size() && showFooter) { | ||||
|             return FOOTER_TYPE; | ||||
|         } | ||||
|         final LocalItem item = localItems.get(position); | ||||
|  | ||||
|         switch (item.getLocalItemType()) { | ||||
|             case PLAYLIST_LOCAL_ITEM: return LOCAL_PLAYLIST_HOLDER_TYPE; | ||||
|             case PLAYLIST_REMOTE_ITEM: return REMOTE_PLAYLIST_HOLDER_TYPE; | ||||
|  | ||||
|             case PLAYLIST_STREAM_ITEM: return STREAM_PLAYLIST_HOLDER_TYPE; | ||||
|             case STATISTIC_STREAM_ITEM: return STREAM_STATISTICS_HOLDER_TYPE; | ||||
|             default: | ||||
|                 Log.e(TAG, "No holder type has been considered for item: [" + | ||||
|                         item.getLocalItemType() + "]"); | ||||
|                 return -1; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) { | ||||
|         if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + | ||||
|                 parent + "], type = [" + type + "]"); | ||||
|         switch (type) { | ||||
|             case HEADER_TYPE: | ||||
|                 return new HeaderFooterHolder(header); | ||||
|             case FOOTER_TYPE: | ||||
|                 return new HeaderFooterHolder(footer); | ||||
|             case LOCAL_PLAYLIST_HOLDER_TYPE: | ||||
|                 return new LocalPlaylistItemHolder(localItemBuilder, parent); | ||||
|             case REMOTE_PLAYLIST_HOLDER_TYPE: | ||||
|                 return new RemotePlaylistItemHolder(localItemBuilder, parent); | ||||
|             case STREAM_PLAYLIST_HOLDER_TYPE: | ||||
|                 return new LocalPlaylistStreamItemHolder(localItemBuilder, parent); | ||||
|             case STREAM_STATISTICS_HOLDER_TYPE: | ||||
|                 return new LocalStatisticStreamItemHolder(localItemBuilder, parent); | ||||
|             default: | ||||
|                 Log.e(TAG, "No view type has been considered for holder: [" + type + "]"); | ||||
|                 return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { | ||||
|         if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + | ||||
|                 holder.getClass().getSimpleName() + "], position = [" + position + "]"); | ||||
|  | ||||
|         if (holder instanceof LocalItemHolder) { | ||||
|             // If header isn't null, offset the items by -1 | ||||
|             if (header != null) position--; | ||||
|  | ||||
|             ((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat); | ||||
|         } else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) { | ||||
|             ((HeaderFooterHolder) holder).view = header; | ||||
|         } else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader() | ||||
|                 && footer != null && showFooter) { | ||||
|             ((HeaderFooterHolder) holder).view = footer; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,120 @@ | ||||
| package org.schabi.newpipe.fragments.local; | ||||
|  | ||||
| import android.support.annotation.Nullable; | ||||
|  | ||||
| import org.schabi.newpipe.database.AppDatabase; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; | ||||
| import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; | ||||
| import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistEntity; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; | ||||
| import org.schabi.newpipe.database.stream.dao.StreamDAO; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.Flowable; | ||||
| import io.reactivex.Maybe; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
|  | ||||
| public class LocalPlaylistManager { | ||||
|  | ||||
|     private final AppDatabase database; | ||||
|     private final StreamDAO streamTable; | ||||
|     private final PlaylistDAO playlistTable; | ||||
|     private final PlaylistStreamDAO playlistStreamTable; | ||||
|  | ||||
|     public LocalPlaylistManager(final AppDatabase db) { | ||||
|         database = db; | ||||
|         streamTable = db.streamDAO(); | ||||
|         playlistTable = db.playlistDAO(); | ||||
|         playlistStreamTable = db.playlistStreamDAO(); | ||||
|     } | ||||
|  | ||||
|     public Maybe<List<Long>> createPlaylist(final String name, final List<StreamEntity> streams) { | ||||
|         // Disallow creation of empty playlists | ||||
|         if (streams.isEmpty()) return Maybe.empty(); | ||||
|         final StreamEntity defaultStream = streams.get(0); | ||||
|         final PlaylistEntity newPlaylist = | ||||
|                 new PlaylistEntity(name, defaultStream.getThumbnailUrl()); | ||||
|  | ||||
|         return Maybe.fromCallable(() -> database.runInTransaction(() -> | ||||
|                 upsertStreams(playlistTable.insert(newPlaylist), streams, 0)) | ||||
|         ).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Maybe<List<Long>> appendToPlaylist(final long playlistId, | ||||
|                                               final List<StreamEntity> streams) { | ||||
|         return playlistStreamTable.getMaximumIndexOf(playlistId) | ||||
|                 .firstElement() | ||||
|                 .map(maxJoinIndex -> database.runInTransaction(() -> | ||||
|                         upsertStreams(playlistId, streams, maxJoinIndex + 1)) | ||||
|                 ).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     private List<Long> upsertStreams(final long playlistId, | ||||
|                                      final List<StreamEntity> streams, | ||||
|                                      final int indexOffset) { | ||||
|  | ||||
|         List<PlaylistStreamEntity> joinEntities = new ArrayList<>(streams.size()); | ||||
|         final List<Long> streamIds = streamTable.upsertAll(streams); | ||||
|         for (int index = 0; index < streamIds.size(); index++) { | ||||
|             joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(index), | ||||
|                     index + indexOffset)); | ||||
|         } | ||||
|         return playlistStreamTable.insertAll(joinEntities); | ||||
|     } | ||||
|  | ||||
|     public Completable updateJoin(final long playlistId, final List<Long> streamIds) { | ||||
|         List<PlaylistStreamEntity> joinEntities = new ArrayList<>(streamIds.size()); | ||||
|         for (int i = 0; i < streamIds.size(); i++) { | ||||
|             joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(i), i)); | ||||
|         } | ||||
|  | ||||
|         return Completable.fromRunnable(() -> database.runInTransaction(() -> { | ||||
|             playlistStreamTable.deleteBatch(playlistId); | ||||
|             playlistStreamTable.insertAll(joinEntities); | ||||
|         })).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Flowable<List<PlaylistMetadataEntry>> getPlaylists() { | ||||
|         return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) { | ||||
|         return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Single<Integer> deletePlaylist(final long playlistId) { | ||||
|         return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId)) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Maybe<Integer> renamePlaylist(final long playlistId, final String name) { | ||||
|         return modifyPlaylist(playlistId, name, null); | ||||
|     } | ||||
|  | ||||
|     public Maybe<Integer> changePlaylistThumbnail(final long playlistId, | ||||
|                                                   final String thumbnailUrl) { | ||||
|         return modifyPlaylist(playlistId, null, thumbnailUrl); | ||||
|     } | ||||
|  | ||||
|     private Maybe<Integer> modifyPlaylist(final long playlistId, | ||||
|                                           @Nullable final String name, | ||||
|                                           @Nullable final String thumbnailUrl) { | ||||
|         return playlistTable.getPlaylist(playlistId) | ||||
|                 .firstElement() | ||||
|                 .filter(playlistEntities -> !playlistEntities.isEmpty()) | ||||
|                 .map(playlistEntities -> { | ||||
|                     PlaylistEntity playlist = playlistEntities.get(0); | ||||
|                     if (name != null) playlist.setName(name); | ||||
|                     if (thumbnailUrl != null) playlist.setThumbnailUrl(thumbnailUrl); | ||||
|                     return playlistTable.update(playlist); | ||||
|                 }).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,49 @@ | ||||
| package org.schabi.newpipe.fragments.local; | ||||
|  | ||||
| import org.schabi.newpipe.database.AppDatabase; | ||||
| import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.Flowable; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
|  | ||||
| public class RemotePlaylistManager { | ||||
|  | ||||
|     private final AppDatabase database; | ||||
|     private final PlaylistRemoteDAO playlistRemoteTable; | ||||
|  | ||||
|     public RemotePlaylistManager(final AppDatabase db) { | ||||
|         database = db; | ||||
|         playlistRemoteTable = db.playlistRemoteDAO(); | ||||
|     } | ||||
|  | ||||
|     public Flowable<List<PlaylistRemoteEntity>> getPlaylists() { | ||||
|         return playlistRemoteTable.getAll().subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) { | ||||
|         return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl()) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Single<Integer> deletePlaylist(final long playlistId) { | ||||
|         return Single.fromCallable(() -> playlistRemoteTable.deletePlaylist(playlistId)) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Single<Long> onBookmark(final PlaylistInfo playlistInfo) { | ||||
|         return Single.fromCallable(() -> { | ||||
|             final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); | ||||
|             return playlistRemoteTable.upsert(playlist); | ||||
|         }).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Single<Integer> onUpdate(final PlaylistInfo playlistInfo) { | ||||
|         return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo))) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,175 @@ | ||||
| package org.schabi.newpipe.fragments.local.bookmark; | ||||
|  | ||||
| import android.os.Bundle; | ||||
| import android.support.v4.app.Fragment; | ||||
| import android.support.v7.app.ActionBar; | ||||
| import android.support.v7.widget.LinearLayoutManager; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.util.Log; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.View; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.fragments.BaseStateFragment; | ||||
| import org.schabi.newpipe.fragments.list.ListViewContract; | ||||
| import org.schabi.newpipe.fragments.local.LocalItemListAdapter; | ||||
|  | ||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||
|  | ||||
| /** | ||||
|  * This fragment is design to be used with persistent data such as | ||||
|  * {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained | ||||
|  * in the list adapter to avoid extra writes when the it exits or re-enters its lifecycle. | ||||
|  * | ||||
|  * This fragment destroys its adapter and views when {@link Fragment#onDestroyView()} is | ||||
|  * called and is memory efficient when in backstack. | ||||
|  * */ | ||||
| public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I> | ||||
|         implements ListViewContract<I, N> { | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Views | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     protected View headerRootView; | ||||
|     protected View footerRootView; | ||||
|  | ||||
|     protected LocalItemListAdapter itemListAdapter; | ||||
|     protected RecyclerView itemsList; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Lifecycle - Creation | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setHasOptionsMenu(true); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Lifecycle - View | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     protected View getListHeader() { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     protected View getListFooter() { | ||||
|         return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false); | ||||
|     } | ||||
|  | ||||
|     protected RecyclerView.LayoutManager getListLayoutManager() { | ||||
|         return new LinearLayoutManager(activity); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void initViews(View rootView, Bundle savedInstanceState) { | ||||
|         super.initViews(rootView, savedInstanceState); | ||||
|  | ||||
|         itemsList = rootView.findViewById(R.id.items_list); | ||||
|         itemsList.setLayoutManager(getListLayoutManager()); | ||||
|  | ||||
|         itemListAdapter = new LocalItemListAdapter(activity); | ||||
|         itemListAdapter.setHeader(headerRootView = getListHeader()); | ||||
|         itemListAdapter.setFooter(footerRootView = getListFooter()); | ||||
|  | ||||
|         itemsList.setAdapter(itemListAdapter); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void initListeners() { | ||||
|         super.initListeners(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Lifecycle - Menu | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||
|         super.onCreateOptionsMenu(menu, inflater); | ||||
|         if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + | ||||
|                 "], inflater = [" + inflater + "]"); | ||||
|  | ||||
|         final ActionBar supportActionBar = activity.getSupportActionBar(); | ||||
|         if (supportActionBar == null) return; | ||||
|  | ||||
|         supportActionBar.setDisplayShowTitleEnabled(true); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Lifecycle - Destruction | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         itemsList = null; | ||||
|         itemListAdapter = null; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Contract | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void startLoading(boolean forceLoad) { | ||||
|         super.startLoading(forceLoad); | ||||
|         resetFragment(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void showLoading() { | ||||
|         super.showLoading(); | ||||
|         if (itemsList != null) animateView(itemsList, false, 200); | ||||
|         if (headerRootView != null) animateView(headerRootView, false, 200); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void hideLoading() { | ||||
|         super.hideLoading(); | ||||
|         if (itemsList != null) animateView(itemsList, true, 200); | ||||
|         if (headerRootView != null) animateView(headerRootView, true, 200); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void showError(String message, boolean showRetryButton) { | ||||
|         super.showError(message, showRetryButton); | ||||
|         showListFooter(false); | ||||
|  | ||||
|         if (itemsList != null) animateView(itemsList, false, 200); | ||||
|         if (headerRootView != null) animateView(headerRootView, false, 200); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void showEmptyState() { | ||||
|         super.showEmptyState(); | ||||
|         showListFooter(false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void showListFooter(final boolean show) { | ||||
|         itemsList.post(() -> itemListAdapter.showFooter(show)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void handleNextItems(N result) { | ||||
|         isLoading.set(false); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Error handling | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     protected void resetFragment() { | ||||
|         if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected boolean onError(Throwable exception) { | ||||
|         resetFragment(); | ||||
|         return super.onError(exception); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,309 @@ | ||||
| package org.schabi.newpipe.fragments.local.bookmark; | ||||
|  | ||||
| import android.app.AlertDialog; | ||||
| import android.os.Bundle; | ||||
| import android.os.Parcelable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.FragmentManager; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
|  | ||||
| import org.reactivestreams.Subscriber; | ||||
| import org.reactivestreams.Subscription; | ||||
| import org.schabi.newpipe.NewPipeDatabase; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.AppDatabase; | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistLocalItem; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||
| import org.schabi.newpipe.fragments.local.LocalPlaylistManager; | ||||
| import org.schabi.newpipe.fragments.local.RemotePlaylistManager; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| import icepick.State; | ||||
| import io.reactivex.Flowable; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
|  | ||||
| public final class BookmarkFragment | ||||
|         extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> { | ||||
|  | ||||
|     private View lastPlayedButton; | ||||
|     private View mostPlayedButton; | ||||
|  | ||||
|     @State | ||||
|     protected Parcelable itemsListState; | ||||
|  | ||||
|     private Subscription databaseSubscription; | ||||
|     private CompositeDisposable disposables = new CompositeDisposable(); | ||||
|     private LocalPlaylistManager localPlaylistManager; | ||||
|     private RemotePlaylistManager remotePlaylistManager; | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment LifeCycle - Creation | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         final AppDatabase database = NewPipeDatabase.getInstance(getContext()); | ||||
|         localPlaylistManager = new LocalPlaylistManager(database); | ||||
|         remotePlaylistManager = new RemotePlaylistManager(database); | ||||
|         disposables = new CompositeDisposable(); | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, | ||||
|                              @Nullable ViewGroup container, | ||||
|                              Bundle savedInstanceState) { | ||||
|         if (activity != null && activity.getSupportActionBar() != null) { | ||||
|             activity.getSupportActionBar().setDisplayShowTitleEnabled(true); | ||||
|             activity.setTitle(R.string.tab_subscriptions); | ||||
|         } | ||||
|  | ||||
|         return inflater.inflate(R.layout.fragment_bookmarks, container, false); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @Override | ||||
|     public void setUserVisibleHint(boolean isVisibleToUser) { | ||||
|         super.setUserVisibleHint(isVisibleToUser); | ||||
|         if (isVisibleToUser) setTitle(getString(R.string.tab_bookmarks)); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment LifeCycle - Views | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     protected void initViews(View rootView, Bundle savedInstanceState) { | ||||
|         super.initViews(rootView, savedInstanceState); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected View getListHeader() { | ||||
|         final View headerRootLayout = activity.getLayoutInflater() | ||||
|                 .inflate(R.layout.bookmark_header, itemsList, false); | ||||
|         lastPlayedButton = headerRootLayout.findViewById(R.id.lastPlayed); | ||||
|         mostPlayedButton = headerRootLayout.findViewById(R.id.mostPlayed); | ||||
|         return headerRootLayout; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void initListeners() { | ||||
|         super.initListeners(); | ||||
|  | ||||
|         itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() { | ||||
|             @Override | ||||
|             public void selected(LocalItem selectedItem) { | ||||
|                 // Requires the parent fragment to find holder for fragment replacement | ||||
|                 if (getParentFragment() == null) return; | ||||
|                 final FragmentManager fragmentManager = getParentFragment().getFragmentManager(); | ||||
|  | ||||
|                 if (selectedItem instanceof PlaylistMetadataEntry) { | ||||
|                     final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); | ||||
|                     NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid, | ||||
|                             entry.name); | ||||
|  | ||||
|                 } else if (selectedItem instanceof PlaylistRemoteEntity) { | ||||
|                     final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); | ||||
|                     NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(), | ||||
|                             entry.getUrl(), entry.getName()); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void held(LocalItem selectedItem) { | ||||
|                 if (selectedItem instanceof PlaylistMetadataEntry) { | ||||
|                     showLocalDeleteDialog((PlaylistMetadataEntry) selectedItem); | ||||
|  | ||||
|                 } else if (selectedItem instanceof PlaylistRemoteEntity) { | ||||
|                     showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         lastPlayedButton.setOnClickListener(view -> { | ||||
|             if (getParentFragment() != null) { | ||||
|                 NavigationHelper.openLastPlayedFragment(getParentFragment().getFragmentManager()); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         mostPlayedButton.setOnClickListener(view -> { | ||||
|             if (getParentFragment() != null) { | ||||
|                 NavigationHelper.openMostPlayedFragment(getParentFragment().getFragmentManager()); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment LifeCycle - Loading | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void startLoading(boolean forceLoad) { | ||||
|         super.startLoading(forceLoad); | ||||
|  | ||||
|         Flowable.combineLatest( | ||||
|                 localPlaylistManager.getPlaylists(), | ||||
|                 remotePlaylistManager.getPlaylists(), | ||||
|                 BookmarkFragment::merge | ||||
|         ).onBackpressureLatest() | ||||
|          .observeOn(AndroidSchedulers.mainThread()) | ||||
|          .subscribe(getPlaylistsSubscriber()); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment LifeCycle - Destruction | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void onPause() { | ||||
|         super.onPause(); | ||||
|         itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         if (mostPlayedButton != null) mostPlayedButton.setOnClickListener(null); | ||||
|         if (lastPlayedButton != null) lastPlayedButton.setOnClickListener(null); | ||||
|  | ||||
|         if (disposables != null) disposables.clear(); | ||||
|         if (databaseSubscription != null) databaseSubscription.cancel(); | ||||
|  | ||||
|         databaseSubscription = null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         if (disposables != null) disposables.dispose(); | ||||
|  | ||||
|         disposables = null; | ||||
|         localPlaylistManager = null; | ||||
|         remotePlaylistManager = null; | ||||
|         itemsListState = null; | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Subscriptions Loader | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() { | ||||
|         return new Subscriber<List<PlaylistLocalItem>>() { | ||||
|             @Override | ||||
|             public void onSubscribe(Subscription s) { | ||||
|                 showLoading(); | ||||
|                 if (databaseSubscription != null) databaseSubscription.cancel(); | ||||
|                 databaseSubscription = s; | ||||
|                 databaseSubscription.request(1); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onNext(List<PlaylistLocalItem> subscriptions) { | ||||
|                 handleResult(subscriptions); | ||||
|                 if (databaseSubscription != null) databaseSubscription.request(1); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onError(Throwable exception) { | ||||
|                 BookmarkFragment.this.onError(exception); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onComplete() { | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void handleResult(@NonNull List<PlaylistLocalItem> result) { | ||||
|         super.handleResult(result); | ||||
|  | ||||
|         itemListAdapter.clearStreamItemList(); | ||||
|  | ||||
|         if (result.isEmpty()) { | ||||
|             showEmptyState(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         itemListAdapter.addItems(result); | ||||
|         if (itemsListState != null) { | ||||
|             itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); | ||||
|             itemsListState = null; | ||||
|         } | ||||
|         hideLoading(); | ||||
|     } | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment Error Handling | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     protected boolean onError(Throwable exception) { | ||||
|         if (super.onError(exception)) return true; | ||||
|  | ||||
|         onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, | ||||
|                 "none", "Bookmark", R.string.general_error); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void resetFragment() { | ||||
|         super.resetFragment(); | ||||
|         if (disposables != null) disposables.clear(); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private void showLocalDeleteDialog(final PlaylistMetadataEntry item) { | ||||
|         showDeleteDialog(item.name, localPlaylistManager.deletePlaylist(item.uid)); | ||||
|     } | ||||
|  | ||||
|     private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) { | ||||
|         showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid())); | ||||
|     } | ||||
|  | ||||
|     private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) { | ||||
|         if (activity == null || disposables == null) return; | ||||
|  | ||||
|         new AlertDialog.Builder(activity) | ||||
|                 .setTitle(name) | ||||
|                 .setMessage(R.string.delete_playlist_prompt) | ||||
|                 .setCancelable(true) | ||||
|                 .setPositiveButton(R.string.delete, (dialog, i) -> | ||||
|                         disposables.add(deleteReactor | ||||
|                                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                                 .subscribe(ignored -> {/*Do nothing on success*/}, this::onError)) | ||||
|                 ) | ||||
|                 .setNegativeButton(R.string.cancel, null) | ||||
|                 .show(); | ||||
|     } | ||||
|  | ||||
|     private static List<PlaylistLocalItem> merge(final List<PlaylistMetadataEntry> localPlaylists, | ||||
|                                                  final List<PlaylistRemoteEntity> remotePlaylists) { | ||||
|         List<PlaylistLocalItem> items = new ArrayList<>( | ||||
|                 localPlaylists.size() + remotePlaylists.size()); | ||||
|         items.addAll(localPlaylists); | ||||
|         items.addAll(remotePlaylists); | ||||
|  | ||||
|         Collections.sort(items, (left, right) -> | ||||
|                 left.getOrderingName().compareToIgnoreCase(right.getOrderingName())); | ||||
|  | ||||
|         return items; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,21 @@ | ||||
| package org.schabi.newpipe.fragments.local.bookmark; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.stream.StreamStatisticsEntry; | ||||
|  | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| public final class LastPlayedFragment extends StatisticsPlaylistFragment { | ||||
|     @Override | ||||
|     protected String getName() { | ||||
|         return getString(R.string.title_last_played); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected List<StreamStatisticsEntry> processResult(List<StreamStatisticsEntry> results)  { | ||||
|         Collections.sort(results, (left, right) -> | ||||
|                 right.latestAccessDate.compareTo(left.latestAccessDate)); | ||||
|         return results; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,590 @@ | ||||
| package org.schabi.newpipe.fragments.local.bookmark; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.DialogInterface; | ||||
| import android.os.Bundle; | ||||
| import android.os.Parcelable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.support.v7.widget.helper.ItemTouchHelper; | ||||
| import android.text.TextUtils; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.EditText; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.reactivestreams.Subscriber; | ||||
| import org.reactivestreams.Subscription; | ||||
| import org.schabi.newpipe.NewPipeDatabase; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.fragments.local.LocalPlaylistManager; | ||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | ||||
| import org.schabi.newpipe.playlist.PlayQueue; | ||||
| import org.schabi.newpipe.playlist.SinglePlayQueue; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import java.util.concurrent.atomic.AtomicBoolean; | ||||
|  | ||||
| import icepick.State; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import io.reactivex.disposables.Disposables; | ||||
| import io.reactivex.subjects.PublishSubject; | ||||
|  | ||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||
|  | ||||
| public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> { | ||||
|  | ||||
|     // Save the list 10 seconds after the last change occurred | ||||
|     private static final long SAVE_DEBOUNCE_MILLIS = 10000; | ||||
|     private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; | ||||
|  | ||||
|     private View headerRootLayout; | ||||
|     private TextView headerTitleView; | ||||
|     private TextView headerStreamCount; | ||||
|  | ||||
|     private View playlistControl; | ||||
|     private View headerPlayAllButton; | ||||
|     private View headerPopupButton; | ||||
|     private View headerBackgroundButton; | ||||
|  | ||||
|     @State | ||||
|     protected Long playlistId; | ||||
|     @State | ||||
|     protected String name; | ||||
|     @State | ||||
|     protected Parcelable itemsListState; | ||||
|  | ||||
|     private ItemTouchHelper itemTouchHelper; | ||||
|  | ||||
|     private LocalPlaylistManager playlistManager; | ||||
|     private Subscription databaseSubscription; | ||||
|  | ||||
|     private PublishSubject<Long> debouncedSaveSignal; | ||||
|     private CompositeDisposable disposables; | ||||
|  | ||||
|     /* Has the playlist been fully loaded from db */ | ||||
|     private AtomicBoolean isLoadingComplete; | ||||
|     /* Has the playlist been modified (e.g. items reordered or deleted) */ | ||||
|     private AtomicBoolean isModified; | ||||
|  | ||||
|     public static LocalPlaylistFragment getInstance(long playlistId, String name) { | ||||
|         LocalPlaylistFragment instance = new LocalPlaylistFragment(); | ||||
|         instance.setInitialData(playlistId, name); | ||||
|         return instance; | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment LifeCycle - Creation | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); | ||||
|         debouncedSaveSignal = PublishSubject.create(); | ||||
|  | ||||
|         disposables = new CompositeDisposable(); | ||||
|  | ||||
|         isLoadingComplete = new AtomicBoolean(); | ||||
|         isModified = new AtomicBoolean(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, | ||||
|                              @Nullable ViewGroup container, | ||||
|                              @Nullable Bundle savedInstanceState) { | ||||
|         return inflater.inflate(R.layout.fragment_playlist, container, false); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment Lifecycle - Views | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void setTitle(final String title) { | ||||
|         super.setTitle(title); | ||||
|  | ||||
|         if (headerTitleView != null) { | ||||
|             headerTitleView.setText(title); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void initViews(View rootView, Bundle savedInstanceState) { | ||||
|         super.initViews(rootView, savedInstanceState); | ||||
|         setTitle(name); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected View getListHeader() { | ||||
|         headerRootLayout = activity.getLayoutInflater().inflate(R.layout.local_playlist_header, | ||||
|                 itemsList, false); | ||||
|  | ||||
|         headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); | ||||
|         headerTitleView.setSelected(true); | ||||
|  | ||||
|         headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count); | ||||
|  | ||||
|         playlistControl = headerRootLayout.findViewById(R.id.playlist_control); | ||||
|         headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); | ||||
|         headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); | ||||
|         headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); | ||||
|  | ||||
|         return headerRootLayout; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void initListeners() { | ||||
|         super.initListeners(); | ||||
|  | ||||
|         headerTitleView.setOnClickListener(view -> createRenameDialog()); | ||||
|  | ||||
|         itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); | ||||
|         itemTouchHelper.attachToRecyclerView(itemsList); | ||||
|  | ||||
|         itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() { | ||||
|             @Override | ||||
|             public void selected(LocalItem selectedItem) { | ||||
|                 if (selectedItem instanceof PlaylistStreamEntry) { | ||||
|                     final PlaylistStreamEntry item = (PlaylistStreamEntry) selectedItem; | ||||
|                     NavigationHelper.openVideoDetailFragment(getFragmentManager(), | ||||
|                             item.serviceId, item.url, item.title); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void held(LocalItem selectedItem) { | ||||
|                 if (selectedItem instanceof PlaylistStreamEntry) { | ||||
|                     showStreamDialog((PlaylistStreamEntry) selectedItem); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void drag(LocalItem selectedItem, RecyclerView.ViewHolder viewHolder) { | ||||
|                 if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment Lifecycle - Loading | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void showLoading() { | ||||
|         super.showLoading(); | ||||
|         if (headerRootLayout != null) animateView(headerRootLayout, false, 200); | ||||
|         if (playlistControl != null) animateView(playlistControl, false, 200); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void hideLoading() { | ||||
|         super.hideLoading(); | ||||
|         if (headerRootLayout != null) animateView(headerRootLayout, true, 200); | ||||
|         if (playlistControl != null) animateView(playlistControl, true, 200); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void startLoading(boolean forceLoad) { | ||||
|         super.startLoading(forceLoad); | ||||
|  | ||||
|         if (disposables != null) disposables.clear(); | ||||
|         disposables.add(getDebouncedSaver()); | ||||
|  | ||||
|         isLoadingComplete.set(false); | ||||
|         isModified.set(false); | ||||
|  | ||||
|         playlistManager.getPlaylistStreams(playlistId) | ||||
|                 .onBackpressureLatest() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(getPlaylistObserver()); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment Lifecycle - Destruction | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void onPause() { | ||||
|         super.onPause(); | ||||
|         itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); | ||||
|  | ||||
|         // Save on exit | ||||
|         saveImmediate(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|  | ||||
|         if (itemListAdapter != null) itemListAdapter.unsetSelectedListener(); | ||||
|         if (headerBackgroundButton != null) headerBackgroundButton.setOnClickListener(null); | ||||
|         if (headerPlayAllButton != null) headerPlayAllButton.setOnClickListener(null); | ||||
|         if (headerPopupButton != null) headerPopupButton.setOnClickListener(null); | ||||
|  | ||||
|         if (databaseSubscription != null) databaseSubscription.cancel(); | ||||
|         if (disposables != null) disposables.clear(); | ||||
|  | ||||
|         databaseSubscription = null; | ||||
|         itemTouchHelper = null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         if (debouncedSaveSignal != null) debouncedSaveSignal.onComplete(); | ||||
|         if (disposables != null) disposables.dispose(); | ||||
|  | ||||
|         debouncedSaveSignal = null; | ||||
|         playlistManager = null; | ||||
|         disposables = null; | ||||
|  | ||||
|         isLoadingComplete = null; | ||||
|         isModified = null; | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Playlist Stream Loader | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private Subscriber<List<PlaylistStreamEntry>> getPlaylistObserver() { | ||||
|         return new Subscriber<List<PlaylistStreamEntry>>() { | ||||
|             @Override | ||||
|             public void onSubscribe(Subscription s) { | ||||
|                 showLoading(); | ||||
|                 isLoadingComplete.set(false); | ||||
|  | ||||
|                 if (databaseSubscription != null) databaseSubscription.cancel(); | ||||
|                 databaseSubscription = s; | ||||
|                 databaseSubscription.request(1); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onNext(List<PlaylistStreamEntry> streams) { | ||||
|                 // Skip handling the result after it has been modified | ||||
|                 if (isModified == null || !isModified.get()) { | ||||
|                     handleResult(streams); | ||||
|                     isLoadingComplete.set(true); | ||||
|                 } | ||||
|  | ||||
|                 if (databaseSubscription != null) databaseSubscription.request(1); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onError(Throwable exception) { | ||||
|                 LocalPlaylistFragment.this.onError(exception); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onComplete() {} | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void handleResult(@NonNull List<PlaylistStreamEntry> result) { | ||||
|         super.handleResult(result); | ||||
|         if (itemListAdapter == null) return; | ||||
|  | ||||
|         itemListAdapter.clearStreamItemList(); | ||||
|  | ||||
|         if (result.isEmpty()) { | ||||
|             showEmptyState(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         itemListAdapter.addItems(result); | ||||
|         if (itemsListState != null) { | ||||
|             itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); | ||||
|             itemsListState = null; | ||||
|         } | ||||
|         setVideoCount(itemListAdapter.getItemsList().size()); | ||||
|  | ||||
|         headerPlayAllButton.setOnClickListener(view -> | ||||
|                 NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); | ||||
|         headerPopupButton.setOnClickListener(view -> | ||||
|                 NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); | ||||
|         headerBackgroundButton.setOnClickListener(view -> | ||||
|                 NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); | ||||
|  | ||||
|         hideLoading(); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment Error Handling | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     protected void resetFragment() { | ||||
|         super.resetFragment(); | ||||
|         if (databaseSubscription != null) databaseSubscription.cancel(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected boolean onError(Throwable exception) { | ||||
|         if (super.onError(exception)) return true; | ||||
|  | ||||
|         onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, | ||||
|                 "none", "Local Playlist", R.string.general_error); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Playlist Metadata/Streams Manipulation | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void createRenameDialog() { | ||||
|         if (playlistId == null || name == null || getContext() == null) return; | ||||
|  | ||||
|         final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); | ||||
|         EditText nameEdit = dialogView.findViewById(R.id.playlist_name); | ||||
|         nameEdit.setText(name); | ||||
|         nameEdit.setSelection(nameEdit.getText().length()); | ||||
|  | ||||
|         final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) | ||||
|                 .setTitle(R.string.rename_playlist) | ||||
|                 .setView(dialogView) | ||||
|                 .setCancelable(true) | ||||
|                 .setNegativeButton(R.string.cancel, null) | ||||
|                 .setPositiveButton(R.string.rename, (dialogInterface, i) -> { | ||||
|                     changePlaylistName(nameEdit.getText().toString()); | ||||
|                 }); | ||||
|  | ||||
|         dialogBuilder.show(); | ||||
|     } | ||||
|  | ||||
|     private void changePlaylistName(final String name) { | ||||
|         if (playlistManager == null) return; | ||||
|  | ||||
|         this.name = name; | ||||
|         setTitle(name); | ||||
|  | ||||
|         Log.d(TAG, "Updating playlist id=[" + playlistId + | ||||
|                 "] with new name=[" + name + "] items"); | ||||
|  | ||||
|         final Disposable disposable = playlistManager.renamePlaylist(playlistId, name) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(longs -> {/*Do nothing on success*/}, this::onError); | ||||
|         disposables.add(disposable); | ||||
|     } | ||||
|  | ||||
|     private void changeThumbnailUrl(final String thumbnailUrl) { | ||||
|         if (playlistManager == null) return; | ||||
|  | ||||
|         final Toast successToast = Toast.makeText(getActivity(), | ||||
|                 R.string.playlist_thumbnail_change_success, | ||||
|                 Toast.LENGTH_SHORT); | ||||
|  | ||||
|         Log.d(TAG, "Updating playlist id=[" + playlistId + | ||||
|                 "] with new thumbnail url=[" + thumbnailUrl + "]"); | ||||
|  | ||||
|         final Disposable disposable = playlistManager | ||||
|                 .changePlaylistThumbnail(playlistId, thumbnailUrl) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(ignore -> successToast.show(), this::onError); | ||||
|         disposables.add(disposable); | ||||
|     } | ||||
|  | ||||
|     private void deleteItem(final PlaylistStreamEntry item) { | ||||
|         if (itemListAdapter == null) return; | ||||
|  | ||||
|         itemListAdapter.removeItem(item); | ||||
|         setVideoCount(itemListAdapter.getItemsList().size()); | ||||
|         saveChanges(); | ||||
|     } | ||||
|  | ||||
|     private void saveChanges() { | ||||
|         if (isModified == null || debouncedSaveSignal == null) return; | ||||
|  | ||||
|         isModified.set(true); | ||||
|         debouncedSaveSignal.onNext(System.currentTimeMillis()); | ||||
|     } | ||||
|  | ||||
|     private Disposable getDebouncedSaver() { | ||||
|         if (debouncedSaveSignal == null) return Disposables.empty(); | ||||
|  | ||||
|         return debouncedSaveSignal | ||||
|                 .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(ignored -> saveImmediate(), this::onError); | ||||
|     } | ||||
|  | ||||
|     private void saveImmediate() { | ||||
|         if (playlistManager == null || itemListAdapter == null) return; | ||||
|  | ||||
|         // List must be loaded and modified in order to save | ||||
|         if (isLoadingComplete == null || isModified == null || | ||||
|                 !isLoadingComplete.get() || !isModified.get()) { | ||||
|             Log.w(TAG, "Attempting to save playlist when local playlist " + | ||||
|                     "is not loaded or not modified: playlist id=[" + playlistId + "]"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final List<LocalItem> items = itemListAdapter.getItemsList(); | ||||
|         List<Long> streamIds = new ArrayList<>(items.size()); | ||||
|         for (final LocalItem item : items) { | ||||
|             if (item instanceof PlaylistStreamEntry) { | ||||
|                 streamIds.add(((PlaylistStreamEntry) item).streamId); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Log.d(TAG, "Updating playlist id=[" + playlistId + | ||||
|                 "] with [" + streamIds.size() + "] items"); | ||||
|  | ||||
|         final Disposable disposable = playlistManager.updateJoin(playlistId, streamIds) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe( | ||||
|                         () -> { if (isModified != null) isModified.set(false); }, | ||||
|                         this::onError | ||||
|                 ); | ||||
|         disposables.add(disposable); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private ItemTouchHelper.SimpleCallback getItemTouchCallback() { | ||||
|         return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, | ||||
|                 ItemTouchHelper.ACTION_STATE_IDLE) { | ||||
|             @Override | ||||
|             public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, | ||||
|                                                     int viewSizeOutOfBounds, int totalSize, | ||||
|                                                     long msSinceStartScroll) { | ||||
|                 final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, | ||||
|                         viewSizeOutOfBounds, totalSize, msSinceStartScroll); | ||||
|                 final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, | ||||
|                         Math.abs(standardSpeed)); | ||||
|                 return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, | ||||
|                                   RecyclerView.ViewHolder target) { | ||||
|                 if (source.getItemViewType() != target.getItemViewType() || | ||||
|                         itemListAdapter == null) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 final int sourceIndex = source.getAdapterPosition(); | ||||
|                 final int targetIndex = target.getAdapterPosition(); | ||||
|                 final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); | ||||
|                 if (isSwapped) saveChanges(); | ||||
|                 return isSwapped; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean isLongPressDragEnabled() { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean isItemViewSwipeEnabled() { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     protected void showStreamDialog(final PlaylistStreamEntry item) { | ||||
|         final Context context = getContext(); | ||||
|         final Activity activity = getActivity(); | ||||
|         if (context == null || context.getResources() == null || getActivity() == null) return; | ||||
|  | ||||
|         final StreamInfoItem infoItem = item.toStreamInfoItem(); | ||||
|  | ||||
|         final String[] commands = new String[]{ | ||||
|                 context.getResources().getString(R.string.enqueue_on_background), | ||||
|                 context.getResources().getString(R.string.enqueue_on_popup), | ||||
|                 context.getResources().getString(R.string.start_here_on_main), | ||||
|                 context.getResources().getString(R.string.start_here_on_background), | ||||
|                 context.getResources().getString(R.string.start_here_on_popup), | ||||
|                 context.getResources().getString(R.string.set_as_playlist_thumbnail), | ||||
|                 context.getResources().getString(R.string.delete) | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { | ||||
|             final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); | ||||
|             switch (i) { | ||||
|                 case 0: | ||||
|                     NavigationHelper.enqueueOnBackgroundPlayer(context, | ||||
|                             new SinglePlayQueue(infoItem)); | ||||
|                     break; | ||||
|                 case 1: | ||||
|                     NavigationHelper.enqueueOnPopupPlayer(activity, new | ||||
|                             SinglePlayQueue(infoItem)); | ||||
|                     break; | ||||
|                 case 2: | ||||
|                     NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); | ||||
|                     break; | ||||
|                 case 3: | ||||
|                     NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); | ||||
|                     break; | ||||
|                 case 4: | ||||
|                     NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); | ||||
|                     break; | ||||
|                 case 5: | ||||
|                     changeThumbnailUrl(item.thumbnailUrl); | ||||
|                     break; | ||||
|                 case 6: | ||||
|                     deleteItem(item); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); | ||||
|     } | ||||
|  | ||||
|     private void setInitialData(long playlistId, String name) { | ||||
|         this.playlistId = playlistId; | ||||
|         this.name = !TextUtils.isEmpty(name) ? name : ""; | ||||
|     } | ||||
|  | ||||
|     private void setVideoCount(final long count) { | ||||
|         if (activity != null && headerStreamCount != null) { | ||||
|             headerStreamCount.setText(Localization.localizeStreamCount(activity, count)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private PlayQueue getPlayQueue() { | ||||
|         return getPlayQueue(0); | ||||
|     } | ||||
|  | ||||
|     private PlayQueue getPlayQueue(final int index) { | ||||
|         if (itemListAdapter == null) { | ||||
|             return new SinglePlayQueue(Collections.emptyList(), 0); | ||||
|         } | ||||
|  | ||||
|         final List<LocalItem> infoItems = itemListAdapter.getItemsList(); | ||||
|         List<StreamInfoItem> streamInfoItems = new ArrayList<>(infoItems.size()); | ||||
|         for (final LocalItem item : infoItems) { | ||||
|             if (item instanceof PlaylistStreamEntry) { | ||||
|                 streamInfoItems.add(((PlaylistStreamEntry) item).toStreamInfoItem()); | ||||
|             } | ||||
|         } | ||||
|         return new SinglePlayQueue(streamInfoItems, index); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,22 @@ | ||||
| package org.schabi.newpipe.fragments.local.bookmark; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.stream.StreamStatisticsEntry; | ||||
|  | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| public final class MostPlayedFragment extends StatisticsPlaylistFragment { | ||||
|     @Override | ||||
|     protected String getName() { | ||||
|         return getString(R.string.title_most_played); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected List<StreamStatisticsEntry> processResult(List<StreamStatisticsEntry> results)  { | ||||
|         Collections.sort(results, (left, right) -> | ||||
|                 ((Long) right.watchCount).compareTo(left.watchCount)); | ||||
|         return results; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,300 @@ | ||||
| package org.schabi.newpipe.fragments.local.bookmark; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.DialogInterface; | ||||
| import android.os.Bundle; | ||||
| import android.os.Parcelable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
|  | ||||
| import org.reactivestreams.Subscriber; | ||||
| import org.reactivestreams.Subscription; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.stream.StreamStatisticsEntry; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.history.HistoryRecordManager; | ||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | ||||
| import org.schabi.newpipe.playlist.PlayQueue; | ||||
| import org.schabi.newpipe.playlist.SinglePlayQueue; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| import icepick.State; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
|  | ||||
| public abstract class StatisticsPlaylistFragment | ||||
|         extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> { | ||||
|  | ||||
|     private View headerPlayAllButton; | ||||
|     private View headerPopupButton; | ||||
|     private View headerBackgroundButton; | ||||
|  | ||||
|     @State | ||||
|     protected Parcelable itemsListState; | ||||
|  | ||||
|     /* Used for independent events */ | ||||
|     private Subscription databaseSubscription; | ||||
|     private HistoryRecordManager recordManager; | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Abstracts | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     protected abstract String getName(); | ||||
|  | ||||
|     protected abstract List<StreamStatisticsEntry> processResult(final List<StreamStatisticsEntry> results); | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment LifeCycle - Creation | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         recordManager = new HistoryRecordManager(getContext()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, | ||||
|                              @Nullable ViewGroup container, | ||||
|                              @Nullable Bundle savedInstanceState) { | ||||
|         return inflater.inflate(R.layout.fragment_playlist, container, false); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment LifeCycle - Views | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     protected void initViews(View rootView, Bundle savedInstanceState) { | ||||
|         super.initViews(rootView, savedInstanceState); | ||||
|         setTitle(getName()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected View getListHeader() { | ||||
|         final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control, | ||||
|                 itemsList, false); | ||||
|         headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); | ||||
|         headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); | ||||
|         headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); | ||||
|         return headerRootLayout; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void initListeners() { | ||||
|         super.initListeners(); | ||||
|  | ||||
|         itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() { | ||||
|             @Override | ||||
|             public void selected(LocalItem selectedItem) { | ||||
|                 if (selectedItem instanceof StreamStatisticsEntry) { | ||||
|                     final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem; | ||||
|                     NavigationHelper.openVideoDetailFragment(getFragmentManager(), | ||||
|                             item.serviceId, item.url, item.title); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void held(LocalItem selectedItem) { | ||||
|                 if (selectedItem instanceof StreamStatisticsEntry) { | ||||
|                     showStreamDialog((StreamStatisticsEntry) selectedItem); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment LifeCycle - Loading | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void startLoading(boolean forceLoad) { | ||||
|         super.startLoading(forceLoad); | ||||
|         recordManager.getStreamStatistics() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(getHistoryObserver()); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment LifeCycle - Destruction | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void onPause() { | ||||
|         super.onPause(); | ||||
|         itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|  | ||||
|         if (itemListAdapter != null) itemListAdapter.unsetSelectedListener(); | ||||
|         if (headerBackgroundButton != null) headerBackgroundButton.setOnClickListener(null); | ||||
|         if (headerPlayAllButton != null) headerPlayAllButton.setOnClickListener(null); | ||||
|         if (headerPopupButton != null) headerPopupButton.setOnClickListener(null); | ||||
|  | ||||
|         if (databaseSubscription != null) databaseSubscription.cancel(); | ||||
|         databaseSubscription = null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         recordManager = null; | ||||
|         itemsListState = null; | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Statistics Loader | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private Subscriber<List<StreamStatisticsEntry>> getHistoryObserver() { | ||||
|         return new Subscriber<List<StreamStatisticsEntry>>() { | ||||
|             @Override | ||||
|             public void onSubscribe(Subscription s) { | ||||
|                 showLoading(); | ||||
|  | ||||
|                 if (databaseSubscription != null) databaseSubscription.cancel(); | ||||
|                 databaseSubscription = s; | ||||
|                 databaseSubscription.request(1); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onNext(List<StreamStatisticsEntry> streams) { | ||||
|                 handleResult(streams); | ||||
|                 if (databaseSubscription != null) databaseSubscription.request(1); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onError(Throwable exception) { | ||||
|                 StatisticsPlaylistFragment.this.onError(exception); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onComplete() { | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void handleResult(@NonNull List<StreamStatisticsEntry> result) { | ||||
|         super.handleResult(result); | ||||
|         if (itemListAdapter == null) return; | ||||
|  | ||||
|         itemListAdapter.clearStreamItemList(); | ||||
|  | ||||
|         if (result.isEmpty()) { | ||||
|             showEmptyState(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         itemListAdapter.addItems(processResult(result)); | ||||
|         if (itemsListState != null) { | ||||
|             itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); | ||||
|             itemsListState = null; | ||||
|         } | ||||
|  | ||||
|         headerPlayAllButton.setOnClickListener(view -> | ||||
|                 NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); | ||||
|         headerPopupButton.setOnClickListener(view -> | ||||
|                 NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); | ||||
|         headerBackgroundButton.setOnClickListener(view -> | ||||
|                 NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); | ||||
|  | ||||
|         hideLoading(); | ||||
|     } | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment Error Handling | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     protected void resetFragment() { | ||||
|         super.resetFragment(); | ||||
|         if (databaseSubscription != null) databaseSubscription.cancel(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected boolean onError(Throwable exception) { | ||||
|         if (super.onError(exception)) return true; | ||||
|  | ||||
|         onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, | ||||
|                 "none", "History Statistics", R.string.general_error); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void showStreamDialog(final StreamStatisticsEntry item) { | ||||
|         final Context context = getContext(); | ||||
|         final Activity activity = getActivity(); | ||||
|         if (context == null || context.getResources() == null || getActivity() == null) return; | ||||
|         final StreamInfoItem infoItem = item.toStreamInfoItem(); | ||||
|  | ||||
|         final String[] commands = new String[]{ | ||||
|                 context.getResources().getString(R.string.enqueue_on_background), | ||||
|                 context.getResources().getString(R.string.enqueue_on_popup), | ||||
|                 context.getResources().getString(R.string.start_here_on_main), | ||||
|                 context.getResources().getString(R.string.start_here_on_background), | ||||
|                 context.getResources().getString(R.string.start_here_on_popup), | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { | ||||
|             final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); | ||||
|             switch (i) { | ||||
|                 case 0: | ||||
|                     NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem)); | ||||
|                     break; | ||||
|                 case 1: | ||||
|                     NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem)); | ||||
|                     break; | ||||
|                 case 2: | ||||
|                     NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); | ||||
|                     break; | ||||
|                 case 3: | ||||
|                     NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); | ||||
|                     break; | ||||
|                 case 4: | ||||
|                     NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); | ||||
|     } | ||||
|  | ||||
|     private PlayQueue getPlayQueue() { | ||||
|         return getPlayQueue(0); | ||||
|     } | ||||
|  | ||||
|     private PlayQueue getPlayQueue(final int index) { | ||||
|         if (itemListAdapter == null) { | ||||
|             return new SinglePlayQueue(Collections.emptyList(), 0); | ||||
|         } | ||||
|  | ||||
|         final List<LocalItem> infoItems = itemListAdapter.getItemsList(); | ||||
|         List<StreamInfoItem> streamInfoItems = new ArrayList<>(infoItems.size()); | ||||
|         for (final LocalItem item : infoItems) { | ||||
|             if (item instanceof StreamStatisticsEntry) { | ||||
|                 streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem()); | ||||
|             } | ||||
|         } | ||||
|         return new SinglePlayQueue(streamInfoItems, index); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,163 @@ | ||||
| package org.schabi.newpipe.fragments.local.dialog; | ||||
|  | ||||
| import android.annotation.SuppressLint; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v7.widget.LinearLayoutManager; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.NewPipeDatabase; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.fragments.local.LocalItemListAdapter; | ||||
| import org.schabi.newpipe.fragments.local.LocalPlaylistManager; | ||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
|  | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.Disposable; | ||||
|  | ||||
| public final class PlaylistAppendDialog extends PlaylistDialog { | ||||
|     private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); | ||||
|  | ||||
|     private RecyclerView playlistRecyclerView; | ||||
|     private LocalItemListAdapter playlistAdapter; | ||||
|  | ||||
|     private Disposable playlistReactor; | ||||
|  | ||||
|     public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) { | ||||
|         PlaylistAppendDialog dialog = new PlaylistAppendDialog(); | ||||
|         dialog.setInfo(Collections.singletonList(new StreamEntity(info))); | ||||
|         return dialog; | ||||
|     } | ||||
|  | ||||
|     public static PlaylistAppendDialog fromStreamInfoItems(final List<StreamInfoItem> items) { | ||||
|         PlaylistAppendDialog dialog = new PlaylistAppendDialog(); | ||||
|         List<StreamEntity> entities = new ArrayList<>(items.size()); | ||||
|         for (final StreamInfoItem item : items) { | ||||
|             entities.add(new StreamEntity(item)); | ||||
|         } | ||||
|         dialog.setInfo(entities); | ||||
|         return dialog; | ||||
|     } | ||||
|  | ||||
|     public static PlaylistAppendDialog fromPlayQueueItems(final List<PlayQueueItem> items) { | ||||
|         PlaylistAppendDialog dialog = new PlaylistAppendDialog(); | ||||
|         List<StreamEntity> entities = new ArrayList<>(items.size()); | ||||
|         for (final PlayQueueItem item : items) { | ||||
|             entities.add(new StreamEntity(item)); | ||||
|         } | ||||
|         dialog.setInfo(entities); | ||||
|         return dialog; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // LifeCycle - Creation | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, | ||||
|                              Bundle savedInstanceState) { | ||||
|         return inflater.inflate(R.layout.dialog_playlists, container); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
|  | ||||
|         final LocalPlaylistManager playlistManager = | ||||
|                 new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); | ||||
|  | ||||
|         playlistAdapter = new LocalItemListAdapter(getActivity()); | ||||
|         playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() { | ||||
|             @Override | ||||
|             public void selected(LocalItem selectedItem) { | ||||
|                 if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) | ||||
|                     return; | ||||
|                 onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, | ||||
|                         getStreams()); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         playlistRecyclerView = view.findViewById(R.id.playlist_list); | ||||
|         playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); | ||||
|         playlistRecyclerView.setAdapter(playlistAdapter); | ||||
|  | ||||
|         final View newPlaylistButton = view.findViewById(R.id.newPlaylist); | ||||
|         newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); | ||||
|  | ||||
|         playlistReactor = playlistManager.getPlaylists() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(this::onPlaylistsReceived); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // LifeCycle - Destruction | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         if (playlistReactor != null) playlistReactor.dispose(); | ||||
|         if (playlistAdapter != null) playlistAdapter.unsetSelectedListener(); | ||||
|  | ||||
|         playlistReactor = null; | ||||
|         playlistRecyclerView = null; | ||||
|         playlistAdapter = null; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Helper | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public void openCreatePlaylistDialog() { | ||||
|         if (getStreams() == null || getFragmentManager() == null) return; | ||||
|  | ||||
|         PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG); | ||||
|         getDialog().dismiss(); | ||||
|     } | ||||
|  | ||||
|     private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) { | ||||
|         if (playlists.isEmpty()) { | ||||
|             openCreatePlaylistDialog(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (playlistAdapter != null && playlistRecyclerView != null) { | ||||
|             playlistAdapter.clearStreamItemList(); | ||||
|             playlistAdapter.addItems(playlists); | ||||
|             playlistRecyclerView.setVisibility(View.VISIBLE); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void onPlaylistSelected(@NonNull LocalPlaylistManager manager, | ||||
|                                     @NonNull PlaylistMetadataEntry playlist, | ||||
|                                     @Nonnull List<StreamEntity> streams) { | ||||
|         if (getStreams() == null) return; | ||||
|  | ||||
|         @SuppressLint("ShowToast") | ||||
|         final Toast successToast = Toast.makeText(getContext(), | ||||
|                 R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); | ||||
|  | ||||
|         manager.appendToPlaylist(playlist.uid, streams) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(ignored -> successToast.show()); | ||||
|  | ||||
|         getDialog().dismiss(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,62 @@ | ||||
| package org.schabi.newpipe.fragments.local.dialog; | ||||
|  | ||||
| import android.app.AlertDialog; | ||||
| import android.app.Dialog; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.view.View; | ||||
| import android.widget.EditText; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.NewPipeDatabase; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.fragments.local.LocalPlaylistManager; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
|  | ||||
| public final class PlaylistCreationDialog extends PlaylistDialog { | ||||
|     private static final String TAG = PlaylistCreationDialog.class.getCanonicalName(); | ||||
|  | ||||
|     public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) { | ||||
|         PlaylistCreationDialog dialog = new PlaylistCreationDialog(); | ||||
|         dialog.setInfo(streams); | ||||
|         return dialog; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Dialog | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { | ||||
|         if (getStreams() == null) return super.onCreateDialog(savedInstanceState); | ||||
|  | ||||
|         View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); | ||||
|         EditText nameInput = dialogView.findViewById(R.id.playlist_name); | ||||
|  | ||||
|         final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) | ||||
|                 .setTitle(R.string.create_playlist) | ||||
|                 .setView(dialogView) | ||||
|                 .setCancelable(true) | ||||
|                 .setNegativeButton(R.string.cancel, null) | ||||
|                 .setPositiveButton(R.string.create, (dialogInterface, i) -> { | ||||
|                     final String name = nameInput.getText().toString(); | ||||
|                     final LocalPlaylistManager playlistManager = | ||||
|                             new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); | ||||
|                     final Toast successToast = Toast.makeText(getActivity(), | ||||
|                             R.string.playlist_creation_success, | ||||
|                             Toast.LENGTH_SHORT); | ||||
|  | ||||
|                     playlistManager.createPlaylist(name, getStreams()) | ||||
|                             .observeOn(AndroidSchedulers.mainThread()) | ||||
|                             .subscribe(longs -> successToast.show()); | ||||
|                 }); | ||||
|  | ||||
|         return dialogBuilder.create(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,73 @@ | ||||
| package org.schabi.newpipe.fragments.local.dialog; | ||||
|  | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.DialogFragment; | ||||
|  | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.util.StateSaver; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Queue; | ||||
|  | ||||
| public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead { | ||||
|  | ||||
|     private List<StreamEntity> streamEntities; | ||||
|  | ||||
|     private StateSaver.SavedState savedState; | ||||
|  | ||||
|     protected void setInfo(final List<StreamEntity> entities) { | ||||
|         this.streamEntities = entities; | ||||
|     } | ||||
|  | ||||
|     protected List<StreamEntity> getStreams() { | ||||
|         return streamEntities; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // LifeCycle | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         savedState = StateSaver.tryToRestore(savedInstanceState, this); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         StateSaver.onDestroy(savedState); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // State Saving | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public String generateSuffix() { | ||||
|         final int size = streamEntities == null ? 0 : streamEntities.size(); | ||||
|         return "." + size + ".list"; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void writeTo(Queue<Object> objectsToSave) { | ||||
|         objectsToSave.add(streamEntities); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception { | ||||
|         streamEntities = (List<StreamEntity>) savedObjects.poll(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSaveInstanceState(Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|         if (getActivity() != null) { | ||||
|             savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(), | ||||
|                     savedState, outState, this); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,63 @@ | ||||
| package org.schabi.newpipe.fragments.local.holder; | ||||
|  | ||||
| import android.graphics.Bitmap; | ||||
| import android.support.annotation.DimenRes; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
|  | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
| import com.nostra13.universalimageloader.core.process.BitmapProcessor; | ||||
|  | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||
|  | ||||
| import java.text.DateFormat; | ||||
|  | ||||
| /* | ||||
|  * Created by Christian Schabesberger on 12.02.17. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * InfoItemHolder.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public abstract class LocalItemHolder extends RecyclerView.ViewHolder { | ||||
|     protected final LocalItemBuilder itemBuilder; | ||||
|  | ||||
|     public LocalItemHolder(LocalItemBuilder itemBuilder, int layoutId, ViewGroup parent) { | ||||
|         super(LayoutInflater.from(itemBuilder.getContext()) | ||||
|                 .inflate(layoutId, parent, false)); | ||||
|         this.itemBuilder = itemBuilder; | ||||
|     } | ||||
|  | ||||
|     public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // ImageLoaderOptions | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     /** | ||||
|      * Base display options | ||||
|      */ | ||||
|     public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = | ||||
|             new DisplayImageOptions.Builder() | ||||
|                     .cacheInMemory(true) | ||||
|                     .cacheOnDisk(true) | ||||
|                     .bitmapConfig(Bitmap.Config.RGB_565) | ||||
|                     .resetViewBeforeLoading(false) | ||||
|                     .build(); | ||||
| } | ||||
| @@ -0,0 +1,36 @@ | ||||
| package org.schabi.newpipe.fragments.local.holder; | ||||
|  | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | ||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||
|  | ||||
| import java.text.DateFormat; | ||||
|  | ||||
| public class LocalPlaylistItemHolder extends PlaylistItemHolder { | ||||
|  | ||||
|     public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { | ||||
|         super(infoItemBuilder, parent); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { | ||||
|         if (!(localItem instanceof PlaylistMetadataEntry)) return; | ||||
|         final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; | ||||
|  | ||||
|         itemTitleView.setText(item.name); | ||||
|         itemStreamCountView.setText(String.valueOf(item.streamCount)); | ||||
|         itemUploaderView.setVisibility(View.INVISIBLE); | ||||
|  | ||||
|         itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); | ||||
|  | ||||
|         super.updateFromItem(localItem, dateFormat); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,106 @@ | ||||
| package org.schabi.newpipe.fragments.local.holder; | ||||
|  | ||||
| import android.graphics.Bitmap; | ||||
| import android.support.v4.content.ContextCompat; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
| import com.nostra13.universalimageloader.core.assist.ImageScaleType; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
|  | ||||
| import java.text.DateFormat; | ||||
|  | ||||
| public class LocalPlaylistStreamItemHolder extends LocalItemHolder { | ||||
|  | ||||
|     public final ImageView itemThumbnailView; | ||||
|     public final TextView itemVideoTitleView; | ||||
|     public final TextView itemAdditionalDetailsView; | ||||
|     public final TextView itemDurationView; | ||||
|     public final View itemHandleView; | ||||
|  | ||||
|     LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { | ||||
|         super(infoItemBuilder, layoutId, parent); | ||||
|  | ||||
|         itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); | ||||
|         itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); | ||||
|         itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails); | ||||
|         itemDurationView = itemView.findViewById(R.id.itemDurationView); | ||||
|         itemHandleView = itemView.findViewById(R.id.itemHandle); | ||||
|     } | ||||
|  | ||||
|     public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { | ||||
|         this(infoItemBuilder, R.layout.list_stream_playlist_item, parent); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { | ||||
|         if (!(localItem instanceof PlaylistStreamEntry)) return; | ||||
|         final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; | ||||
|  | ||||
|         itemVideoTitleView.setText(item.title); | ||||
|         itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.uploader, | ||||
|                 NewPipe.getNameOfService(item.serviceId))); | ||||
|  | ||||
|         if (item.duration > 0) { | ||||
|             itemDurationView.setText(Localization.getDurationString(item.duration)); | ||||
|             itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), | ||||
|                     R.color.duration_background_color)); | ||||
|             itemDurationView.setVisibility(View.VISIBLE); | ||||
|         } else { | ||||
|             itemDurationView.setVisibility(View.GONE); | ||||
|         } | ||||
|  | ||||
|         // Default thumbnail is shown on error, while loading and if the url is empty | ||||
|         itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); | ||||
|  | ||||
|         itemView.setOnClickListener(view -> { | ||||
|             if (itemBuilder.getOnItemSelectedListener() != null) { | ||||
|                 itemBuilder.getOnItemSelectedListener().selected(item); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         itemView.setLongClickable(true); | ||||
|         itemView.setOnLongClickListener(view -> { | ||||
|             if (itemBuilder.getOnItemSelectedListener() != null) { | ||||
|                 itemBuilder.getOnItemSelectedListener().held(item); | ||||
|             } | ||||
|             return true; | ||||
|         }); | ||||
|  | ||||
|         itemThumbnailView.setOnTouchListener(getOnTouchListener(item)); | ||||
|         itemHandleView.setOnTouchListener(getOnTouchListener(item)); | ||||
|     } | ||||
|  | ||||
|     private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) { | ||||
|         return (view, motionEvent) -> { | ||||
|             view.performClick(); | ||||
|             if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null && | ||||
|                     motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { | ||||
|                 itemBuilder.getOnItemSelectedListener().drag(item, | ||||
|                         LocalPlaylistStreamItemHolder.this); | ||||
|             } | ||||
|             return false; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Display options for stream thumbnails | ||||
|      */ | ||||
|     private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = | ||||
|             new DisplayImageOptions.Builder() | ||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) | ||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail) | ||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail) | ||||
|                     .showImageOnLoading(R.drawable.dummy_thumbnail) | ||||
|                     .build(); | ||||
| } | ||||
| @@ -0,0 +1,114 @@ | ||||
| package org.schabi.newpipe.fragments.local.holder; | ||||
|  | ||||
| import android.support.v4.content.ContextCompat; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.stream.StreamStatisticsEntry; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
|  | ||||
| import java.text.DateFormat; | ||||
|  | ||||
| /* | ||||
|  * Created by Christian Schabesberger on 01.08.16. | ||||
|  * <p> | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * StreamInfoItemHolder.java is part of NewPipe. | ||||
|  * <p> | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * <p> | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * <p> | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class LocalStatisticStreamItemHolder extends LocalItemHolder { | ||||
|  | ||||
|     public final ImageView itemThumbnailView; | ||||
|     public final TextView itemVideoTitleView; | ||||
|     public final TextView itemUploaderView; | ||||
|     public final TextView itemDurationView; | ||||
|     public final TextView itemAdditionalDetails; | ||||
|  | ||||
|     public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { | ||||
|         super(infoItemBuilder, R.layout.list_stream_item, parent); | ||||
|  | ||||
|         itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); | ||||
|         itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); | ||||
|         itemUploaderView = itemView.findViewById(R.id.itemUploaderView); | ||||
|         itemDurationView = itemView.findViewById(R.id.itemDurationView); | ||||
|         itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); | ||||
|     } | ||||
|  | ||||
|     private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, | ||||
|                                            final DateFormat dateFormat) { | ||||
|         final String watchCount = Localization.shortViewCount(itemBuilder.getContext(), | ||||
|                 entry.watchCount); | ||||
|         final String uploadDate = dateFormat.format(entry.latestAccessDate); | ||||
|         final String serviceName = NewPipe.getNameOfService(entry.serviceId); | ||||
|         return Localization.concatenateStrings(watchCount, uploadDate, serviceName); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { | ||||
|         if (!(localItem instanceof StreamStatisticsEntry)) return; | ||||
|         final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; | ||||
|  | ||||
|         itemVideoTitleView.setText(item.title); | ||||
|         itemUploaderView.setText(item.uploader); | ||||
|  | ||||
|         if (item.duration > 0) { | ||||
|             itemDurationView.setText(Localization.getDurationString(item.duration)); | ||||
|             itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), | ||||
|                     R.color.duration_background_color)); | ||||
|             itemDurationView.setVisibility(View.VISIBLE); | ||||
|         } else { | ||||
|             itemDurationView.setVisibility(View.GONE); | ||||
|         } | ||||
|  | ||||
|         itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); | ||||
|  | ||||
|         // Default thumbnail is shown on error, while loading and if the url is empty | ||||
|         itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); | ||||
|  | ||||
|         itemView.setOnClickListener(view -> { | ||||
|             if (itemBuilder.getOnItemSelectedListener() != null) { | ||||
|                 itemBuilder.getOnItemSelectedListener().selected(item); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         itemView.setLongClickable(true); | ||||
|         itemView.setOnLongClickListener(view -> { | ||||
|             if (itemBuilder.getOnItemSelectedListener() != null) { | ||||
|                 itemBuilder.getOnItemSelectedListener().held(item); | ||||
|             } | ||||
|             return true; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Display options for stream thumbnails | ||||
|      */ | ||||
|     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = | ||||
|             new DisplayImageOptions.Builder() | ||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) | ||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail) | ||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail) | ||||
|                     .showImageOnLoading(R.drawable.dummy_thumbnail) | ||||
|                     .build(); | ||||
| } | ||||
| @@ -0,0 +1,62 @@ | ||||
| package org.schabi.newpipe.fragments.local.holder; | ||||
|  | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||
|  | ||||
| import java.text.DateFormat; | ||||
|  | ||||
| public abstract class PlaylistItemHolder extends LocalItemHolder { | ||||
|     public final ImageView itemThumbnailView; | ||||
|     public final TextView itemStreamCountView; | ||||
|     public final TextView itemTitleView; | ||||
|     public final TextView itemUploaderView; | ||||
|  | ||||
|     public PlaylistItemHolder(LocalItemBuilder infoItemBuilder, | ||||
|                               int layoutId, ViewGroup parent) { | ||||
|         super(infoItemBuilder, layoutId, parent); | ||||
|  | ||||
|         itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); | ||||
|         itemTitleView = itemView.findViewById(R.id.itemTitleView); | ||||
|         itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); | ||||
|         itemUploaderView = itemView.findViewById(R.id.itemUploaderView); | ||||
|     } | ||||
|  | ||||
|     public PlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { | ||||
|         this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { | ||||
|         itemView.setOnClickListener(view -> { | ||||
|             if (itemBuilder.getOnItemSelectedListener() != null) { | ||||
|                 itemBuilder.getOnItemSelectedListener().selected(localItem); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         itemView.setLongClickable(true); | ||||
|         itemView.setOnLongClickListener(view -> { | ||||
|             if (itemBuilder.getOnItemSelectedListener() != null) { | ||||
|                 itemBuilder.getOnItemSelectedListener().held(localItem); | ||||
|             } | ||||
|             return true; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Display options for playlist thumbnails | ||||
|      */ | ||||
|     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = | ||||
|             new DisplayImageOptions.Builder() | ||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) | ||||
|                     .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) | ||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) | ||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail_playlist) | ||||
|                     .build(); | ||||
| } | ||||
| @@ -0,0 +1,33 @@ | ||||
| package org.schabi.newpipe.fragments.local.holder; | ||||
|  | ||||
| import android.view.ViewGroup; | ||||
|  | ||||
| import org.schabi.newpipe.database.LocalItem; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
|  | ||||
| import java.text.DateFormat; | ||||
|  | ||||
| public class RemotePlaylistItemHolder extends PlaylistItemHolder { | ||||
|     public RemotePlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { | ||||
|         super(infoItemBuilder, parent); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { | ||||
|         if (!(localItem instanceof PlaylistRemoteEntity)) return; | ||||
|         final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; | ||||
|  | ||||
|         itemTitleView.setText(item.getName()); | ||||
|         itemStreamCountView.setText(String.valueOf(item.getStreamCount())); | ||||
|         itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(), | ||||
|                 NewPipe.getNameOfService(item.getServiceId()))); | ||||
|  | ||||
|         itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, | ||||
|                 DISPLAY_THUMBNAIL_OPTIONS); | ||||
|  | ||||
|         super.updateFromItem(localItem, dateFormat); | ||||
|     } | ||||
| } | ||||
| @@ -16,10 +16,10 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; | ||||
| import org.schabi.newpipe.extractor.InfoItem; | ||||
| import org.schabi.newpipe.extractor.channel.ChannelInfoItem; | ||||
| import org.schabi.newpipe.fragments.BaseStateFragment; | ||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||
| import org.schabi.newpipe.info_list.InfoListAdapter; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| @@ -125,24 +125,17 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt | ||||
|     protected void initListeners() { | ||||
|         super.initListeners(); | ||||
|  | ||||
|         infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() { | ||||
|         infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() { | ||||
|             @Override | ||||
|             public void selected(ChannelInfoItem selectedItem) { | ||||
|                 // Requires the parent fragment to find holder for fragment replacement | ||||
|                 NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void held(ChannelInfoItem selectedItem) {} | ||||
|         }); | ||||
|  | ||||
|         headerRootLayout.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View view) { | ||||
|                 NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()); | ||||
|                 NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), | ||||
|                         selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         headerRootLayout.setOnClickListener(view -> | ||||
|                 NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager())); | ||||
|     } | ||||
|  | ||||
|     private void resetFragment() { | ||||
|   | ||||
| @@ -11,7 +11,6 @@ import android.support.v4.app.FragmentStatePagerAdapter; | ||||
| import android.support.v4.view.ViewPager; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.support.v7.widget.Toolbar; | ||||
| import android.util.Log; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
|  | ||||
| @@ -22,7 +21,6 @@ import org.schabi.newpipe.settings.SettingsActivity; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.functions.Consumer; | ||||
|  | ||||
| public class HistoryActivity extends AppCompatActivity { | ||||
|  | ||||
| @@ -50,8 +48,10 @@ public class HistoryActivity extends AppCompatActivity { | ||||
|  | ||||
|         Toolbar toolbar = findViewById(R.id.toolbar); | ||||
|         setSupportActionBar(toolbar); | ||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||
|         getSupportActionBar().setTitle(R.string.title_activity_history); | ||||
|         if (getSupportActionBar() != null) { | ||||
|             getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||
|             getSupportActionBar().setTitle(R.string.title_activity_history); | ||||
|         } | ||||
|         // Create the adapter that will return a fragment for each of the three | ||||
|         // primary sections of the activity. | ||||
|         mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); | ||||
| @@ -66,17 +66,11 @@ public class HistoryActivity extends AppCompatActivity { | ||||
|         final FloatingActionButton fab = findViewById(R.id.fab); | ||||
|         RxView.clicks(fab) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(new Consumer<Object>() { | ||||
|                     @Override | ||||
|                     public void accept(Object o) { | ||||
|                         int currentItem = mViewPager.getCurrentItem(); | ||||
|                         HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.instantiateItem(mViewPager, currentItem); | ||||
|                         if(fragment != null) { | ||||
|                             fragment.onHistoryCleared(); | ||||
|                         } else { | ||||
|                             Log.w(TAG, "Couldn't find current fragment"); | ||||
|                         } | ||||
|                     } | ||||
|                 .subscribe(ignored -> { | ||||
|                     int currentItem = mViewPager.getCurrentItem(); | ||||
|                     HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter | ||||
|                             .instantiateItem(mViewPager, currentItem); | ||||
|                     fragment.onHistoryCleared(); | ||||
|                 }); | ||||
|     } | ||||
|  | ||||
| @@ -119,7 +113,7 @@ public class HistoryActivity extends AppCompatActivity { | ||||
|                     fragment = SearchHistoryFragment.newInstance(); | ||||
|                     break; | ||||
|                 case 1: | ||||
|                     fragment = WatchedHistoryFragment.newInstance(); | ||||
|                     fragment = WatchHistoryFragment.newInstance(); | ||||
|                     break; | ||||
|                 default: | ||||
|                     throw new IllegalArgumentException("position: " + position); | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| package org.schabi.newpipe.history; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.res.Resources; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.view.View; | ||||
|  | ||||
| import org.schabi.newpipe.database.history.model.HistoryEntry; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
|  | ||||
| import java.text.DateFormat; | ||||
| import java.util.ArrayList; | ||||
| @@ -19,19 +19,20 @@ import java.util.Date; | ||||
|  * @param <E> the type of the entries | ||||
|  * @param <VH> the type of the view holder | ||||
|  */ | ||||
| public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> { | ||||
| public abstract class HistoryEntryAdapter<E, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> { | ||||
|  | ||||
|     private final ArrayList<E> mEntries; | ||||
|     private final DateFormat mDateFormat; | ||||
|     private final Context mContext; | ||||
|     private OnHistoryItemClickListener<E> onHistoryItemClickListener = null; | ||||
|  | ||||
|  | ||||
|     public HistoryEntryAdapter(Context context) { | ||||
|         super(); | ||||
|         mContext = context; | ||||
|         mEntries = new ArrayList<>(); | ||||
|         mDateFormat = android.text.format.DateFormat.getDateFormat(context.getApplicationContext()); | ||||
|  | ||||
|         setHasStableIds(true); | ||||
|         mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, | ||||
|                 Localization.getPreferredLocale(context)); | ||||
|     } | ||||
|  | ||||
|     public void setEntries(@NonNull Collection<E> historyEntries) { | ||||
| @@ -53,9 +54,8 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec | ||||
|         return mDateFormat.format(date); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long getItemId(int position) { | ||||
|         return mEntries.get(position).getId(); | ||||
|     protected String getFormattedViewString(final long viewCount) { | ||||
|         return Localization.shortViewCount(mContext, viewCount); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -66,15 +66,20 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec | ||||
|     @Override | ||||
|     public void onBindViewHolder(VH holder, int position) { | ||||
|         final E entry = mEntries.get(position); | ||||
|         holder.itemView.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View v) { | ||||
|                 final OnHistoryItemClickListener<E> historyItemClickListener = onHistoryItemClickListener; | ||||
|                 if(historyItemClickListener != null) { | ||||
|                     historyItemClickListener.onHistoryItemClick(entry); | ||||
|                 } | ||||
|         holder.itemView.setOnClickListener(v -> { | ||||
|             if(onHistoryItemClickListener != null) { | ||||
|                 onHistoryItemClickListener.onHistoryItemClick(entry); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         holder.itemView.setOnLongClickListener(view -> { | ||||
|             if (onHistoryItemClickListener != null) { | ||||
|                 onHistoryItemClickListener.onHistoryItemLongClick(entry); | ||||
|                 return true; | ||||
|             } | ||||
|             return false; | ||||
|         }); | ||||
|  | ||||
|         onBindViewHolder(holder, entry, position); | ||||
|     } | ||||
|  | ||||
| @@ -94,13 +99,8 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec | ||||
|         return mEntries.isEmpty(); | ||||
|     } | ||||
|  | ||||
|     public E removeItemAt(int position) { | ||||
|         E entry = mEntries.remove(position); | ||||
|         notifyItemRemoved(position); | ||||
|         return entry; | ||||
|     } | ||||
|  | ||||
|     public interface OnHistoryItemClickListener<E extends HistoryEntry> { | ||||
|         void onHistoryItemClick(E historyItem); | ||||
|     public interface OnHistoryItemClickListener<E> { | ||||
|         void onHistoryItemClick(E item); | ||||
|         void onHistoryItemLongClick(E item); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package org.schabi.newpipe.history; | ||||
|  | ||||
|  | ||||
| import android.content.SharedPreferences; | ||||
| import android.graphics.Color; | ||||
| import android.os.Bundle; | ||||
| import android.os.Parcelable; | ||||
| import android.preference.PreferenceManager; | ||||
| @@ -12,34 +11,33 @@ import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.annotation.StringRes; | ||||
| import android.support.design.widget.Snackbar; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.widget.LinearLayoutManager; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.support.v7.widget.helper.ItemTouchHelper; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
|  | ||||
| import org.reactivestreams.Subscriber; | ||||
| import org.reactivestreams.Subscription; | ||||
| import org.schabi.newpipe.BaseFragment; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.history.dao.HistoryDAO; | ||||
| import org.schabi.newpipe.database.history.model.HistoryEntry; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| import icepick.State; | ||||
| import io.reactivex.Observer; | ||||
| import io.reactivex.Flowable; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| 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 BaseFragment | ||||
| public abstract class HistoryFragment<E> extends BaseFragment | ||||
|         implements HistoryEntryAdapter.OnHistoryItemClickListener<E> { | ||||
|  | ||||
|     private SharedPreferences mSharedPreferences; | ||||
| @@ -54,12 +52,11 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme | ||||
|     Parcelable mRecyclerViewState; | ||||
|     private RecyclerView mRecyclerView; | ||||
|     private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter; | ||||
|     private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback; | ||||
|     // private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; | ||||
|  | ||||
|     private HistoryDAO<E> mHistoryDataSource; | ||||
|     private PublishSubject<Collection<E>> mHistoryEntryDeleteSubject; | ||||
|     private PublishSubject<Collection<E>> mHistoryEntryInsertSubject; | ||||
|     private Subscription historySubscription; | ||||
|  | ||||
|     protected HistoryRecordManager historyRecordManager; | ||||
|     protected CompositeDisposable disposables; | ||||
|  | ||||
|     @StringRes | ||||
|     abstract int getEnabledConfigKey(); | ||||
| @@ -77,88 +74,47 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme | ||||
|         // Register history enabled listener | ||||
|         mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); | ||||
|  | ||||
|         mHistoryDataSource = createHistoryDAO(); | ||||
|  | ||||
|         mHistoryEntryDeleteSubject = PublishSubject.create(); | ||||
|         mHistoryEntryDeleteSubject | ||||
|                 .observeOn(Schedulers.io()) | ||||
|                 .subscribe(new Consumer<Collection<E>>() { | ||||
|                     @Override | ||||
|                     public void accept(Collection<E> historyEntries) throws Exception { | ||||
|                         mHistoryDataSource.delete(historyEntries); | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|         mHistoryEntryInsertSubject = PublishSubject.create(); | ||||
|         mHistoryEntryInsertSubject | ||||
|                 .observeOn(Schedulers.io()) | ||||
|                 .subscribe(new Consumer<Collection<E>>() { | ||||
|                     @Override | ||||
|                     public void accept(Collection<E> historyEntries) throws Exception { | ||||
|                         mHistoryDataSource.insertAll(historyEntries); | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|  | ||||
|     } | ||||
|  | ||||
|     protected void historyItemSwipeCallback(int swipeDirection) { | ||||
|         mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, swipeDirection) { | ||||
|             @Override | ||||
|             public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { | ||||
|                 if (mHistoryAdapter != null) { | ||||
|                     final E historyEntry = mHistoryAdapter.removeItemAt(viewHolder.getAdapterPosition()); | ||||
|                     mHistoryEntryDeleteSubject.onNext(Collections.singletonList(historyEntry)); | ||||
|  | ||||
|                     View view = getActivity().findViewById(R.id.main_content); | ||||
|                     if (view == null) view = mRecyclerView.getRootView(); | ||||
|  | ||||
|                     Snackbar.make(view, R.string.item_deleted, 5 * 1000) | ||||
|                             .setActionTextColor(Color.WHITE) | ||||
|                             .setAction(R.string.undo, new View.OnClickListener() { | ||||
|                                 @Override | ||||
|                                 public void onClick(View v) { | ||||
|                                     mHistoryEntryInsertSubject.onNext(Collections.singletonList(historyEntry)); | ||||
|                                 } | ||||
|                             }).show(); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         historyRecordManager = new HistoryRecordManager(getContext()); | ||||
|         disposables = new CompositeDisposable(); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     protected abstract HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> createAdapter(); | ||||
|  | ||||
|     protected abstract Single<List<Long>> insert(final Collection<E> entries); | ||||
|  | ||||
|     protected abstract Single<Integer> delete(final Collection<E> entries); | ||||
|  | ||||
|     @NonNull | ||||
|     protected abstract Flowable<List<E>> getAll(); | ||||
|  | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         mHistoryDataSource.getAll() | ||||
|                 .toObservable() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(getHistoryListConsumer()); | ||||
|         boolean newEnabled = isHistoryEnabled(); | ||||
|  | ||||
|         getAll().observeOn(AndroidSchedulers.mainThread()).subscribe(getHistorySubscriber()); | ||||
|  | ||||
|         final boolean newEnabled = isHistoryEnabled(); | ||||
|         if (newEnabled != mHistoryIsEnabled) { | ||||
|             onHistoryIsEnabledChanged(newEnabled); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     private Observer<List<E>> getHistoryListConsumer() { | ||||
|         return new Observer<List<E>>() { | ||||
|     private Subscriber<List<E>> getHistorySubscriber() { | ||||
|         return new Subscriber<List<E>>() { | ||||
|             @Override | ||||
|             public void onSubscribe(@NonNull Disposable d) { | ||||
|             public void onSubscribe(Subscription s) { | ||||
|                 if (historySubscription != null) historySubscription.cancel(); | ||||
|  | ||||
|                 historySubscription = s; | ||||
|                 historySubscription.request(1); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onNext(@NonNull List<E> historyEntries) { | ||||
|                 if (!historyEntries.isEmpty()) { | ||||
|                     mHistoryAdapter.setEntries(historyEntries); | ||||
|             public void onNext(List<E> entries) { | ||||
|                 if (!entries.isEmpty()) { | ||||
|                     mHistoryAdapter.setEntries(entries); | ||||
|                     animateView(mEmptyHistoryView, false, 200); | ||||
|  | ||||
|                     if (mRecyclerViewState != null) { | ||||
| @@ -169,11 +125,13 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme | ||||
|                     mHistoryAdapter.clear(); | ||||
|                     showEmptyHistory(); | ||||
|                 } | ||||
|  | ||||
|                 if (historySubscription != null) historySubscription.request(1); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onError(@NonNull Throwable e) { | ||||
|                 // TODO: error handling like in (see e.g. subscription fragment) | ||||
|             public void onError(Throwable t) { | ||||
|  | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
| @@ -192,30 +150,48 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme | ||||
|      */ | ||||
|     @MainThread | ||||
|     public void onHistoryCleared() { | ||||
|         final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState(); | ||||
|         final Collection<E> itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); | ||||
|         mHistoryEntryDeleteSubject.onNext(itemsToDelete); | ||||
|         if (getContext() == null) return; | ||||
|  | ||||
|         new AlertDialog.Builder(getContext()) | ||||
|                 .setTitle(R.string.delete_all) | ||||
|                 .setMessage(R.string.delete_all_history_prompt) | ||||
|                 .setCancelable(true) | ||||
|                 .setNegativeButton(R.string.cancel, null) | ||||
|                 .setPositiveButton(R.string.delete_all, (dialog, i) -> clearHistory()) | ||||
|                 .show(); | ||||
|     } | ||||
|  | ||||
|     protected void makeSnackbar(@StringRes final int text) { | ||||
|         if (getActivity() == null) return; | ||||
|  | ||||
|         View view = getActivity().findViewById(R.id.main_content); | ||||
|         if (view == null) view = mRecyclerView.getRootView(); | ||||
|         Snackbar.make(view, text, Snackbar.LENGTH_LONG).show(); | ||||
|     } | ||||
|  | ||||
|         if (!itemsToDelete.isEmpty()) { | ||||
|             Snackbar.make(view, R.string.history_cleared, 5 * 1000) | ||||
|                     .setActionTextColor(Color.WHITE) | ||||
|                     .setAction(R.string.undo, new View.OnClickListener() { | ||||
|                         @Override | ||||
|                         public void onClick(View v) { | ||||
|                             mRecyclerViewState = stateBeforeClear; | ||||
|                             mHistoryEntryInsertSubject.onNext(itemsToDelete); | ||||
|                         } | ||||
|                     }).show(); | ||||
|         } else { | ||||
|             Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show(); | ||||
|         } | ||||
|     private void clearHistory() { | ||||
|         final Collection<E> itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); | ||||
|  | ||||
|         final Disposable deletion = delete(itemsToDelete) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe( | ||||
|                         ignored -> Log.d(TAG, "Clear history deleted [" + | ||||
|                         itemsToDelete.size() + "] items."), | ||||
|                         error -> Log.e(TAG, "Clear history delete step failed", error) | ||||
|                 ); | ||||
|  | ||||
|         final Disposable cleanUp = historyRecordManager.removeOrphanedRecords() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe( | ||||
|                         ignored -> Log.d(TAG, "Clear history deleted orphaned stream records"), | ||||
|                         error -> Log.e(TAG, "Clear history remove orphaned records failed", error) | ||||
|                 ); | ||||
|  | ||||
|         disposables.addAll(deletion, cleanUp); | ||||
|  | ||||
|         makeSnackbar(R.string.history_cleared); | ||||
|         mHistoryAdapter.clear(); | ||||
|         showEmptyHistory(); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private void showEmptyHistory() { | ||||
| @@ -227,18 +203,18 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme | ||||
|     @Nullable | ||||
|     @CallSuper | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, | ||||
|                              @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { | ||||
|         View rootView = inflater.inflate(R.layout.fragment_history, container, false); | ||||
|         mRecyclerView = rootView.findViewById(R.id.history_view); | ||||
|  | ||||
|         RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); | ||||
|         RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), | ||||
|                 LinearLayoutManager.VERTICAL, false); | ||||
|         mRecyclerView.setLayoutManager(layoutManager); | ||||
|  | ||||
|         mHistoryAdapter = createAdapter(); | ||||
|         mHistoryAdapter.setOnHistoryItemClickListener(this); | ||||
|         mRecyclerView.setAdapter(mHistoryAdapter); | ||||
|         ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mHistoryItemSwipeCallback); | ||||
|         itemTouchHelper.attachToRecyclerView(mRecyclerView); | ||||
|         mDisabledView = rootView.findViewById(R.id.history_disabled_view); | ||||
|         mEmptyHistoryView = rootView.findViewById(R.id.history_empty); | ||||
|  | ||||
| @@ -256,11 +232,16 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|  | ||||
|         if (disposables != null) disposables.dispose(); | ||||
|         if (historySubscription != null) historySubscription.cancel(); | ||||
|  | ||||
|         mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); | ||||
|         mSharedPreferences = null; | ||||
|         mHistoryIsEnabledChangeListener = null; | ||||
|         mHistoryIsEnabledKey = null; | ||||
|         mHistoryDataSource = null; | ||||
|         historySubscription = null; | ||||
|         disposables = null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -290,15 +271,8 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a new history DAO | ||||
|      * | ||||
|      * @return the history DAO | ||||
|      */ | ||||
|     @NonNull | ||||
|     protected abstract HistoryDAO<E> createHistoryDAO(); | ||||
|  | ||||
|     private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener { | ||||
|     private class HistoryIsEnabledChangeListener | ||||
|             implements SharedPreferences.OnSharedPreferenceChangeListener { | ||||
|         @Override | ||||
|         public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { | ||||
|             if (key.equals(mHistoryIsEnabledKey)) { | ||||
|   | ||||
| @@ -0,0 +1,191 @@ | ||||
| package org.schabi.newpipe.history; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
|  | ||||
| import org.schabi.newpipe.NewPipeDatabase; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.AppDatabase; | ||||
| import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; | ||||
| import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; | ||||
| import org.schabi.newpipe.database.history.model.SearchHistoryEntry; | ||||
| import org.schabi.newpipe.database.history.model.StreamHistoryEntity; | ||||
| import org.schabi.newpipe.database.history.model.StreamHistoryEntry; | ||||
| import org.schabi.newpipe.database.stream.StreamStatisticsEntry; | ||||
| import org.schabi.newpipe.database.stream.dao.StreamDAO; | ||||
| import org.schabi.newpipe.database.stream.dao.StreamStateDAO; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.database.stream.model.StreamStateEntity; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.Flowable; | ||||
| import io.reactivex.Maybe; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
|  | ||||
| public class HistoryRecordManager { | ||||
|  | ||||
|     private final AppDatabase database; | ||||
|     private final StreamDAO streamTable; | ||||
|     private final StreamHistoryDAO streamHistoryTable; | ||||
|     private final SearchHistoryDAO searchHistoryTable; | ||||
|     private final StreamStateDAO streamStateTable; | ||||
|     private final SharedPreferences sharedPreferences; | ||||
|     private final String searchHistoryKey; | ||||
|     private final String streamHistoryKey; | ||||
|  | ||||
|     public HistoryRecordManager(final Context context) { | ||||
|         database = NewPipeDatabase.getInstance(context); | ||||
|         streamTable = database.streamDAO(); | ||||
|         streamHistoryTable = database.streamHistoryDAO(); | ||||
|         searchHistoryTable = database.searchHistoryDAO(); | ||||
|         streamStateTable = database.streamStateDAO(); | ||||
|         sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|         searchHistoryKey = context.getString(R.string.enable_search_history_key); | ||||
|         streamHistoryKey = context.getString(R.string.enable_watch_history_key); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////// | ||||
|     // Watch History | ||||
|     /////////////////////////////////////////////////////// | ||||
|  | ||||
|     public Maybe<Long> onViewed(final StreamInfo info) { | ||||
|         if (!isStreamHistoryEnabled()) return Maybe.empty(); | ||||
|  | ||||
|         final Date currentTime = new Date(); | ||||
|         return Maybe.fromCallable(() -> database.runInTransaction(() -> { | ||||
|             final long streamId = streamTable.upsert(new StreamEntity(info)); | ||||
|             StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(); | ||||
|  | ||||
|             if (latestEntry != null && latestEntry.getStreamUid() == streamId) { | ||||
|                 streamHistoryTable.delete(latestEntry); | ||||
|                 latestEntry.setAccessDate(currentTime); | ||||
|                 latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1); | ||||
|                 return streamHistoryTable.insert(latestEntry); | ||||
|             } else { | ||||
|                 return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); | ||||
|             } | ||||
|         })).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Single<Integer> deleteStreamHistory(final long streamId) { | ||||
|         return Single.fromCallable(() -> streamHistoryTable.deleteStreamHistory(streamId)) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Flowable<List<StreamHistoryEntry>> getStreamHistory() { | ||||
|         return streamHistoryTable.getHistory().subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Flowable<List<StreamStatisticsEntry>> getStreamStatistics() { | ||||
|         return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Single<List<Long>> insertStreamHistory(final Collection<StreamHistoryEntry> entries) { | ||||
|         List<StreamHistoryEntity> entities = new ArrayList<>(entries.size()); | ||||
|         for (final StreamHistoryEntry entry : entries) { | ||||
|             entities.add(entry.toStreamHistoryEntity()); | ||||
|         } | ||||
|         return Single.fromCallable(() -> streamHistoryTable.insertAll(entities)) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Single<Integer> deleteStreamHistory(final Collection<StreamHistoryEntry> entries) { | ||||
|         List<StreamHistoryEntity> entities = new ArrayList<>(entries.size()); | ||||
|         for (final StreamHistoryEntry entry : entries) { | ||||
|             entities.add(entry.toStreamHistoryEntity()); | ||||
|         } | ||||
|         return Single.fromCallable(() -> streamHistoryTable.delete(entities)) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     private boolean isStreamHistoryEnabled() { | ||||
|         return sharedPreferences.getBoolean(streamHistoryKey, false); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////// | ||||
|     // Search History | ||||
|     /////////////////////////////////////////////////////// | ||||
|  | ||||
|     public Single<List<Long>> insertSearches(final Collection<SearchHistoryEntry> entries) { | ||||
|         return Single.fromCallable(() -> searchHistoryTable.insertAll(entries)) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Single<Integer> deleteSearches(final Collection<SearchHistoryEntry> entries) { | ||||
|         return Single.fromCallable(() -> searchHistoryTable.delete(entries)) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Flowable<List<SearchHistoryEntry>> getSearchHistory() { | ||||
|         return searchHistoryTable.getAll(); | ||||
|     } | ||||
|  | ||||
|     public Maybe<Long> onSearched(final int serviceId, final String search) { | ||||
|         if (!isSearchHistoryEnabled()) return Maybe.empty(); | ||||
|  | ||||
|         final Date currentTime = new Date(); | ||||
|         final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search); | ||||
|  | ||||
|         return Maybe.fromCallable(() -> database.runInTransaction(() -> { | ||||
|             SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry(); | ||||
|             if (latestEntry != null && latestEntry.hasEqualValues(newEntry)) { | ||||
|                 latestEntry.setCreationDate(currentTime); | ||||
|                 return (long) searchHistoryTable.update(latestEntry); | ||||
|             } else { | ||||
|                 return searchHistoryTable.insert(newEntry); | ||||
|             } | ||||
|         })).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Single<Integer> deleteSearchHistory(final String search) { | ||||
|         return Single.fromCallable(() -> searchHistoryTable.deleteAllWhereQuery(search)) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Flowable<List<SearchHistoryEntry>> getRelatedSearches(final String query, | ||||
|                                                                  final int similarQueryLimit, | ||||
|                                                                  final int uniqueQueryLimit) { | ||||
|         return query.length() > 0 | ||||
|                 ? searchHistoryTable.getSimilarEntries(query, similarQueryLimit) | ||||
|                 : searchHistoryTable.getUniqueEntries(uniqueQueryLimit); | ||||
|     } | ||||
|  | ||||
|     private boolean isSearchHistoryEnabled() { | ||||
|         return sharedPreferences.getBoolean(searchHistoryKey, false); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////// | ||||
|     // Stream State History | ||||
|     /////////////////////////////////////////////////////// | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) { | ||||
|         return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info))) | ||||
|                 .flatMap(streamId -> streamStateTable.getState(streamId).firstElement()) | ||||
|                 .flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0))) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Maybe<Long> saveStreamState(@NonNull final StreamInfo info, final long progressTime) { | ||||
|         return Maybe.fromCallable(() -> database.runInTransaction(() -> { | ||||
|             final long streamId = streamTable.upsert(new StreamEntity(info)); | ||||
|             return streamStateTable.upsert(new StreamStateEntity(streamId, progressTime)); | ||||
|         })).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////// | ||||
|     // Utility | ||||
|     /////////////////////////////////////////////////////// | ||||
|  | ||||
|     public Single<Integer> removeOrphanedRecords() { | ||||
|         return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
| } | ||||
| @@ -5,22 +5,30 @@ import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.annotation.StringRes; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.support.v7.widget.helper.ItemTouchHelper; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import org.schabi.newpipe.NewPipeDatabase; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.history.dao.HistoryDAO; | ||||
| import org.schabi.newpipe.database.history.model.SearchHistoryEntry; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
|  | ||||
| public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> { | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
|     private static int allowedSwipeToDeleteDirections = ItemTouchHelper.RIGHT; | ||||
| import io.reactivex.Flowable; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.Disposable; | ||||
|  | ||||
| public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> { | ||||
|  | ||||
|     @NonNull | ||||
|     public static SearchHistoryFragment newInstance() { | ||||
| @@ -30,7 +38,6 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> { | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         historyItemSwipeCallback(allowedSwipeToDeleteDirections); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
| @@ -39,38 +46,82 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> { | ||||
|         return new SearchHistoryAdapter(getContext()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Single<List<Long>> insert(Collection<SearchHistoryEntry> entries) { | ||||
|         return historyRecordManager.insertSearches(entries); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Single<Integer> delete(Collection<SearchHistoryEntry> entries) { | ||||
|         return historyRecordManager.deleteSearches(entries); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     protected Flowable<List<SearchHistoryEntry>> getAll() { | ||||
|         return historyRecordManager.getSearchHistory(); | ||||
|     } | ||||
|  | ||||
|     @StringRes | ||||
|     @Override | ||||
|     int getEnabledConfigKey() { | ||||
|         return R.string.enable_search_history_key; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     protected HistoryDAO<SearchHistoryEntry> createHistoryDAO() { | ||||
|         return NewPipeDatabase.getInstance().searchHistoryDAO(); | ||||
|     public void onHistoryItemClick(final SearchHistoryEntry historyItem) { | ||||
|         NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), | ||||
|                 historyItem.getSearch()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onHistoryItemClick(SearchHistoryEntry historyItem) { | ||||
|         NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), historyItem.getSearch()); | ||||
|     public void onHistoryItemLongClick(final SearchHistoryEntry item) { | ||||
|         if (activity == null) return; | ||||
|  | ||||
|         new AlertDialog.Builder(activity) | ||||
|                 .setTitle(item.getSearch()) | ||||
|                 .setMessage(R.string.delete_item_search_history) | ||||
|                 .setCancelable(true) | ||||
|                 .setNeutralButton(R.string.cancel, null) | ||||
|                 .setPositiveButton(R.string.delete_one, (dialog, i) -> { | ||||
|                     final Disposable onDelete = historyRecordManager | ||||
|                             .deleteSearches(Collections.singleton(item)) | ||||
|                             .observeOn(AndroidSchedulers.mainThread()) | ||||
|                             .subscribe( | ||||
|                                     ignored -> {/*successful*/}, | ||||
|                                     error -> Log.e(TAG, "Search history Delete One failed:", error) | ||||
|                             ); | ||||
|                     disposables.add(onDelete); | ||||
|                     makeSnackbar(R.string.item_deleted); | ||||
|                 }) | ||||
|                 .setNegativeButton(R.string.delete_all, (dialog, i) -> { | ||||
|                     final Disposable onDeleteAll = historyRecordManager | ||||
|                             .deleteSearchHistory(item.getSearch()) | ||||
|                             .observeOn(AndroidSchedulers.mainThread()) | ||||
|                             .subscribe( | ||||
|                                     ignored -> {/*successful*/}, | ||||
|                                     error -> Log.e(TAG, "Search history Delete All failed:", error) | ||||
|                             ); | ||||
|                     disposables.add(onDeleteAll); | ||||
|                     makeSnackbar(R.string.item_deleted); | ||||
|                 }) | ||||
|                 .show(); | ||||
|     } | ||||
|  | ||||
|     private static class ViewHolder extends RecyclerView.ViewHolder { | ||||
|         private final TextView search; | ||||
|         private final TextView time; | ||||
|         private final TextView info; | ||||
|  | ||||
|         public ViewHolder(View itemView) { | ||||
|             super(itemView); | ||||
|             search = itemView.findViewById(R.id.search); | ||||
|             time = itemView.findViewById(R.id.time); | ||||
|             info = itemView.findViewById(R.id.info); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected class SearchHistoryAdapter extends HistoryEntryAdapter<SearchHistoryEntry, ViewHolder> { | ||||
|  | ||||
|  | ||||
|         public SearchHistoryAdapter(Context context) { | ||||
|         SearchHistoryAdapter(Context context) { | ||||
|             super(context); | ||||
|         } | ||||
|  | ||||
| @@ -84,7 +135,11 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> { | ||||
|         @Override | ||||
|         void onBindViewHolder(ViewHolder holder, SearchHistoryEntry entry, int position) { | ||||
|             holder.search.setText(entry.getSearch()); | ||||
|             holder.time.setText(getFormattedDate(entry.getCreationDate())); | ||||
|  | ||||
|             final String info = Localization.concatenateStrings( | ||||
|                     getFormattedDate(entry.getCreationDate()), | ||||
|                     NewPipe.getNameOfService(entry.getServiceId())); | ||||
|             holder.info.setText(info); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,170 @@ | ||||
| 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.app.AlertDialog; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.util.Log; | ||||
| 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.R; | ||||
| import org.schabi.newpipe.database.history.model.StreamHistoryEntry; | ||||
| import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
|  | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| import io.reactivex.Flowable; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.Disposable; | ||||
|  | ||||
|  | ||||
| public class WatchHistoryFragment extends HistoryFragment<StreamHistoryEntry> { | ||||
|  | ||||
|     @NonNull | ||||
|     public static WatchHistoryFragment newInstance() { | ||||
|         return new WatchHistoryFragment(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|     } | ||||
|  | ||||
|     @StringRes | ||||
|     @Override | ||||
|     int getEnabledConfigKey() { | ||||
|         return R.string.enable_watch_history_key; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     protected StreamHistoryAdapter createAdapter() { | ||||
|         return new StreamHistoryAdapter(getContext()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Single<List<Long>> insert(Collection<StreamHistoryEntry> entries) { | ||||
|         return historyRecordManager.insertStreamHistory(entries); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Single<Integer> delete(Collection<StreamHistoryEntry> entries) { | ||||
|         return historyRecordManager.deleteStreamHistory(entries); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     protected Flowable<List<StreamHistoryEntry>> getAll() { | ||||
|         return historyRecordManager.getStreamHistory(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onHistoryItemClick(StreamHistoryEntry historyItem) { | ||||
|         NavigationHelper.openVideoDetail(getContext(), historyItem.serviceId, historyItem.url, | ||||
|                 historyItem.title); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onHistoryItemLongClick(StreamHistoryEntry item) { | ||||
|         new AlertDialog.Builder(activity) | ||||
|                 .setTitle(item.title) | ||||
|                 .setMessage(R.string.delete_stream_history_prompt) | ||||
|                 .setCancelable(true) | ||||
|                 .setNeutralButton(R.string.cancel, null) | ||||
|                 .setPositiveButton(R.string.delete_one, (dialog, i) -> { | ||||
|                     final Disposable onDelete = historyRecordManager | ||||
|                             .deleteStreamHistory(Collections.singleton(item)) | ||||
|                             .observeOn(AndroidSchedulers.mainThread()) | ||||
|                             .subscribe( | ||||
|                                     ignored -> {/*successful*/}, | ||||
|                                     error -> Log.e(TAG, "Watch history Delete One failed:", error) | ||||
|                             ); | ||||
|                     disposables.add(onDelete); | ||||
|                     makeSnackbar(R.string.item_deleted); | ||||
|                 }) | ||||
|                 .setNegativeButton(R.string.delete_all, (dialog, i) -> { | ||||
|                     final Disposable onDeleteAll = historyRecordManager | ||||
|                             .deleteStreamHistory(item.streamId) | ||||
|                             .observeOn(AndroidSchedulers.mainThread()) | ||||
|                             .subscribe( | ||||
|                                     ignored -> {/*successful*/}, | ||||
|                                     error -> Log.e(TAG, "Watch history Delete All failed:", error) | ||||
|                             ); | ||||
|                     disposables.add(onDeleteAll); | ||||
|                     makeSnackbar(R.string.item_deleted); | ||||
|                 }) | ||||
|                 .show(); | ||||
|     } | ||||
|  | ||||
|     private static class StreamHistoryAdapter extends HistoryEntryAdapter<StreamHistoryEntry, ViewHolder> { | ||||
|  | ||||
|         StreamHistoryAdapter(Context context) { | ||||
|             super(context); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { | ||||
|             LayoutInflater inflater = LayoutInflater.from(parent.getContext()); | ||||
|             View itemView = inflater.inflate(R.layout.list_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, StreamHistoryEntry entry, int position) { | ||||
|             final String formattedDate = getFormattedDate(entry.accessDate); | ||||
|             final String info; | ||||
|             if (entry.repeatCount > 1) { | ||||
|                 info = Localization.concatenateStrings(formattedDate, | ||||
|                         getFormattedViewString(entry.repeatCount)); | ||||
|             } else { | ||||
|                 info = formattedDate; | ||||
|             } | ||||
|  | ||||
|             holder.info.setText(info); | ||||
|             holder.streamTitle.setText(entry.title); | ||||
|             holder.uploader.setText(entry.uploader); | ||||
|             holder.duration.setText(Localization.getDurationString(entry.duration)); | ||||
|             ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView, | ||||
|                     StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class ViewHolder extends RecyclerView.ViewHolder { | ||||
|         private final TextView info; | ||||
|         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); | ||||
|             info = itemView.findViewById(R.id.itemAdditionalDetails); | ||||
|             streamTitle = itemView.findViewById(R.id.itemVideoTitleView); | ||||
|             uploader = itemView.findViewById(R.id.itemUploaderView); | ||||
|             duration = itemView.findViewById(R.id.itemDurationView); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,116 +0,0 @@ | ||||
| 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.support.v7.widget.helper.ItemTouchHelper; | ||||
| 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.info_list.holder.StreamInfoItemHolder; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
|  | ||||
|  | ||||
| public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> { | ||||
|  | ||||
|     private static int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT; | ||||
|  | ||||
|     @NonNull | ||||
|     public static WatchedHistoryFragment newInstance() { | ||||
|         return new WatchedHistoryFragment(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         historyItemSwipeCallback(allowedSwipeToDeleteDirections); | ||||
|     } | ||||
|  | ||||
|     @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() { | ||||
|         return NewPipeDatabase.getInstance().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.list_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(Localization.getDurationString(entry.getDuration())); | ||||
|             ImageLoader.getInstance() | ||||
|                     .displayImage(entry.getThumbnailURL(), holder.thumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -16,8 +16,10 @@ import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.InfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
|  | ||||
| /* | ||||
|  * Created by Christian Schabesberger on 26.09.16. | ||||
| @@ -42,17 +44,12 @@ import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; | ||||
| public class InfoItemBuilder { | ||||
|     private static final String TAG = InfoItemBuilder.class.toString(); | ||||
|  | ||||
|     public interface OnInfoItemSelectedListener<T extends InfoItem> { | ||||
|         void selected(T selectedItem); | ||||
|         void held(T selectedItem); | ||||
|     } | ||||
|  | ||||
|     private final Context context; | ||||
|     private ImageLoader imageLoader = ImageLoader.getInstance(); | ||||
|  | ||||
|     private OnInfoItemSelectedListener<StreamInfoItem> onStreamSelectedListener; | ||||
|     private OnInfoItemSelectedListener<ChannelInfoItem> onChannelSelectedListener; | ||||
|     private OnInfoItemSelectedListener<PlaylistInfoItem> onPlaylistSelectedListener; | ||||
|     private OnClickGesture<StreamInfoItem> onStreamSelectedListener; | ||||
|     private OnClickGesture<ChannelInfoItem> onChannelSelectedListener; | ||||
|     private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener; | ||||
|  | ||||
|     public InfoItemBuilder(Context context) { | ||||
|         this.context = context; | ||||
| @@ -75,7 +72,7 @@ public class InfoItemBuilder { | ||||
|             case CHANNEL: | ||||
|                 return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent); | ||||
|             case PLAYLIST: | ||||
|                 return new PlaylistInfoItemHolder(this, parent); | ||||
|                 return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent); | ||||
|             default: | ||||
|                 Log.e(TAG, "Trollolo"); | ||||
|                 throw new RuntimeException("InfoType not expected = " + infoType.name()); | ||||
| @@ -90,27 +87,27 @@ public class InfoItemBuilder { | ||||
|         return imageLoader; | ||||
|     } | ||||
|  | ||||
|     public OnInfoItemSelectedListener<StreamInfoItem> getOnStreamSelectedListener() { | ||||
|     public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() { | ||||
|         return onStreamSelectedListener; | ||||
|     } | ||||
|  | ||||
|     public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) { | ||||
|     public void setOnStreamSelectedListener(OnClickGesture<StreamInfoItem> listener) { | ||||
|         this.onStreamSelectedListener = listener; | ||||
|     } | ||||
|  | ||||
|     public OnInfoItemSelectedListener<ChannelInfoItem> getOnChannelSelectedListener() { | ||||
|     public OnClickGesture<ChannelInfoItem> getOnChannelSelectedListener() { | ||||
|         return onChannelSelectedListener; | ||||
|     } | ||||
|  | ||||
|     public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) { | ||||
|     public void setOnChannelSelectedListener(OnClickGesture<ChannelInfoItem> listener) { | ||||
|         this.onChannelSelectedListener = listener; | ||||
|     } | ||||
|  | ||||
|     public OnInfoItemSelectedListener<PlaylistInfoItem> getOnPlaylistSelectedListener() { | ||||
|     public OnClickGesture<PlaylistInfoItem> getOnPlaylistSelectedListener() { | ||||
|         return onPlaylistSelectedListener; | ||||
|     } | ||||
|  | ||||
|     public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) { | ||||
|     public void setOnPlaylistSelectedListener(OnClickGesture<PlaylistInfoItem> listener) { | ||||
|         this.onPlaylistSelectedListener = listener; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -19,7 +19,7 @@ public class InfoItemDialog { | ||||
|                           @NonNull final StreamInfoItem info, | ||||
|                           @NonNull final String[] commands, | ||||
|                           @NonNull final DialogInterface.OnClickListener actions) { | ||||
|         this(activity, commands, actions, info.getName(), info.uploader_name); | ||||
|         this(activity, commands, actions, info.getName(), info.getUploaderName()); | ||||
|     } | ||||
|  | ||||
|     public InfoItemDialog(@NonNull final Activity activity, | ||||
| @@ -28,8 +28,7 @@ public class InfoItemDialog { | ||||
|                           @NonNull final String title, | ||||
|                           @Nullable final String additionalDetail) { | ||||
|  | ||||
|         final LayoutInflater inflater = activity.getLayoutInflater(); | ||||
|         final View bannerView = inflater.inflate(R.layout.dialog_title, null); | ||||
|         final View bannerView = View.inflate(activity, R.layout.dialog_title, null); | ||||
|         bannerView.setSelected(true); | ||||
|  | ||||
|         TextView titleView = bannerView.findViewById(R.id.itemTitleView); | ||||
|   | ||||
| @@ -10,13 +10,14 @@ import org.schabi.newpipe.extractor.InfoItem; | ||||
| import org.schabi.newpipe.extractor.channel.ChannelInfoItem; | ||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.info_list.InfoItemBuilder.OnInfoItemSelectedListener; | ||||
| import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.InfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; | ||||
| import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| @@ -52,6 +53,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | ||||
|     private static final int STREAM_HOLDER_TYPE = 0x101; | ||||
|     private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200; | ||||
|     private static final int CHANNEL_HOLDER_TYPE = 0x201; | ||||
|     private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300; | ||||
|     private static final int PLAYLIST_HOLDER_TYPE = 0x301; | ||||
|  | ||||
|     private final InfoItemBuilder infoItemBuilder; | ||||
| @@ -75,15 +77,15 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | ||||
|         infoItemList = new ArrayList<>(); | ||||
|     } | ||||
|  | ||||
|     public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) { | ||||
|     public void setOnStreamSelectedListener(OnClickGesture<StreamInfoItem> listener) { | ||||
|         infoItemBuilder.setOnStreamSelectedListener(listener); | ||||
|     } | ||||
|  | ||||
|     public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) { | ||||
|     public void setOnChannelSelectedListener(OnClickGesture<ChannelInfoItem> listener) { | ||||
|         infoItemBuilder.setOnChannelSelectedListener(listener); | ||||
|     } | ||||
|  | ||||
|     public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) { | ||||
|     public void setOnPlaylistSelectedListener(OnClickGesture<PlaylistInfoItem> listener) { | ||||
|         infoItemBuilder.setOnPlaylistSelectedListener(listener); | ||||
|     } | ||||
|  | ||||
| @@ -200,14 +202,14 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | ||||
|         if (footer != null && position == infoItemList.size() && showFooter) { | ||||
|             return FOOTER_TYPE; | ||||
|         } | ||||
|         InfoItem item = infoItemList.get(position); | ||||
|         final InfoItem item = infoItemList.get(position); | ||||
|         switch (item.info_type) { | ||||
|             case STREAM: | ||||
|                 return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE; | ||||
|             case CHANNEL: | ||||
|                 return useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE; | ||||
|             case PLAYLIST: | ||||
|                 return PLAYLIST_HOLDER_TYPE; | ||||
|                 return useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE; | ||||
|             default: | ||||
|                 Log.e(TAG, "Trollolo"); | ||||
|                 return -1; | ||||
| @@ -230,6 +232,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | ||||
|                 return new ChannelMiniInfoItemHolder(infoItemBuilder, parent); | ||||
|             case CHANNEL_HOLDER_TYPE: | ||||
|                 return new ChannelInfoItemHolder(infoItemBuilder, parent); | ||||
|             case MINI_PLAYLIST_HOLDER_TYPE: | ||||
|                 return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent); | ||||
|             case PLAYLIST_HOLDER_TYPE: | ||||
|                 return new PlaylistInfoItemHolder(infoItemBuilder, parent); | ||||
|             default: | ||||
|   | ||||
| @@ -1,62 +1,13 @@ | ||||
| package org.schabi.newpipe.info_list.holder; | ||||
|  | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.InfoItem; | ||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; | ||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||
|  | ||||
| public class PlaylistInfoItemHolder extends InfoItemHolder { | ||||
|     public final ImageView itemThumbnailView; | ||||
|     public final TextView itemStreamCountView; | ||||
|     public final TextView itemTitleView; | ||||
|     public final TextView itemUploaderView; | ||||
| public class PlaylistInfoItemHolder extends PlaylistMiniInfoItemHolder { | ||||
|  | ||||
|     public PlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { | ||||
|         super(infoItemBuilder, R.layout.list_playlist_item, parent); | ||||
|  | ||||
|         itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); | ||||
|         itemTitleView = itemView.findViewById(R.id.itemTitleView); | ||||
|         itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); | ||||
|         itemUploaderView = itemView.findViewById(R.id.itemUploaderView); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void updateFromItem(final InfoItem infoItem) { | ||||
|         if (!(infoItem instanceof PlaylistInfoItem)) return; | ||||
|         final PlaylistInfoItem item = (PlaylistInfoItem) infoItem; | ||||
|  | ||||
|         itemTitleView.setText(item.getName()); | ||||
|         itemStreamCountView.setText(item.stream_count + ""); | ||||
|         itemUploaderView.setText(item.uploader_name); | ||||
|  | ||||
|         itemBuilder.getImageLoader() | ||||
|                 .displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); | ||||
|  | ||||
|         itemView.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View view) { | ||||
|                 if (itemBuilder.getOnPlaylistSelectedListener() != null) { | ||||
|                     itemBuilder.getOnPlaylistSelectedListener().selected(item); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Display options for playlist thumbnails | ||||
|      */ | ||||
|     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = | ||||
|             new DisplayImageOptions.Builder() | ||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) | ||||
|                     .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) | ||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) | ||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail_playlist) | ||||
|                     .build(); | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,70 @@ | ||||
| package org.schabi.newpipe.info_list.holder; | ||||
|  | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.InfoItem; | ||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; | ||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||
|  | ||||
| public class PlaylistMiniInfoItemHolder extends InfoItemHolder { | ||||
|     public final ImageView itemThumbnailView; | ||||
|     public final TextView itemStreamCountView; | ||||
|     public final TextView itemTitleView; | ||||
|     public final TextView itemUploaderView; | ||||
|  | ||||
|     public PlaylistMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { | ||||
|         super(infoItemBuilder, layoutId, parent); | ||||
|  | ||||
|         itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); | ||||
|         itemTitleView = itemView.findViewById(R.id.itemTitleView); | ||||
|         itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); | ||||
|         itemUploaderView = itemView.findViewById(R.id.itemUploaderView); | ||||
|     } | ||||
|  | ||||
|     public PlaylistMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { | ||||
|         this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void updateFromItem(final InfoItem infoItem) { | ||||
|         if (!(infoItem instanceof PlaylistInfoItem)) return; | ||||
|         final PlaylistInfoItem item = (PlaylistInfoItem) infoItem; | ||||
|  | ||||
|         itemTitleView.setText(item.getName()); | ||||
|         itemStreamCountView.setText(String.valueOf(item.getStreamCount())); | ||||
|         itemUploaderView.setText(item.getUploaderName()); | ||||
|  | ||||
|         itemBuilder.getImageLoader() | ||||
|                 .displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); | ||||
|  | ||||
|         itemView.setOnClickListener(view -> { | ||||
|             if (itemBuilder.getOnPlaylistSelectedListener() != null) { | ||||
|                 itemBuilder.getOnPlaylistSelectedListener().selected(item); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         itemView.setLongClickable(true); | ||||
|         itemView.setOnLongClickListener(view -> { | ||||
|             if (itemBuilder.getOnPlaylistSelectedListener() != null) { | ||||
|                 itemBuilder.getOnPlaylistSelectedListener().held(item); | ||||
|             } | ||||
|             return true; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Display options for playlist thumbnails | ||||
|      */ | ||||
|     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = | ||||
|             new DisplayImageOptions.Builder() | ||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) | ||||
|                     .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) | ||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) | ||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail_playlist) | ||||
|                     .build(); | ||||
| } | ||||
| @@ -63,6 +63,7 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.history.HistoryRecordManager; | ||||
| import org.schabi.newpipe.player.helper.AudioReactor; | ||||
| import org.schabi.newpipe.player.helper.CacheFactory; | ||||
| import org.schabi.newpipe.player.helper.LoadController; | ||||
| @@ -77,9 +78,8 @@ import java.util.concurrent.TimeUnit; | ||||
|  | ||||
| import io.reactivex.Observable; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import io.reactivex.functions.Consumer; | ||||
| import io.reactivex.functions.Predicate; | ||||
|  | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; | ||||
|  | ||||
| @@ -147,6 +147,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|     protected DefaultExtractorsFactory extractorsFactory; | ||||
|  | ||||
|     protected Disposable progressUpdateReactor; | ||||
|     protected CompositeDisposable databaseUpdateReactor; | ||||
|  | ||||
|     protected HistoryRecordManager recordManager; | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
| @@ -172,6 +175,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|     public void initPlayer() { | ||||
|         if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); | ||||
|  | ||||
|         if (recordManager == null) recordManager = new HistoryRecordManager(context); | ||||
|         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); | ||||
|         databaseUpdateReactor = new CompositeDisposable(); | ||||
|  | ||||
|         final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); | ||||
|         final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); | ||||
|         final LoadControl loadControl = new LoadController(context); | ||||
| @@ -193,18 +200,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|     private Disposable getProgressReactor() { | ||||
|         return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .filter(new Predicate<Long>() { | ||||
|                     @Override | ||||
|                     public boolean test(Long aLong) throws Exception { | ||||
|                         return isProgressLoopRunning(); | ||||
|                     } | ||||
|                 }) | ||||
|                 .subscribe(new Consumer<Long>() { | ||||
|                     @Override | ||||
|                     public void accept(Long aLong) throws Exception { | ||||
|                         triggerProgressUpdate(); | ||||
|                     } | ||||
|                 }); | ||||
|                 .filter(ignored -> isProgressLoopRunning()) | ||||
|                 .subscribe(ignored -> triggerProgressUpdate()); | ||||
|     } | ||||
|  | ||||
|     public void handleIntent(Intent intent) { | ||||
| @@ -281,6 +278,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|         if (playQueue != null) playQueue.dispose(); | ||||
|         if (playbackManager != null) playbackManager.dispose(); | ||||
|         if (audioReactor != null) audioReactor.abandonAudioFocus(); | ||||
|         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); | ||||
|     } | ||||
|  | ||||
|     public void destroy() { | ||||
| @@ -291,6 +289,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|  | ||||
|         trackSelector = null; | ||||
|         simpleExoPlayer = null; | ||||
|         recordManager = null; | ||||
|     } | ||||
|  | ||||
|     public MediaSource buildMediaSource(String url, String overrideExtension) { | ||||
| @@ -582,6 +581,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|             errorToast = null; | ||||
|         } | ||||
|  | ||||
|         savePlaybackState(); | ||||
|  | ||||
|         switch (error.type) { | ||||
|             case ExoPlaybackException.TYPE_SOURCE: | ||||
|                 if (simpleExoPlayer.getCurrentPosition() < | ||||
| @@ -612,7 +613,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|         // If the user selects a new track, then the discontinuity occurs after the index is changed. | ||||
|         // Therefore, the only source that causes a discrepancy would be gapless transition, | ||||
|         // which can only offset the current track by +1. | ||||
|         if (newWindowIndex == playQueue.getIndex() + 1) { | ||||
|         if (newWindowIndex == playQueue.getIndex() + 1 || | ||||
|                 (newWindowIndex == 0 && playQueue.getIndex() == playQueue.size() - 1)) { | ||||
|             playQueue.offsetIndex(+1); | ||||
|         } | ||||
|         playbackManager.load(); | ||||
| @@ -668,10 +670,17 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|                     "], queue index=[" + playQueue.getIndex() + "]"); | ||||
|         } else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) { | ||||
|             final long startPos = info != null ? info.start_position : 0; | ||||
|             if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos)); | ||||
|             if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + | ||||
|                     " at: " + getTimeString((int)startPos)); | ||||
|             simpleExoPlayer.seekTo(currentSourceIndex, startPos); | ||||
|         } | ||||
|  | ||||
|         // TODO: update exoplayer to 2.6.x in order to register view count on repeated streams | ||||
|         databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() | ||||
|                 .subscribe( | ||||
|                         ignored -> {/* successful */}, | ||||
|                         error -> Log.e(TAG, "Player onViewed() failure: ", error) | ||||
|                 )); | ||||
|         initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); | ||||
|     } | ||||
|  | ||||
| @@ -755,6 +764,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|         if (simpleExoPlayer == null || playQueue == null) return; | ||||
|         if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); | ||||
|  | ||||
|         savePlaybackState(); | ||||
|  | ||||
|         /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track. | ||||
|         * Also restart the track if the current track is the first in a queue.*/ | ||||
|         if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) { | ||||
| @@ -769,6 +780,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|         if (playQueue == null) return; | ||||
|         if (DEBUG) Log.d(TAG, "onPlayNext() called"); | ||||
|  | ||||
|         savePlaybackState(); | ||||
|  | ||||
|         playQueue.offsetIndex(+1); | ||||
|     } | ||||
|  | ||||
| @@ -830,6 +843,27 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     protected void savePlaybackState(final StreamInfo info, final long progress) { | ||||
|         if (context == null || info == null || databaseUpdateReactor == null) return; | ||||
|         final Disposable stateSaver = recordManager.saveStreamState(info, progress) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .onErrorComplete() | ||||
|                 .subscribe( | ||||
|                         ignored -> {/* successful */}, | ||||
|                         error -> Log.e(TAG, "savePlaybackState() failure: ", error) | ||||
|                 ); | ||||
|         databaseUpdateReactor.add(stateSaver); | ||||
|     } | ||||
|  | ||||
|     private void savePlaybackState() { | ||||
|         if (simpleExoPlayer == null || currentInfo == null) return; | ||||
|  | ||||
|         if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD && | ||||
|                 simpleExoPlayer.getCurrentPosition() < | ||||
|                         simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { | ||||
|             savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); | ||||
|         } | ||||
|     } | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Getters and Setters | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|   | ||||
| @@ -35,7 +35,9 @@ import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.support.v7.widget.helper.ItemTouchHelper; | ||||
| import android.util.DisplayMetrics; | ||||
| import android.util.Log; | ||||
| import android.util.TypedValue; | ||||
| import android.view.GestureDetector; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| @@ -48,6 +50,8 @@ import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import com.google.android.exoplayer2.Player; | ||||
| import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | ||||
| import com.google.android.exoplayer2.ui.SubtitleView; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| @@ -61,7 +65,6 @@ import org.schabi.newpipe.util.AnimationUtils; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.PermissionHelper; | ||||
| import org.schabi.newpipe.util.PopupMenuIconHacker; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.util.List; | ||||
| @@ -194,7 +197,6 @@ public final class MainVideoPlayer extends Activity { | ||||
|         super.onConfigurationChanged(newConfig); | ||||
|  | ||||
|         if (playerImpl.isSomePopupMenuVisible()) { | ||||
|             playerImpl.moreOptionsPopupMenu.dismiss(); | ||||
|             playerImpl.getQualityPopupMenu().dismiss(); | ||||
|             playerImpl.getPlaybackSpeedPopupMenu().dismiss(); | ||||
|         } | ||||
| @@ -301,8 +303,11 @@ public final class MainVideoPlayer extends Activity { | ||||
|         private boolean queueVisible; | ||||
|  | ||||
|         private ImageButton moreOptionsButton; | ||||
|         public int moreOptionsPopupMenuGroupId = 89; | ||||
|         public PopupMenu moreOptionsPopupMenu; | ||||
|         private ImageButton toggleOrientationButton; | ||||
|         private ImageButton switchPopupButton; | ||||
|         private ImageButton switchBackgroundButton; | ||||
|  | ||||
|         private View secondaryControls; | ||||
|  | ||||
|         VideoPlayerImpl(final Context context) { | ||||
|             super("VideoPlayerImpl" + MainVideoPlayer.TAG, context); | ||||
| @@ -322,9 +327,12 @@ public final class MainVideoPlayer extends Activity { | ||||
|             this.playPauseButton = rootView.findViewById(R.id.playPauseButton); | ||||
|             this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton); | ||||
|             this.playNextButton = rootView.findViewById(R.id.playNextButton); | ||||
|  | ||||
|             this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton); | ||||
|             this.moreOptionsPopupMenu = new PopupMenu(context, moreOptionsButton); | ||||
|             buildMoreOptionsMenu(); | ||||
|             this.secondaryControls = rootView.findViewById(R.id.secondaryControls); | ||||
|             this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation); | ||||
|             this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground); | ||||
|             this.switchPopupButton = rootView.findViewById(R.id.switchPopup); | ||||
|  | ||||
|             titleTextView.setSelected(true); | ||||
|             channelTextView.setSelected(true); | ||||
| @@ -332,6 +340,24 @@ public final class MainVideoPlayer extends Activity { | ||||
|             getRootView().setKeepScreenOn(true); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void setupSubtitleView(@NonNull SubtitleView view, | ||||
|                                          @NonNull String captionSizeKey) { | ||||
|             final float captionRatioInverse; | ||||
|             if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) { | ||||
|                 captionRatioInverse = 22f; | ||||
|             } else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) { | ||||
|                 captionRatioInverse = 18f; | ||||
|             } else { | ||||
|                 captionRatioInverse = 20f; | ||||
|             } | ||||
|  | ||||
|             final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); | ||||
|             final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); | ||||
|             view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, | ||||
|                     (float) minimumLength / captionRatioInverse); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void initListeners() { | ||||
|             super.initListeners(); | ||||
| @@ -348,7 +374,11 @@ public final class MainVideoPlayer extends Activity { | ||||
|             playPauseButton.setOnClickListener(this); | ||||
|             playPreviousButton.setOnClickListener(this); | ||||
|             playNextButton.setOnClickListener(this); | ||||
|  | ||||
|             moreOptionsButton.setOnClickListener(this); | ||||
|             toggleOrientationButton.setOnClickListener(this); | ||||
|             switchBackgroundButton.setOnClickListener(this); | ||||
|             switchPopupButton.setOnClickListener(this); | ||||
|         } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -464,6 +494,16 @@ public final class MainVideoPlayer extends Activity { | ||||
|                 return; | ||||
|             } else if (v.getId() == moreOptionsButton.getId()) { | ||||
|                 onMoreOptionsClicked(); | ||||
|  | ||||
|             } else if (v.getId() == toggleOrientationButton.getId()) { | ||||
|                 onScreenRotationClicked(); | ||||
|  | ||||
|             } else if (v.getId() == switchPopupButton.getId()) { | ||||
|                 onFullScreenButtonClicked(); | ||||
|  | ||||
|             } else if (v.getId() == switchBackgroundButton.getId()) { | ||||
|                 onPlayBackgroundButtonClicked(); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             if (getCurrentState() != STATE_COMPLETED) { | ||||
| @@ -497,8 +537,15 @@ public final class MainVideoPlayer extends Activity { | ||||
|         private void onMoreOptionsClicked() { | ||||
|             if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); | ||||
|  | ||||
|             moreOptionsPopupMenu.show(); | ||||
|             isSomePopupMenuVisible = true; | ||||
|             if (secondaryControls.getVisibility() == View.VISIBLE) { | ||||
|                 moreOptionsButton.setImageDrawable(getResources().getDrawable( | ||||
|                         R.drawable.ic_expand_more_white_24dp)); | ||||
|                 animateView(secondaryControls, false, 200); | ||||
|             } else { | ||||
|                 moreOptionsButton.setImageDrawable(getResources().getDrawable( | ||||
|                         R.drawable.ic_expand_less_white_24dp)); | ||||
|                 animateView(secondaryControls, true, 200); | ||||
|             } | ||||
|             showControls(300); | ||||
|         } | ||||
|  | ||||
| @@ -522,6 +569,18 @@ public final class MainVideoPlayer extends Activity { | ||||
|             if (isPlaying()) hideControls(300, 0); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected int nextResizeMode(int currentResizeMode) { | ||||
|             switch (currentResizeMode) { | ||||
|                 case AspectRatioFrameLayout.RESIZE_MODE_FIT: | ||||
|                     return AspectRatioFrameLayout.RESIZE_MODE_FILL; | ||||
|                 case AspectRatioFrameLayout.RESIZE_MODE_FILL: | ||||
|                     return AspectRatioFrameLayout.RESIZE_MODE_ZOOM; | ||||
|                 default: | ||||
|                     return AspectRatioFrameLayout.RESIZE_MODE_FIT; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) { | ||||
|             return ListHelper.getDefaultResolutionIndex(context, sortedVideos); | ||||
| @@ -637,42 +696,6 @@ public final class MainVideoPlayer extends Activity { | ||||
|             setShuffleButton(shuffleButton, playQueue.isShuffled()); | ||||
|         } | ||||
|  | ||||
|         private void buildMoreOptionsMenu() { | ||||
|             this.moreOptionsPopupMenu.getMenuInflater().inflate(R.menu.menu_videooptions, | ||||
|                     moreOptionsPopupMenu.getMenu()); | ||||
|  | ||||
|             moreOptionsPopupMenu.setOnMenuItemClickListener(menuItem -> { | ||||
|                 switch (menuItem.getItemId()) { | ||||
|                     case R.id.toggleOrientation: | ||||
|                         onScreenRotationClicked(); | ||||
|                         break; | ||||
|                     case R.id.switchPopup: | ||||
|                         onFullScreenButtonClicked(); | ||||
|                         break; | ||||
|                     case R.id.switchBackground: | ||||
|                         onPlayBackgroundButtonClicked(); | ||||
|                         break; | ||||
|                 } | ||||
|                 return false; | ||||
|             }); | ||||
|  | ||||
|             try { | ||||
|                 PopupMenuIconHacker.setShowPopupIcon(moreOptionsPopupMenu); | ||||
|             } catch (Exception e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|  | ||||
|             // fix icon theme | ||||
|             if(ThemeHelper.isLightThemeSelected(MainVideoPlayer.this)) { | ||||
|                 moreOptionsPopupMenu.getMenu() | ||||
|                         .findItem(R.id.toggleOrientation) | ||||
|                         .setIcon(R.drawable.ic_screen_rotation_black_24dp); | ||||
|                 moreOptionsPopupMenu.getMenu() | ||||
|                         .findItem(R.id.switchPopup) | ||||
|                         .setIcon(R.drawable.ic_fullscreen_exit_black_24dp); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void buildQueue() { | ||||
|             queueLayout = findViewById(R.id.playQueuePanel); | ||||
|  | ||||
|   | ||||
| @@ -49,8 +49,11 @@ import android.widget.RemoteViews; | ||||
| import android.widget.SeekBar; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import com.google.android.exoplayer2.C; | ||||
| import com.google.android.exoplayer2.PlaybackParameters; | ||||
| import com.google.android.exoplayer2.Player; | ||||
| import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | ||||
| import com.google.android.exoplayer2.ui.SubtitleView; | ||||
|  | ||||
| import org.schabi.newpipe.BuildConfig; | ||||
| import org.schabi.newpipe.R; | ||||
| @@ -88,6 +91,8 @@ public final class PopupVideoPlayer extends Service { | ||||
|     private static final String POPUP_SAVED_X = "popup_saved_x"; | ||||
|     private static final String POPUP_SAVED_Y = "popup_saved_y"; | ||||
|  | ||||
|     private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; | ||||
|  | ||||
|     private WindowManager windowManager; | ||||
|     private WindowManager.LayoutParams windowLayoutParams; | ||||
|     private GestureDetector gestureDetector; | ||||
| @@ -358,10 +363,12 @@ public final class PopupVideoPlayer extends Service { | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     protected class VideoPlayerImpl extends VideoPlayer { | ||||
|     protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener { | ||||
|         private TextView resizingIndicator; | ||||
|         private ImageButton fullScreenButton; | ||||
|  | ||||
|         private View extraOptionsView; | ||||
|  | ||||
|         @Override | ||||
|         public void handleIntent(Intent intent) { | ||||
|             super.handleIntent(intent); | ||||
| @@ -380,6 +387,29 @@ public final class PopupVideoPlayer extends Service { | ||||
|             resizingIndicator = rootView.findViewById(R.id.resizing_indicator); | ||||
|             fullScreenButton = rootView.findViewById(R.id.fullScreenButton); | ||||
|             fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked()); | ||||
|  | ||||
|             extraOptionsView = rootView.findViewById(R.id.extraOptionsView); | ||||
|             rootView.addOnLayoutChangeListener(this); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void setupSubtitleView(@NonNull SubtitleView view, | ||||
|                                          @NonNull String captionSizeKey) { | ||||
|             float captionRatio = SubtitleView.DEFAULT_TEXT_SIZE_FRACTION; | ||||
|             if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) { | ||||
|                 captionRatio *= 0.9; | ||||
|             } else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) { | ||||
|                 captionRatio *= 1.1; | ||||
|             } | ||||
|             view.setFractionalTextSize(captionRatio); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onLayoutChange(final View view, int left, int top, int right, int bottom, | ||||
|                                    int oldLeft, int oldTop, int oldRight, int oldBottom) { | ||||
|             float widthDp = Math.abs(right - left) / getResources().getDisplayMetrics().density; | ||||
|             final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP ? View.VISIBLE : View.GONE; | ||||
|             extraOptionsView.setVisibility(visibility); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -438,6 +468,15 @@ public final class PopupVideoPlayer extends Service { | ||||
|             if (isPlaying()) hideControls(500, 0); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected int nextResizeMode(int resizeMode) { | ||||
|             if (resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { | ||||
|                 return AspectRatioFrameLayout.RESIZE_MODE_FIT; | ||||
|             } else { | ||||
|                 return AspectRatioFrameLayout.RESIZE_MODE_FILL; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onStopTrackingTouch(SeekBar seekBar) { | ||||
|             super.onStopTrackingTouch(seekBar); | ||||
| @@ -642,8 +681,8 @@ public final class PopupVideoPlayer extends Service { | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|         /*package-private*/ void enableVideoRenderer(final boolean enable) { | ||||
|             final int videoRendererIndex = getVideoRendererIndex(); | ||||
|             if (trackSelector != null && videoRendererIndex != -1) { | ||||
|             final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); | ||||
|             if (trackSelector != null && videoRendererIndex != RENDERER_UNAVAILABLE) { | ||||
|                 trackSelector.setRendererDisabled(videoRendererIndex, !enable); | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.Player; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | ||||
| import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; | ||||
| import org.schabi.newpipe.player.event.PlayerEventListener; | ||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | ||||
| import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | ||||
| @@ -149,8 +150,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | ||||
|             case android.R.id.home: | ||||
|                 finish(); | ||||
|                 return true; | ||||
|             case R.id.action_history: | ||||
|                 NavigationHelper.openHistory(this); | ||||
|             case R.id.action_append_playlist: | ||||
|                 appendToPlaylist(); | ||||
|                 return true; | ||||
|             case R.id.action_settings: | ||||
|                 NavigationHelper.openSettings(this); | ||||
| @@ -185,6 +186,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | ||||
|                 null | ||||
|         ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|     } | ||||
|  | ||||
|     private void appendToPlaylist() { | ||||
|         if (this.player != null && this.player.getPlayQueue() != null) { | ||||
|             PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams()) | ||||
|                     .show(getSupportFragmentManager(), getTag()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Service Connection | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|   | ||||
| @@ -29,8 +29,10 @@ import android.content.Intent; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.Color; | ||||
| import android.graphics.PorterDuff; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import android.os.Handler; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.content.ContextCompat; | ||||
| @@ -46,17 +48,25 @@ import android.widget.SeekBar; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import com.google.android.exoplayer2.C; | ||||
| import com.google.android.exoplayer2.Format; | ||||
| import com.google.android.exoplayer2.Player; | ||||
| import com.google.android.exoplayer2.SimpleExoPlayer; | ||||
| import com.google.android.exoplayer2.source.MediaSource; | ||||
| import com.google.android.exoplayer2.source.MergingMediaSource; | ||||
| import com.google.android.exoplayer2.source.SingleSampleMediaSource; | ||||
| import com.google.android.exoplayer2.source.TrackGroup; | ||||
| import com.google.android.exoplayer2.source.TrackGroupArray; | ||||
| import com.google.android.exoplayer2.trackselection.TrackSelectionArray; | ||||
| import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | ||||
| import com.google.android.exoplayer2.ui.SubtitleView; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.MediaFormat; | ||||
| import org.schabi.newpipe.extractor.Subtitles; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | ||||
| import org.schabi.newpipe.util.AnimationUtils; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
| @@ -64,6 +74,8 @@ import org.schabi.newpipe.util.ListHelper; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; | ||||
| import static com.google.android.exoplayer2.C.TIME_UNSET; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; | ||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||
| @@ -88,6 +100,7 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|     // Player | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     protected static final int RENDERER_UNAVAILABLE = -1; | ||||
|     public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000;  // 2 Seconds | ||||
|  | ||||
|     private ArrayList<VideoStream> availableStreams; | ||||
| @@ -123,6 +136,11 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|     private View topControlsRoot; | ||||
|     private TextView qualityTextView; | ||||
|  | ||||
|     private SubtitleView subtitleView; | ||||
|  | ||||
|     private TextView resizeView; | ||||
|     private TextView captionTextView; | ||||
|  | ||||
|     private ValueAnimator controlViewAnimator; | ||||
|     private Handler controlsVisibilityHandler = new Handler(); | ||||
|  | ||||
| @@ -133,6 +151,9 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|     private int playbackSpeedPopupMenuGroupId = 79; | ||||
|     private PopupMenu playbackSpeedPopupMenu; | ||||
|  | ||||
|     private int captionPopupMenuGroupId = 89; | ||||
|     private PopupMenu captionPopupMenu; | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     public VideoPlayer(String debugTag, Context context) { | ||||
| @@ -164,6 +185,17 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         this.topControlsRoot = rootView.findViewById(R.id.topControls); | ||||
|         this.qualityTextView = rootView.findViewById(R.id.qualityTextView); | ||||
|  | ||||
|         this.subtitleView = rootView.findViewById(R.id.subtitleView); | ||||
|         final String captionSizeKey = PreferenceManager.getDefaultSharedPreferences(context) | ||||
|                 .getString(context.getString(R.string.caption_size_key), | ||||
|                         context.getString(R.string.caption_size_default)); | ||||
|         setupSubtitleView(subtitleView, captionSizeKey); | ||||
|  | ||||
|         this.resizeView =  rootView.findViewById(R.id.resizeTextView); | ||||
|         resizeView.setText(PlayerHelper.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode())); | ||||
|  | ||||
|         this.captionTextView = rootView.findViewById(R.id.captionTextView); | ||||
|  | ||||
|         //this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f); | ||||
|  | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) | ||||
| @@ -172,25 +204,37 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|  | ||||
|         this.qualityPopupMenu = new PopupMenu(context, qualityTextView); | ||||
|         this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeedTextView); | ||||
|         this.captionPopupMenu = new PopupMenu(context, captionTextView); | ||||
|  | ||||
|         ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); | ||||
|  | ||||
|         ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)) | ||||
|                 .getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); | ||||
|     } | ||||
|  | ||||
|     protected abstract void setupSubtitleView(@NonNull SubtitleView view, | ||||
|                                               @NonNull String captionSizeKey); | ||||
|  | ||||
|     @Override | ||||
|     public void initListeners() { | ||||
|         super.initListeners(); | ||||
|         playbackSeekBar.setOnSeekBarChangeListener(this); | ||||
|         playbackSpeedTextView.setOnClickListener(this); | ||||
|         qualityTextView.setOnClickListener(this); | ||||
|         captionTextView.setOnClickListener(this); | ||||
|         resizeView.setOnClickListener(this); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void initPlayer() { | ||||
|         super.initPlayer(); | ||||
|  | ||||
|         // Setup video view | ||||
|         simpleExoPlayer.setVideoSurfaceView(surfaceView); | ||||
|         simpleExoPlayer.addVideoListener(this); | ||||
|  | ||||
|         // Setup subtitle view | ||||
|         simpleExoPlayer.addTextOutput(cues -> subtitleView.onCues(cues)); | ||||
|  | ||||
|         // Setup audio session with onboard equalizer | ||||
|         if (Build.VERSION.SDK_INT >= 21) { | ||||
|             trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)); | ||||
|         } | ||||
| @@ -236,6 +280,38 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         playbackSpeedPopupMenu.setOnDismissListener(this); | ||||
|     } | ||||
|  | ||||
|     private void buildCaptionMenu(final List<String> availableLanguages) { | ||||
|         if (captionPopupMenu == null) return; | ||||
|         captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); | ||||
|  | ||||
|         // Add option for turning off caption | ||||
|         MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, | ||||
|                 0, Menu.NONE, R.string.caption_none); | ||||
|         captionOffItem.setOnMenuItemClickListener(menuItem -> { | ||||
|             final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); | ||||
|             if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { | ||||
|                 trackSelector.setRendererDisabled(textRendererIndex, true); | ||||
|             } | ||||
|             return true; | ||||
|         }); | ||||
|  | ||||
|         // Add all available captions | ||||
|         for (int i = 0; i < availableLanguages.size(); i++) { | ||||
|             final String captionLanguage = availableLanguages.get(i); | ||||
|             MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, | ||||
|                     i + 1, Menu.NONE, captionLanguage); | ||||
|             captionItem.setOnMenuItemClickListener(menuItem -> { | ||||
|                 final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); | ||||
|                 if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { | ||||
|                     trackSelector.setParameters(trackSelector.getParameters() | ||||
|                             .withPreferredTextLanguage(captionLanguage)); | ||||
|                     trackSelector.setRendererDisabled(textRendererIndex, false); | ||||
|                 } | ||||
|                 return true; | ||||
|             }); | ||||
|         } | ||||
|         captionPopupMenu.setOnDismissListener(this); | ||||
|     } | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Playback Listener | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -251,7 +327,8 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         playbackSpeedTextView.setVisibility(View.GONE); | ||||
|  | ||||
|         if (info != null) { | ||||
|             final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); | ||||
|             final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, | ||||
|                     info.video_streams, info.video_only_streams, false); | ||||
|             availableStreams = new ArrayList<>(videos); | ||||
|             if (playbackQuality == null) { | ||||
|                 selectedStreamIndex = getDefaultResolutionIndex(videos); | ||||
| @@ -280,13 +357,39 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         if (index < 0 || index >= videos.size()) return null; | ||||
|         final VideoStream video = videos.get(index); | ||||
|  | ||||
|         final MediaSource streamSource = buildMediaSource(video.getUrl(), MediaFormat.getSuffixById(video.format)); | ||||
|         final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); | ||||
|         if (!video.isVideoOnly || audio == null) return streamSource; | ||||
|         List<MediaSource> mediaSources = new ArrayList<>(); | ||||
|         // Create video stream source | ||||
|         final MediaSource streamSource = buildMediaSource(video.getUrl(), | ||||
|                 MediaFormat.getSuffixById(video.getFormatId())); | ||||
|         mediaSources.add(streamSource); | ||||
|  | ||||
|         // Merge with audio stream in case if video does not contain audio | ||||
|         final MediaSource audioSource = buildMediaSource(audio.getUrl(), MediaFormat.getSuffixById(audio.format)); | ||||
|         return new MergingMediaSource(streamSource, audioSource); | ||||
|         // Create optional audio stream source | ||||
|         final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); | ||||
|         if (video.isVideoOnly && audio != null) { | ||||
|             // Merge with audio stream in case if video does not contain audio | ||||
|             final MediaSource audioSource = buildMediaSource(audio.getUrl(), | ||||
|                     MediaFormat.getSuffixById(audio.getFormatId())); | ||||
|             mediaSources.add(audioSource); | ||||
|         } | ||||
|  | ||||
|         // Create subtitle sources | ||||
|         for (final Subtitles subtitle : info.getSubtitles()) { | ||||
|             final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); | ||||
|             if (mimeType == null) continue; | ||||
|  | ||||
|             final Format textFormat = Format.createTextSampleFormat(null, mimeType, | ||||
|                     SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(subtitle)); | ||||
|             final MediaSource textSource = new SingleSampleMediaSource( | ||||
|                     Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET); | ||||
|             mediaSources.add(textSource); | ||||
|         } | ||||
|  | ||||
|         if (mediaSources.size() == 1) { | ||||
|             return mediaSources.get(0); | ||||
|         } else { | ||||
|             return new MergingMediaSource(mediaSources.toArray( | ||||
|                     new MediaSource[mediaSources.size()])); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -364,6 +467,12 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|     // ExoPlayer Video Listener | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { | ||||
|         super.onTracksChanged(trackGroups, trackSelections); | ||||
|         onTextTrackUpdate(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { | ||||
|         if (DEBUG) { | ||||
| @@ -377,6 +486,57 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         animateView(surfaceForeground, false, 100); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // ExoPlayer Track Updates | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void onTextTrackUpdate() { | ||||
|         final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT); | ||||
|  | ||||
|         if (captionTextView == null) return; | ||||
|         if (trackSelector == null || trackSelector.getCurrentMappedTrackInfo() == null || | ||||
|                 textRenderer == RENDERER_UNAVAILABLE) { | ||||
|             captionTextView.setVisibility(View.GONE); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final TrackGroupArray textTracks = trackSelector.getCurrentMappedTrackInfo() | ||||
|                 .getTrackGroups(textRenderer); | ||||
|  | ||||
|         // Extract all loaded languages | ||||
|         List<String> availableLanguages = new ArrayList<>(textTracks.length); | ||||
|         for (int i = 0; i < textTracks.length; i++) { | ||||
|             final TrackGroup textTrack = textTracks.get(i); | ||||
|             if (textTrack.length > 0 && textTrack.getFormat(0) != null) { | ||||
|                 availableLanguages.add(textTrack.getFormat(0).language); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Normalize mismatching language strings | ||||
|         final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; | ||||
|         // Because ExoPlayer normalizes the preferred language string but not the text track | ||||
|         // language strings, some preferred language string will have the language name in lowercase | ||||
|         String formattedPreferredLanguage = null; | ||||
|         if (preferredLanguage != null) { | ||||
|             for (final String language : availableLanguages) { | ||||
|                 if (language.compareToIgnoreCase(preferredLanguage) == 0) { | ||||
|                     formattedPreferredLanguage = language; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Build UI | ||||
|         buildCaptionMenu(availableLanguages); | ||||
|         if (trackSelector.getRendererDisabled(textRenderer) || formattedPreferredLanguage == null || | ||||
|                 !availableLanguages.contains(formattedPreferredLanguage)) { | ||||
|             captionTextView.setText(R.string.caption_none); | ||||
|         } else { | ||||
|             captionTextView.setText(formattedPreferredLanguage); | ||||
|         } | ||||
|         captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // General Player | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -453,6 +613,10 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|             onQualitySelectorClicked(); | ||||
|         } else if (v.getId() == playbackSpeedTextView.getId()) { | ||||
|             onPlaybackSpeedClicked(); | ||||
|         } else if (v.getId() == resizeView.getId()) { | ||||
|             onResizeClicked(); | ||||
|         } else if (v.getId() == captionTextView.getId()) { | ||||
|             onCaptionClicked(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -516,6 +680,23 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         showControls(300); | ||||
|     } | ||||
|  | ||||
|     private void onCaptionClicked() { | ||||
|         if (DEBUG) Log.d(TAG, "onCaptionClicked() called"); | ||||
|         captionPopupMenu.show(); | ||||
|         isSomePopupMenuVisible = true; | ||||
|         showControls(300); | ||||
|     } | ||||
|  | ||||
|     private void onResizeClicked() { | ||||
|         if (getAspectRatioFrameLayout() != null && context != null) { | ||||
|             final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); | ||||
|             final int newResizeMode = nextResizeMode(currentResizeMode); | ||||
|             getAspectRatioFrameLayout().setResizeMode(newResizeMode); | ||||
|             getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode); | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // SeekBar Listener | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -557,16 +738,16 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public int getVideoRendererIndex() { | ||||
|         if (simpleExoPlayer == null) return -1; | ||||
|     public int getRendererIndex(final int trackIndex) { | ||||
|         if (simpleExoPlayer == null) return RENDERER_UNAVAILABLE; | ||||
|  | ||||
|         for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { | ||||
|             if (simpleExoPlayer.getRendererType(t) == C.TRACK_TYPE_VIDEO) { | ||||
|             if (simpleExoPlayer.getRendererType(t) == trackIndex) { | ||||
|                 return t; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return -1; | ||||
|         return RENDERER_UNAVAILABLE; | ||||
|     } | ||||
|  | ||||
|     public boolean isControlsVisible() { | ||||
| @@ -755,4 +936,15 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         return currentDisplaySeek; | ||||
|     } | ||||
|  | ||||
|     public SubtitleView getSubtitleView() { | ||||
|         return subtitleView; | ||||
|     } | ||||
|  | ||||
|     public TextView getResizeView() { | ||||
|         return resizeView; | ||||
|     } | ||||
|  | ||||
|     public TextView getCaptionTextView() { | ||||
|         return captionTextView; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,12 @@ import android.content.SharedPreferences; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
|  | ||||
| import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | ||||
| import com.google.android.exoplayer2.util.MimeTypes; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.Subtitles; | ||||
| import org.schabi.newpipe.extractor.stream.SubtitlesFormat; | ||||
|  | ||||
| import java.text.DecimalFormat; | ||||
| import java.text.NumberFormat; | ||||
| @@ -14,6 +19,12 @@ import java.util.Locale; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
|  | ||||
| import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL; | ||||
| import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; | ||||
| import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT; | ||||
| import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH; | ||||
| import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; | ||||
|  | ||||
| public class PlayerHelper { | ||||
|     private PlayerHelper() {} | ||||
|  | ||||
| @@ -46,6 +57,30 @@ public class PlayerHelper { | ||||
|         return pitchFormatter.format(pitch); | ||||
|     } | ||||
|  | ||||
|     public static String mimeTypesOf(final SubtitlesFormat format) { | ||||
|         switch (format) { | ||||
|             case VTT: return MimeTypes.TEXT_VTT; | ||||
|             case TTML: return MimeTypes.APPLICATION_TTML; | ||||
|             default: throw new IllegalArgumentException("Unrecognized mime type: " + format.name()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public static String captionLanguageOf(@NonNull final Subtitles subtitles) { | ||||
|         final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale()); | ||||
|         return displayName + (subtitles.isAutoGenerated() ? " (auto-generated)" : ""); | ||||
|     } | ||||
|  | ||||
|     public static String resizeTypeOf(@NonNull final Context context, | ||||
|                                       @AspectRatioFrameLayout.ResizeMode final int resizeMode) { | ||||
|         switch (resizeMode) { | ||||
|             case RESIZE_MODE_FIT: return context.getResources().getString(R.string.resize_fit); | ||||
|             case RESIZE_MODE_FILL: return context.getResources().getString(R.string.resize_fill); | ||||
|             case RESIZE_MODE_ZOOM: return context.getResources().getString(R.string.resize_zoom); | ||||
|             default: throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { | ||||
|         return isResumeAfterAudioFocusGain(context, false); | ||||
|     } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import android.support.annotation.Nullable; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| @@ -23,6 +24,7 @@ public class PlayQueueItem implements Serializable { | ||||
|     final private long duration; | ||||
|     final private String thumbnailUrl; | ||||
|     final private String uploader; | ||||
|     final private StreamType streamType; | ||||
|  | ||||
|     private long recoveryPosition; | ||||
|     private Throwable error; | ||||
| @@ -30,22 +32,26 @@ public class PlayQueueItem implements Serializable { | ||||
|     private transient Single<StreamInfo> stream; | ||||
|  | ||||
|     PlayQueueItem(@NonNull final StreamInfo info) { | ||||
|         this(info.getName(), info.getUrl(), info.getServiceId(), info.duration, info.thumbnail_url, info.uploader_name); | ||||
|         this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(), | ||||
|                 info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType()); | ||||
|         this.stream = Single.just(info); | ||||
|     } | ||||
|  | ||||
|     PlayQueueItem(@NonNull final StreamInfoItem item) { | ||||
|         this(item.getName(), item.getUrl(), item.getServiceId(), item.duration, item.thumbnail_url, item.uploader_name); | ||||
|         this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(), | ||||
|                 item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType()); | ||||
|     } | ||||
|  | ||||
|     private PlayQueueItem(final String name, final String url, final int serviceId, | ||||
|                           final long duration, final String thumbnailUrl, final String uploader) { | ||||
|                           final long duration, final String thumbnailUrl, final String uploader, | ||||
|                           final StreamType streamType) { | ||||
|         this.title = name; | ||||
|         this.url = url; | ||||
|         this.serviceId = serviceId; | ||||
|         this.duration = duration; | ||||
|         this.thumbnailUrl = thumbnailUrl; | ||||
|         this.uploader = uploader; | ||||
|         this.streamType = streamType; | ||||
|  | ||||
|         this.recoveryPosition = RECOVERY_UNSET; | ||||
|     } | ||||
| @@ -78,6 +84,10 @@ public class PlayQueueItem implements Serializable { | ||||
|         return uploader; | ||||
|     } | ||||
|  | ||||
|     public StreamType getStreamType() { | ||||
|         return streamType; | ||||
|     } | ||||
|  | ||||
|     public long getRecoveryPosition() { | ||||
|         return recoveryPosition; | ||||
|     } | ||||
|   | ||||
| @@ -3,19 +3,29 @@ package org.schabi.newpipe.playlist; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| public final class SinglePlayQueue extends PlayQueue { | ||||
|     public SinglePlayQueue(final StreamInfoItem item) { | ||||
|         this(new PlayQueueItem(item)); | ||||
|         super(0, Collections.singletonList(new PlayQueueItem(item))); | ||||
|     } | ||||
|  | ||||
|     public SinglePlayQueue(final StreamInfo info) { | ||||
|         this(new PlayQueueItem(info)); | ||||
|         super(0, Collections.singletonList(new PlayQueueItem(info))); | ||||
|     } | ||||
|  | ||||
|     private SinglePlayQueue(final PlayQueueItem playQueueItem) { | ||||
|         super(0, Collections.singletonList(playQueueItem)); | ||||
|     public SinglePlayQueue(final List<StreamInfoItem> items, final int index) { | ||||
|         super(index, playQueueItemsOf(items)); | ||||
|     } | ||||
|  | ||||
|     private static List<PlayQueueItem> playQueueItemsOf(List<StreamInfoItem> items) { | ||||
|         List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size()); | ||||
|         for (final StreamInfoItem item : items) { | ||||
|             playQueueItems.add(new PlayQueueItem(item)); | ||||
|         } | ||||
|         return playQueueItems; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -73,7 +73,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | ||||
|                             .putString(getString(R.string.main_page_selectd_kiosk_id), kioskId).apply(); | ||||
|                     String serviceName = ""; | ||||
|                     try { | ||||
|                         serviceName = NewPipe.getService(service_id).getServiceInfo().name; | ||||
|                         serviceName = NewPipe.getService(service_id).getServiceInfo().getName(); | ||||
|                     } catch (ExtractionException e) { | ||||
|                         onError(e); | ||||
|                     } | ||||
| @@ -245,7 +245,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | ||||
|  | ||||
|                 String summary = | ||||
|                         String.format(getString(R.string.service_kiosk_string), | ||||
|                                 service.getServiceInfo().name, | ||||
|                                 service.getServiceInfo().getName(), | ||||
|                                 kioskName); | ||||
|  | ||||
|                 mainPagePref.setSummary(summary); | ||||
|   | ||||
| @@ -122,11 +122,11 @@ public class SelectKioskFragment extends DialogFragment { | ||||
|  | ||||
|             for(StreamingService service : NewPipe.getServices()) { | ||||
|                 //TODO: Multi-service support | ||||
|                 if (service.getServiceId() != ServiceList.YouTube.getId()) continue; | ||||
|                 if (service.getServiceId() != ServiceList.YouTube.getServiceId()) continue; | ||||
|  | ||||
|                 for(String kioskId : service.getKioskList().getAvailableKiosks()) { | ||||
|                     String name = String.format(getString(R.string.service_kiosk_string), | ||||
|                             service.getServiceInfo().name, | ||||
|                             service.getServiceInfo().getName(), | ||||
|                             KioskTranslator.getTranslatedKioskName(kioskId, getContext())); | ||||
|                     kioskList.add(new Entry( | ||||
|                             ServiceHelper.getIcon(service.getServiceId()), | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.res.Resources; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.PluralsRes; | ||||
| import android.support.annotation.StringRes; | ||||
| import android.text.TextUtils; | ||||
| @@ -14,7 +15,9 @@ import java.text.DateFormat; | ||||
| import java.text.NumberFormat; | ||||
| import java.text.ParseException; | ||||
| import java.text.SimpleDateFormat; | ||||
| import java.util.Arrays; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
|  | ||||
| /* | ||||
| @@ -39,9 +42,33 @@ import java.util.Locale; | ||||
|  | ||||
| public class Localization { | ||||
|  | ||||
|     public final static String DOT_SEPARATOR = " • "; | ||||
|  | ||||
|     private Localization() { | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public static String concatenateStrings(final String... strings) { | ||||
|         return concatenateStrings(Arrays.asList(strings)); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public static String concatenateStrings(final List<String> strings) { | ||||
|         if (strings.isEmpty()) return ""; | ||||
|  | ||||
|         final StringBuilder stringBuilder = new StringBuilder(); | ||||
|         stringBuilder.append(strings.get(0)); | ||||
|  | ||||
|         for (int i = 1; i < strings.size(); i++) { | ||||
|             final String string = strings.get(i); | ||||
|             if (!TextUtils.isEmpty(string)) { | ||||
|                 stringBuilder.append(DOT_SEPARATOR).append(strings.get(i)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return stringBuilder.toString(); | ||||
|     } | ||||
|  | ||||
|     public static Locale getPreferredLocale(Context context) { | ||||
|         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|  | ||||
|   | ||||
| @@ -33,6 +33,9 @@ import org.schabi.newpipe.fragments.list.feed.FeedFragment; | ||||
| import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; | ||||
| import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; | ||||
| import org.schabi.newpipe.fragments.list.search.SearchFragment; | ||||
| import org.schabi.newpipe.fragments.local.bookmark.LocalPlaylistFragment; | ||||
| import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; | ||||
| import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; | ||||
| import org.schabi.newpipe.history.HistoryActivity; | ||||
| import org.schabi.newpipe.player.BackgroundPlayer; | ||||
| import org.schabi.newpipe.player.BackgroundPlayerActivity; | ||||
| @@ -322,6 +325,30 @@ public class NavigationHelper { | ||||
|                 .commit(); | ||||
|     } | ||||
|  | ||||
|     public static void openLocalPlaylistFragment(FragmentManager fragmentManager, long playlistId, String name) { | ||||
|         if (name == null) name = ""; | ||||
|         fragmentManager.beginTransaction() | ||||
|                 .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) | ||||
|                 .replace(R.id.fragment_holder, LocalPlaylistFragment.getInstance(playlistId, name)) | ||||
|                 .addToBackStack(null) | ||||
|                 .commit(); | ||||
|     } | ||||
|  | ||||
|     public static void openLastPlayedFragment(FragmentManager fragmentManager) { | ||||
|         fragmentManager.beginTransaction() | ||||
|                 .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) | ||||
|                 .replace(R.id.fragment_holder, new LastPlayedFragment()) | ||||
|                 .addToBackStack(null) | ||||
|                 .commit(); | ||||
|     } | ||||
|  | ||||
|     public static void openMostPlayedFragment(FragmentManager fragmentManager) { | ||||
|         fragmentManager.beginTransaction() | ||||
|                 .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) | ||||
|                 .replace(R.id.fragment_holder, new MostPlayedFragment()) | ||||
|                 .addToBackStack(null) | ||||
|                 .commit(); | ||||
|     } | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Through Intents | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|   | ||||
| @@ -0,0 +1,16 @@ | ||||
| package org.schabi.newpipe.util; | ||||
|  | ||||
| import android.support.v7.widget.RecyclerView; | ||||
|  | ||||
| public abstract class OnClickGesture<T> { | ||||
|  | ||||
|     public abstract void selected(T selectedItem); | ||||
|  | ||||
|     public void held(T selectedItem) { | ||||
|         // Optional gesture | ||||
|     } | ||||
|  | ||||
|     public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) { | ||||
|         // Optional gesture | ||||
|     } | ||||
| } | ||||
| @@ -12,7 +12,7 @@ import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
|  | ||||
| public class ServiceHelper { | ||||
|     private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube.getService(); | ||||
|     private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; | ||||
|  | ||||
|     @DrawableRes | ||||
|     public static int getIcon(int serviceId) { | ||||
| @@ -45,9 +45,9 @@ public class ServiceHelper { | ||||
|     public static void setSelectedServiceId(Context context, int serviceId) { | ||||
|         String serviceName; | ||||
|         try { | ||||
|             serviceName = NewPipe.getService(serviceId).getServiceInfo().name; | ||||
|             serviceName = NewPipe.getService(serviceId).getServiceInfo().getName(); | ||||
|         } catch (ExtractionException e) { | ||||
|             serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().name; | ||||
|             serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName(); | ||||
|         } | ||||
|  | ||||
|         setSelectedServicePreferences(context, serviceName); | ||||
| @@ -55,7 +55,7 @@ public class ServiceHelper { | ||||
|  | ||||
|     public static void setSelectedServiceId(Context context, String serviceName) { | ||||
|         int serviceId = NewPipe.getIdOfService(serviceName); | ||||
|         if (serviceId == -1) serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().name; | ||||
|         if (serviceId == -1) serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName(); | ||||
|  | ||||
|         setSelectedServicePreferences(context, serviceName); | ||||
|     } | ||||
|   | ||||
| @@ -73,7 +73,7 @@ public class ThemeHelper { | ||||
|         else if (selectedTheme.equals(blackTheme)) themeName = "BlackTheme"; | ||||
|         else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme"; | ||||
|  | ||||
|         themeName += "." + service.getServiceInfo().name; | ||||
|         themeName += "." + service.getServiceInfo().getName(); | ||||
|         int resourceId = context.getResources().getIdentifier(themeName, "style", context.getPackageName()); | ||||
|  | ||||
|         if (resourceId > 0) { | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 180 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 185 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 487 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 485 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 106 B | 
| After Width: | Height: | Size: 163 B | 
| After Width: | Height: | Size: 159 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 107 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 137 B | 
 Weblate
					Weblate