Conflict resolution.
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,2 +1,3 @@ | ||||
| - [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them. | ||||
| - [ ] I checked if the issue/feature exists in the latest version. | ||||
| - [ ] I did use the [incredible bugreport to markdown converter](https://teamnewpipe.github.io/CrashReportToMarkdown/) to paste bug reports. | ||||
|   | ||||
| @@ -8,8 +8,8 @@ android { | ||||
|         applicationId "org.schabi.newpipe" | ||||
|         minSdkVersion 15 | ||||
|         targetSdkVersion 27 | ||||
|         versionCode 64 | ||||
|         versionName "0.13.5" | ||||
|         versionCode 68 | ||||
|         versionName "0.14.1" | ||||
|  | ||||
|         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | ||||
|         vectorDrawables.useSupportLibrary = true | ||||
| @@ -59,22 +59,23 @@ android { | ||||
|  | ||||
| ext { | ||||
|     supportLibVersion = '27.1.1' | ||||
|     exoPlayerLibVersion = '2.7.3' | ||||
|     roomDbLibVersion = '1.0.0' | ||||
|     exoPlayerLibVersion = '2.8.2' | ||||
|     roomDbLibVersion = '1.1.1' | ||||
|     leakCanaryLibVersion = '1.5.4' | ||||
|     okHttpLibVersion = '1.5.0' | ||||
|     okHttpLibVersion = '3.10.0' | ||||
|     icepickLibVersion = '3.2.0' | ||||
|     stethoLibVersion = '1.5.0' | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { | ||||
|         exclude module: 'support-annotations' | ||||
|     } | ||||
|  | ||||
|     implementation 'com.github.TeamNewPipe:NewPipeExtractor:bf1c771' | ||||
|     implementation 'com.github.TeamNewPipe:NewPipeExtractor:66c3c3f45241d4b0c909' | ||||
|  | ||||
|     testImplementation 'junit:junit:4.12' | ||||
|     testImplementation 'org.mockito:mockito-core:1.10.19' | ||||
|     testImplementation 'org.mockito:mockito-core:2.8.9' | ||||
|  | ||||
|     implementation "com.android.support:appcompat-v7:$supportLibVersion" | ||||
|     implementation "com.android.support:support-v4:$supportLibVersion" | ||||
| @@ -96,7 +97,7 @@ dependencies { | ||||
|     debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion" | ||||
|     debugImplementation 'com.android.support:multidex:1.0.3' | ||||
|  | ||||
|     implementation 'io.reactivex.rxjava2:rxjava:2.1.10' | ||||
|     implementation 'io.reactivex.rxjava2:rxjava:2.1.14' | ||||
|     implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' | ||||
|     implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' | ||||
|  | ||||
| @@ -110,6 +111,9 @@ dependencies { | ||||
|     debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion" | ||||
|     releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion" | ||||
|  | ||||
|     implementation 'com.squareup.okhttp3:okhttp:3.9.1' | ||||
|     debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion" | ||||
|  | ||||
|     implementation "com.squareup.okhttp3:okhttp:$okHttpLibVersion" | ||||
|     debugImplementation "com.facebook.stetho:stetho-okhttp3:$stethoLibVersion" | ||||
|     implementation 'com.android.support.constraint:constraint-layout:1.1.2' | ||||
|     implementation 'com.android.support:cardview-v7:27.1.1' | ||||
| } | ||||
|   | ||||
| @@ -76,10 +76,6 @@ | ||||
|             android:name=".about.AboutActivity" | ||||
|             android:label="@string/title_activity_about"/> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".history.HistoryActivity" | ||||
|             android:label="@string/title_activity_history"/> | ||||
|  | ||||
|         <service android:name=".local.subscription.services.SubscriptionsImportService"/> | ||||
|         <service android:name=".local.subscription.services.SubscriptionsExportService"/> | ||||
|  | ||||
| @@ -122,6 +118,7 @@ | ||||
|         <activity | ||||
|             android:name=".ReCaptchaActivity" | ||||
|             android:label="@string/reCaptchaActivity"/> | ||||
|         <activity android:name=".download.ExtSDDownloadFailedActivity" /> | ||||
|  | ||||
|         <provider | ||||
|             android:name="android.support.v4.content.FileProvider" | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import android.content.Context; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.v4.app.Fragment; | ||||
| import android.support.v4.app.FragmentManager; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.util.Log; | ||||
| import android.view.View; | ||||
| @@ -11,7 +12,10 @@ import android.view.View; | ||||
| import com.nostra13.universalimageloader.core.ImageLoader; | ||||
| import com.squareup.leakcanary.RefWatcher; | ||||
|  | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
|  | ||||
| import icepick.Icepick; | ||||
| import icepick.State; | ||||
|  | ||||
| public abstract class BaseFragment extends Fragment { | ||||
|     protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); | ||||
| @@ -20,6 +24,15 @@ public abstract class BaseFragment extends Fragment { | ||||
|     protected AppCompatActivity activity; | ||||
|     public static final ImageLoader imageLoader = ImageLoader.getInstance(); | ||||
|  | ||||
|     //These values are used for controlling framgents when they are part of the frontpage | ||||
|     @State | ||||
|     protected boolean useAsFrontPage = false; | ||||
|     protected boolean mIsVisibleToUser = false; | ||||
|  | ||||
|     public void useAsFrontPage(boolean value) { | ||||
|         useAsFrontPage = value; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment's Lifecycle | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -72,6 +85,12 @@ public abstract class BaseFragment extends Fragment { | ||||
|         if (refWatcher != null) refWatcher.watch(this); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void setUserVisibleHint(boolean isVisibleToUser) { | ||||
|         super.setUserVisibleHint(isVisibleToUser); | ||||
|         mIsVisibleToUser = isVisibleToUser; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Init | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -88,8 +107,15 @@ public abstract class BaseFragment extends Fragment { | ||||
|  | ||||
|     public void setTitle(String title) { | ||||
|         if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]"); | ||||
|         if (activity != null && activity.getSupportActionBar() != null) { | ||||
|         if((!useAsFrontPage || mIsVisibleToUser) | ||||
|             && (activity != null && activity.getSupportActionBar() != null)) { | ||||
|             activity.getSupportActionBar().setTitle(title); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected FragmentManager getFM() { | ||||
|         return getParentFragment() == null | ||||
|                 ? getFragmentManager() | ||||
|                 : getParentFragment().getFragmentManager(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| @@ -43,24 +44,30 @@ import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.view.Window; | ||||
| import android.view.WindowManager; | ||||
| import android.widget.Button; | ||||
| import android.widget.ImageButton; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
| 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.report.ErrorActivity; | ||||
| import org.schabi.newpipe.util.Constants; | ||||
| import org.schabi.newpipe.util.KioskTranslator; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.PermissionHelper; | ||||
| import org.schabi.newpipe.util.ServiceHelper; | ||||
| import org.schabi.newpipe.util.StateSaver; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import static org.schabi.newpipe.extractor.InfoItem.InfoType.PLAYLIST; | ||||
|  | ||||
| public class MainActivity extends AppCompatActivity { | ||||
|     private static final String TAG = "MainActivity"; | ||||
|     public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); | ||||
| @@ -70,6 +77,19 @@ public class MainActivity extends AppCompatActivity { | ||||
|     private NavigationView drawerItems = null; | ||||
|     private TextView headerServiceView = null; | ||||
|  | ||||
|     private boolean servicesShown = false; | ||||
|     private ImageView serviceArrow; | ||||
|  | ||||
|     private static final int ITEM_ID_SUBSCRIPTIONS = - 1; | ||||
|     private static final int ITEM_ID_FEED = - 2; | ||||
|     private static final int ITEM_ID_BOOKMARKS = - 3; | ||||
|     private static final int ITEM_ID_DOWNLOADS = - 4; | ||||
|     private static final int ITEM_ID_HISTORY = - 5; | ||||
|     private static final int ITEM_ID_SETTINGS = 0; | ||||
|     private static final int ITEM_ID_ABOUT = 1; | ||||
|  | ||||
|     private static final int ORDER = 0; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Activity's LifeCycle | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -83,28 +103,64 @@ public class MainActivity extends AppCompatActivity { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_main); | ||||
|  | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|             Window w = getWindow(); | ||||
|             w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); | ||||
|         } | ||||
|  | ||||
|         if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) { | ||||
|             initFragments(); | ||||
|         } | ||||
|  | ||||
|         setSupportActionBar(findViewById(R.id.toolbar)); | ||||
|         setupDrawer(); | ||||
|         try { | ||||
|             setupDrawer(); | ||||
|         } catch (Exception e) { | ||||
|             ErrorActivity.reportUiError(this, e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void setupDrawer() { | ||||
|     private void setupDrawer() throws Exception { | ||||
|         final Toolbar toolbar = findViewById(R.id.toolbar); | ||||
|         drawer = findViewById(R.id.drawer_layout); | ||||
|         drawerItems = findViewById(R.id.navigation); | ||||
|  | ||||
|         for(StreamingService s : NewPipe.getServices()) { | ||||
|             final String title = s.getServiceInfo().getName() + | ||||
|                     (ServiceHelper.isBeta(s) ? " (beta)" : ""); | ||||
|             final MenuItem item = drawerItems.getMenu() | ||||
|                     .add(R.id.menu_services_group, s.getServiceId(), 0, title); | ||||
|             item.setIcon(ServiceHelper.getIcon(s.getServiceId())); | ||||
|         //Tabs | ||||
|         int currentServiceId = ServiceHelper.getSelectedServiceId(this); | ||||
|         StreamingService service = NewPipe.getService(currentServiceId); | ||||
|  | ||||
|         int kioskId = 0; | ||||
|  | ||||
|         for (final String ks : service.getKioskList().getAvailableKiosks()) { | ||||
|             drawerItems.getMenu() | ||||
|                     .add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator.getTranslatedKioskName(ks, this)) | ||||
|                     .setIcon(KioskTranslator.getKioskIcons(ks, this)); | ||||
|             kioskId ++; | ||||
|         } | ||||
|  | ||||
|         drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel)); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss)); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark)); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download)); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history)); | ||||
|  | ||||
|         //Settings and About | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings)); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info)); | ||||
|  | ||||
|         toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close); | ||||
|         toggle.syncState(); | ||||
| @@ -119,53 +175,179 @@ public class MainActivity extends AppCompatActivity { | ||||
|  | ||||
|             @Override | ||||
|             public void onDrawerClosed(View drawerView) { | ||||
|                 if(servicesShown) { | ||||
|                     toggleServices(); | ||||
|                 } | ||||
|                 if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) { | ||||
|                     new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         drawerItems.setNavigationItemSelectedListener(this::changeService); | ||||
|  | ||||
|         setupDrawerFooter(); | ||||
|         drawerItems.setNavigationItemSelectedListener(this::drawerItemSelected); | ||||
|         setupDrawerHeader(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private boolean changeService(MenuItem item) { | ||||
|         if (item.getGroupId() == R.id.menu_services_group) { | ||||
|             drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false); | ||||
|             ServiceHelper.setSelectedServiceId(this, item.getItemId()); | ||||
|             drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); | ||||
|         } else { | ||||
|             return false; | ||||
|     private boolean drawerItemSelected(MenuItem item) { | ||||
|         switch (item.getGroupId()) { | ||||
|             case R.id.menu_services_group: | ||||
|                 changeService(item); | ||||
|                 break; | ||||
|             case R.id.menu_tabs_group: | ||||
|                 try { | ||||
|                     tabSelected(item); | ||||
|                 } catch (Exception e) { | ||||
|                     ErrorActivity.reportUiError(this, e); | ||||
|                 } | ||||
|                 break; | ||||
|             case R.id.menu_options_about_group: | ||||
|                 optionsAboutSelected(item); | ||||
|                 break; | ||||
|             default: | ||||
|                 return false; | ||||
|         } | ||||
|  | ||||
|         drawer.closeDrawers(); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     private void setupDrawerFooter() { | ||||
|         ImageButton settings = findViewById(R.id.drawer_settings); | ||||
|         ImageButton downloads = findViewById(R.id.drawer_downloads); | ||||
|         ImageButton history = findViewById(R.id.drawer_history); | ||||
|     private  void changeService(MenuItem item) { | ||||
|         drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false); | ||||
|         ServiceHelper.setSelectedServiceId(this, item.getItemId()); | ||||
|         drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); | ||||
|     } | ||||
|  | ||||
|         settings.setOnClickListener(view -> NavigationHelper.openSettings(this)); | ||||
|         downloads.setOnClickListener(view ->NavigationHelper.openDownloads(this)); | ||||
|         history.setOnClickListener(view -> | ||||
|                 NavigationHelper.openStatisticFragment(getSupportFragmentManager())); | ||||
|     private void tabSelected(MenuItem item) throws ExtractionException { | ||||
|         switch(item.getItemId()) { | ||||
|             case ITEM_ID_SUBSCRIPTIONS: | ||||
|                 NavigationHelper.openSubscriptionFragment(getSupportFragmentManager()); | ||||
|                 break; | ||||
|             case ITEM_ID_FEED: | ||||
|                 NavigationHelper.openWhatsNewFragment(getSupportFragmentManager()); | ||||
|                 break; | ||||
|             case ITEM_ID_BOOKMARKS: | ||||
|                 NavigationHelper.openBookmarksFragment(getSupportFragmentManager()); | ||||
|                 break; | ||||
|             case ITEM_ID_DOWNLOADS: | ||||
|                 NavigationHelper.openDownloads(this); | ||||
|                 break; | ||||
|             case ITEM_ID_HISTORY: | ||||
|                 NavigationHelper.openStatisticFragment(getSupportFragmentManager()); | ||||
|                 break; | ||||
|             default: | ||||
|                 int currentServiceId = ServiceHelper.getSelectedServiceId(this); | ||||
|                 StreamingService service = NewPipe.getService(currentServiceId); | ||||
|                 String serviceName = ""; | ||||
|  | ||||
|                 int kioskId = 0; | ||||
|                 for (final String ks : service.getKioskList().getAvailableKiosks()) { | ||||
|                     if(kioskId == item.getItemId()) { | ||||
|                         serviceName = ks; | ||||
|                     } | ||||
|                     kioskId ++; | ||||
|                 } | ||||
|  | ||||
|                 NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId, serviceName); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void optionsAboutSelected(MenuItem item) { | ||||
|         switch(item.getItemId()) { | ||||
|             case ITEM_ID_SETTINGS: | ||||
|                 NavigationHelper.openSettings(this); | ||||
|                 break; | ||||
|             case ITEM_ID_ABOUT: | ||||
|                 NavigationHelper.openAbout(this); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void setupDrawerHeader() { | ||||
|         headerServiceView = findViewById(R.id.drawer_header_service_view); | ||||
|         Button action = findViewById(R.id.drawer_header_action_button); | ||||
|         NavigationView navigationView = findViewById(R.id.navigation); | ||||
|         View hView =  navigationView.getHeaderView(0); | ||||
|  | ||||
|         serviceArrow = hView.findViewById(R.id.drawer_arrow); | ||||
|         headerServiceView = hView.findViewById(R.id.drawer_header_service_view); | ||||
|         Button action = hView.findViewById(R.id.drawer_header_action_button); | ||||
|         action.setOnClickListener(view -> { | ||||
|             Intent intent = new Intent(Intent.ACTION_VIEW); | ||||
|             intent.setData(Uri.parse("https://newpipe.schabi.org/blog/")); | ||||
|             startActivity(intent); | ||||
|             drawer.closeDrawers(); | ||||
|             toggleServices(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private void toggleServices() { | ||||
|         servicesShown = !servicesShown; | ||||
|  | ||||
|         drawerItems.getMenu().removeGroup(R.id.menu_services_group); | ||||
|         drawerItems.getMenu().removeGroup(R.id.menu_tabs_group); | ||||
|         drawerItems.getMenu().removeGroup(R.id.menu_options_about_group); | ||||
|  | ||||
|         if(servicesShown) { | ||||
|             showServices(); | ||||
|         } else { | ||||
|             try { | ||||
|                 showTabs(); | ||||
|             } catch (Exception e) { | ||||
|                 ErrorActivity.reportUiError(this, e); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void showServices() { | ||||
|         serviceArrow.setImageResource(R.drawable.ic_arrow_up_white); | ||||
|  | ||||
|         for(StreamingService s : NewPipe.getServices()) { | ||||
|             final String title = s.getServiceInfo().getName() + | ||||
|                     (ServiceHelper.isBeta(s) ? " (beta)" : ""); | ||||
|  | ||||
|             drawerItems.getMenu() | ||||
|                     .add(R.id.menu_services_group, s.getServiceId(), ORDER, title) | ||||
|                     .setIcon(ServiceHelper.getIcon(s.getServiceId())); | ||||
|         } | ||||
|         drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); | ||||
|     } | ||||
|  | ||||
|     private void showTabs() throws ExtractionException { | ||||
|         serviceArrow.setImageResource(R.drawable.ic_arrow_down_white); | ||||
|  | ||||
|         //Tabs | ||||
|         int currentServiceId = ServiceHelper.getSelectedServiceId(this); | ||||
|         StreamingService service = NewPipe.getService(currentServiceId); | ||||
|  | ||||
|         int kioskId = 0; | ||||
|  | ||||
|         for (final String ks : service.getKioskList().getAvailableKiosks()) { | ||||
|             drawerItems.getMenu() | ||||
|                     .add(R.id.menu_tabs_group, kioskId, ORDER, KioskTranslator.getTranslatedKioskName(ks, this)) | ||||
|                     .setIcon(KioskTranslator.getKioskIcons(ks, this)); | ||||
|             kioskId ++; | ||||
|         } | ||||
|  | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel)); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss)); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark)); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download)); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history)); | ||||
|  | ||||
|         //Settings and About | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings)); | ||||
|         drawerItems.getMenu() | ||||
|                 .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) | ||||
|                 .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onDestroy() { | ||||
|         super.onDestroy(); | ||||
| @@ -329,16 +511,13 @@ public class MainActivity extends AppCompatActivity { | ||||
|                 onHomeButtonPressed(); | ||||
|                 return true; | ||||
|             case R.id.action_show_downloads: | ||||
|                 return NavigationHelper.openDownloads(this); | ||||
|                     return NavigationHelper.openDownloads(this); | ||||
|             case R.id.action_history: | ||||
|                 NavigationHelper.openStatisticFragment(getSupportFragmentManager()); | ||||
|                 return true; | ||||
|             case R.id.action_about: | ||||
|                 NavigationHelper.openAbout(this); | ||||
|                 return true; | ||||
|                     NavigationHelper.openStatisticFragment(getSupportFragmentManager()); | ||||
|                     return true; | ||||
|             case R.id.action_settings: | ||||
|                 NavigationHelper.openSettings(this); | ||||
|                 return true; | ||||
|                     NavigationHelper.openSettings(this); | ||||
|                     return true; | ||||
|             default: | ||||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
| @@ -382,31 +561,45 @@ public class MainActivity extends AppCompatActivity { | ||||
|     } | ||||
|  | ||||
|     private void handleIntent(Intent intent) { | ||||
|         if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); | ||||
|         try { | ||||
|             if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); | ||||
|  | ||||
|         if (intent.hasExtra(Constants.KEY_LINK_TYPE)) { | ||||
|             String url = intent.getStringExtra(Constants.KEY_URL); | ||||
|             int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); | ||||
|             String title = intent.getStringExtra(Constants.KEY_TITLE); | ||||
|             switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) { | ||||
|                 case STREAM: | ||||
|                     boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); | ||||
|                     NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay); | ||||
|                     break; | ||||
|                 case CHANNEL: | ||||
|                     NavigationHelper.openChannelFragment(getSupportFragmentManager(), serviceId, url, title); | ||||
|                     break; | ||||
|                 case PLAYLIST: | ||||
|                     NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), serviceId, url, title); | ||||
|                     break; | ||||
|             if (intent.hasExtra(Constants.KEY_LINK_TYPE)) { | ||||
|                 String url = intent.getStringExtra(Constants.KEY_URL); | ||||
|                 int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); | ||||
|                 String title = intent.getStringExtra(Constants.KEY_TITLE); | ||||
|                 switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) { | ||||
|                     case STREAM: | ||||
|                         boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); | ||||
|                         NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay); | ||||
|                         break; | ||||
|                     case CHANNEL: | ||||
|                         NavigationHelper.openChannelFragment(getSupportFragmentManager(), | ||||
|                                 serviceId, | ||||
|                                 url, | ||||
|                                 title); | ||||
|                         break; | ||||
|                     case PLAYLIST: | ||||
|                         NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), | ||||
|                                 serviceId, | ||||
|                                 url, | ||||
|                                 title); | ||||
|                         break; | ||||
|                 } | ||||
|             } else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) { | ||||
|                 String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING); | ||||
|                 if (searchString == null) searchString = ""; | ||||
|                 int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); | ||||
|                 NavigationHelper.openSearchFragment( | ||||
|                         getSupportFragmentManager(), | ||||
|                         serviceId, | ||||
|                         searchString); | ||||
|  | ||||
|             } else { | ||||
|                 NavigationHelper.gotoMainFragment(getSupportFragmentManager()); | ||||
|             } | ||||
|         } else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) { | ||||
|             String searchQuery = intent.getStringExtra(Constants.KEY_QUERY); | ||||
|             if (searchQuery == null) searchQuery = ""; | ||||
|             int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); | ||||
|             NavigationHelper.openSearchFragment(getSupportFragmentManager(), serviceId, searchQuery); | ||||
|         } else { | ||||
|             NavigationHelper.gotoMainFragment(getSupportFragmentManager()); | ||||
|         } catch (Exception e) { | ||||
|             ErrorActivity.reportUiError(this, e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,19 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.FragmentManager; | ||||
| import android.app.IntentService; | ||||
| import android.content.Context; | ||||
| import android.content.DialogInterface; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.os.Bundle; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.DrawableRes; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.Fragment; | ||||
| import android.support.v4.app.NotificationCompat; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| @@ -23,6 +28,7 @@ import android.widget.RadioButton; | ||||
| import android.widget.RadioGroup; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.download.DownloadDialog; | ||||
| import org.schabi.newpipe.extractor.Info; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| @@ -31,6 +37,8 @@ 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.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| @@ -38,16 +46,19 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.PermissionHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.io.Serializable; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collection; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Observer; | ||||
|  | ||||
| import icepick.Icepick; | ||||
| import icepick.State; | ||||
| @@ -77,6 +88,8 @@ public class RouterActivity extends AppCompatActivity { | ||||
|     protected String currentUrl; | ||||
|     protected CompositeDisposable disposables = new CompositeDisposable(); | ||||
|  | ||||
|     private boolean selectionIsDownload = false; | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
| @@ -104,7 +117,7 @@ public class RouterActivity extends AppCompatActivity { | ||||
|     @Override | ||||
|     protected void onStart() { | ||||
|         super.onStart(); | ||||
|  | ||||
|          | ||||
|         handleUrl(currentUrl); | ||||
|     } | ||||
|  | ||||
| @@ -165,6 +178,7 @@ public class RouterActivity extends AppCompatActivity { | ||||
|         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 String downloadKey = getString(R.string.download_key); | ||||
|         final String alwaysAskKey = getString(R.string.always_ask_open_action_key); | ||||
|  | ||||
|         if (selectedChoiceKey.equals(alwaysAskKey)) { | ||||
| @@ -179,6 +193,8 @@ public class RouterActivity extends AppCompatActivity { | ||||
|             } | ||||
|         } else if (selectedChoiceKey.equals(showInfoKey)) { | ||||
|             handleChoice(showInfoKey); | ||||
|         } else if (selectedChoiceKey.equals(downloadKey)) { | ||||
|             handleChoice(downloadKey); | ||||
|         } else { | ||||
|             final boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); | ||||
|             final boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); | ||||
| @@ -236,7 +252,9 @@ public class RouterActivity extends AppCompatActivity { | ||||
|                 .setCancelable(true) | ||||
|                 .setNegativeButton(R.string.just_once, dialogButtonsClickListener) | ||||
|                 .setPositiveButton(R.string.always, dialogButtonsClickListener) | ||||
|                 .setOnDismissListener((dialog) -> finish()) | ||||
|                 .setOnDismissListener((dialog) -> { | ||||
|                     if(!selectionIsDownload) finish(); | ||||
|                 }) | ||||
|                 .create(); | ||||
|  | ||||
|         //noinspection CodeBlock2Expr | ||||
| @@ -316,6 +334,9 @@ public class RouterActivity extends AppCompatActivity { | ||||
|                     resolveResourceIdFromAttr(context, R.attr.audio))); | ||||
|         } | ||||
|  | ||||
|         returnList.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download), | ||||
|                 resolveResourceIdFromAttr(context, R.attr.download))); | ||||
|  | ||||
|         return returnList; | ||||
|     } | ||||
|  | ||||
| @@ -347,6 +368,14 @@ public class RouterActivity extends AppCompatActivity { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (selectedChoiceKey.equals(getString(R.string.download_key))) { | ||||
|             if (PermissionHelper.checkStoragePermissions(this, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { | ||||
|                 selectionIsDownload = true; | ||||
|                 openDownloadDialog(); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // stop and bypass FetcherService if InfoScreen was selected since | ||||
|         // StreamDetailFragment can fetch data itself | ||||
|         if (selectedChoiceKey.equals(getString(R.string.show_info_key))) { | ||||
| @@ -373,6 +402,47 @@ public class RouterActivity extends AppCompatActivity { | ||||
|         finish(); | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("CheckResult") | ||||
|     private void openDownloadDialog() { | ||||
|         ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe((@NonNull StreamInfo result) -> { | ||||
|                     List<VideoStream> sortedVideoStreams = ListHelper.getSortedStreamVideosList(this, | ||||
|                             result.getVideoStreams(), | ||||
|                             result.getVideoOnlyStreams(), | ||||
|                             false); | ||||
|                     int selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(this, | ||||
|                             sortedVideoStreams); | ||||
|  | ||||
|                     android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); | ||||
|                     DownloadDialog downloadDialog = DownloadDialog.newInstance(result); | ||||
|                     downloadDialog.setVideoStreams(sortedVideoStreams); | ||||
|                     downloadDialog.setAudioStreams(result.getAudioStreams()); | ||||
|                     downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); | ||||
|                     downloadDialog.show(fm, "downloadDialog"); | ||||
|                     fm.executePendingTransactions(); | ||||
|                     downloadDialog.getDialog().setOnDismissListener(dialog -> { | ||||
|                         finish(); | ||||
|                     }); | ||||
|                 }, (@NonNull Throwable throwable) -> { | ||||
|                     onError(); | ||||
|                 }); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { | ||||
|         for (int i: grantResults){ | ||||
|             if (i == PackageManager.PERMISSION_DENIED){ | ||||
|                 finish(); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|         if (requestCode == PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE) { | ||||
|             openDownloadDialog(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class AdapterChoiceItem { | ||||
|         final String description, key; | ||||
|         @DrawableRes final int icon; | ||||
|   | ||||
| @@ -71,6 +71,14 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { | ||||
|                 info.getUploaderName(), info.getStreamCount()); | ||||
|     } | ||||
|  | ||||
|     @Ignore | ||||
|     public boolean isIdenticalTo(final PlaylistInfo info) { | ||||
|         return getServiceId() == info.getServiceId() && getName().equals(info.getName()) && | ||||
|                 getStreamCount() == info.getStreamCount() && getUrl().equals(info.getUrl()) && | ||||
|                 getThumbnailUrl().equals(info.getThumbnailUrl()) && | ||||
|                 getUploader().equals(info.getUploaderName()); | ||||
|     } | ||||
|  | ||||
|     public long getUid() { | ||||
|         return uid; | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,158 @@ | ||||
| package org.schabi.newpipe.download; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.design.widget.BaseTransientBottomBar; | ||||
| import android.support.design.widget.Snackbar; | ||||
| import android.view.View; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.concurrent.TimeUnit; | ||||
|  | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.Observable; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import io.reactivex.subjects.PublishSubject; | ||||
| import us.shandian.giga.get.DownloadManager; | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
|  | ||||
| public class DeleteDownloadManager { | ||||
|  | ||||
|     private static final String KEY_STATE = "delete_manager_state"; | ||||
|  | ||||
|     private View mView; | ||||
|     private HashSet<String> mPendingMap; | ||||
|     private List<Disposable> mDisposableList; | ||||
|     private DownloadManager mDownloadManager; | ||||
|     private PublishSubject<DownloadMission> publishSubject = PublishSubject.create(); | ||||
|  | ||||
|     DeleteDownloadManager(Activity activity) { | ||||
|         mPendingMap = new HashSet<>(); | ||||
|         mDisposableList = new ArrayList<>(); | ||||
|         mView = activity.findViewById(android.R.id.content); | ||||
|     } | ||||
|  | ||||
|     public Observable<DownloadMission> getUndoObservable() { | ||||
|         return publishSubject; | ||||
|     } | ||||
|  | ||||
|     public boolean contains(@NonNull DownloadMission mission) { | ||||
|         return mPendingMap.contains(mission.url); | ||||
|     } | ||||
|  | ||||
|     public void add(@NonNull DownloadMission mission) { | ||||
|         mPendingMap.add(mission.url); | ||||
|  | ||||
|         if (mPendingMap.size() == 1) { | ||||
|             showUndoDeleteSnackbar(mission); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void setDownloadManager(@NonNull DownloadManager downloadManager) { | ||||
|         mDownloadManager = downloadManager; | ||||
|  | ||||
|         if (mPendingMap.size() < 1) return; | ||||
|  | ||||
|         showUndoDeleteSnackbar(); | ||||
|     } | ||||
|  | ||||
|     public void restoreState(@Nullable Bundle savedInstanceState) { | ||||
|         if (savedInstanceState == null) return; | ||||
|  | ||||
|         List<String> list = savedInstanceState.getStringArrayList(KEY_STATE); | ||||
|         if (list != null) { | ||||
|             mPendingMap.addAll(list); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void saveState(@Nullable Bundle outState) { | ||||
|         if (outState == null) return; | ||||
|  | ||||
|         for (Disposable disposable : mDisposableList) { | ||||
|             disposable.dispose(); | ||||
|         } | ||||
|  | ||||
|         outState.putStringArrayList(KEY_STATE, new ArrayList<>(mPendingMap)); | ||||
|     } | ||||
|  | ||||
|     private void showUndoDeleteSnackbar() { | ||||
|         if (mPendingMap.size() < 1) return; | ||||
|  | ||||
|         String url = mPendingMap.iterator().next(); | ||||
|  | ||||
|         for (int i = 0; i < mDownloadManager.getCount(); i++) { | ||||
|             DownloadMission mission = mDownloadManager.getMission(i); | ||||
|             if (url.equals(mission.url)) { | ||||
|                 showUndoDeleteSnackbar(mission); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void showUndoDeleteSnackbar(@NonNull DownloadMission mission) { | ||||
|         final Snackbar snackbar = Snackbar.make(mView, mission.name, Snackbar.LENGTH_INDEFINITE); | ||||
|         final Disposable disposable = Observable.timer(3, TimeUnit.SECONDS) | ||||
|                 .subscribeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(l -> snackbar.dismiss()); | ||||
|  | ||||
|         mDisposableList.add(disposable); | ||||
|  | ||||
|         snackbar.setAction(R.string.undo, v -> { | ||||
|             mPendingMap.remove(mission.url); | ||||
|             publishSubject.onNext(mission); | ||||
|             disposable.dispose(); | ||||
|             snackbar.dismiss(); | ||||
|         }); | ||||
|  | ||||
|         snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() { | ||||
|             @Override | ||||
|             public void onDismissed(Snackbar transientBottomBar, int event) { | ||||
|                 if (!disposable.isDisposed()) { | ||||
|                     Completable.fromAction(() -> deletePending(mission)) | ||||
|                             .subscribeOn(Schedulers.io()) | ||||
|                             .subscribe(); | ||||
|                 } | ||||
|                 mPendingMap.remove(mission.url); | ||||
|                 snackbar.removeCallback(this); | ||||
|                 mDisposableList.remove(disposable); | ||||
|                 showUndoDeleteSnackbar(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         snackbar.show(); | ||||
|     } | ||||
|  | ||||
|     public void deletePending() { | ||||
|         if (mPendingMap.size() < 1) return; | ||||
|  | ||||
|         HashSet<Integer> idSet = new HashSet<>(); | ||||
|         for (int i = 0; i < mDownloadManager.getCount(); i++) { | ||||
|             if (contains(mDownloadManager.getMission(i))) { | ||||
|                 idSet.add(i); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         for (Integer id : idSet) { | ||||
|             mDownloadManager.deleteMission(id); | ||||
|         } | ||||
|  | ||||
|         mPendingMap.clear(); | ||||
|     } | ||||
|  | ||||
|     private void deletePending(@NonNull DownloadMission mission) { | ||||
|         for (int i = 0; i < mDownloadManager.getCount(); i++) { | ||||
|             if (mission.url.equals(mDownloadManager.getMission(i).url)) { | ||||
|                 mDownloadManager.deleteMission(i); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -15,12 +15,17 @@ import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.settings.SettingsActivity; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import us.shandian.giga.service.DownloadManagerService; | ||||
| import us.shandian.giga.ui.fragment.AllMissionsFragment; | ||||
| import us.shandian.giga.ui.fragment.MissionsFragment; | ||||
|  | ||||
| public class DownloadActivity extends AppCompatActivity { | ||||
|  | ||||
|     private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag"; | ||||
|     private DeleteDownloadManager mDeleteDownloadManager; | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         // Service | ||||
| @@ -42,21 +47,35 @@ public class DownloadActivity extends AppCompatActivity { | ||||
|             actionBar.setDisplayShowTitleEnabled(true); | ||||
|         } | ||||
|  | ||||
|         // Fragment | ||||
|         getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { | ||||
|             @Override | ||||
|             public void onGlobalLayout() { | ||||
|                 updateFragments(); | ||||
|                 getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this); | ||||
|             } | ||||
|         }); | ||||
|         mDeleteDownloadManager = new DeleteDownloadManager(this); | ||||
|         mDeleteDownloadManager.restoreState(savedInstanceState); | ||||
|  | ||||
|         MissionsFragment fragment = (MissionsFragment) getFragmentManager().findFragmentByTag(MISSIONS_FRAGMENT_TAG); | ||||
|         if (fragment != null) { | ||||
|             fragment.setDeleteManager(mDeleteDownloadManager); | ||||
|         } else { | ||||
|             getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { | ||||
|                 @Override | ||||
|                 public void onGlobalLayout() { | ||||
|                     updateFragments(); | ||||
|                     getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onSaveInstanceState(Bundle outState) { | ||||
|         mDeleteDownloadManager.saveState(outState); | ||||
|         super.onSaveInstanceState(outState); | ||||
|     } | ||||
|  | ||||
|     private void updateFragments() { | ||||
|  | ||||
|         MissionsFragment fragment = new AllMissionsFragment(); | ||||
|         fragment.setDeleteManager(mDeleteDownloadManager); | ||||
|  | ||||
|         getFragmentManager().beginTransaction() | ||||
|                 .replace(R.id.frame, fragment) | ||||
|                 .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG) | ||||
|                 .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) | ||||
|                 .commit(); | ||||
|     } | ||||
| @@ -80,6 +99,7 @@ public class DownloadActivity extends AppCompatActivity { | ||||
|             case R.id.action_settings: { | ||||
|                 Intent intent = new Intent(this, SettingsActivity.class); | ||||
|                 startActivity(intent); | ||||
|                 deletePending(); | ||||
|                 return true; | ||||
|             } | ||||
|             default: | ||||
| @@ -87,4 +107,15 @@ public class DownloadActivity extends AppCompatActivity { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBackPressed() { | ||||
|         super.onBackPressed(); | ||||
|         deletePending(); | ||||
|     } | ||||
|  | ||||
|     private void deletePending() { | ||||
|         Completable.fromAction(mDeleteDownloadManager::deletePending) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .subscribe(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,38 @@ | ||||
| package org.schabi.newpipe.download; | ||||
|  | ||||
| import android.app.AlertDialog; | ||||
| import android.content.DialogInterface; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.settings.NewPipeSettings; | ||||
| import org.schabi.newpipe.util.ServiceHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| public class ExtSDDownloadFailedActivity extends AppCompatActivity { | ||||
|     @Override | ||||
|     protected void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onStart() { | ||||
|         super.onStart(); | ||||
|         new AlertDialog.Builder(this) | ||||
|                 .setTitle(R.string.download_to_sdcard_error_title) | ||||
|                 .setMessage(R.string.download_to_sdcard_error_message) | ||||
|                 .setPositiveButton(R.string.yes, (DialogInterface dialogInterface, int i) -> { | ||||
|                     NewPipeSettings.resetDownloadFolders(this); | ||||
|                     finish(); | ||||
|                 }) | ||||
|                 .setNegativeButton(R.string.cancel, (DialogInterface dialogInterface, int i) -> { | ||||
|                     dialogInterface.dismiss(); | ||||
|                     finish(); | ||||
|                 }) | ||||
|                 .create() | ||||
|                 .show(); | ||||
|     } | ||||
| } | ||||
| @@ -51,9 +51,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC | ||||
|     protected Button errorButtonRetry; | ||||
|     protected TextView errorTextView; | ||||
|  | ||||
|     @State | ||||
|     protected boolean useAsFrontPage = false; | ||||
|  | ||||
|     @Override | ||||
|     public void onViewCreated(View rootView, Bundle savedInstanceState) { | ||||
|         super.onViewCreated(rootView, savedInstanceState); | ||||
| @@ -66,9 +63,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC | ||||
|         wasLoading.set(isLoading.get()); | ||||
|     } | ||||
|  | ||||
|     public void useAsFrontPage(boolean value) { | ||||
|         useAsFrontPage = value; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Init | ||||
| @@ -93,12 +87,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC | ||||
|         RxView.clicks(errorButtonRetry) | ||||
|                 .debounce(300, TimeUnit.MILLISECONDS) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(new Consumer<Object>() { | ||||
|                     @Override | ||||
|                     public void accept(Object o) throws Exception { | ||||
|                         onRetryButtonClicked(); | ||||
|                     } | ||||
|                 }); | ||||
|                 .subscribe(o -> onRetryButtonClicked()); | ||||
|     } | ||||
|  | ||||
|     protected void onRetryButtonClicked() { | ||||
|   | ||||
| @@ -14,24 +14,16 @@ public class BlankFragment extends BaseFragment { | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { | ||||
|         if(activity != null && activity.getSupportActionBar() != null) { | ||||
|             activity.getSupportActionBar() | ||||
|                     .setTitle("NewPipe"); | ||||
|         } | ||||
|         setTitle("NewPipe"); | ||||
|         return inflater.inflate(R.layout.fragment_blank, container, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void setUserVisibleHint(boolean isVisibleToUser) { | ||||
|         super.setUserVisibleHint(isVisibleToUser); | ||||
|         if(isVisibleToUser) { | ||||
|             if(activity != null && activity.getSupportActionBar() != null) { | ||||
|                 activity.getSupportActionBar() | ||||
|                         .setTitle("NewPipe"); | ||||
|             } | ||||
|             // leave this inline. Will make it harder for copy cats. | ||||
|             // If you are a Copy cat FUCK YOU. | ||||
|             // I WILL FIND YOU, AND I WILL ... | ||||
|         } | ||||
|         setTitle("NewPipe"); | ||||
|         // leave this inline. Will make it harder for copy cats. | ||||
|         // If you are a Copy cat FUCK YOU. | ||||
|         // I WILL FIND YOU, AND I WILL ... | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package org.schabi.newpipe.fragments; | ||||
|  | ||||
| import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| @@ -10,48 +9,37 @@ import android.support.v4.app.FragmentManager; | ||||
| import android.support.v4.app.FragmentPagerAdapter; | ||||
| import android.support.v4.view.ViewPager; | ||||
| import android.support.v7.app.ActionBar; | ||||
| import android.support.v7.preference.PreferenceManager; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.view.SubMenu; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
|  | ||||
| import org.schabi.newpipe.BaseFragment; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.kiosk.KioskList; | ||||
| import org.schabi.newpipe.fragments.list.channel.ChannelFragment; | ||||
| import org.schabi.newpipe.local.feed.FeedFragment; | ||||
| import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; | ||||
| import org.schabi.newpipe.local.bookmark.BookmarkFragment; | ||||
| import org.schabi.newpipe.local.subscription.SubscriptionFragment; | ||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.KioskTranslator; | ||||
| import org.schabi.newpipe.settings.tabs.Tab; | ||||
| import org.schabi.newpipe.settings.tabs.TabsManager; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.ServiceHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener { | ||||
|  | ||||
|     public int currentServiceId = -1; | ||||
|     private ViewPager viewPager; | ||||
|     private SelectedTabsPagerAdapter pagerAdapter; | ||||
|     private TabLayout tabLayout; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Constants | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     private List<Tab> tabsList = new ArrayList<>(); | ||||
|     private TabsManager tabsManager; | ||||
|  | ||||
|     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"; | ||||
|     private static final int KIOSK_MENU_OFFSET = 2000; | ||||
|     private boolean hasTabsChanged = false; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment's LifeCycle | ||||
| @@ -61,11 +49,22 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setHasOptionsMenu(true); | ||||
|  | ||||
|         tabsManager = TabsManager.getManager(activity); | ||||
|         tabsManager.setSavedTabsListener(() -> { | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed()); | ||||
|             } | ||||
|             if (isResumed()) { | ||||
|                 updateTabs(); | ||||
|             } else { | ||||
|                 hasTabsChanged = true; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { | ||||
|         currentServiceId = ServiceHelper.getSelectedServiceId(activity); | ||||
|         return inflater.inflate(R.layout.fragment_main, container, false); | ||||
|     } | ||||
|  | ||||
| @@ -73,30 +72,34 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|     protected void initViews(View rootView, Bundle savedInstanceState) { | ||||
|         super.initViews(rootView, savedInstanceState); | ||||
|  | ||||
|         TabLayout tabLayout = rootView.findViewById(R.id.main_tab_layout); | ||||
|         tabLayout = rootView.findViewById(R.id.main_tab_layout); | ||||
|         viewPager = rootView.findViewById(R.id.pager); | ||||
|  | ||||
|         /*  Nested fragment, use child fragment here to maintain backstack in view pager. */ | ||||
|         PagerAdapter adapter = new PagerAdapter(getChildFragmentManager()); | ||||
|         viewPager.setAdapter(adapter); | ||||
|         viewPager.setOffscreenPageLimit(adapter.getCount()); | ||||
|         pagerAdapter = new SelectedTabsPagerAdapter(getChildFragmentManager()); | ||||
|         viewPager.setAdapter(pagerAdapter); | ||||
|  | ||||
|         tabLayout.setupWithViewPager(viewPager); | ||||
|         tabLayout.addOnTabSelectedListener(this); | ||||
|         updateTabs(); | ||||
|     } | ||||
|  | ||||
|         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); | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|  | ||||
|         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); | ||||
|         if (hasTabsChanged) { | ||||
|             hasTabsChanged = false; | ||||
|             updateTabs(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         tabsManager.unsetSavedTabsListener(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Menu | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -106,16 +109,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|         super.onCreateOptionsMenu(menu, inflater); | ||||
|         if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); | ||||
|         inflater.inflate(R.menu.main_fragment_menu, menu); | ||||
|         SubMenu kioskMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, 200, getString(R.string.kiosk)); | ||||
|         try { | ||||
|             createKioskMenu(kioskMenu, inflater); | ||||
|         } catch (Exception e) { | ||||
|             ErrorActivity.reportError(activity, e, | ||||
|                     activity.getClass(), | ||||
|                     null, | ||||
|                     ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, | ||||
|                             "none", "", R.string.app_ui_crash)); | ||||
|         } | ||||
|  | ||||
|         ActionBar supportActionBar = activity.getSupportActionBar(); | ||||
|         if (supportActionBar != null) { | ||||
| @@ -127,7 +120,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         switch (item.getItemId()) { | ||||
|             case R.id.action_search: | ||||
|                 NavigationHelper.openSearchFragment(getFragmentManager(), ServiceHelper.getSelectedServiceId(activity), ""); | ||||
|                 try { | ||||
|                     NavigationHelper.openSearchFragment( | ||||
|                             getFragmentManager(), | ||||
|                             ServiceHelper.getSelectedServiceId(activity), | ||||
|                             ""); | ||||
|                 } catch (Exception e) { | ||||
|                     ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); | ||||
|                 } | ||||
|                 return true; | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item); | ||||
| @@ -137,9 +137,33 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|     // Tabs | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public void updateTabs() { | ||||
|         tabsList.clear(); | ||||
|         tabsList.addAll(tabsManager.getTabs()); | ||||
|         pagerAdapter.notifyDataSetChanged(); | ||||
|  | ||||
|         viewPager.setOffscreenPageLimit(pagerAdapter.getCount()); | ||||
|         updateTabsIcon(); | ||||
|         updateCurrentTitle(); | ||||
|     } | ||||
|  | ||||
|     private void updateTabsIcon() { | ||||
|         for (int i = 0; i < tabsList.size(); i++) { | ||||
|             final TabLayout.Tab tabToSet = tabLayout.getTabAt(i); | ||||
|             if (tabToSet != null) { | ||||
|                 tabToSet.setIcon(tabsList.get(i).getTabIconRes(activity)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void updateCurrentTitle() { | ||||
|         setTitle(tabsList.get(viewPager.getCurrentItem()).getTabName(requireContext())); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onTabSelected(TabLayout.Tab tab) { | ||||
|         viewPager.setCurrentItem(tab.getPosition()); | ||||
|     public void onTabSelected(TabLayout.Tab selectedTab) { | ||||
|         if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]"); | ||||
|         updateCurrentTitle(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -148,129 +172,58 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | ||||
|  | ||||
|     @Override | ||||
|     public void onTabReselected(TabLayout.Tab tab) { | ||||
|         if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]"); | ||||
|         updateCurrentTitle(); | ||||
|     } | ||||
|  | ||||
|     private class PagerAdapter extends FragmentPagerAdapter { | ||||
|         PagerAdapter(FragmentManager fm) { | ||||
|             super(fm); | ||||
|     private class SelectedTabsPagerAdapter extends FragmentPagerAdapter { | ||||
|         private SelectedTabsPagerAdapter(FragmentManager fragmentManager) { | ||||
|             super(fragmentManager); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public Fragment getItem(int position) { | ||||
|             switch (position) { | ||||
|                 case 0: | ||||
|                     return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment(); | ||||
|                 case 1: | ||||
|                     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(); | ||||
|             final Tab tab = tabsList.get(position); | ||||
|  | ||||
|             Throwable throwable = null; | ||||
|             Fragment fragment = null; | ||||
|             try { | ||||
|                 fragment = tab.getFragment(); | ||||
|             } catch (ExtractionException e) { | ||||
|                 throwable = e; | ||||
|             } | ||||
|  | ||||
|             if (throwable != null) { | ||||
|                 ErrorActivity.reportError(activity, throwable, activity.getClass(), null, | ||||
|                         ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); | ||||
|                 return new BlankFragment(); | ||||
|             } | ||||
|  | ||||
|             if (fragment instanceof BaseFragment) { | ||||
|                 ((BaseFragment) fragment).useAsFrontPage(true); | ||||
|             } | ||||
|  | ||||
|             return fragment; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public CharSequence getPageTitle(int position) { | ||||
|             //return getString(this.tabTitles[position]); | ||||
|             return ""; | ||||
|         public int getItemPosition(Object object) { | ||||
|             // Causes adapter to reload all Fragments when | ||||
|             // notifyDataSetChanged is called | ||||
|             return POSITION_NONE; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public int getCount() { | ||||
|             return isSubscriptionsPageOnlySelected() ? 2 : 3; | ||||
|             return tabsList.size(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Main page content | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private boolean isSubscriptionsPageOnlySelected() { | ||||
|         return PreferenceManager.getDefaultSharedPreferences(activity) | ||||
|                 .getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key)) | ||||
|                 .equals(getString(R.string.subscription_page_key)); | ||||
|     } | ||||
|  | ||||
|     private Fragment getMainPageFragment() { | ||||
|         if (getActivity() == null) return new BlankFragment(); | ||||
|  | ||||
|         try { | ||||
|             SharedPreferences preferences = | ||||
|                     PreferenceManager.getDefaultSharedPreferences(getActivity()); | ||||
|             final String setMainPage = preferences.getString(getString(R.string.main_page_content_key), | ||||
|                     getString(R.string.main_page_selectd_kiosk_id)); | ||||
|             if (setMainPage.equals(getString(R.string.blank_page_key))) { | ||||
|                 return new BlankFragment(); | ||||
|             } else if (setMainPage.equals(getString(R.string.kiosk_page_key))) { | ||||
|                 int serviceId = preferences.getInt(getString(R.string.main_page_selected_service), | ||||
|                         FALLBACK_SERVICE_ID); | ||||
|                 String kioskId = preferences.getString(getString(R.string.main_page_selectd_kiosk_id), | ||||
|                         FALLBACK_KIOSK_ID); | ||||
|                 KioskFragment fragment = KioskFragment.getInstance(serviceId, kioskId); | ||||
|                 fragment.useAsFrontPage(true); | ||||
|                 return fragment; | ||||
|             } else if (setMainPage.equals(getString(R.string.feed_page_key))) { | ||||
|                 FeedFragment fragment = new FeedFragment(); | ||||
|                 fragment.useAsFrontPage(true); | ||||
|                 return fragment; | ||||
|             } else if (setMainPage.equals(getString(R.string.channel_page_key))) { | ||||
|                 int serviceId = preferences.getInt(getString(R.string.main_page_selected_service), | ||||
|                         FALLBACK_SERVICE_ID); | ||||
|                 String url = preferences.getString(getString(R.string.main_page_selected_channel_url), | ||||
|                         FALLBACK_CHANNEL_URL); | ||||
|                 String name = preferences.getString(getString(R.string.main_page_selected_channel_name), | ||||
|                         FALLBACK_CHANNEL_NAME); | ||||
|                 ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name); | ||||
|                 fragment.useAsFrontPage(true); | ||||
|                 return fragment; | ||||
|             } else { | ||||
|                 return new BlankFragment(); | ||||
|             } | ||||
|  | ||||
|         } catch (Exception e) { | ||||
|             ErrorActivity.reportError(activity, e, | ||||
|                     activity.getClass(), | ||||
|                     null, | ||||
|                     ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, | ||||
|                             "none", "", R.string.app_ui_crash)); | ||||
|             return new BlankFragment(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Select Kiosk | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void createKioskMenu(Menu menu, MenuInflater menuInflater) | ||||
|             throws Exception { | ||||
|         StreamingService service = NewPipe.getService(currentServiceId); | ||||
|         KioskList kl = service.getKioskList(); | ||||
|         int i = 0; | ||||
|         for (final String ks : kl.getAvailableKiosks()) { | ||||
|             menu.add(0, KIOSK_MENU_OFFSET + i, Menu.NONE, | ||||
|                     KioskTranslator.getTranslatedKioskName(ks, getContext())) | ||||
|                     .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { | ||||
|                         @Override | ||||
|                         public boolean onMenuItemClick(MenuItem menuItem) { | ||||
|                             try { | ||||
|                                 NavigationHelper.openKioskFragment(getFragmentManager(), currentServiceId, ks); | ||||
|                             } catch (Exception e) { | ||||
|                                 ErrorActivity.reportError(activity, e, | ||||
|                                         activity.getClass(), | ||||
|                                         null, | ||||
|                                         ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, | ||||
|                                                 "none", "", R.string.app_ui_crash)); | ||||
|                             } | ||||
|                             return true; | ||||
|                         } | ||||
|                     }); | ||||
|             i++; | ||||
|         @Override | ||||
|         public void destroyItem(ViewGroup container, int position, Object object) { | ||||
|             getChildFragmentManager() | ||||
|                     .beginTransaction() | ||||
|                     .remove((Fragment) object) | ||||
|                     .commitNowAllowingStateLoss(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import android.support.v4.content.ContextCompat; | ||||
| import android.support.v4.view.animation.FastOutSlowInInterpolator; | ||||
| import android.support.v7.app.ActionBar; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.text.Html; | ||||
| import android.text.Spanned; | ||||
| import android.text.TextUtils; | ||||
| @@ -54,14 +55,17 @@ import org.schabi.newpipe.extractor.InfoItem; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; | ||||
| import org.schabi.newpipe.extractor.exceptions.ParsingException; | ||||
| import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; | ||||
| import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.Stream; | ||||
| 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.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.fragments.BackPressable; | ||||
| import org.schabi.newpipe.fragments.BaseStateFragment; | ||||
| import org.schabi.newpipe.local.history.HistoryRecordManager; | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.util.StreamItemAdapter; | ||||
| import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; | ||||
| import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; | ||||
| @@ -128,7 +132,7 @@ public class VideoDetailFragment | ||||
|  | ||||
|     private StreamInfo currentInfo; | ||||
|     private Disposable currentWorker; | ||||
|     private CompositeDisposable disposables = new CompositeDisposable(); | ||||
|     @NonNull private CompositeDisposable disposables = new CompositeDisposable(); | ||||
|  | ||||
|     private List<VideoStream> sortedVideoStreams; | ||||
|     private int selectedVideoStreamIndex = -1; | ||||
| @@ -363,11 +367,15 @@ public class VideoDetailFragment | ||||
|                 if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { | ||||
|                     Log.w(TAG, "Can't open channel because we got no channel URL"); | ||||
|                 } else { | ||||
|                     NavigationHelper.openChannelFragment( | ||||
|                             getFragmentManager(), | ||||
|                             currentInfo.getServiceId(), | ||||
|                             currentInfo.getUploaderUrl(), | ||||
|                             currentInfo.getUploaderName()); | ||||
|                     try { | ||||
|                         NavigationHelper.openChannelFragment( | ||||
|                                 getFragmentManager(), | ||||
|                                 currentInfo.getServiceId(), | ||||
|                                 currentInfo.getUploaderUrl(), | ||||
|                                 currentInfo.getUploaderName()); | ||||
|                     } catch (Exception e) { | ||||
|                         ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); | ||||
|                     } | ||||
|                 } | ||||
|                 break; | ||||
|             case R.id.detail_thumbnail_root_layout: | ||||
| @@ -540,7 +548,8 @@ public class VideoDetailFragment | ||||
|         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.append_playlist) | ||||
|                 context.getResources().getString(R.string.append_playlist), | ||||
|                 context.getResources().getString(R.string.share) | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { | ||||
| @@ -557,6 +566,9 @@ public class VideoDetailFragment | ||||
|                                 .show(getFragmentManager(), TAG); | ||||
|                     } | ||||
|                     break; | ||||
|                 case 3: | ||||
|                     shareUrl(item.getName(), item.getUrl()); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
| @@ -872,10 +884,7 @@ public class VideoDetailFragment | ||||
|         if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { | ||||
|             openNormalBackgroundPlayer(append); | ||||
|         } else { | ||||
|             NavigationHelper.playOnExternalPlayer(activity, | ||||
|                     currentInfo.getName(), | ||||
|                     currentInfo.getUploaderName(), | ||||
|                     audioStream); | ||||
|             startOnExternalPlayer(activity, currentInfo, audioStream); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -902,10 +911,7 @@ public class VideoDetailFragment | ||||
|  | ||||
|         if (PreferenceManager.getDefaultSharedPreferences(activity) | ||||
|                 .getBoolean(this.getString(R.string.use_external_video_player_key), false)) { | ||||
|             NavigationHelper.playOnExternalPlayer(activity, | ||||
|                     currentInfo.getName(), | ||||
|                     currentInfo.getUploaderName(), | ||||
|                     selectedVideoStream); | ||||
|             startOnExternalPlayer(activity, currentInfo, selectedVideoStream); | ||||
|         } else { | ||||
|             openNormalPlayer(selectedVideoStream); | ||||
|         } | ||||
| @@ -949,6 +955,20 @@ public class VideoDetailFragment | ||||
|         this.autoPlayEnabled = autoplay; | ||||
|     } | ||||
|  | ||||
|     private void startOnExternalPlayer(@NonNull final Context context, | ||||
|                                        @NonNull final StreamInfo info, | ||||
|                                        @NonNull final Stream selectedStream) { | ||||
|         NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(), | ||||
|                 currentInfo.getUploaderName(), selectedStream); | ||||
|  | ||||
|         final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); | ||||
|         disposables.add(recordManager.onViewed(info).onErrorComplete() | ||||
|                 .subscribe( | ||||
|                         ignored -> {/* successful */}, | ||||
|                         error -> Log.e(TAG, "Register view failure: ", error) | ||||
|                 )); | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     private VideoStream getSelectedVideoStream() { | ||||
|         return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; | ||||
| @@ -1207,10 +1227,10 @@ public class VideoDetailFragment | ||||
|                 spinnerToolbar.setVisibility(View.GONE); | ||||
|                 break; | ||||
|             default: | ||||
|                 if(info.getAudioStreams().isEmpty()) detailControlsBackground.setVisibility(View.GONE); | ||||
|                 if (!info.getVideoStreams().isEmpty() | ||||
|                         || !info.getVideoOnlyStreams().isEmpty()) break; | ||||
|  | ||||
|                 detailControlsBackground.setVisibility(View.GONE); | ||||
|                 detailControlsPopup.setVisibility(View.GONE); | ||||
|                 spinnerToolbar.setVisibility(View.GONE); | ||||
|                 thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp); | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import android.content.DialogInterface; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.v7.app.ActionBar; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.support.v7.widget.LinearLayoutManager; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.util.Log; | ||||
| @@ -24,6 +25,7 @@ import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; | ||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | ||||
| import org.schabi.newpipe.info_list.InfoListAdapter; | ||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.OnClickGesture; | ||||
| import org.schabi.newpipe.util.StateSaver; | ||||
| @@ -152,18 +154,30 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem | ||||
|         infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() { | ||||
|             @Override | ||||
|             public void selected(ChannelInfoItem selectedItem) { | ||||
|                 onItemSelected(selectedItem); | ||||
|                 NavigationHelper.openChannelFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), | ||||
|                         selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); | ||||
|                 try { | ||||
|                     onItemSelected(selectedItem); | ||||
|                     NavigationHelper.openChannelFragment(getFM(), | ||||
|                             selectedItem.getServiceId(), | ||||
|                             selectedItem.getUrl(), | ||||
|                             selectedItem.getName()); | ||||
|                 } catch (Exception e) { | ||||
|                     ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() { | ||||
|             @Override | ||||
|             public void selected(PlaylistInfoItem selectedItem) { | ||||
|                 onItemSelected(selectedItem); | ||||
|                 NavigationHelper.openPlaylistFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), | ||||
|                         selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); | ||||
|                 try { | ||||
|                     onItemSelected(selectedItem); | ||||
|                     NavigationHelper.openPlaylistFragment(getFM(), | ||||
|                             selectedItem.getServiceId(), | ||||
|                             selectedItem.getUrl(), | ||||
|                             selectedItem.getName()); | ||||
|                 } catch (Exception e) { | ||||
|                     ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
| @@ -178,7 +192,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem | ||||
|  | ||||
|     private void onStreamSelected(StreamInfoItem selectedItem) { | ||||
|         onItemSelected(selectedItem); | ||||
|         NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), | ||||
|         NavigationHelper.openVideoDetailFragment(getFM(), | ||||
|                 selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); | ||||
|     } | ||||
|  | ||||
| @@ -196,7 +210,8 @@ 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.append_playlist) | ||||
|                 context.getResources().getString(R.string.append_playlist), | ||||
|                 context.getResources().getString(R.string.share) | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { | ||||
| @@ -213,6 +228,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem | ||||
|                                 .show(getFragmentManager(), TAG); | ||||
|                     } | ||||
|                     break; | ||||
|                 case 3: | ||||
|                     shareUrl(item.getName(), item.getUrl()); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|   | ||||
| @@ -8,6 +8,9 @@ import android.view.View; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.ListExtractor; | ||||
| import org.schabi.newpipe.extractor.ListInfo; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; | ||||
| import org.schabi.newpipe.extractor.linkhandler.LinkHandler; | ||||
| import org.schabi.newpipe.util.Constants; | ||||
|  | ||||
| import java.util.Queue; | ||||
| @@ -166,7 +169,6 @@ public abstract class BaseListInfoFragment<I extends ListInfo> | ||||
|     public void handleResult(@NonNull I result) { | ||||
|         super.handleResult(result); | ||||
|  | ||||
|         url = result.getUrl(); | ||||
|         name = result.getName(); | ||||
|         setTitle(name); | ||||
|  | ||||
|   | ||||
| @@ -36,11 +36,11 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | ||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | ||||
| import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; | ||||
| import org.schabi.newpipe.local.subscription.SubscriptionService; | ||||
| import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.local.subscription.SubscriptionService; | ||||
| import org.schabi.newpipe.util.AnimationUtils; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
| import org.schabi.newpipe.util.ImageDisplayConstants; | ||||
| @@ -90,6 +90,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|  | ||||
|     private MenuItem menuRssButton; | ||||
|  | ||||
|     private boolean mIsVisibleToUser = false; | ||||
|  | ||||
|     public static ChannelFragment getInstance(int serviceId, String url, String name) { | ||||
|         ChannelFragment instance = new ChannelFragment(); | ||||
|         instance.setInitialData(serviceId, url, name); | ||||
| @@ -103,6 +105,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|     @Override | ||||
|     public void setUserVisibleHint(boolean isVisibleToUser) { | ||||
|         super.setUserVisibleHint(isVisibleToUser); | ||||
|         mIsVisibleToUser = isVisibleToUser; | ||||
|         if(activity != null | ||||
|                 && useAsFrontPage | ||||
|                 && isVisibleToUser) { | ||||
| @@ -161,38 +164,39 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|                 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.append_playlist) | ||||
|                 context.getResources().getString(R.string.append_playlist), | ||||
|                 context.getResources().getString(R.string.share) | ||||
|         }; | ||||
|  | ||||
|         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; | ||||
|                     case 5: | ||||
|                         if (getFragmentManager() != null) { | ||||
|                             PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) | ||||
|                                     .show(getFragmentManager(), TAG); | ||||
|                         } | ||||
|                         break; | ||||
|                     default: | ||||
|                         break; | ||||
|                 } | ||||
|         final DialogInterface.OnClickListener actions = (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; | ||||
|                 case 5: | ||||
|                     if (getFragmentManager() != null) { | ||||
|                         PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) | ||||
|                                 .show(getFragmentManager(), TAG); | ||||
|                     } | ||||
|                     break; | ||||
|                 case 6: | ||||
|                     shareUrl(item.getName(), item.getUrl()); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @@ -250,12 +254,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|     private static final int BUTTON_DEBOUNCE_INTERVAL = 100; | ||||
|  | ||||
|     private void monitorSubscription(final ChannelInfo info) { | ||||
|         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @Override | ||||
|             public void accept(Throwable throwable) throws Exception { | ||||
|         final Consumer<Throwable> onError = (Throwable throwable) -> { | ||||
|                 animateView(headerSubscribeButton, false, 100); | ||||
|                 showSnackBarError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.getServiceId()), "Get subscription status", 0); | ||||
|             } | ||||
|                 showSnackBarError(throwable, UserAction.SUBSCRIPTION, | ||||
|                         NewPipe.getNameOfService(currentInfo.getServiceId()), | ||||
|                         "Get subscription status", | ||||
|                         0); | ||||
|         }; | ||||
|  | ||||
|         final Observable<List<SubscriptionEntity>> observable = subscriptionService.subscriptionTable() | ||||
| @@ -271,50 +275,38 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|                 // so only update the UI for the latest emission ("sync" the subscribe button's state) | ||||
|                 .debounce(100, TimeUnit.MILLISECONDS) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(new Consumer<List<SubscriptionEntity>>() { | ||||
|                     @Override | ||||
|                     public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception { | ||||
|                         updateSubscribeButton(!subscriptionEntities.isEmpty()); | ||||
|                     } | ||||
|                 }, onError)); | ||||
|                 .subscribe((List<SubscriptionEntity> subscriptionEntities) -> | ||||
|                         updateSubscribeButton(!subscriptionEntities.isEmpty()) | ||||
|                         , onError)); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) { | ||||
|         return new Function<Object, Object>() { | ||||
|             @Override | ||||
|             public Object apply(@NonNull Object o) throws Exception { | ||||
|                 subscriptionService.subscriptionTable().insert(subscription); | ||||
|                 return o; | ||||
|             } | ||||
|         return (@NonNull Object o) -> { | ||||
|             subscriptionService.subscriptionTable().insert(subscription); | ||||
|             return o; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) { | ||||
|         return new Function<Object, Object>() { | ||||
|             @Override | ||||
|             public Object apply(@NonNull Object o) throws Exception { | ||||
|                 subscriptionService.subscriptionTable().delete(subscription); | ||||
|                 return o; | ||||
|             } | ||||
|         return (@NonNull Object o) -> { | ||||
|             subscriptionService.subscriptionTable().delete(subscription); | ||||
|             return o; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private void updateSubscription(final ChannelInfo info) { | ||||
|         if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]"); | ||||
|         final Action onComplete = new Action() { | ||||
|             @Override | ||||
|             public void run() throws Exception { | ||||
|         final Action onComplete = () -> { | ||||
|                 if (DEBUG) Log.d(TAG, "Updated subscription: " + info.getUrl()); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @Override | ||||
|             public void accept(@NonNull Throwable throwable) throws Exception { | ||||
|                 onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(info.getServiceId()), "Updating Subscription for " + info.getUrl(), R.string.subscription_update_failed); | ||||
|             } | ||||
|         }; | ||||
|         final Consumer<Throwable> onError = (@NonNull Throwable throwable) -> | ||||
|                 onUnrecoverableError(throwable, | ||||
|                         UserAction.SUBSCRIPTION, | ||||
|                         NewPipe.getNameOfService(info.getServiceId()), | ||||
|                         "Updating Subscription for " + info.getUrl(), | ||||
|                         R.string.subscription_update_failed); | ||||
|  | ||||
|         disposables.add(subscriptionService.updateChannelInfo(info) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
| @@ -323,19 +315,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|     } | ||||
|  | ||||
|     private Disposable monitorSubscribeButton(final Button subscribeButton, final Function<Object, Object> action) { | ||||
|         final Consumer<Object> onNext = new Consumer<Object>() { | ||||
|             @Override | ||||
|             public void accept(@NonNull Object o) throws Exception { | ||||
|         final Consumer<Object> onNext = (@NonNull Object o) -> { | ||||
|                 if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!"); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @Override | ||||
|             public void accept(@NonNull Throwable throwable) throws Exception { | ||||
|                 onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.getServiceId()), "Subscription Change", R.string.subscription_change_failed); | ||||
|             } | ||||
|         }; | ||||
|         final Consumer<Throwable> onError = (@NonNull Throwable throwable) -> | ||||
|                 onUnrecoverableError(throwable, | ||||
|                         UserAction.SUBSCRIPTION, | ||||
|                         NewPipe.getNameOfService(currentInfo.getServiceId()), | ||||
|                         "Subscription Change", | ||||
|                         R.string.subscription_change_failed); | ||||
|  | ||||
|         /* Emit clicks from main thread unto io thread */ | ||||
|         return RxView.clicks(subscribeButton) | ||||
| @@ -347,25 +336,25 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|     } | ||||
|  | ||||
|     private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) { | ||||
|         return new Consumer<List<SubscriptionEntity>>() { | ||||
|             @Override | ||||
|             public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception { | ||||
|                 if (DEBUG) | ||||
|                     Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]"); | ||||
|                 if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); | ||||
|         return (List<SubscriptionEntity> subscriptionEntities) -> { | ||||
|             if (DEBUG) | ||||
|                 Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]"); | ||||
|             if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); | ||||
|  | ||||
|                 if (subscriptionEntities.isEmpty()) { | ||||
|                     if (DEBUG) Log.d(TAG, "No subscription to this channel!"); | ||||
|                     SubscriptionEntity channel = new SubscriptionEntity(); | ||||
|                     channel.setServiceId(info.getServiceId()); | ||||
|                     channel.setUrl(info.getUrl()); | ||||
|                     channel.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount()); | ||||
|                     subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel)); | ||||
|                 } else { | ||||
|                     if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); | ||||
|                     final SubscriptionEntity subscription = subscriptionEntities.get(0); | ||||
|                     subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); | ||||
|                 } | ||||
|             if (subscriptionEntities.isEmpty()) { | ||||
|                 if (DEBUG) Log.d(TAG, "No subscription to this channel!"); | ||||
|                 SubscriptionEntity channel = new SubscriptionEntity(); | ||||
|                 channel.setServiceId(info.getServiceId()); | ||||
|                 channel.setUrl(info.getUrl()); | ||||
|                 channel.setData(info.getName(), | ||||
|                         info.getAvatarUrl(), | ||||
|                         info.getDescription(), | ||||
|                         info.getSubscriberCount()); | ||||
|                 subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel)); | ||||
|             } else { | ||||
|                 if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); | ||||
|                 final SubscriptionEntity subscription = subscriptionEntities.get(0); | ||||
|                 subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| @@ -432,10 +421,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|         imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, | ||||
|         		ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); | ||||
|  | ||||
|         if (result.getSubscriberCount() != -1) { | ||||
|         headerSubscribersTextView.setVisibility(View.VISIBLE); | ||||
|         if (result.getSubscriberCount() >= 0) { | ||||
|             headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount())); | ||||
|             headerSubscribersTextView.setVisibility(View.VISIBLE); | ||||
|         } else headerSubscribersTextView.setVisibility(View.GONE); | ||||
|         } else { | ||||
|             headerSubscribersTextView.setText(R.string.subscribers_count_not_available); | ||||
|         } | ||||
|  | ||||
|         if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); | ||||
|  | ||||
| @@ -483,8 +474,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|         super.handleNextItems(result); | ||||
|  | ||||
|         if (!result.getErrors().isEmpty()) { | ||||
|             showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), | ||||
|                     "Get next page of: " + url, R.string.general_error); | ||||
|             showSnackBarError(result.getErrors(), | ||||
|                     UserAction.REQUESTED_CHANNEL, | ||||
|                     NewPipe.getNameOfService(serviceId), | ||||
|                     "Get next page of: " + url, | ||||
|                     R.string.general_error); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -497,7 +491,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|         if (super.onError(exception)) return true; | ||||
|  | ||||
|         int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; | ||||
|         onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, errorId); | ||||
|         onUnrecoverableError(exception, | ||||
|                 UserAction.REQUESTED_CHANNEL, | ||||
|                 NewPipe.getNameOfService(serviceId), | ||||
|                 url, | ||||
|                 errorId); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
| @@ -508,6 +506,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | ||||
|     @Override | ||||
|     public void setTitle(String title) { | ||||
|         super.setTitle(title); | ||||
|         headerTitleView.setText(title); | ||||
|         if (!useAsFrontPage) headerTitleView.setText(title); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,22 +11,20 @@ import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.ListExtractor; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.UrlIdHandler; | ||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.kiosk.KioskInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; | ||||
| import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; | ||||
| import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory; | ||||
| import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | ||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
| import org.schabi.newpipe.util.KioskTranslator; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
|  | ||||
| import icepick.State; | ||||
| import io.reactivex.Single; | ||||
| @@ -59,6 +57,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> { | ||||
|     protected String kioskId = ""; | ||||
|     protected String kioskTranslatedName; | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Views | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -74,10 +73,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> { | ||||
|             throws ExtractionException { | ||||
|         KioskFragment instance = new KioskFragment(); | ||||
|         StreamingService service = NewPipe.getService(serviceId); | ||||
|         UrlIdHandler kioskTypeUrlIdHandler = service.getKioskList() | ||||
|                 .getUrlIdHandlerByType(kioskId); | ||||
|         ListLinkHandlerFactory kioskLinkHandlerFactory = service.getKioskList() | ||||
|                 .getListLinkHandlerFactoryByType(kioskId); | ||||
|         instance.setInitialData(serviceId, | ||||
|                 kioskTypeUrlIdHandler.getUrl(kioskId), kioskId); | ||||
|                 kioskLinkHandlerFactory.fromId(kioskId).getUrl(), kioskId); | ||||
|         instance.kioskId = kioskId; | ||||
|         return instance; | ||||
|     } | ||||
| @@ -136,7 +135,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> { | ||||
|                 .getDefaultSharedPreferences(activity) | ||||
|                 .getString(getString(R.string.content_country_key), | ||||
|                         getString(R.string.default_country_value)); | ||||
|         return ExtractorHelper.getKioskInfo(serviceId, url, contentCountry, forceReload); | ||||
|         return ExtractorHelper.getKioskInfo(serviceId, | ||||
|                 url, | ||||
|                 contentCountry, | ||||
|                 forceReload); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -145,7 +147,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> { | ||||
|                 .getDefaultSharedPreferences(activity) | ||||
|                 .getString(getString(R.string.content_country_key), | ||||
|                         getString(R.string.default_country_value)); | ||||
|         return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPageUrl, contentCountry); | ||||
|         return ExtractorHelper.getMoreKioskItems(serviceId, | ||||
|                 url, | ||||
|                 currentNextPageUrl, | ||||
|                 contentCountry); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -163,7 +168,9 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> { | ||||
|         super.handleResult(result); | ||||
|  | ||||
|         name = kioskTranslatedName; | ||||
|         setTitle(kioskTranslatedName); | ||||
|         if(!useAsFrontPage) { | ||||
|             setTitle(kioskTranslatedName); | ||||
|         } | ||||
|  | ||||
|         if (!result.getErrors().isEmpty()) { | ||||
|             showSnackBarError(result.getErrors(), | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import android.content.DialogInterface; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.text.TextUtils; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| @@ -19,6 +20,7 @@ import android.widget.TextView; | ||||
|  | ||||
| import org.reactivestreams.Subscriber; | ||||
| import org.reactivestreams.Subscription; | ||||
| import org.schabi.newpipe.App; | ||||
| import org.schabi.newpipe.NewPipeDatabase; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||
| @@ -28,12 +30,14 @@ 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.extractor.linkhandler.ListLinkHandler; | ||||
| import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | ||||
| import org.schabi.newpipe.local.playlist.RemotePlaylistManager; | ||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | ||||
| import org.schabi.newpipe.local.playlist.RemotePlaylistManager; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
| import org.schabi.newpipe.util.ImageDisplayConstants; | ||||
| @@ -44,6 +48,7 @@ import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.concurrent.atomic.AtomicBoolean; | ||||
|  | ||||
| import io.reactivex.Flowable; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| @@ -93,7 +98,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         disposables = new CompositeDisposable(); | ||||
|         isBookmarkButtonReady = new AtomicBoolean(false); | ||||
|         remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext())); | ||||
|         remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance( | ||||
|                 requireContext())); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -142,6 +148,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|                 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.share) | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { | ||||
| @@ -162,6 +169,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|                 case 4: | ||||
|                     NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); | ||||
|                     break; | ||||
|                 case 5: | ||||
|                     shareUrl(item.getName(), item.getUrl()); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
| @@ -261,11 +271,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|         if (!TextUtils.isEmpty(result.getUploaderName())) { | ||||
|             headerUploaderName.setText(result.getUploaderName()); | ||||
|             if (!TextUtils.isEmpty(result.getUploaderUrl())) { | ||||
|                 headerUploaderLayout.setOnClickListener(v -> | ||||
|                 headerUploaderLayout.setOnClickListener(v -> { | ||||
|                     try { | ||||
|                         NavigationHelper.openChannelFragment(getFragmentManager(), | ||||
|                                 result.getServiceId(), result.getUploaderUrl(), | ||||
|                                 result.getUploaderName()) | ||||
|                 ); | ||||
|                                 result.getServiceId(), | ||||
|                                 result.getUploaderUrl(), | ||||
|                                 result.getUploaderName()); | ||||
|                     } catch (Exception e) { | ||||
|                         ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -281,14 +296,11 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|         } | ||||
|  | ||||
|         remotePlaylistManager.getPlaylist(result) | ||||
|                 .flatMap(lists -> getUpdateProcessor(lists, result), (lists, id) -> lists) | ||||
|                 .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 -> | ||||
| @@ -336,7 +348,11 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|         if (super.onError(exception)) return true; | ||||
|  | ||||
|         int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; | ||||
|         onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), url, errorId); | ||||
|         onUnrecoverableError(exception, | ||||
|                 UserAction.REQUESTED_PLAYLIST, | ||||
|                 NewPipe.getNameOfService(serviceId), | ||||
|                 url, | ||||
|                 errorId); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
| @@ -344,6 +360,17 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private Flowable<Integer> getUpdateProcessor(@NonNull List<PlaylistRemoteEntity> playlists, | ||||
|                                                  @NonNull PlaylistInfo result) { | ||||
|         final Flowable<Integer> noItemToUpdate = Flowable.just(/*noItemToUpdate=*/-1); | ||||
|         if (playlists.isEmpty()) return noItemToUpdate; | ||||
|  | ||||
|         final PlaylistRemoteEntity playlistEntity = playlists.get(0); | ||||
|         if (playlistEntity.isIdenticalTo(result)) return noItemToUpdate; | ||||
|  | ||||
|         return remotePlaylistManager.onUpdate(playlists.get(0).getUid(), result).toFlowable(); | ||||
|     } | ||||
|  | ||||
|     private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() { | ||||
|         return new Subscriber<List<PlaylistRemoteEntity>>() { | ||||
|             @Override | ||||
| @@ -416,4 +443,4 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|         playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); | ||||
|         playlistBookmarkButton.setTitle(titleRes); | ||||
|     } | ||||
| } | ||||
| } | ||||
| @@ -37,26 +37,30 @@ import org.schabi.newpipe.extractor.ListExtractor; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.exceptions.ParsingException; | ||||
| import org.schabi.newpipe.extractor.search.SearchEngine; | ||||
| import org.schabi.newpipe.extractor.search.SearchResult; | ||||
| import org.schabi.newpipe.extractor.search.SearchExtractor; | ||||
| import org.schabi.newpipe.extractor.search.SearchInfo; | ||||
| import org.schabi.newpipe.fragments.BackPressable; | ||||
| import org.schabi.newpipe.fragments.list.BaseListFragment; | ||||
| import org.schabi.newpipe.local.history.HistoryRecordManager; | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.Constants; | ||||
| import org.schabi.newpipe.util.AnimationUtils; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
| import org.schabi.newpipe.util.LayoutManagerSmoothScroller; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.ServiceHelper; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.io.InterruptedIOException; | ||||
| import java.net.SocketException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.HashMap; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Queue; | ||||
| import java.util.concurrent.Callable; | ||||
| import java.util.concurrent.TimeUnit; | ||||
|  | ||||
| import icepick.State; | ||||
| @@ -65,14 +69,15 @@ 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.schedulers.Schedulers; | ||||
| import io.reactivex.subjects.PublishSubject; | ||||
|  | ||||
| import static java.util.Arrays.asList; | ||||
|  | ||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||
|  | ||||
| public class SearchFragment | ||||
|         extends BaseListFragment<SearchResult, ListExtractor.InfoItemsPage> | ||||
|         extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage> | ||||
|         implements BackPressable { | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -92,19 +97,29 @@ public class SearchFragment | ||||
|  | ||||
|     @State | ||||
|     protected int filterItemCheckedId = -1; | ||||
|     private SearchEngine.Filter filter = SearchEngine.Filter.ANY; | ||||
|  | ||||
|     @State | ||||
|     protected int serviceId = Constants.NO_SERVICE_ID; | ||||
|      | ||||
|     // this three represet the current search query | ||||
|     @State | ||||
|     protected String searchQuery; | ||||
|     protected String searchString; | ||||
|     @State | ||||
|     protected String lastSearchedQuery; | ||||
|     protected String[] contentFilter; | ||||
|     @State | ||||
|     protected String sortFilter; | ||||
|      | ||||
|     // these represtent the last search | ||||
|     @State | ||||
|     protected String lastSearchedString; | ||||
|      | ||||
|     @State | ||||
|     protected boolean wasSearchFocused = false; | ||||
|  | ||||
|     private int currentPage = 0; | ||||
|     private int currentNextPage = 0; | ||||
|     private Map<Integer, String> menuItemToFilterName; | ||||
|     private StreamingService service; | ||||
|     private String currentPageUrl; | ||||
|     private String nextPageUrl; | ||||
|     private String contentCountry; | ||||
|     private boolean isSuggestionsEnabled = true; | ||||
|     private boolean isSearchHistoryEnabled = true; | ||||
| @@ -130,11 +145,11 @@ public class SearchFragment | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public static SearchFragment getInstance(int serviceId, String query) { | ||||
|     public static SearchFragment getInstance(int serviceId, String searchString) { | ||||
|         SearchFragment searchFragment = new SearchFragment(); | ||||
|         searchFragment.setQuery(serviceId, query); | ||||
|         searchFragment.setQuery(serviceId, searchString, new String[0], ""); | ||||
|  | ||||
|         if (!TextUtils.isEmpty(query)) { | ||||
|         if (!TextUtils.isEmpty(searchString)) { | ||||
|             searchFragment.setSearchOnResume(); | ||||
|         } | ||||
|  | ||||
| @@ -202,13 +217,22 @@ public class SearchFragment | ||||
|         if (DEBUG) Log.d(TAG, "onResume() called"); | ||||
|         super.onResume(); | ||||
|  | ||||
|         if (!TextUtils.isEmpty(searchQuery)) { | ||||
|         try { | ||||
|             service = NewPipe.getService(serviceId); | ||||
|         } catch (Exception e) { | ||||
|             ErrorActivity.reportError(getActivity(), e, getActivity().getClass(), | ||||
|                     getActivity().findViewById(android.R.id.content), | ||||
|                     ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, | ||||
|                             "", | ||||
|                             "", R.string.general_error)); | ||||
|         } | ||||
|  | ||||
|         if (!TextUtils.isEmpty(searchString)) { | ||||
|             if (wasLoading.getAndSet(false)) { | ||||
|                 if (currentNextPage > currentPage) loadMoreItems(); | ||||
|                 else search(searchQuery); | ||||
|                 search(searchString, contentFilter, sortFilter); | ||||
|             } else if (infoListAdapter.getItemsList().size() == 0) { | ||||
|                 if (savedState == null) { | ||||
|                     search(searchQuery); | ||||
|                     search(searchString, contentFilter, sortFilter); | ||||
|                 } else if (!isLoading.get() && !wasSearchFocused) { | ||||
|                     infoListAdapter.clearStreamItemList(); | ||||
|                     showEmptyState(); | ||||
| @@ -218,7 +242,7 @@ public class SearchFragment | ||||
|  | ||||
|         if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver(); | ||||
|  | ||||
|         if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) { | ||||
|         if (TextUtils.isEmpty(searchString) || wasSearchFocused) { | ||||
|             showKeyboardSearch(); | ||||
|             showSuggestionsPanel(); | ||||
|         } else { | ||||
| @@ -247,8 +271,9 @@ public class SearchFragment | ||||
|     public void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||
|         switch (requestCode) { | ||||
|             case ReCaptchaActivity.RECAPTCHA_REQUEST: | ||||
|                 if (resultCode == Activity.RESULT_OK && !TextUtils.isEmpty(searchQuery)) { | ||||
|                     search(searchQuery); | ||||
|                 if (resultCode == Activity.RESULT_OK | ||||
|                         && !TextUtils.isEmpty(searchString)) { | ||||
|                     search(searchString, contentFilter, sortFilter); | ||||
|                 } else Log.e(TAG, "ReCaptcha failed"); | ||||
|                 break; | ||||
|  | ||||
| @@ -282,20 +307,22 @@ public class SearchFragment | ||||
|     @Override | ||||
|     public void writeTo(Queue<Object> objectsToSave) { | ||||
|         super.writeTo(objectsToSave); | ||||
|         objectsToSave.add(currentPage); | ||||
|         objectsToSave.add(currentNextPage); | ||||
|         objectsToSave.add(currentPageUrl); | ||||
|         objectsToSave.add(nextPageUrl); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception { | ||||
|         super.readFrom(savedObjects); | ||||
|         currentPage = (int) savedObjects.poll(); | ||||
|         currentNextPage = (int) savedObjects.poll(); | ||||
|         currentPageUrl = (String) savedObjects.poll(); | ||||
|         nextPageUrl = (String) savedObjects.poll(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSaveInstanceState(Bundle bundle) { | ||||
|         searchQuery = searchEditText != null ? searchEditText.getText().toString() : searchQuery; | ||||
|         searchString = searchEditText != null | ||||
|                 ? searchEditText.getText().toString() | ||||
|                 : searchString; | ||||
|         super.onSaveInstanceState(bundle); | ||||
|     } | ||||
|  | ||||
| @@ -305,8 +332,11 @@ public class SearchFragment | ||||
|  | ||||
|     @Override | ||||
|     public void reloadContent() { | ||||
|         if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { | ||||
|             search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString()); | ||||
|         if (!TextUtils.isEmpty(searchString) | ||||
|                 || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { | ||||
|             search(!TextUtils.isEmpty(searchString) | ||||
|                     ? searchString | ||||
|                     : searchEditText.getText().toString(), new String[0], ""); | ||||
|         } else { | ||||
|             if (searchEditText != null) { | ||||
|                 searchEditText.setText(""); | ||||
| @@ -330,22 +360,35 @@ public class SearchFragment | ||||
|             supportActionBar.setDisplayHomeAsUpEnabled(true); | ||||
|         } | ||||
|  | ||||
|         inflater.inflate(R.menu.menu_search, menu); | ||||
|         menuItemToFilterName = new HashMap<>(); | ||||
|  | ||||
|         int itemId = 0; | ||||
|         boolean isFirstItem = true; | ||||
|         final Context c = getContext(); | ||||
|         for(String filter : service.getSearchQHFactory().getAvailableContentFilter()) { | ||||
|             menuItemToFilterName.put(itemId, filter); | ||||
|             MenuItem item = menu.add(1, | ||||
|                     itemId++, | ||||
|                     0, | ||||
|                     ServiceHelper.getTranslatedFilterString(filter, c)); | ||||
|             if(isFirstItem) { | ||||
|                 item.setChecked(true); | ||||
|                 isFirstItem = false; | ||||
|             } | ||||
|         } | ||||
|         menu.setGroupCheckable(1, true, true); | ||||
|  | ||||
|         restoreFilterChecked(menu, filterItemCheckedId); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         switch (item.getItemId()) { | ||||
|             case R.id.menu_filter_all: | ||||
|             case R.id.menu_filter_video: | ||||
|             case R.id.menu_filter_channel: | ||||
|             case R.id.menu_filter_playlist: | ||||
|                 changeFilter(item, getFilterFromMenuId(item.getItemId())); | ||||
|                 return true; | ||||
|             default: | ||||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
|  | ||||
|         List<String> contentFilter = new ArrayList<>(1); | ||||
|         contentFilter.add(menuItemToFilterName.get(item.getItemId())); | ||||
|         changeContentFilter(item, contentFilter); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     private void restoreFilterChecked(Menu menu, int itemId) { | ||||
| @@ -354,21 +397,6 @@ public class SearchFragment | ||||
|             if (item == null) return; | ||||
|  | ||||
|             item.setChecked(true); | ||||
|             filter = getFilterFromMenuId(itemId); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private SearchEngine.Filter getFilterFromMenuId(int itemId) { | ||||
|         switch (itemId) { | ||||
|             case R.id.menu_filter_video: | ||||
|                 return SearchEngine.Filter.STREAM; | ||||
|             case R.id.menu_filter_channel: | ||||
|                 return SearchEngine.Filter.CHANNEL; | ||||
|             case R.id.menu_filter_playlist: | ||||
|                 return SearchEngine.Filter.PLAYLIST; | ||||
|             case R.id.menu_filter_all: | ||||
|             default: | ||||
|                 return SearchEngine.Filter.ANY; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -379,14 +407,21 @@ public class SearchFragment | ||||
|     private TextWatcher textWatcher; | ||||
|  | ||||
|     private void showSearchOnStart() { | ||||
|         if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " + searchQuery+", lastSearchedQuery → " + lastSearchedQuery); | ||||
|         searchEditText.setText(searchQuery); | ||||
|         if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " | ||||
|                 + searchString | ||||
|                 + ", lastSearchedQuery → " | ||||
|                 + lastSearchedString); | ||||
|         searchEditText.setText(searchString); | ||||
|  | ||||
|         if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) { | ||||
|         if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) { | ||||
|             searchToolbarContainer.setTranslationX(100); | ||||
|             searchToolbarContainer.setAlpha(0f); | ||||
|             searchToolbarContainer.setVisibility(View.VISIBLE); | ||||
|             searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(200).setInterpolator(new DecelerateInterpolator()).start(); | ||||
|             searchToolbarContainer.animate() | ||||
|                     .translationX(0) | ||||
|                     .alpha(1f) | ||||
|                     .setDuration(200) | ||||
|                     .setInterpolator(new DecelerateInterpolator()).start(); | ||||
|         } else { | ||||
|             searchToolbarContainer.setTranslationX(0); | ||||
|             searchToolbarContainer.setAlpha(1f); | ||||
| @@ -396,47 +431,38 @@ public class SearchFragment | ||||
|  | ||||
|     private void initSearchListeners() { | ||||
|         if (DEBUG) Log.d(TAG, "initSearchListeners() called"); | ||||
|         searchClear.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View v) { | ||||
|                 if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); | ||||
|                 if (TextUtils.isEmpty(searchEditText.getText())) { | ||||
|                     NavigationHelper.gotoMainFragment(getFragmentManager()); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 searchEditText.setText(""); | ||||
|                 suggestionListAdapter.setItems(new ArrayList<SuggestionItem>()); | ||||
|                 showKeyboardSearch(); | ||||
|         searchClear.setOnClickListener(v -> { | ||||
|             if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); | ||||
|             if (TextUtils.isEmpty(searchEditText.getText())) { | ||||
|                 NavigationHelper.gotoMainFragment(getFragmentManager()); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             searchEditText.setText(""); | ||||
|             suggestionListAdapter.setItems(new ArrayList<>()); | ||||
|             showKeyboardSearch(); | ||||
|         }); | ||||
|  | ||||
|         TooltipCompat.setTooltipText(searchClear, getString(R.string.clear)); | ||||
|  | ||||
|         searchEditText.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View v) { | ||||
|                 if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); | ||||
|                 if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { | ||||
|                     showSuggestionsPanel(); | ||||
|                 } | ||||
|         searchEditText.setOnClickListener(v -> { | ||||
|             if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); | ||||
|             if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { | ||||
|                 showSuggestionsPanel(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { | ||||
|             @Override | ||||
|             public void onFocusChange(View v, boolean hasFocus) { | ||||
|                 if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]"); | ||||
|                 if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) { | ||||
|                     showSuggestionsPanel(); | ||||
|                 } | ||||
|         searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { | ||||
|             if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]"); | ||||
|             if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) { | ||||
|                 showSuggestionsPanel(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() { | ||||
|             @Override | ||||
|             public void onSuggestionItemSelected(SuggestionItem item) { | ||||
|                 search(item.query); | ||||
|                 search(item.query, new String[0], ""); | ||||
|                 searchEditText.setText(item.query); | ||||
|             } | ||||
|  | ||||
| @@ -469,21 +495,22 @@ public class SearchFragment | ||||
|             } | ||||
|         }; | ||||
|         searchEditText.addTextChangedListener(textWatcher); | ||||
|         searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { | ||||
|             @Override | ||||
|             public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { | ||||
|                 if (DEBUG) { | ||||
|                     Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); | ||||
|                 } | ||||
|                 if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { | ||||
|                     search(searchEditText.getText().toString()); | ||||
|                     return true; | ||||
|                 } | ||||
|                 return false; | ||||
|             } | ||||
|         }); | ||||
|         searchEditText.setOnEditorActionListener( | ||||
|                 (TextView v, int actionId, KeyEvent event) -> { | ||||
|                     if (DEBUG) { | ||||
|                         Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); | ||||
|                     } | ||||
|                     if (event != null | ||||
|                             && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER | ||||
|                                 || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { | ||||
|                         search(searchEditText.getText().toString(), new String[0], ""); | ||||
|                         return true; | ||||
|                     } | ||||
|                     return false; | ||||
|                 }); | ||||
|  | ||||
|         if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver(); | ||||
|         if (suggestionDisposable == null || suggestionDisposable.isDisposed()) | ||||
|             initSuggestionObserver(); | ||||
|     } | ||||
|  | ||||
|     private void unsetSearchListeners() { | ||||
| @@ -513,7 +540,8 @@ public class SearchFragment | ||||
|         if (searchEditText == null) return; | ||||
|  | ||||
|         if (searchEditText.requestFocus()) { | ||||
|             InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); | ||||
|             InputMethodManager imm = (InputMethodManager) activity.getSystemService( | ||||
|                     Context.INPUT_METHOD_SERVICE); | ||||
|             imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT); | ||||
|         } | ||||
|     } | ||||
| @@ -522,8 +550,10 @@ public class SearchFragment | ||||
|         if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called"); | ||||
|         if (searchEditText == null) return; | ||||
|  | ||||
|         InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); | ||||
|         imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); | ||||
|         InputMethodManager imm = (InputMethodManager) activity.getSystemService( | ||||
|                 Context.INPUT_METHOD_SERVICE); | ||||
|         imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), | ||||
|                 InputMethodManager.HIDE_NOT_ALWAYS); | ||||
|  | ||||
|         searchEditText.clearFocus(); | ||||
|     } | ||||
| @@ -545,8 +575,7 @@ public class SearchFragment | ||||
|                                             .onNext(searchEditText.getText().toString()), | ||||
|                                     throwable -> showSnackBarError(throwable, | ||||
|                                             UserAction.DELETE_FROM_HISTORY, "none", | ||||
|                                             "Deleting item failed", R.string.general_error) | ||||
|                             ); | ||||
|                                             "Deleting item failed", R.string.general_error)); | ||||
|                     disposables.add(onDelete); | ||||
|                 }) | ||||
|                 .show(); | ||||
| @@ -554,10 +583,12 @@ public class SearchFragment | ||||
|  | ||||
|     @Override | ||||
|     public boolean onBackPressed() { | ||||
|         if (suggestionsPanel.getVisibility() == View.VISIBLE && infoListAdapter.getItemsList().size() > 0 && !isLoading.get()) { | ||||
|         if (suggestionsPanel.getVisibility() == View.VISIBLE | ||||
|                 && infoListAdapter.getItemsList().size() > 0 | ||||
|                 && !isLoading.get()) { | ||||
|             hideSuggestionsPanel(); | ||||
|             hideKeyboardSearch(); | ||||
|             searchEditText.setText(lastSearchedQuery); | ||||
|             searchEditText.setText(lastSearchedString); | ||||
|             return true; | ||||
|         } | ||||
|         return false; | ||||
| @@ -573,8 +604,10 @@ public class SearchFragment | ||||
|  | ||||
|         final Observable<String> observable = suggestionPublisher | ||||
|                 .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) | ||||
|                 .startWith(searchQuery != null ? searchQuery : "") | ||||
|                 .filter(query -> isSuggestionsEnabled); | ||||
|                 .startWith(searchString != null | ||||
|                         ? searchString | ||||
|                         : "") | ||||
|                 .filter(searchString -> isSuggestionsEnabled); | ||||
|  | ||||
|         suggestionDisposable = observable | ||||
|                 .switchMap(query -> { | ||||
| @@ -645,56 +678,44 @@ public class SearchFragment | ||||
|         // no-op | ||||
|     } | ||||
|  | ||||
|     private void search(final String query) { | ||||
|         if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]"); | ||||
|         if (query.isEmpty()) return; | ||||
|     private void search(final String searchString, String[] contentFilter, String sortFilter) { | ||||
|         if (DEBUG) Log.d(TAG, "search() called with: query = [" + searchString + "]"); | ||||
|         if (searchString.isEmpty()) return; | ||||
|  | ||||
|         try { | ||||
|             final StreamingService service = NewPipe.getServiceByUrl(query); | ||||
|             final StreamingService service = NewPipe.getServiceByUrl(searchString); | ||||
|             if (service != null) { | ||||
|                 showLoading(); | ||||
|                 disposables.add(Observable | ||||
|                         .fromCallable(new Callable<Intent>() { | ||||
|                             @Override | ||||
|                             public Intent call() throws Exception { | ||||
|                                 return NavigationHelper.getIntentByLink(activity, service, query); | ||||
|                             } | ||||
|                         }) | ||||
|                         .fromCallable(() -> | ||||
|                                 NavigationHelper.getIntentByLink(activity, service, searchString)) | ||||
|                         .subscribeOn(Schedulers.io()) | ||||
|                         .observeOn(AndroidSchedulers.mainThread()) | ||||
|                         .subscribe(new Consumer<Intent>() { | ||||
|                             @Override | ||||
|                             public void accept(Intent intent) throws Exception { | ||||
|                                 getFragmentManager().popBackStackImmediate(); | ||||
|                                 activity.startActivity(intent); | ||||
|                             } | ||||
|                         }, new Consumer<Throwable>() { | ||||
|                             @Override | ||||
|                             public void accept(Throwable throwable) throws Exception { | ||||
|                                 showError(getString(R.string.url_not_supported_toast), false); | ||||
|                             } | ||||
|                         })); | ||||
|                         .subscribe(intent -> { | ||||
|                             getFragmentManager().popBackStackImmediate(); | ||||
|                             activity.startActivity(intent); | ||||
|                         }, throwable -> | ||||
|                                 showError(getString(R.string.url_not_supported_toast), false))); | ||||
|                 return; | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             // Exception occurred, it's not a url | ||||
|         } | ||||
|  | ||||
|         lastSearchedQuery = query; | ||||
|         searchQuery = query; | ||||
|         currentPage = 0; | ||||
|         lastSearchedString = this.searchString; | ||||
|         this.searchString = searchString; | ||||
|         infoListAdapter.clearStreamItemList(); | ||||
|         hideSuggestionsPanel(); | ||||
|         hideKeyboardSearch(); | ||||
|  | ||||
|         historyRecordManager.onSearched(serviceId, query) | ||||
|         historyRecordManager.onSearched(serviceId, searchString) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe( | ||||
|                         ignored -> {}, | ||||
|                         error -> showSnackBarError(error, UserAction.SEARCHED, | ||||
|                                 NewPipe.getNameOfService(serviceId), query, 0) | ||||
|                                 NewPipe.getNameOfService(serviceId), searchString, 0) | ||||
|                 ); | ||||
|         suggestionPublisher.onNext(query); | ||||
|         suggestionPublisher.onNext(searchString); | ||||
|         startLoading(false); | ||||
|     } | ||||
|  | ||||
| @@ -703,11 +724,16 @@ public class SearchFragment | ||||
|         super.startLoading(forceLoad); | ||||
|         if (disposables != null) disposables.clear(); | ||||
|         if (searchDisposable != null) searchDisposable.dispose(); | ||||
|         searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter) | ||||
|         searchDisposable = ExtractorHelper.searchFor(serviceId, | ||||
|                     searchString, | ||||
|                     Arrays.asList(contentFilter), | ||||
|                     sortFilter, | ||||
|                     contentCountry) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .doOnEvent((searchResult, throwable) -> isLoading.set(false)) | ||||
|                 .subscribe(this::handleResult, this::onError); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -715,8 +741,13 @@ public class SearchFragment | ||||
|         isLoading.set(true); | ||||
|         showListFooter(true); | ||||
|         if (searchDisposable != null) searchDisposable.dispose(); | ||||
|         currentNextPage = currentPage + 1; | ||||
|         searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter) | ||||
|         searchDisposable = ExtractorHelper.getMoreSearchItems( | ||||
|                     serviceId, | ||||
|                     searchString, | ||||
|                     asList(contentFilter), | ||||
|                     sortFilter, | ||||
|                     nextPageUrl, | ||||
|                     contentCountry) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) | ||||
| @@ -739,19 +770,22 @@ public class SearchFragment | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void changeFilter(MenuItem item, SearchEngine.Filter filter) { | ||||
|         this.filter = filter; | ||||
|     private void changeContentFilter(MenuItem item, List<String> contentFilter) { | ||||
|         this.filterItemCheckedId = item.getItemId(); | ||||
|         item.setChecked(true); | ||||
|  | ||||
|         if (!TextUtils.isEmpty(searchQuery)) { | ||||
|             search(searchQuery); | ||||
|         this.contentFilter = new String[] {contentFilter.get(0)}; | ||||
|  | ||||
|         if (!TextUtils.isEmpty(searchString)) { | ||||
|             search(searchString, this.contentFilter, sortFilter); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void setQuery(int serviceId, String searchQuery) { | ||||
|     private void setQuery(int serviceId, String searchString, String[] contentfilter, String sortFilter) { | ||||
|         this.serviceId = serviceId; | ||||
|         this.searchQuery = searchQuery; | ||||
|         this.searchString = searchString; | ||||
|         this.contentFilter = contentfilter; | ||||
|         this.sortFilter = sortFilter; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -772,8 +806,11 @@ public class SearchFragment | ||||
|         if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]"); | ||||
|         if (super.onError(exception)) return; | ||||
|  | ||||
|         int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; | ||||
|         onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, NewPipe.getNameOfService(serviceId), searchQuery, errorId); | ||||
|         int errorId = exception instanceof ParsingException | ||||
|                 ? R.string.parsing_error | ||||
|                 : R.string.general_error; | ||||
|         onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, | ||||
|                 NewPipe.getNameOfService(serviceId), searchString, errorId); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -798,16 +835,22 @@ public class SearchFragment | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void handleResult(@NonNull SearchResult result) { | ||||
|         if (!result.errors.isEmpty()) { | ||||
|             showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0); | ||||
|     public void handleResult(@NonNull SearchInfo result) { | ||||
|         final List<Throwable> exceptions = result.getErrors(); | ||||
|         if (!exceptions.isEmpty() | ||||
|             && !(exceptions.size() == 1 | ||||
|                 && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)){ | ||||
|             showSnackBarError(result.getErrors(), UserAction.SEARCHED, | ||||
|                     NewPipe.getNameOfService(serviceId), searchString, 0); | ||||
|         } | ||||
|  | ||||
|         lastSearchedQuery = searchQuery; | ||||
|         lastSearchedString = searchString; | ||||
|         nextPageUrl = result.getNextPageUrl(); | ||||
|         currentPageUrl = result.getUrl(); | ||||
|  | ||||
|         if (infoListAdapter.getItemsList().size() == 0) { | ||||
|             if (!result.getResults().isEmpty()) { | ||||
|                 infoListAdapter.addInfoItemList(result.getResults()); | ||||
|             if (!result.getRelatedItems().isEmpty()) { | ||||
|                 infoListAdapter.addInfoItemList(result.getRelatedItems()); | ||||
|             } else { | ||||
|                 infoListAdapter.clearStreamItemList(); | ||||
|                 showEmptyState(); | ||||
| @@ -821,12 +864,14 @@ public class SearchFragment | ||||
|     @Override | ||||
|     public void handleNextItems(ListExtractor.InfoItemsPage result) { | ||||
|         showListFooter(false); | ||||
|         currentPage = Integer.parseInt(result.getNextPageUrl()); | ||||
|         currentPageUrl = result.getNextPageUrl(); | ||||
|         infoListAdapter.addInfoItemList(result.getItems()); | ||||
|         nextPageUrl = result.getNextPageUrl(); | ||||
|  | ||||
|         if (!result.getErrors().isEmpty()) { | ||||
|             showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId) | ||||
|                     , "\"" + searchQuery + "\" → page " + currentPage, 0); | ||||
|             showSnackBarError(result.getErrors(), UserAction.SEARCHED, | ||||
|                     NewPipe.getNameOfService(serviceId) | ||||
|                     , "\"" + searchString + "\" → page: " + nextPageUrl, 0); | ||||
|         } | ||||
|         super.handleNextItems(result); | ||||
|     } | ||||
| @@ -835,12 +880,15 @@ public class SearchFragment | ||||
|     protected boolean onError(Throwable exception) { | ||||
|         if (super.onError(exception)) return true; | ||||
|  | ||||
|         if (exception instanceof SearchEngine.NothingFoundException) { | ||||
|         if (exception instanceof SearchExtractor.NothingFoundException) { | ||||
|             infoListAdapter.clearStreamItemList(); | ||||
|             showEmptyState(); | ||||
|         } else { | ||||
|             int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; | ||||
|             onUnrecoverableError(exception, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, errorId); | ||||
|             int errorId = exception instanceof ParsingException | ||||
|                     ? R.string.parsing_error | ||||
|                     : R.string.general_error; | ||||
|             onUnrecoverableError(exception, UserAction.SEARCHED, | ||||
|                     NewPipe.getNameOfService(serviceId), searchString, errorId); | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|   | ||||
| @@ -66,11 +66,10 @@ public final class BookmarkFragment | ||||
|     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); | ||||
|         } | ||||
|  | ||||
|         if(!useAsFrontPage) { | ||||
|             setTitle(activity.getString(R.string.tab_bookmarks)); | ||||
|         } | ||||
|         return inflater.inflate(R.layout.fragment_bookmarks, container, false); | ||||
|     } | ||||
|  | ||||
| @@ -99,9 +98,7 @@ public final class BookmarkFragment | ||||
|         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(); | ||||
|                 final FragmentManager fragmentManager = getFM(); | ||||
|  | ||||
|                 if (selectedItem instanceof PlaylistMetadataEntry) { | ||||
|                     final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); | ||||
| @@ -110,8 +107,11 @@ public final class BookmarkFragment | ||||
|  | ||||
|                 } else if (selectedItem instanceof PlaylistRemoteEntity) { | ||||
|                     final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); | ||||
|                     NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(), | ||||
|                             entry.getUrl(), entry.getName()); | ||||
|                     NavigationHelper.openPlaylistFragment( | ||||
|                             fragmentManager, | ||||
|                             entry.getServiceId(), | ||||
|                             entry.getUrl(), | ||||
|                             entry.getName()); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -71,6 +71,10 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { | ||||
|  | ||||
|         if(!useAsFrontPage) { | ||||
|             setTitle(activity.getString(R.string.fragment_whats_new)); | ||||
|         } | ||||
|         return inflater.inflate(R.layout.fragment_feed, container, false); | ||||
|     } | ||||
|  | ||||
| @@ -105,20 +109,19 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi | ||||
|         super.onDestroyView(); | ||||
|     } | ||||
|  | ||||
|     /*@Override | ||||
|     protected RecyclerView.LayoutManager getListLayoutManager() { | ||||
|         boolean isPortrait = getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels; | ||||
|         return new GridLayoutManager(activity, isPortrait ? 1 : 2); | ||||
|     }*/ | ||||
|     @Override | ||||
|     public void setUserVisibleHint(boolean isVisibleToUser) { | ||||
|         super.setUserVisibleHint(isVisibleToUser); | ||||
|         if (activity != null && isVisibleToUser) { | ||||
|             setTitle(activity.getString(R.string.fragment_whats_new)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||
|         super.onCreateOptionsMenu(menu, inflater); | ||||
|  | ||||
|         ActionBar supportActionBar = activity.getSupportActionBar(); | ||||
|         if (supportActionBar != null) { | ||||
|             supportActionBar.setTitle(R.string.fragment_whats_new); | ||||
|         } | ||||
|  | ||||
|         if(useAsFrontPage) { | ||||
|             supportActionBar.setDisplayShowTitleEnabled(true); | ||||
| @@ -176,19 +179,9 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi | ||||
|         showLoading(); | ||||
|         showListFooter(true); | ||||
|         subscriptionObserver = subscriptionService.getSubscription() | ||||
|                 .onErrorReturnItem(Collections.<SubscriptionEntity>emptyList()) | ||||
|                 .onErrorReturnItem(Collections.emptyList()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(new Consumer<List<SubscriptionEntity>>() { | ||||
|                     @Override | ||||
|                     public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception { | ||||
|                         handleResult(subscriptionEntities); | ||||
|                     } | ||||
|                 }, new Consumer<Throwable>() { | ||||
|                     @Override | ||||
|                     public void accept(Throwable throwable) throws Exception { | ||||
|                         onError(throwable); | ||||
|                     } | ||||
|                 }); | ||||
|                 .subscribe(this::handleResult, this::onError); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -239,13 +232,12 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi | ||||
|                 if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) { | ||||
|                     subscriptionService.getChannelInfo(subscriptionEntity) | ||||
|                             .observeOn(AndroidSchedulers.mainThread()) | ||||
|                             .onErrorComplete(new Predicate<Throwable>() { | ||||
|                                 @Override | ||||
|                                 public boolean test(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception { | ||||
|                                     return FeedFragment.super.onError(throwable); | ||||
|                                 } | ||||
|                             }) | ||||
|                             .subscribe(getChannelInfoObserver(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl())); | ||||
|                             .onErrorComplete( | ||||
|                                     (@io.reactivex.annotations.NonNull Throwable throwable) -> | ||||
|                                             FeedFragment.super.onError(throwable)) | ||||
|                             .subscribe( | ||||
|                                     getChannelInfoObserver(subscriptionEntity.getServiceId(), | ||||
|                                             subscriptionEntity.getUrl())); | ||||
|                 } else { | ||||
|                     requestFeed(1); | ||||
|                 } | ||||
| @@ -316,7 +308,10 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi | ||||
|  | ||||
|             @Override | ||||
|             public void onError(Throwable exception) { | ||||
|                 showSnackBarError(exception, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(serviceId), url, 0); | ||||
|                 showSnackBarError(exception, | ||||
|                         UserAction.SUBSCRIPTION, | ||||
|                         NewPipe.getNameOfService(serviceId), | ||||
|                         url, 0); | ||||
|                 requestFeed(1); | ||||
|                 onDone(); | ||||
|             } | ||||
| @@ -361,12 +356,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi | ||||
|         delayHandler.removeCallbacksAndMessages(null); | ||||
|         // Add a little of a delay when requesting more items because the cache is so fast, | ||||
|         // that the view seems stuck to the user when he scroll to the bottom | ||||
|         delayHandler.postDelayed(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 requestFeed(FEED_LOAD_COUNT); | ||||
|             } | ||||
|         }, 300); | ||||
|         delayHandler.postDelayed(() -> requestFeed(FEED_LOAD_COUNT), 300); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -423,7 +413,9 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi | ||||
|         int heightPixels = getResources().getDisplayMetrics().heightPixels; | ||||
|         int itemHeightPixels = activity.getResources().getDimensionPixelSize(R.dimen.video_item_search_height); | ||||
|  | ||||
|         int items = itemHeightPixels > 0 ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT : MIN_ITEMS_INITIAL_LOAD; | ||||
|         int items = itemHeightPixels > 0 | ||||
|                 ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT | ||||
|                 : MIN_ITEMS_INITIAL_LOAD; | ||||
|         return Math.max(MIN_ITEMS_INITIAL_LOAD, items); | ||||
|     } | ||||
|  | ||||
| @@ -441,8 +433,14 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi | ||||
|     protected boolean onError(Throwable exception) { | ||||
|         if (super.onError(exception)) return true; | ||||
|  | ||||
|         int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; | ||||
|         onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Requesting feed", errorId); | ||||
|         int errorId = exception instanceof ExtractionException | ||||
|                 ? R.string.parsing_error | ||||
|                 : R.string.general_error; | ||||
|         onUnrecoverableError(exception, | ||||
|                 UserAction.SOMETHING_ELSE, | ||||
|                 "none", | ||||
|                 "Requesting feed", | ||||
|                 errorId); | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -21,6 +21,7 @@ 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.fragments.list.BaseListFragment; | ||||
| import org.schabi.newpipe.local.BaseLocalListFragment; | ||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| @@ -73,7 +74,7 @@ public class StatisticsPlaylistFragment | ||||
|                 return results; | ||||
|             case MOST_PLAYED: | ||||
|                 Collections.sort(results, (left, right) -> | ||||
|                         ((Long) right.watchCount).compareTo(left.watchCount)); | ||||
|                         Long.compare(right.watchCount, left.watchCount)); | ||||
|                 return results; | ||||
|             default: return null; | ||||
|         } | ||||
| @@ -96,6 +97,14 @@ public class StatisticsPlaylistFragment | ||||
|         return inflater.inflate(R.layout.fragment_playlist, container, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void setUserVisibleHint(boolean isVisibleToUser) { | ||||
|         super.setUserVisibleHint(isVisibleToUser); | ||||
|         if (activity != null && isVisibleToUser) { | ||||
|             setTitle(activity.getString(R.string.title_activity_history)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment LifeCycle - Views | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
| @@ -103,7 +112,9 @@ public class StatisticsPlaylistFragment | ||||
|     @Override | ||||
|     protected void initViews(View rootView, Bundle savedInstanceState) { | ||||
|         super.initViews(rootView, savedInstanceState); | ||||
|         setTitle(getString(R.string.title_last_played)); | ||||
|         if(!useAsFrontPage) { | ||||
|             setTitle(getString(R.string.title_last_played)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -129,8 +140,10 @@ public class StatisticsPlaylistFragment | ||||
|             public void selected(LocalItem selectedItem) { | ||||
|                 if (selectedItem instanceof StreamStatisticsEntry) { | ||||
|                     final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem; | ||||
|                     NavigationHelper.openVideoDetailFragment(getFragmentManager(), | ||||
|                             item.serviceId, item.url, item.title); | ||||
|                     NavigationHelper.openVideoDetailFragment(getFM(), | ||||
|                             item.serviceId, | ||||
|                             item.url, | ||||
|                             item.title); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @@ -298,6 +311,7 @@ public class StatisticsPlaylistFragment | ||||
|                 context.getResources().getString(R.string.start_here_on_background), | ||||
|                 context.getResources().getString(R.string.start_here_on_popup), | ||||
|                 context.getResources().getString(R.string.delete), | ||||
|                 context.getResources().getString(R.string.share) | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { | ||||
| @@ -321,6 +335,9 @@ public class StatisticsPlaylistFragment | ||||
|                 case 5: | ||||
|                     deleteEntry(index); | ||||
|                     break; | ||||
|                 case 6: | ||||
|                     shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl()); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
| @@ -337,7 +354,7 @@ public class StatisticsPlaylistFragment | ||||
|             final Disposable onDelete = recordManager.deleteStreamHistory(entry.streamId) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe( | ||||
|                             howManyDelted -> { | ||||
|                             howManyDeleted -> { | ||||
|                                 if(getView() != null) { | ||||
|                                     Snackbar.make(getView(), R.string.one_item_deleted, | ||||
|                                             Snackbar.LENGTH_SHORT).show(); | ||||
|   | ||||
| @@ -520,7 +520,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | ||||
|                 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) | ||||
|                 context.getResources().getString(R.string.delete), | ||||
|                 context.getResources().getString(R.string.share) | ||||
|         }; | ||||
|  | ||||
|         final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { | ||||
| @@ -549,6 +550,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | ||||
|                 case 6: | ||||
|                     deleteItem(item); | ||||
|                     break; | ||||
|                 case 7: | ||||
|                     shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl()); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|   | ||||
| @@ -40,8 +40,11 @@ public class RemotePlaylistManager { | ||||
|         }).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
|  | ||||
|     public Single<Integer> onUpdate(final PlaylistInfo playlistInfo) { | ||||
|         return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo))) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|     public Single<Integer> onUpdate(final long playlistId, final PlaylistInfo playlistInfo) { | ||||
|         return Single.fromCallable(() -> { | ||||
|             PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); | ||||
|             playlist.setUid(playlistId); | ||||
|             return playlistRemoteTable.update(playlist); | ||||
|         }).subscribeOn(Schedulers.io()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,8 +13,10 @@ import android.os.Parcelable; | ||||
| import android.support.annotation.DrawableRes; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.FragmentManager; | ||||
| import android.support.v4.content.LocalBroadcastManager; | ||||
| import android.support.v7.app.ActionBar; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.support.v7.widget.LinearLayoutManager; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.view.LayoutInflater; | ||||
| @@ -38,6 +40,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; | ||||
| import org.schabi.newpipe.fragments.BaseStateFragment; | ||||
| import org.schabi.newpipe.info_list.InfoListAdapter; | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService; | ||||
| import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; | ||||
| @@ -207,7 +210,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt | ||||
|     } | ||||
|  | ||||
|     private void setupImportFromItems(final ViewGroup listHolder) { | ||||
|         final View previousBackupItem = addItemView(getString(R.string.previous_export), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_backup), listHolder); | ||||
|         final View previousBackupItem = addItemView(getString(R.string.previous_export), | ||||
|                 ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_backup), listHolder); | ||||
|         previousBackupItem.setOnClickListener(item -> onImportPreviousSelected()); | ||||
|  | ||||
|         final int iconColor = ThemeHelper.isLightThemeSelected(getContext()) ? Color.BLACK : Color.WHITE; | ||||
| @@ -239,8 +243,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt | ||||
|     } | ||||
|  | ||||
|     private void onImportFromServiceSelected(int serviceId) { | ||||
|         if (getParentFragment() == null) return; | ||||
|         NavigationHelper.openSubscriptionsImportFragment(getParentFragment().getFragmentManager(), serviceId); | ||||
|         FragmentManager fragmentManager = getFM(); | ||||
|         NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId); | ||||
|     } | ||||
|  | ||||
|     private void onImportPreviousSelected() { | ||||
| @@ -318,15 +322,19 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt | ||||
|         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.getUrl(), selectedItem.getName()); | ||||
|                 final FragmentManager fragmentManager = getFM(); | ||||
|                 NavigationHelper.openChannelFragment(fragmentManager, | ||||
|                         selectedItem.getServiceId(), | ||||
|                         selectedItem.getUrl(), | ||||
|                         selectedItem.getName()); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //noinspection ConstantConditions | ||||
|         whatsNewItemListHeader.setOnClickListener(v -> | ||||
|                 NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager())); | ||||
|         whatsNewItemListHeader.setOnClickListener(v -> { | ||||
|             FragmentManager fragmentManager = getFM(); | ||||
|             NavigationHelper.openWhatsNewFragment(fragmentManager); | ||||
|         }); | ||||
|         importExportListHeader.setOnClickListener(v -> importExportOptions.switchState()); | ||||
|     } | ||||
|  | ||||
| @@ -397,10 +405,13 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt | ||||
|  | ||||
|     private List<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) { | ||||
|         List<InfoItem> items = new ArrayList<>(); | ||||
|         for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem()); | ||||
|         for (final SubscriptionEntity subscription : subscriptions) { | ||||
|             items.add(subscription.toChannelInfoItem()); | ||||
|         } | ||||
|  | ||||
|         Collections.sort(items, | ||||
|                 (InfoItem o1, InfoItem o2) -> o1.getName().compareToIgnoreCase(o2.getName())); | ||||
|                 (InfoItem o1, InfoItem o2) -> | ||||
|                         o1.getName().compareToIgnoreCase(o2.getName())); | ||||
|         return items; | ||||
|     } | ||||
|  | ||||
| @@ -429,7 +440,11 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt | ||||
|         resetFragment(); | ||||
|         if (super.onError(exception)) return true; | ||||
|  | ||||
|         onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error); | ||||
|         onUnrecoverableError(exception, | ||||
|                 UserAction.SOMETHING_ELSE, | ||||
|                 "none", | ||||
|                 "Subscriptions", | ||||
|                 R.string.general_error); | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -28,7 +28,6 @@ import android.content.IntentFilter; | ||||
| import android.graphics.Bitmap; | ||||
| import android.os.Build; | ||||
| import android.os.IBinder; | ||||
| import android.support.annotation.IntRange; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.NotificationCompat; | ||||
| @@ -39,17 +38,16 @@ import android.widget.RemoteViews; | ||||
| import com.google.android.exoplayer2.PlaybackParameters; | ||||
| import com.google.android.exoplayer2.Player; | ||||
| import com.google.android.exoplayer2.source.MediaSource; | ||||
| import com.nostra13.universalimageloader.core.assist.FailReason; | ||||
|  | ||||
| import org.schabi.newpipe.BuildConfig; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.MediaFormat; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.player.event.PlayerEventListener; | ||||
| import org.schabi.newpipe.player.helper.LockManager; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
| import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; | ||||
| import org.schabi.newpipe.player.resolver.MediaSourceTag; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| @@ -94,7 +92,6 @@ public final class BackgroundPlayer extends Service { | ||||
|     private NotificationCompat.Builder notBuilder; | ||||
|     private RemoteViews notRemoteView; | ||||
|     private RemoteViews bigNotRemoteView; | ||||
|     private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha"; | ||||
|  | ||||
|     private boolean shouldUpdateOnProgress; | ||||
|  | ||||
| @@ -192,7 +189,9 @@ public final class BackgroundPlayer extends Service { | ||||
|                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||
|                 .setCustomContentView(notRemoteView) | ||||
|                 .setCustomBigContentView(bigNotRemoteView); | ||||
|         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(NotificationCompat.PRIORITY_MAX); | ||||
|         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { | ||||
|             builder.setPriority(NotificationCompat.PRIORITY_MAX); | ||||
|         } | ||||
|         return builder; | ||||
|     } | ||||
|  | ||||
| @@ -249,15 +248,6 @@ public final class BackgroundPlayer extends Service { | ||||
|         notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); | ||||
|     } | ||||
|  | ||||
|     private void setControlsOpacity(@IntRange(from = 0, to = 255) int opacity) { | ||||
|         if (notRemoteView != null) notRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity); | ||||
|         if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity); | ||||
|         if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity); | ||||
|         if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity); | ||||
|         if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity); | ||||
|         if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -279,8 +269,16 @@ public final class BackgroundPlayer extends Service { | ||||
|  | ||||
|     protected class BasePlayerImpl extends BasePlayer { | ||||
|  | ||||
|         @NonNull final private AudioPlaybackResolver resolver; | ||||
|  | ||||
|         BasePlayerImpl(Context context) { | ||||
|             super(context); | ||||
|             this.resolver = new AudioPlaybackResolver(context, dataSource); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void initPlayer(boolean playOnReady) { | ||||
|             super.initPlayer(playOnReady); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -293,30 +291,41 @@ public final class BackgroundPlayer extends Service { | ||||
|             startForeground(NOTIFICATION_ID, notBuilder.build()); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void initThumbnail(final String url) { | ||||
|             resetNotification(); | ||||
|             if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); | ||||
|             if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); | ||||
|             updateNotification(-1); | ||||
|             super.initThumbnail(url); | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
|         // Thumbnail Loading | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|         private void updateNotificationThumbnail() { | ||||
|             if (basePlayerImpl == null) return; | ||||
|             if (notRemoteView != null) { | ||||
|                 notRemoteView.setImageViewBitmap(R.id.notificationCover, | ||||
|                         basePlayerImpl.getThumbnail()); | ||||
|             } | ||||
|             if (bigNotRemoteView != null) { | ||||
|                 bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, | ||||
|                         basePlayerImpl.getThumbnail()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | ||||
|             super.onLoadingComplete(imageUri, view, loadedImage); | ||||
|  | ||||
|             if (loadedImage != null) { | ||||
|                 // rebuild notification here since remote view does not release bitmaps, causing memory leaks | ||||
|                 resetNotification(); | ||||
|  | ||||
|                 if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); | ||||
|                 if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); | ||||
|  | ||||
|                 updateNotification(-1); | ||||
|             } | ||||
|             resetNotification(); | ||||
|             updateNotificationThumbnail(); | ||||
|             updateNotification(-1); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onLoadingFailed(String imageUri, View view, FailReason failReason) { | ||||
|             super.onLoadingFailed(imageUri, view, failReason); | ||||
|             resetNotification(); | ||||
|             updateNotificationThumbnail(); | ||||
|             updateNotification(-1); | ||||
|         } | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
|         // States Implementation | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|         @Override | ||||
|         public void onPrepared(boolean playWhenReady) { | ||||
|             super.onPrepared(playWhenReady); | ||||
| @@ -335,6 +344,7 @@ public final class BackgroundPlayer extends Service { | ||||
|  | ||||
|             if (!shouldUpdateOnProgress) return; | ||||
|             resetNotification(); | ||||
|             if(Build.VERSION.SDK_INT >= 26 /*Oreo*/) updateNotificationThumbnail(); | ||||
|             if (bigNotRemoteView != null) { | ||||
|                 bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); | ||||
|                 bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration)); | ||||
| @@ -390,29 +400,18 @@ public final class BackgroundPlayer extends Service { | ||||
|         // Playback Listener | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|         protected void onMetadataChanged(@NonNull final PlayQueueItem item, | ||||
|                                          @Nullable final StreamInfo info, | ||||
|                                          final int newPlayQueueIndex, | ||||
|                                          final boolean hasPlayQueueItemChanged) { | ||||
|             if (shouldUpdateOnProgress || hasPlayQueueItemChanged) { | ||||
|                 resetNotification(); | ||||
|                 updateNotification(-1); | ||||
|                 updateMetadata(); | ||||
|             } | ||||
|         protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { | ||||
|             super.onMetadataChanged(tag); | ||||
|             resetNotification(); | ||||
|             updateNotificationThumbnail(); | ||||
|             updateNotification(-1); | ||||
|             updateMetadata(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         @Nullable | ||||
|         public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { | ||||
|             final MediaSource liveSource = super.sourceOf(item, info); | ||||
|             if (liveSource != null) return liveSource; | ||||
|  | ||||
|             final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); | ||||
|             if (index < 0 || index >= info.getAudioStreams().size()) return null; | ||||
|  | ||||
|             final AudioStream audio = info.getAudioStreams().get(index); | ||||
|             return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), | ||||
|                     MediaFormat.getSuffixById(audio.getFormatId())); | ||||
|             return resolver.resolve(info); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -439,8 +438,8 @@ public final class BackgroundPlayer extends Service { | ||||
|         } | ||||
|  | ||||
|         private void updateMetadata() { | ||||
|             if (activityListener != null && currentInfo != null) { | ||||
|                 activityListener.onMetadataUpdate(currentInfo); | ||||
|             if (activityListener != null && getCurrentMetadata() != null) { | ||||
|                 activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -531,44 +530,36 @@ public final class BackgroundPlayer extends Service { | ||||
|             updatePlayback(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onBlocked() { | ||||
|             super.onBlocked(); | ||||
|  | ||||
|             setControlsOpacity(77); | ||||
|             updateNotification(-1); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onPlaying() { | ||||
|             super.onPlaying(); | ||||
|  | ||||
|             setControlsOpacity(255); | ||||
|             resetNotification(); | ||||
|             updateNotificationThumbnail(); | ||||
|             updateNotification(R.drawable.ic_pause_white); | ||||
|  | ||||
|             lockManager.acquireWifiAndCpu(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onPaused() { | ||||
|             super.onPaused(); | ||||
|  | ||||
|             resetNotification(); | ||||
|             updateNotificationThumbnail(); | ||||
|             updateNotification(R.drawable.ic_play_arrow_white); | ||||
|  | ||||
|             lockManager.releaseWifiAndCpu(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onCompleted() { | ||||
|             super.onCompleted(); | ||||
|  | ||||
|             setControlsOpacity(255); | ||||
|  | ||||
|             resetNotification(); | ||||
|             if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); | ||||
|             if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); | ||||
|             if (bigNotRemoteView != null) { | ||||
|                 bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); | ||||
|             } | ||||
|             if (notRemoteView != null) { | ||||
|                 notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); | ||||
|             } | ||||
|             updateNotificationThumbnail(); | ||||
|             updateNotification(R.drawable.ic_replay_white); | ||||
|  | ||||
|             lockManager.releaseWifiAndCpu(); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -24,16 +24,14 @@ import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.IntentFilter; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.media.AudioManager; | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.TextUtils; | ||||
| import android.util.Log; | ||||
| import android.view.View; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import com.google.android.exoplayer2.C; | ||||
| import com.google.android.exoplayer2.DefaultRenderersFactory; | ||||
| import com.google.android.exoplayer2.ExoPlaybackException; | ||||
| import com.google.android.exoplayer2.ExoPlayerFactory; | ||||
| @@ -49,15 +47,14 @@ import com.google.android.exoplayer2.source.TrackGroupArray; | ||||
| import com.google.android.exoplayer2.trackselection.TrackSelection; | ||||
| import com.google.android.exoplayer2.trackselection.TrackSelectionArray; | ||||
| import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; | ||||
| import com.google.android.exoplayer2.util.Util; | ||||
| import com.nostra13.universalimageloader.core.ImageLoader; | ||||
| import com.nostra13.universalimageloader.core.assist.FailReason; | ||||
| import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; | ||||
|  | ||||
| import org.schabi.newpipe.BuildConfig; | ||||
| import org.schabi.newpipe.Downloader; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
| import org.schabi.newpipe.local.history.HistoryRecordManager; | ||||
| import org.schabi.newpipe.player.helper.AudioReactor; | ||||
| import org.schabi.newpipe.player.helper.LoadController; | ||||
| @@ -72,6 +69,8 @@ import org.schabi.newpipe.player.playback.PlaybackListener; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.player.resolver.MediaSourceTag; | ||||
| import org.schabi.newpipe.util.ImageDisplayConstants; | ||||
| import org.schabi.newpipe.util.SerializedCache; | ||||
|  | ||||
| import java.io.IOException; | ||||
| @@ -82,12 +81,12 @@ import io.reactivex.Observable; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import io.reactivex.disposables.SerialDisposable; | ||||
|  | ||||
| import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; | ||||
| import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; | ||||
| import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; | ||||
| import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; | ||||
|  | ||||
| /** | ||||
|  * Base for the players, joining the common properties | ||||
| @@ -98,7 +97,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; | ||||
| public abstract class BasePlayer implements | ||||
|         Player.EventListener, PlaybackListener, ImageLoadingListener { | ||||
|  | ||||
|     public static final boolean DEBUG = true; | ||||
|     public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); | ||||
|     @NonNull public static final String TAG = "BasePlayer"; | ||||
|  | ||||
|     @NonNull final protected Context context; | ||||
| @@ -108,17 +107,26 @@ public abstract class BasePlayer implements | ||||
|  | ||||
|     @NonNull final protected HistoryRecordManager recordManager; | ||||
|  | ||||
|     @NonNull final protected CustomTrackSelector trackSelector; | ||||
|     @NonNull final protected PlayerDataSource dataSource; | ||||
|  | ||||
|     @NonNull final private LoadControl loadControl; | ||||
|     @NonNull final private RenderersFactory renderFactory; | ||||
|  | ||||
|     @NonNull final private SerialDisposable progressUpdateReactor; | ||||
|     @NonNull final private CompositeDisposable databaseUpdateReactor; | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Intent | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public static final String REPEAT_MODE = "repeat_mode"; | ||||
|     public static final String PLAYBACK_PITCH = "playback_pitch"; | ||||
|     public static final String PLAYBACK_SPEED = "playback_speed"; | ||||
|     public static final String PLAYBACK_QUALITY = "playback_quality"; | ||||
|     public static final String PLAY_QUEUE_KEY = "play_queue_key"; | ||||
|     public static final String APPEND_ONLY = "append_only"; | ||||
|     public static final String SELECT_ON_APPEND = "select_on_append"; | ||||
|     @NonNull public static final String REPEAT_MODE = "repeat_mode"; | ||||
|     @NonNull public static final String PLAYBACK_PITCH = "playback_pitch"; | ||||
|     @NonNull public static final String PLAYBACK_SPEED = "playback_speed"; | ||||
|     @NonNull public static final String PLAYBACK_SKIP_SILENCE = "playback_skip_silence"; | ||||
|     @NonNull public static final String PLAYBACK_QUALITY = "playback_quality"; | ||||
|     @NonNull public static final String PLAY_QUEUE_KEY = "play_queue_key"; | ||||
|     @NonNull public static final String APPEND_ONLY = "append_only"; | ||||
|     @NonNull public static final String SELECT_ON_APPEND = "select_on_append"; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Playback | ||||
| @@ -129,12 +137,13 @@ public abstract class BasePlayer implements | ||||
|     protected PlayQueue playQueue; | ||||
|     protected PlayQueueAdapter playQueueAdapter; | ||||
|  | ||||
|     protected MediaSourceManager playbackManager; | ||||
|     @Nullable protected MediaSourceManager playbackManager; | ||||
|  | ||||
|     protected StreamInfo currentInfo; | ||||
|     protected PlayQueueItem currentItem; | ||||
|     @Nullable private PlayQueueItem currentItem; | ||||
|     @Nullable private MediaSourceTag currentMetadata; | ||||
|     @Nullable private Bitmap currentThumbnail; | ||||
|  | ||||
|     protected Toast errorToast; | ||||
|     @Nullable protected Toast errorToast; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Player | ||||
| @@ -145,18 +154,11 @@ public abstract class BasePlayer implements | ||||
|     protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500; | ||||
|     protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds | ||||
|  | ||||
|     protected CustomTrackSelector trackSelector; | ||||
|     protected PlayerDataSource dataSource; | ||||
|  | ||||
|     protected SimpleExoPlayer simpleExoPlayer; | ||||
|     protected AudioReactor audioReactor; | ||||
|     protected MediaSessionManager mediaSessionManager; | ||||
|  | ||||
|     private boolean isPrepared = false; | ||||
|     private boolean isSynchronizing = false; | ||||
|  | ||||
|     protected Disposable progressUpdateReactor; | ||||
|     protected CompositeDisposable databaseUpdateReactor; | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
| @@ -171,32 +173,34 @@ public abstract class BasePlayer implements | ||||
|         }; | ||||
|         this.intentFilter = new IntentFilter(); | ||||
|         setupBroadcastReceiver(intentFilter); | ||||
|         context.registerReceiver(broadcastReceiver, intentFilter); | ||||
|  | ||||
|         this.recordManager = new HistoryRecordManager(context); | ||||
|  | ||||
|         this.progressUpdateReactor = new SerialDisposable(); | ||||
|         this.databaseUpdateReactor = new CompositeDisposable(); | ||||
|  | ||||
|         final String userAgent = Downloader.USER_AGENT; | ||||
|         final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); | ||||
|         this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); | ||||
|  | ||||
|         final TrackSelection.Factory trackSelectionFactory = | ||||
|                 PlayerHelper.getQualitySelector(context, bandwidthMeter); | ||||
|         this.trackSelector = new CustomTrackSelector(trackSelectionFactory); | ||||
|  | ||||
|         this.loadControl = new LoadController(context); | ||||
|         this.renderFactory = new DefaultRenderersFactory(context); | ||||
|     } | ||||
|  | ||||
|     public void setup() { | ||||
|         if (simpleExoPlayer == null) initPlayer(/*playOnInit=*/true); | ||||
|         if (simpleExoPlayer == null) { | ||||
|             initPlayer(/*playOnInit=*/true); | ||||
|         } | ||||
|         initListeners(); | ||||
|     } | ||||
|  | ||||
|     public void initPlayer(final boolean playOnReady) { | ||||
|         if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); | ||||
|  | ||||
|         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); | ||||
|         databaseUpdateReactor = new CompositeDisposable(); | ||||
|  | ||||
|         final String userAgent = Downloader.USER_AGENT; | ||||
|         final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); | ||||
|         dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); | ||||
|  | ||||
|         final TrackSelection.Factory trackSelectionFactory = | ||||
|                 PlayerHelper.getQualitySelector(context, bandwidthMeter); | ||||
|         trackSelector = new CustomTrackSelector(trackSelectionFactory); | ||||
|  | ||||
|         final LoadControl loadControl = new LoadController(context); | ||||
|         final RenderersFactory renderFactory = new DefaultRenderersFactory(context); | ||||
|         simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); | ||||
|         simpleExoPlayer.addListener(this); | ||||
|         simpleExoPlayer.setPlayWhenReady(playOnReady); | ||||
| @@ -205,6 +209,8 @@ public abstract class BasePlayer implements | ||||
|         audioReactor = new AudioReactor(context, simpleExoPlayer); | ||||
|         mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, | ||||
|                 new BasePlayerMediaSession(this)); | ||||
|  | ||||
|         registerBroadcastReceiver(); | ||||
|     } | ||||
|  | ||||
|     public void initListeners() {} | ||||
| @@ -235,20 +241,24 @@ public abstract class BasePlayer implements | ||||
|         final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); | ||||
|         final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()); | ||||
|         final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); | ||||
|         final boolean playbackSkipSilence = intent.getBooleanExtra(PLAYBACK_SKIP_SILENCE, | ||||
|                 getPlaybackSkipSilence()); | ||||
|  | ||||
|         // Good to go... | ||||
|         initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, /*playOnInit=*/true); | ||||
|         initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, | ||||
|                 /*playOnInit=*/true); | ||||
|     } | ||||
|  | ||||
|     protected void initPlayback(@NonNull final PlayQueue queue, | ||||
|                                 @Player.RepeatMode final int repeatMode, | ||||
|                                 final float playbackSpeed, | ||||
|                                 final float playbackPitch, | ||||
|                                 final boolean playbackSkipSilence, | ||||
|                                 final boolean playOnReady) { | ||||
|         destroyPlayer(); | ||||
|         initPlayer(playOnReady); | ||||
|         setRepeatMode(repeatMode); | ||||
|         setPlaybackParameters(playbackSpeed, playbackPitch); | ||||
|         setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); | ||||
|  | ||||
|         playQueue = queue; | ||||
|         playQueue.init(); | ||||
| @@ -270,7 +280,6 @@ public abstract class BasePlayer implements | ||||
|         if (playQueue != null) playQueue.dispose(); | ||||
|         if (audioReactor != null) audioReactor.dispose(); | ||||
|         if (playbackManager != null) playbackManager.dispose(); | ||||
|         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); | ||||
|         if (mediaSessionManager != null) mediaSessionManager.dispose(); | ||||
|  | ||||
|         if (playQueueAdapter != null) { | ||||
| @@ -284,20 +293,22 @@ public abstract class BasePlayer implements | ||||
|         destroyPlayer(); | ||||
|         unregisterBroadcastReceiver(); | ||||
|  | ||||
|         trackSelector = null; | ||||
|         databaseUpdateReactor.clear(); | ||||
|         progressUpdateReactor.set(null); | ||||
|  | ||||
|         simpleExoPlayer = null; | ||||
|         mediaSessionManager = null; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Thumbnail Loading | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public void initThumbnail(final String url) { | ||||
|     private void initThumbnail(final String url) { | ||||
|         if (DEBUG) Log.d(TAG, "Thumbnail - initThumbnail() called"); | ||||
|         if (url == null || url.isEmpty()) return; | ||||
|         ImageLoader.getInstance().resume(); | ||||
|         ImageLoader.getInstance().loadImage(url, this); | ||||
|         ImageLoader.getInstance().loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, | ||||
|                 this); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -310,6 +321,7 @@ public abstract class BasePlayer implements | ||||
|     public void onLoadingFailed(String imageUri, View view, FailReason failReason) { | ||||
|         Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", | ||||
|                 failReason.getCause()); | ||||
|         currentThumbnail = null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -317,64 +329,14 @@ public abstract class BasePlayer implements | ||||
|         if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + | ||||
|                 "imageUri = [" + imageUri + "], view = [" + view + "], " + | ||||
|                 "loadedImage = [" + loadedImage + "]"); | ||||
|         currentThumbnail = loadedImage; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onLoadingCancelled(String imageUri, View view) { | ||||
|         if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + | ||||
|                 "imageUri = [" + imageUri + "], view = [" + view + "]"); | ||||
|     } | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // MediaSource Building | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public MediaSource buildLiveMediaSource(@NonNull final String sourceUrl, | ||||
|                                             @C.ContentType final int type) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl + | ||||
|                     "], content type = [" + type + "]"); | ||||
|         } | ||||
|         if (dataSource == null) return null; | ||||
|  | ||||
|         final Uri uri = Uri.parse(sourceUrl); | ||||
|         switch (type) { | ||||
|             case C.TYPE_SS: | ||||
|                 return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); | ||||
|             case C.TYPE_DASH: | ||||
|                 return dataSource.getLiveDashMediaSourceFactory().createMediaSource(uri); | ||||
|             case C.TYPE_HLS: | ||||
|                 return dataSource.getLiveHlsMediaSourceFactory().createMediaSource(uri); | ||||
|             default: | ||||
|                 throw new IllegalStateException("Unsupported type: " + type); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public MediaSource buildMediaSource(@NonNull final String sourceUrl, | ||||
|                                         @NonNull final String cacheKey, | ||||
|                                         @NonNull final String overrideExtension) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "buildMediaSource() called with: url = [" + sourceUrl + | ||||
|                     "], cacheKey = [" + cacheKey + "]" + | ||||
|                     "], overrideExtension = [" + overrideExtension + "]"); | ||||
|         } | ||||
|         if (dataSource == null) return null; | ||||
|  | ||||
|         final Uri uri = Uri.parse(sourceUrl); | ||||
|         @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? | ||||
|                 Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); | ||||
|  | ||||
|         switch (type) { | ||||
|             case C.TYPE_SS: | ||||
|                 return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); | ||||
|             case C.TYPE_DASH: | ||||
|                 return dataSource.getDashMediaSourceFactory().createMediaSource(uri); | ||||
|             case C.TYPE_HLS: | ||||
|                 return dataSource.getHlsMediaSourceFactory().createMediaSource(uri); | ||||
|             case C.TYPE_OTHER: | ||||
|                 return dataSource.getExtractorMediaSourceFactory(cacheKey).createMediaSource(uri); | ||||
|             default: | ||||
|                 throw new IllegalStateException("Unsupported type: " + type); | ||||
|         } | ||||
|         currentThumbnail = null; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -399,11 +361,17 @@ public abstract class BasePlayer implements | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void unregisterBroadcastReceiver() { | ||||
|     protected void registerBroadcastReceiver() { | ||||
|         // Try to unregister current first | ||||
|         unregisterBroadcastReceiver(); | ||||
|         context.registerReceiver(broadcastReceiver, intentFilter); | ||||
|     } | ||||
|  | ||||
|     protected void unregisterBroadcastReceiver() { | ||||
|         try { | ||||
|             context.unregisterReceiver(broadcastReceiver); | ||||
|         } catch (final IllegalArgumentException unregisteredException) { | ||||
|             Log.e(TAG, "Broadcast receiver already unregistered.", unregisteredException); | ||||
|             Log.w(TAG, "Broadcast receiver already unregistered (" + unregisteredException.getMessage() + ")"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -510,13 +478,11 @@ public abstract class BasePlayer implements | ||||
|     public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); | ||||
|  | ||||
|     protected void startProgressLoop() { | ||||
|         if (progressUpdateReactor != null) progressUpdateReactor.dispose(); | ||||
|         progressUpdateReactor = getProgressReactor(); | ||||
|         progressUpdateReactor.set(getProgressReactor()); | ||||
|     } | ||||
|  | ||||
|     protected void stopProgressLoop() { | ||||
|         if (progressUpdateReactor != null) progressUpdateReactor.dispose(); | ||||
|         progressUpdateReactor = null; | ||||
|         progressUpdateReactor.set(null); | ||||
|     } | ||||
|  | ||||
|     public void triggerProgressUpdate() { | ||||
| @@ -531,7 +497,8 @@ public abstract class BasePlayer implements | ||||
|     private Disposable getProgressReactor() { | ||||
|         return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(ignored -> triggerProgressUpdate()); | ||||
|                 .subscribe(ignored -> triggerProgressUpdate(), | ||||
|                         error -> Log.e(TAG, "Progress update failure: ", error)); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -545,28 +512,16 @@ public abstract class BasePlayer implements | ||||
|                 (manifest == null ? "no manifest" : "available manifest") + ", " + | ||||
|                 "timeline size = [" + timeline.getWindowCount() + "], " + | ||||
|                 "reason = [" + reason + "]"); | ||||
|         if (playQueue == null) return; | ||||
|  | ||||
|         switch (reason) { | ||||
|             case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block | ||||
|             case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock | ||||
|             case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes | ||||
|                 // Ensures MediaSourceManager#update is complete | ||||
|                 final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size(); | ||||
|                 // Ensure dynamic/livestream timeline changes does not cause negative position | ||||
|                 if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) { | ||||
|                     if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " + | ||||
|                             "clamping to default position."); | ||||
|                     seekToDefault(); | ||||
|                 } | ||||
|                 break; | ||||
|         } | ||||
|         maybeUpdateCurrentMetadata(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { | ||||
|         if (DEBUG) Log.d(TAG, "ExoPlayer - onTracksChanged(), " + | ||||
|                 "track group size = " + trackGroups.length); | ||||
|  | ||||
|         maybeUpdateCurrentMetadata(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -586,6 +541,8 @@ public abstract class BasePlayer implements | ||||
|         } else if (isLoading && !isProgressLoopRunning()) { | ||||
|             startProgressLoop(); | ||||
|         } | ||||
|  | ||||
|         maybeUpdateCurrentMetadata(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -609,6 +566,7 @@ public abstract class BasePlayer implements | ||||
|                 } | ||||
|                 break; | ||||
|             case Player.STATE_READY: //3 | ||||
|                 maybeUpdateCurrentMetadata(); | ||||
|                 maybeCorrectSeekPosition(); | ||||
|                 if (!isPrepared) { | ||||
|                     isPrepared = true; | ||||
| @@ -625,38 +583,19 @@ public abstract class BasePlayer implements | ||||
|     } | ||||
|  | ||||
|     private void maybeCorrectSeekPosition() { | ||||
|         if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return; | ||||
|         if (playQueue == null || simpleExoPlayer == null || currentMetadata == null) return; | ||||
|  | ||||
|         final int currentSourceIndex = playQueue.getIndex(); | ||||
|         final PlayQueueItem currentSourceItem = playQueue.getItem(); | ||||
|         if (currentSourceItem == null) return; | ||||
|  | ||||
|         final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition(); | ||||
|         final boolean isCurrentWindowCorrect = | ||||
|                 simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; | ||||
|         final StreamInfo currentInfo = currentMetadata.getMetadata(); | ||||
|         final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; | ||||
|  | ||||
|         if (recoveryPositionMillis != PlayQueueItem.RECOVERY_UNSET && isCurrentWindowCorrect) { | ||||
|             // Is recovering previous playback? | ||||
|             if (DEBUG) Log.d(TAG, "Playback - Rewinding to recovery time=" + | ||||
|                     "[" + getTimeString((int)recoveryPositionMillis) + "]"); | ||||
|             seekTo(recoveryPositionMillis); | ||||
|             playQueue.unsetRecovery(currentSourceIndex); | ||||
|  | ||||
|         } else if (isSynchronizing && isLive()) { | ||||
|             if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); | ||||
|             // Is still synchronizing? | ||||
|             seekToDefault(); | ||||
|  | ||||
|         } else if (isSynchronizing && presetStartPositionMillis > 0L) { | ||||
|         if (presetStartPositionMillis > 0L) { | ||||
|             // Has another start position? | ||||
|             if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + | ||||
|                     "position=[" + presetStartPositionMillis + "]"); | ||||
|             // Has another start position? | ||||
|             seekTo(presetStartPositionMillis); | ||||
|             currentInfo.setStartPosition(0); | ||||
|         } | ||||
|  | ||||
|         isSynchronizing = false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -708,7 +647,7 @@ public abstract class BasePlayer implements | ||||
|         setRecovery(); | ||||
|  | ||||
|         final Throwable cause = error.getCause(); | ||||
|         if (cause instanceof BehindLiveWindowException) { | ||||
|         if (error instanceof BehindLiveWindowException) { | ||||
|             reload(); | ||||
|         } else if (cause instanceof UnknownHostException) { | ||||
|             playQueue.error(/*isNetworkProblem=*/true); | ||||
| @@ -727,22 +666,29 @@ public abstract class BasePlayer implements | ||||
|     public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { | ||||
|         if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + | ||||
|                 "reason = [" + reason + "]"); | ||||
|         // Refresh the playback if there is a transition to the next video | ||||
|         final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex(); | ||||
|         if (playQueue == null) return; | ||||
|  | ||||
|         /* Discontinuity reasons!! Thank you ExoPlayer lords */ | ||||
|         // Refresh the playback if there is a transition to the next video | ||||
|         final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); | ||||
|         switch (reason) { | ||||
|             case DISCONTINUITY_REASON_PERIOD_TRANSITION: | ||||
|                 if (newPeriodIndex == playQueue.getIndex()) { | ||||
|                 // When player is in single repeat mode and a period transition occurs, | ||||
|                 // we need to register a view count here since no metadata has changed | ||||
|                 if (getRepeatMode() == Player.REPEAT_MODE_ONE && | ||||
|                         newWindowIndex == playQueue.getIndex()) { | ||||
|                     registerView(); | ||||
|                 } else { | ||||
|                     playQueue.offsetIndex(+1); | ||||
|                     break; | ||||
|                 } | ||||
|             case DISCONTINUITY_REASON_SEEK: | ||||
|             case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: | ||||
|             case DISCONTINUITY_REASON_INTERNAL: | ||||
|                 if (playQueue.getIndex() != newWindowIndex) { | ||||
|                     playQueue.setIndex(newWindowIndex); | ||||
|                 } | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         maybeUpdateCurrentMetadata(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -788,7 +734,7 @@ public abstract class BasePlayer implements | ||||
|         if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called"); | ||||
|  | ||||
|         currentItem = null; | ||||
|         currentInfo = null; | ||||
|         currentMetadata = null; | ||||
|         simpleExoPlayer.stop(); | ||||
|         isPrepared = false; | ||||
|  | ||||
| @@ -805,42 +751,21 @@ public abstract class BasePlayer implements | ||||
|         simpleExoPlayer.prepare(mediaSource); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, | ||||
|                                       @Nullable final StreamInfo info) { | ||||
|     public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { | ||||
|         if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + | ||||
|                 (info != null ? "available" : "null") + " info, " + | ||||
|                 "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); | ||||
|         if (simpleExoPlayer == null || playQueue == null) return; | ||||
|  | ||||
|         final boolean onPlaybackInitial = currentItem == null; | ||||
|         final boolean hasPlayQueueItemChanged = currentItem != item; | ||||
|         final boolean hasStreamInfoChanged = currentInfo != info; | ||||
|  | ||||
|         final int currentPlayQueueIndex = playQueue.indexOf(item); | ||||
|         final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); | ||||
|         final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); | ||||
|  | ||||
|         // when starting playback on the last item when not repeating, maybe auto queue | ||||
|         if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && | ||||
|                 getRepeatMode() == Player.REPEAT_MODE_OFF && | ||||
|                 PlayerHelper.isAutoQueueEnabled(context)) { | ||||
|             final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); | ||||
|             if (autoQueue != null) playQueue.append(autoQueue.getStreams()); | ||||
|         } | ||||
|         // If nothing to synchronize | ||||
|         if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!hasPlayQueueItemChanged) return; | ||||
|         currentItem = item; | ||||
|         currentInfo = info; | ||||
|         if (hasPlayQueueItemChanged) { | ||||
|             // updates only to the stream info should not trigger another view count | ||||
|             registerView(); | ||||
|             initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); | ||||
|         } | ||||
|         onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); | ||||
|  | ||||
|         // Check if on wrong window | ||||
|         if (currentPlayQueueIndex != playQueue.getIndex()) { | ||||
| @@ -855,39 +780,29 @@ public abstract class BasePlayer implements | ||||
|                     "index=[" + currentPlayQueueIndex + "] with " + | ||||
|                     "playlist length=[" + currentPlaylistSize + "]"); | ||||
|  | ||||
|             // If not playing correct stream, change window position and sets flag | ||||
|             // for synchronizing once window position is corrected | ||||
|             // @see maybeCorrectSeekPosition() | ||||
|         } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial || | ||||
|                 !isPlaying()) { | ||||
|             if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + | ||||
|                     " index=[" + currentPlayQueueIndex + "]," + | ||||
|                     " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "]."); | ||||
|             isSynchronizing = true; | ||||
|             simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); | ||||
|  | ||||
|             if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { | ||||
|                 simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition()); | ||||
|                 playQueue.unsetRecovery(currentPlayQueueIndex); | ||||
|             } else { | ||||
|                 simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     abstract protected void onMetadataChanged(@NonNull final PlayQueueItem item, | ||||
|                                               @Nullable final StreamInfo info, | ||||
|                                               final int newPlayQueueIndex, | ||||
|                                               final boolean hasPlayQueueItemChanged); | ||||
|  | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { | ||||
|         final StreamType streamType = info.getStreamType(); | ||||
|         if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) { | ||||
|             return null; | ||||
|     protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { | ||||
|         final StreamInfo info = tag.getMetadata(); | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName()); | ||||
|         } | ||||
|  | ||||
|         if (!info.getHlsUrl().isEmpty()) { | ||||
|             return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); | ||||
|         } else if (!info.getDashMpdUrl().isEmpty()) { | ||||
|             return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH); | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|         initThumbnail(info.getThumbnailUrl()); | ||||
|         registerView(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -1020,9 +935,7 @@ public abstract class BasePlayer implements | ||||
|  | ||||
|     public void seekTo(long positionMillis) { | ||||
|         if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); | ||||
|         if (simpleExoPlayer == null || positionMillis < 0 || | ||||
|                 positionMillis > simpleExoPlayer.getDuration()) return; | ||||
|         simpleExoPlayer.seekTo(positionMillis); | ||||
|         if (simpleExoPlayer != null) simpleExoPlayer.seekTo(positionMillis); | ||||
|     } | ||||
|  | ||||
|     public void seekBy(long offsetMillis) { | ||||
| @@ -1046,12 +959,14 @@ public abstract class BasePlayer implements | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void registerView() { | ||||
|         if (databaseUpdateReactor == null || currentInfo == null) return; | ||||
|         databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() | ||||
|         if (currentMetadata == null) return; | ||||
|         final StreamInfo currentInfo = currentMetadata.getMetadata(); | ||||
|         final Disposable viewRegister = recordManager.onViewed(currentInfo).onErrorComplete() | ||||
|                 .subscribe( | ||||
|                         ignored -> {/* successful */}, | ||||
|                         error -> Log.e(TAG, "Player onViewed() failure: ", error) | ||||
|                 )); | ||||
|                 ); | ||||
|         databaseUpdateReactor.add(viewRegister); | ||||
|     } | ||||
|  | ||||
|     protected void reload() { | ||||
| @@ -1065,7 +980,7 @@ public abstract class BasePlayer implements | ||||
|     } | ||||
|  | ||||
|     protected void savePlaybackState(final StreamInfo info, final long progress) { | ||||
|         if (info == null || databaseUpdateReactor == null) return; | ||||
|         if (info == null) return; | ||||
|         final Disposable stateSaver = recordManager.saveStreamState(info, progress) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .onErrorComplete() | ||||
| @@ -1077,7 +992,8 @@ public abstract class BasePlayer implements | ||||
|     } | ||||
|  | ||||
|     private void savePlaybackState() { | ||||
|         if (simpleExoPlayer == null || currentInfo == null) return; | ||||
|         if (simpleExoPlayer == null || currentMetadata == null) return; | ||||
|         final StreamInfo currentInfo = currentMetadata.getMetadata(); | ||||
|  | ||||
|         if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS && | ||||
|                 simpleExoPlayer.getCurrentPosition() < | ||||
| @@ -1085,6 +1001,36 @@ public abstract class BasePlayer implements | ||||
|             savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void maybeUpdateCurrentMetadata() { | ||||
|         if (simpleExoPlayer == null) return; | ||||
|  | ||||
|         final MediaSourceTag metadata; | ||||
|         try { | ||||
|             metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); | ||||
|         } catch (IndexOutOfBoundsException | ClassCastException error) { | ||||
|             if(DEBUG) Log.d(TAG, "Could not update metadata: " + error.getMessage()); | ||||
|             if(DEBUG) error.printStackTrace(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (metadata == null) return; | ||||
|         maybeAutoQueueNextStream(metadata); | ||||
|  | ||||
|         if (currentMetadata == metadata) return; | ||||
|         currentMetadata = metadata; | ||||
|         onMetadataChanged(metadata); | ||||
|     } | ||||
|  | ||||
|     private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag currentMetadata) { | ||||
|         if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 || | ||||
|                 getRepeatMode() != Player.REPEAT_MODE_OFF || | ||||
|                 !PlayerHelper.isAutoQueueEnabled(context)) return; | ||||
|         // auto queue when starting playback on the last item when not repeating | ||||
|         final PlayQueue autoQueue = PlayerHelper.autoQueueOf(currentMetadata.getMetadata(), | ||||
|                 playQueue.getStreams()); | ||||
|         if (autoQueue != null) playQueue.append(autoQueue.getStreams()); | ||||
|     } | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Getters and Setters | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -1101,19 +1047,35 @@ public abstract class BasePlayer implements | ||||
|         return currentState; | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public MediaSourceTag getCurrentMetadata() { | ||||
|         return currentMetadata; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public String getVideoUrl() { | ||||
|         return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUrl(); | ||||
|         return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getUrl(); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public String getVideoTitle() { | ||||
|         return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getTitle(); | ||||
|         return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getName(); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public String getUploaderName() { | ||||
|         return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader(); | ||||
|         return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getUploaderName(); | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public Bitmap getThumbnail() { | ||||
|         return currentThumbnail == null ? | ||||
|                 BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail) : | ||||
|                 currentThumbnail; | ||||
|     } | ||||
|  | ||||
|     /** Checks if the current playback is a livestream AND is playing at or beyond the live edge */ | ||||
|     @SuppressWarnings("BooleanMethodIsAlwaysInverted") | ||||
|     public boolean isLiveEdge() { | ||||
|         if (simpleExoPlayer == null || !isLive()) return false; | ||||
|  | ||||
| @@ -1135,6 +1097,9 @@ public abstract class BasePlayer implements | ||||
|             return simpleExoPlayer.isCurrentWindowDynamic(); | ||||
|         } catch (@NonNull IndexOutOfBoundsException ignored) { | ||||
|             // Why would this even happen =( | ||||
|             // But lets log it anyway. Save is save | ||||
|             if(DEBUG) Log.d(TAG, "Could not update metadata: " + ignored.getMessage()); | ||||
|             if(DEBUG) ignored.printStackTrace(); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| @@ -1147,11 +1112,11 @@ public abstract class BasePlayer implements | ||||
|  | ||||
|     @Player.RepeatMode | ||||
|     public int getRepeatMode() { | ||||
|         return simpleExoPlayer.getRepeatMode(); | ||||
|         return simpleExoPlayer == null ? Player.REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode(); | ||||
|     } | ||||
|  | ||||
|     public void setRepeatMode(@Player.RepeatMode final int repeatMode) { | ||||
|         simpleExoPlayer.setRepeatMode(repeatMode); | ||||
|         if (simpleExoPlayer != null) simpleExoPlayer.setRepeatMode(repeatMode); | ||||
|     } | ||||
|  | ||||
|     public float getPlaybackSpeed() { | ||||
| @@ -1162,19 +1127,22 @@ public abstract class BasePlayer implements | ||||
|         return getPlaybackParameters().pitch; | ||||
|     } | ||||
|  | ||||
|     public boolean getPlaybackSkipSilence() { | ||||
|         return getPlaybackParameters().skipSilence; | ||||
|     } | ||||
|  | ||||
|     public void setPlaybackSpeed(float speed) { | ||||
|         setPlaybackParameters(speed, getPlaybackPitch()); | ||||
|         setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence()); | ||||
|     } | ||||
|  | ||||
|     public PlaybackParameters getPlaybackParameters() { | ||||
|         final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f); | ||||
|         if (simpleExoPlayer == null) return defaultParameters; | ||||
|         if (simpleExoPlayer == null) return PlaybackParameters.DEFAULT; | ||||
|         final PlaybackParameters parameters = simpleExoPlayer.getPlaybackParameters(); | ||||
|         return parameters == null ? defaultParameters : parameters; | ||||
|         return parameters == null ? PlaybackParameters.DEFAULT : parameters; | ||||
|     } | ||||
|  | ||||
|     public void setPlaybackParameters(float speed, float pitch) { | ||||
|         simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch)); | ||||
|     public void setPlaybackParameters(float speed, float pitch, boolean skipSilence) { | ||||
|         simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch, skipSilence)); | ||||
|     } | ||||
|  | ||||
|     public PlayQueue getPlayQueue() { | ||||
| @@ -1190,7 +1158,7 @@ public abstract class BasePlayer implements | ||||
|     } | ||||
|  | ||||
|     public boolean isProgressLoopRunning() { | ||||
|         return progressUpdateReactor != null && !progressUpdateReactor.isDisposed(); | ||||
|         return progressUpdateReactor.get() != null; | ||||
|     } | ||||
|  | ||||
|     public void setRecovery() { | ||||
|   | ||||
| @@ -36,6 +36,7 @@ import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.ActivityCompat; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.support.v7.content.res.AppCompatResources; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.support.v7.widget.helper.ItemTouchHelper; | ||||
| import android.util.DisplayMetrics; | ||||
| @@ -46,7 +47,9 @@ import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| import android.view.WindowManager; | ||||
| import android.widget.ImageButton; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.PopupMenu; | ||||
| import android.widget.ProgressBar; | ||||
| import android.widget.RelativeLayout; | ||||
| import android.widget.SeekBar; | ||||
| import android.widget.TextView; | ||||
| @@ -58,7 +61,6 @@ 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; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | ||||
| import org.schabi.newpipe.player.helper.PlaybackParameterDialog; | ||||
| @@ -67,6 +69,8 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; | ||||
| import org.schabi.newpipe.player.resolver.MediaSourceTag; | ||||
| import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; | ||||
| import org.schabi.newpipe.util.AnimationUtils; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| @@ -81,6 +85,7 @@ import java.util.UUID; | ||||
| import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; | ||||
| import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; | ||||
| import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; | ||||
| import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; | ||||
| import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; | ||||
| import static org.schabi.newpipe.util.AnimationUtils.animateRotation; | ||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||
| @@ -104,6 +109,7 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|  | ||||
|     @Nullable private PlayerState playerState; | ||||
|     private boolean isInMultiWindow; | ||||
|     private boolean isBackPressed; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Activity LifeCycle | ||||
| @@ -125,7 +131,7 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|  | ||||
|         hideSystemUi(); | ||||
|         setContentView(R.layout.activity_main_player); | ||||
|         playerImpl = new VideoPlayerImpl(this); | ||||
|         playerImpl = new  VideoPlayerImpl(this); | ||||
|         playerImpl.setup(findViewById(android.R.id.content)); | ||||
|  | ||||
|         if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) { | ||||
| @@ -152,7 +158,10 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|     protected void onNewIntent(Intent intent) { | ||||
|         if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); | ||||
|         super.onNewIntent(intent); | ||||
|         playerImpl.handleIntent(intent); | ||||
|         if (intent != null) { | ||||
|             playerState = null; | ||||
|             playerImpl.handleIntent(intent); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -177,7 +186,7 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|             playerImpl.setPlaybackQuality(playerState.getPlaybackQuality()); | ||||
|             playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(), | ||||
|                     playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(), | ||||
|                     playerState.wasPlaying()); | ||||
|                     playerState.isPlaybackSkipSilence(), playerState.wasPlaying()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -191,6 +200,12 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBackPressed() { | ||||
|         super.onBackPressed(); | ||||
|         isBackPressed = true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onSaveInstanceState(Bundle outState) { | ||||
|         if (DEBUG) Log.d(TAG, "onSaveInstanceState() called"); | ||||
| @@ -200,7 +215,8 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|         playerImpl.setRecovery(); | ||||
|         playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(), | ||||
|                 playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(), | ||||
|                 playerImpl.getPlaybackQuality(), playerImpl.isPlaying()); | ||||
|                 playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(), | ||||
|                 playerImpl.isPlaying()); | ||||
|         StateSaver.tryToSave(isChangingConfigurations(), null, outState, this); | ||||
|     } | ||||
|  | ||||
| @@ -208,10 +224,17 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|     protected void onStop() { | ||||
|         if (DEBUG) Log.d(TAG, "onStop() called"); | ||||
|         super.onStop(); | ||||
|         playerImpl.destroy(); | ||||
|  | ||||
|         PlayerHelper.setScreenBrightness(getApplicationContext(), | ||||
|                 getWindow().getAttributes().screenBrightness); | ||||
|  | ||||
|         if (playerImpl == null) return; | ||||
|         if (!isBackPressed) { | ||||
|             playerImpl.minimize(); | ||||
|         } | ||||
|         playerImpl.destroy(); | ||||
|  | ||||
|         isInMultiWindow = false; | ||||
|         isBackPressed = false; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -335,18 +358,27 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { | ||||
|         if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch); | ||||
|     public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, | ||||
|                                            boolean playbackSkipSilence) { | ||||
|         if (playerImpl != null) { | ||||
|             playerImpl.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @SuppressWarnings({"unused", "WeakerAccess"}) | ||||
|     private class VideoPlayerImpl extends VideoPlayer { | ||||
|         private final float MAX_GESTURE_LENGTH = 0.75f; | ||||
|  | ||||
|         private TextView titleTextView; | ||||
|         private TextView channelTextView; | ||||
|         private TextView volumeTextView; | ||||
|         private TextView brightnessTextView; | ||||
|         private RelativeLayout volumeRelativeLayout; | ||||
|         private ProgressBar volumeProgressBar; | ||||
|         private ImageView volumeImageView; | ||||
|         private RelativeLayout brightnessRelativeLayout; | ||||
|         private ProgressBar brightnessProgressBar; | ||||
|         private ImageView brightnessImageView; | ||||
|         private ImageButton queueButton; | ||||
|         private ImageButton repeatButton; | ||||
|         private ImageButton shuffleButton; | ||||
| @@ -370,6 +402,8 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|         private RelativeLayout windowRootLayout; | ||||
|         private View secondaryControls; | ||||
|  | ||||
|         private int maxGestureLength; | ||||
|  | ||||
|         VideoPlayerImpl(final Context context) { | ||||
|             super("VideoPlayerImpl" + MainVideoPlayer.TAG, context); | ||||
|         } | ||||
| @@ -379,8 +413,12 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|             super.initViews(rootView); | ||||
|             this.titleTextView = rootView.findViewById(R.id.titleTextView); | ||||
|             this.channelTextView = rootView.findViewById(R.id.channelTextView); | ||||
|             this.volumeTextView = rootView.findViewById(R.id.volumeTextView); | ||||
|             this.brightnessTextView = rootView.findViewById(R.id.brightnessTextView); | ||||
|             this.volumeRelativeLayout = rootView.findViewById(R.id.volumeRelativeLayout); | ||||
|             this.volumeProgressBar = rootView.findViewById(R.id.volumeProgressBar); | ||||
|             this.volumeImageView = rootView.findViewById(R.id.volumeImageView); | ||||
|             this.brightnessRelativeLayout = rootView.findViewById(R.id.brightnessRelativeLayout); | ||||
|             this.brightnessProgressBar = rootView.findViewById(R.id.brightnessProgressBar); | ||||
|             this.brightnessImageView = rootView.findViewById(R.id.brightnessImageView); | ||||
|             this.queueButton = rootView.findViewById(R.id.queueButton); | ||||
|             this.repeatButton = rootView.findViewById(R.id.repeatButton); | ||||
|             this.shuffleButton = rootView.findViewById(R.id.shuffleButton); | ||||
| @@ -422,7 +460,7 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|         public void initListeners() { | ||||
|             super.initListeners(); | ||||
|  | ||||
|             MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); | ||||
|             PlayerGestureListener listener = new PlayerGestureListener(); | ||||
|             gestureDetector = new GestureDetector(context, listener); | ||||
|             gestureDetector.setIsLongpressEnabled(false); | ||||
|             getRootView().setOnTouchListener(listener); | ||||
| @@ -439,6 +477,37 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|             toggleOrientationButton.setOnClickListener(this); | ||||
|             switchBackgroundButton.setOnClickListener(this); | ||||
|             switchPopupButton.setOnClickListener(this); | ||||
|  | ||||
|             getRootView().addOnLayoutChangeListener((view, l, t, r, b, ol, ot, or, ob) -> { | ||||
|                 if (l != ol || t != ot || r != or || b != ob) { | ||||
|                     // Use smaller value to be consistent between screen orientations | ||||
|                     // (and to make usage easier) | ||||
|                     int width = r - l, height = b - t; | ||||
|                     maxGestureLength = (int) (Math.min(width, height) * MAX_GESTURE_LENGTH); | ||||
|  | ||||
|                     if (DEBUG) Log.d(TAG, "maxGestureLength = " + maxGestureLength); | ||||
|  | ||||
|                     volumeProgressBar.setMax(maxGestureLength); | ||||
|                     brightnessProgressBar.setMax(maxGestureLength); | ||||
|  | ||||
|                     setInitialGestureValues(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         public void minimize() { | ||||
|             switch (PlayerHelper.getMinimizeOnExitAction(context)) { | ||||
|                 case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND: | ||||
|                     onPlayBackgroundButtonClicked(); | ||||
|                     break; | ||||
|                 case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP: | ||||
|                     onFullScreenButtonClicked(); | ||||
|                     break; | ||||
|                 case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE: | ||||
|                 default: | ||||
|                     // No action | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -461,14 +530,11 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|         // Playback Listener | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|         protected void onMetadataChanged(@NonNull final PlayQueueItem item, | ||||
|                                          @Nullable final StreamInfo info, | ||||
|                                          final int newPlayQueueIndex, | ||||
|                                          final boolean hasPlayQueueItemChanged) { | ||||
|             super.onMetadataChanged(item, info, newPlayQueueIndex, false); | ||||
|         protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { | ||||
|             super.onMetadataChanged(tag); | ||||
|  | ||||
|             titleTextView.setText(getVideoTitle()); | ||||
|             channelTextView.setText(getUploaderName()); | ||||
|             titleTextView.setText(tag.getMetadata().getName()); | ||||
|             channelTextView.setText(tag.getMetadata().getUploaderName()); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -501,6 +567,7 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|                     this.getRepeatMode(), | ||||
|                     this.getPlaybackSpeed(), | ||||
|                     this.getPlaybackPitch(), | ||||
|                     this.getPlaybackSkipSilence(), | ||||
|                     this.getPlaybackQuality() | ||||
|             ); | ||||
|             context.startService(intent); | ||||
| @@ -522,6 +589,7 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|                     this.getRepeatMode(), | ||||
|                     this.getPlaybackSpeed(), | ||||
|                     this.getPlaybackPitch(), | ||||
|                     this.getPlaybackSkipSilence(), | ||||
|                     this.getPlaybackQuality() | ||||
|             ); | ||||
|             context.startService(intent); | ||||
| @@ -617,7 +685,8 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|  | ||||
|         @Override | ||||
|         public void onPlaybackSpeedClicked() { | ||||
|             PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch()) | ||||
|             PlaybackParameterDialog | ||||
|                     .newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence()) | ||||
|                     .show(getSupportFragmentManager(), TAG); | ||||
|         } | ||||
|  | ||||
| @@ -647,14 +716,19 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) { | ||||
|             return ListHelper.getDefaultResolutionIndex(context, sortedVideos); | ||||
|         } | ||||
|         protected VideoPlaybackResolver.QualityResolver getQualityResolver() { | ||||
|             return new VideoPlaybackResolver.QualityResolver() { | ||||
|                 @Override | ||||
|                 public int getDefaultResolutionIndex(List<VideoStream> sortedVideos) { | ||||
|                     return ListHelper.getDefaultResolutionIndex(context, sortedVideos); | ||||
|                 } | ||||
|  | ||||
|         @Override | ||||
|         protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, | ||||
|                                                  final String playbackQuality) { | ||||
|             return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); | ||||
|                 @Override | ||||
|                 public int getOverrideResolutionIndex(List<VideoStream> sortedVideos, | ||||
|                                                       String playbackQuality) { | ||||
|                     return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -678,7 +752,6 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|         @Override | ||||
|         public void onBuffering() { | ||||
|             super.onBuffering(); | ||||
|             animatePlayButtons(false, 100); | ||||
|             getRootView().setKeepScreenOn(true); | ||||
|         } | ||||
|  | ||||
| @@ -728,6 +801,13 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|         // Utils | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|         private void setInitialGestureValues() { | ||||
|             if (getAudioReactor() != null) { | ||||
|                 final float currentVolumeNormalized = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume(); | ||||
|                 volumeProgressBar.setProgress((int) (volumeProgressBar.getMax() * currentVolumeNormalized)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void showControlsThenHide() { | ||||
|             if (queueVisible) return; | ||||
| @@ -831,12 +911,28 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|             return channelTextView; | ||||
|         } | ||||
|  | ||||
|         public TextView getVolumeTextView() { | ||||
|             return volumeTextView; | ||||
|         public RelativeLayout getVolumeRelativeLayout() { | ||||
|             return volumeRelativeLayout; | ||||
|         } | ||||
|  | ||||
|         public TextView getBrightnessTextView() { | ||||
|             return brightnessTextView; | ||||
|         public ProgressBar getVolumeProgressBar() { | ||||
|             return volumeProgressBar; | ||||
|         } | ||||
|  | ||||
|         public ImageView getVolumeImageView() { | ||||
|             return volumeImageView; | ||||
|         } | ||||
|  | ||||
|         public RelativeLayout getBrightnessRelativeLayout() { | ||||
|             return brightnessRelativeLayout; | ||||
|         } | ||||
|  | ||||
|         public ProgressBar getBrightnessProgressBar() { | ||||
|             return brightnessProgressBar; | ||||
|         } | ||||
|  | ||||
|         public ImageView getBrightnessImageView() { | ||||
|             return brightnessImageView; | ||||
|         } | ||||
|  | ||||
|         public ImageButton getRepeatButton() { | ||||
| @@ -846,15 +942,18 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|         public ImageButton getPlayPauseButton() { | ||||
|             return playPauseButton; | ||||
|         } | ||||
|  | ||||
|         public int getMaxGestureLength() { | ||||
|             return maxGestureLength; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { | ||||
|     private class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { | ||||
|         private boolean isMoving; | ||||
|  | ||||
|         @Override | ||||
|         public boolean onDoubleTap(MotionEvent e) { | ||||
|             if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); | ||||
|             if (!playerImpl.isPlaying()) return false; | ||||
|  | ||||
|             if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) { | ||||
|                 playerImpl.onFastForward(); | ||||
| @@ -888,91 +987,91 @@ public final class MainVideoPlayer extends AppCompatActivity | ||||
|             return super.onDown(e); | ||||
|         } | ||||
|  | ||||
|         private static final int MOVEMENT_THRESHOLD = 40; | ||||
|  | ||||
|         private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext()); | ||||
|         private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume(); | ||||
|  | ||||
|         private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; | ||||
|         private float currentBrightness = getWindow().getAttributes().screenBrightness > 0 | ||||
|                 ? getWindow().getAttributes().screenBrightness | ||||
|                 : 0.5f; | ||||
|  | ||||
|         private int currentVolume, maxVolume = playerImpl.getAudioReactor().getMaxVolume(); | ||||
|         private final float stepsVolume = 15, stepVolume = (float) Math.ceil(maxVolume / stepsVolume), minVolume = 0; | ||||
|  | ||||
|         private final String brightnessUnicode = new String(Character.toChars(0x2600)); | ||||
|         private final String volumeUnicode = new String(Character.toChars(0x1F508)); | ||||
|  | ||||
|         private final int MOVEMENT_THRESHOLD = 40; | ||||
|         private final int eventsThreshold = 8; | ||||
|         private boolean triggered = false; | ||||
|         private int eventsNum; | ||||
|  | ||||
|         // TODO: Improve video gesture controls | ||||
|         @Override | ||||
|         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { | ||||
|         public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) { | ||||
|             if (!isPlayerGestureEnabled) return false; | ||||
|  | ||||
|             //noinspection PointlessBooleanExpression | ||||
|             if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " + | ||||
|                     ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + | ||||
|                     ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + | ||||
|                     ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + | ||||
|                     ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + | ||||
|                     ", distanceXy = [" + distanceX + ", " + distanceY + "]"); | ||||
|             float abs = Math.abs(e2.getY() - e1.getY()); | ||||
|             if (!triggered) { | ||||
|                 triggered = abs > MOVEMENT_THRESHOLD; | ||||
|  | ||||
|             final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; | ||||
|             if (!isMoving && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) | ||||
|                     || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             if (eventsNum++ % eventsThreshold != 0 || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) return false; | ||||
|             isMoving = true; | ||||
| //            boolean up = !((e2.getY() - e1.getY()) > 0) && distanceY > 0; // Android's origin point is on top | ||||
|             boolean up = distanceY > 0; | ||||
|  | ||||
|  | ||||
|             if (e1.getX() > playerImpl.getRootView().getWidth() / 2) { | ||||
|                 double floor = Math.floor(up ? stepVolume : -stepVolume); | ||||
|                 currentVolume = (int) (playerImpl.getAudioReactor().getVolume() + floor); | ||||
|                 if (currentVolume >= maxVolume) currentVolume = maxVolume; | ||||
|                 if (currentVolume <= minVolume) currentVolume = (int) minVolume; | ||||
|             if (initialEvent.getX() > playerImpl.getRootView().getWidth() / 2) { | ||||
|                 playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); | ||||
|                 float currentProgressPercent = | ||||
|                         (float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); | ||||
|                 int currentVolume = (int) (maxVolume * currentProgressPercent); | ||||
|                 playerImpl.getAudioReactor().setVolume(currentVolume); | ||||
|  | ||||
|                 currentVolume = playerImpl.getAudioReactor().getVolume(); | ||||
|                 if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); | ||||
|                 final String volumeText = volumeUnicode + " " + Math.round((((float) currentVolume) / maxVolume) * 100) + "%"; | ||||
|                 playerImpl.getVolumeTextView().setText(volumeText); | ||||
|  | ||||
|                 if (playerImpl.getVolumeTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getVolumeTextView(), true, 200); | ||||
|                 if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE); | ||||
|                 final int resId = | ||||
|                         currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp | ||||
|                         : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp | ||||
|                         : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp | ||||
|                         : R.drawable.ic_volume_up_white_72dp; | ||||
|  | ||||
|                 playerImpl.getVolumeImageView().setImageDrawable( | ||||
|                         AppCompatResources.getDrawable(getApplicationContext(), resId) | ||||
|                 ); | ||||
|  | ||||
|                 if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { | ||||
|                     animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); | ||||
|                 } | ||||
|                 if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { | ||||
|                     playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); | ||||
|                 } | ||||
|             } else { | ||||
|                 WindowManager.LayoutParams lp = getWindow().getAttributes(); | ||||
|                 currentBrightness += up ? stepBrightness : -stepBrightness; | ||||
|                 if (currentBrightness >= 1f) currentBrightness = 1f; | ||||
|                 if (currentBrightness <= minBrightness) currentBrightness = minBrightness; | ||||
|                 playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY); | ||||
|                 float currentProgressPercent = | ||||
|                         (float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength(); | ||||
|                 WindowManager.LayoutParams layoutParams = getWindow().getAttributes(); | ||||
|                 layoutParams.screenBrightness = currentProgressPercent; | ||||
|                 getWindow().setAttributes(layoutParams); | ||||
|  | ||||
|                 lp.screenBrightness = currentBrightness; | ||||
|                 getWindow().setAttributes(lp); | ||||
|                 if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentBrightness); | ||||
|                 int brightnessNormalized = Math.round(currentBrightness * 100); | ||||
|                 if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentProgressPercent); | ||||
|  | ||||
|                 final String brightnessText = brightnessUnicode + " " + (brightnessNormalized == 1 ? 0 : brightnessNormalized) + "%"; | ||||
|                 playerImpl.getBrightnessTextView().setText(brightnessText); | ||||
|                 final int resId = | ||||
|                         currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp | ||||
|                         : currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp | ||||
|                         : R.drawable.ic_brightness_high_white_72dp; | ||||
|  | ||||
|                 if (playerImpl.getBrightnessTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), true, 200); | ||||
|                 if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); | ||||
|                 playerImpl.getBrightnessImageView().setImageDrawable( | ||||
|                         AppCompatResources.getDrawable(getApplicationContext(), resId) | ||||
|                 ); | ||||
|  | ||||
|                 if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { | ||||
|                     animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); | ||||
|                 } | ||||
|                 if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { | ||||
|                     playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); | ||||
|                 } | ||||
|             } | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         private void onScrollEnd() { | ||||
|             if (DEBUG) Log.d(TAG, "onScrollEnd() called"); | ||||
|             triggered = false; | ||||
|             eventsNum = 0; | ||||
|             /* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); | ||||
|             if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/ | ||||
|             if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) { | ||||
|                 animateView(playerImpl.getVolumeTextView(), false, 200, 200); | ||||
|  | ||||
|             if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { | ||||
|                 animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); | ||||
|             } | ||||
|             if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) { | ||||
|                 animateView(playerImpl.getBrightnessTextView(), false, 200, 200); | ||||
|             if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { | ||||
|                 animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); | ||||
|             } | ||||
|  | ||||
|             if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { | ||||
|   | ||||
| @@ -14,21 +14,26 @@ public class PlayerState implements Serializable { | ||||
|     private final float playbackSpeed; | ||||
|     private final float playbackPitch; | ||||
|     @Nullable private final String playbackQuality; | ||||
|     private final boolean playbackSkipSilence; | ||||
|     private final boolean wasPlaying; | ||||
|  | ||||
|     PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, | ||||
|                  final float playbackSpeed, final float playbackPitch, final boolean wasPlaying) { | ||||
|         this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, wasPlaying); | ||||
|                 final float playbackSpeed, final float playbackPitch, | ||||
|                 final boolean playbackSkipSilence, final boolean wasPlaying) { | ||||
|         this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, | ||||
|                 playbackSkipSilence, wasPlaying); | ||||
|     } | ||||
|  | ||||
|     PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, | ||||
|                 final float playbackSpeed, final float playbackPitch, | ||||
|                 @Nullable final String playbackQuality, final boolean wasPlaying) { | ||||
|                 @Nullable final String playbackQuality, final boolean playbackSkipSilence, | ||||
|                 final boolean wasPlaying) { | ||||
|         this.playQueue = playQueue; | ||||
|         this.repeatMode = repeatMode; | ||||
|         this.playbackSpeed = playbackSpeed; | ||||
|         this.playbackPitch = playbackPitch; | ||||
|         this.playbackQuality = playbackQuality; | ||||
|         this.playbackSkipSilence = playbackSkipSilence; | ||||
|         this.wasPlaying = wasPlaying; | ||||
|     } | ||||
|  | ||||
| @@ -62,6 +67,10 @@ public class PlayerState implements Serializable { | ||||
|         return playbackQuality; | ||||
|     } | ||||
|  | ||||
|     public boolean isPlaybackSkipSilence() { | ||||
|         return playbackSkipSilence; | ||||
|     } | ||||
|  | ||||
|     public boolean wasPlaying() { | ||||
|         return wasPlaying; | ||||
|     } | ||||
|   | ||||
| @@ -19,6 +19,8 @@ | ||||
|  | ||||
| package org.schabi.newpipe.player; | ||||
|  | ||||
| import android.animation.Animator; | ||||
| import android.animation.AnimatorListenerAdapter; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.NotificationManager; | ||||
| import android.app.PendingIntent; | ||||
| @@ -34,7 +36,7 @@ import android.os.Build; | ||||
| import android.os.IBinder; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.design.widget.FloatingActionButton; | ||||
| import android.support.v4.app.NotificationCompat; | ||||
| import android.util.DisplayMetrics; | ||||
| import android.util.Log; | ||||
| @@ -42,7 +44,9 @@ import android.view.GestureDetector; | ||||
| import android.view.Gravity; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.view.WindowManager; | ||||
| import android.view.animation.AnticipateInterpolator; | ||||
| import android.widget.ImageButton; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.PopupMenu; | ||||
| @@ -56,17 +60,17 @@ import com.google.android.exoplayer2.Player; | ||||
| import com.google.android.exoplayer2.text.CaptionStyleCompat; | ||||
| import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | ||||
| import com.google.android.exoplayer2.ui.SubtitleView; | ||||
| import com.nostra13.universalimageloader.core.assist.FailReason; | ||||
|  | ||||
| import org.schabi.newpipe.BuildConfig; | ||||
| import org.schabi.newpipe.CheckForNewAppVersionTask; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.player.event.PlayerEventListener; | ||||
| import org.schabi.newpipe.player.helper.LockManager; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.player.old.PlayVideoActivity; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.player.resolver.MediaSourceTag; | ||||
| import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
| @@ -99,11 +103,19 @@ public final class PopupVideoPlayer extends Service { | ||||
|  | ||||
|     private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; | ||||
|  | ||||
|     private WindowManager windowManager; | ||||
|     private WindowManager.LayoutParams windowLayoutParams; | ||||
|     private GestureDetector gestureDetector; | ||||
|     private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | | ||||
|             WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; | ||||
|     private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS | | ||||
|             WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; | ||||
|  | ||||
|     private WindowManager windowManager; | ||||
|     private WindowManager.LayoutParams popupLayoutParams; | ||||
|     private GestureDetector popupGestureDetector; | ||||
|  | ||||
|     private View closeOverlayView; | ||||
|     private FloatingActionButton closeOverlayButton; | ||||
|     private WindowManager.LayoutParams closeOverlayLayoutParams; | ||||
|  | ||||
|     private int shutdownFlingVelocity; | ||||
|     private int tossFlingVelocity; | ||||
|  | ||||
|     private float screenWidth, screenHeight; | ||||
| @@ -118,6 +130,7 @@ public final class PopupVideoPlayer extends Service { | ||||
|  | ||||
|     private VideoPlayerImpl playerImpl; | ||||
|     private LockManager lockManager; | ||||
|     private boolean isPopupClosing = false; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Service-Activity Binder | ||||
| @@ -146,7 +159,10 @@ public final class PopupVideoPlayer extends Service { | ||||
|     public int onStartCommand(final Intent intent, int flags, int startId) { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); | ||||
|         if (playerImpl.getPlayer() == null) initPopup(); | ||||
|         if (playerImpl.getPlayer() == null) { | ||||
|             initPopup(); | ||||
|             initPopupCloseOverlay(); | ||||
|         } | ||||
|         if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true); | ||||
|  | ||||
|         playerImpl.handleIntent(intent); | ||||
| @@ -156,15 +172,16 @@ public final class PopupVideoPlayer extends Service { | ||||
|  | ||||
|     @Override | ||||
|     public void onConfigurationChanged(Configuration newConfig) { | ||||
|         if (DEBUG) Log.d(TAG, "onConfigurationChanged() called with: newConfig = [" + newConfig + "]"); | ||||
|         updateScreenSize(); | ||||
|         updatePopupSize(windowLayoutParams.width, -1); | ||||
|         checkPositionBounds(); | ||||
|         updatePopupSize(popupLayoutParams.width, -1); | ||||
|         checkPopupPositionBounds(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         if (DEBUG) Log.d(TAG, "onDestroy() called"); | ||||
|         onClose(); | ||||
|         closePopup(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -182,7 +199,6 @@ public final class PopupVideoPlayer extends Service { | ||||
|         View rootView = View.inflate(this, R.layout.player_popup, null); | ||||
|         playerImpl.setup(rootView); | ||||
|  | ||||
|         shutdownFlingVelocity = PlayerHelper.getShutdownFlingVelocity(this); | ||||
|         tossFlingVelocity = PlayerHelper.getTossFlingVelocity(this); | ||||
|  | ||||
|         updateScreenSize(); | ||||
| @@ -192,28 +208,56 @@ public final class PopupVideoPlayer extends Service { | ||||
|         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize; | ||||
|  | ||||
|         final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_PHONE : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; | ||||
|         final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? | ||||
|                 WindowManager.LayoutParams.TYPE_PHONE : | ||||
|                 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; | ||||
|  | ||||
|         windowLayoutParams = new WindowManager.LayoutParams( | ||||
|         popupLayoutParams = new WindowManager.LayoutParams( | ||||
|                 (int) popupWidth, (int) getMinimumVideoHeight(popupWidth), | ||||
|                 layoutParamType, | ||||
|                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, | ||||
|                 IDLE_WINDOW_FLAGS, | ||||
|                 PixelFormat.TRANSLUCENT); | ||||
|         windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; | ||||
|         popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; | ||||
|         popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; | ||||
|  | ||||
|         int centerX = (int) (screenWidth / 2f - popupWidth / 2f); | ||||
|         int centerY = (int) (screenHeight / 2f - popupHeight / 2f); | ||||
|         windowLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; | ||||
|         windowLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; | ||||
|         popupLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; | ||||
|         popupLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; | ||||
|  | ||||
|         checkPositionBounds(); | ||||
|         checkPopupPositionBounds(); | ||||
|  | ||||
|         MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); | ||||
|         gestureDetector = new GestureDetector(this, listener); | ||||
|         PopupWindowGestureListener listener = new PopupWindowGestureListener(); | ||||
|         popupGestureDetector = new GestureDetector(this, listener); | ||||
|         rootView.setOnTouchListener(listener); | ||||
|         playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width); | ||||
|         playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height); | ||||
|         windowManager.addView(rootView, windowLayoutParams); | ||||
|  | ||||
|         playerImpl.getLoadingPanel().setMinimumWidth(popupLayoutParams.width); | ||||
|         playerImpl.getLoadingPanel().setMinimumHeight(popupLayoutParams.height); | ||||
|         windowManager.addView(rootView, popupLayoutParams); | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("RtlHardcoded") | ||||
|     private void initPopupCloseOverlay() { | ||||
|         if (DEBUG) Log.d(TAG, "initPopupCloseOverlay() called"); | ||||
|         closeOverlayView = View.inflate(this, R.layout.player_popup_close_overlay, null); | ||||
|         closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton); | ||||
|  | ||||
|         final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? | ||||
|                 WindowManager.LayoutParams.TYPE_PHONE : | ||||
|                 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; | ||||
|         final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | ||||
|                 | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; | ||||
|  | ||||
|         closeOverlayLayoutParams = new WindowManager.LayoutParams( | ||||
|                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, | ||||
|                 layoutParamType, | ||||
|                 flags, | ||||
|                 PixelFormat.TRANSLUCENT); | ||||
|         closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; | ||||
|         closeOverlayLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; | ||||
|  | ||||
|         closeOverlayButton.setVisibility(View.GONE); | ||||
|         windowManager.addView(closeOverlayView, closeOverlayLayoutParams); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -229,6 +273,7 @@ public final class PopupVideoPlayer extends Service { | ||||
|  | ||||
|         notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); | ||||
|         notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); | ||||
|         notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail()); | ||||
|  | ||||
|         notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, | ||||
|                 PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); | ||||
| @@ -244,11 +289,15 @@ public final class PopupVideoPlayer extends Service { | ||||
|  | ||||
|         setRepeatModeRemote(notRemoteView, playerImpl.getRepeatMode()); | ||||
|  | ||||
|         return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) | ||||
|         NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) | ||||
|                 .setOngoing(true) | ||||
|                 .setSmallIcon(R.drawable.ic_newpipe_triangle_white) | ||||
|                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||
|                 .setContent(notRemoteView); | ||||
|         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { | ||||
|             builder.setPriority(NotificationCompat.PRIORITY_MAX); | ||||
|         } | ||||
|         return builder; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -268,44 +317,105 @@ public final class PopupVideoPlayer extends Service { | ||||
|     // Misc | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public void onClose() { | ||||
|         if (DEBUG) Log.d(TAG, "onClose() called"); | ||||
|     public void closePopup() { | ||||
|         if (DEBUG) Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); | ||||
|         if (isPopupClosing) return; | ||||
|         isPopupClosing = true; | ||||
|  | ||||
|         if (playerImpl != null) { | ||||
|             if (playerImpl.getRootView() != null) { | ||||
|                 windowManager.removeView(playerImpl.getRootView()); | ||||
|                 playerImpl.setRootView(null); | ||||
|             } | ||||
|             playerImpl.setRootView(null); | ||||
|             playerImpl.stopActivityBinding(); | ||||
|             playerImpl.destroy(); | ||||
|             playerImpl = null; | ||||
|         } | ||||
|  | ||||
|         mBinder = null; | ||||
|         if (lockManager != null) lockManager.releaseWifiAndCpu(); | ||||
|         if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); | ||||
|         mBinder = null; | ||||
|         playerImpl = null; | ||||
|  | ||||
|         stopForeground(true); | ||||
|         stopSelf(); | ||||
|         animateOverlayAndFinishService(); | ||||
|     } | ||||
|  | ||||
|     private void animateOverlayAndFinishService() { | ||||
|         final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - closeOverlayButton.getY()); | ||||
|  | ||||
|         closeOverlayButton.animate().setListener(null).cancel(); | ||||
|         closeOverlayButton.animate() | ||||
|                 .setInterpolator(new AnticipateInterpolator()) | ||||
|                 .translationY(targetTranslationY) | ||||
|                 .setDuration(400) | ||||
|                 .setListener(new AnimatorListenerAdapter() { | ||||
|                     @Override | ||||
|                     public void onAnimationCancel(Animator animation) { | ||||
|                         end(); | ||||
|                     } | ||||
|  | ||||
|                     @Override | ||||
|                     public void onAnimationEnd(Animator animation) { | ||||
|                         end(); | ||||
|                     } | ||||
|  | ||||
|                     private void end() { | ||||
|                         windowManager.removeView(closeOverlayView); | ||||
|  | ||||
|                         stopForeground(true); | ||||
|                         stopSelf(); | ||||
|                     } | ||||
|                 }).start(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void checkPositionBounds() { | ||||
|         if (windowLayoutParams.x > screenWidth - windowLayoutParams.width) | ||||
|             windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width); | ||||
|         if (windowLayoutParams.x < 0) windowLayoutParams.x = 0; | ||||
|         if (windowLayoutParams.y > screenHeight - windowLayoutParams.height) | ||||
|             windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height); | ||||
|         if (windowLayoutParams.y < 0) windowLayoutParams.y = 0; | ||||
|     /** | ||||
|      * @see #checkPopupPositionBounds(float, float) | ||||
|      */ | ||||
|     @SuppressWarnings("UnusedReturnValue") | ||||
|     private boolean checkPopupPositionBounds() { | ||||
|         return checkPopupPositionBounds(screenWidth, screenHeight); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary that goes from (0,0) to (boundaryWidth,boundaryHeight). | ||||
|      * <p> | ||||
|      * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed and {@code true} is returned | ||||
|      * to represent this change. | ||||
|      * | ||||
|      * @return if the popup was out of bounds and have been moved back to it | ||||
|      */ | ||||
|     private boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "checkPopupPositionBounds() called with: boundaryWidth = [" + boundaryWidth + "], boundaryHeight = [" + boundaryHeight + "]"); | ||||
|         } | ||||
|  | ||||
|         if (popupLayoutParams.x < 0) { | ||||
|             popupLayoutParams.x = 0; | ||||
|             return true; | ||||
|         } else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) { | ||||
|             popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         if (popupLayoutParams.y < 0) { | ||||
|             popupLayoutParams.y = 0; | ||||
|             return true; | ||||
|         } else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) { | ||||
|             popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     private void savePositionAndSize() { | ||||
|         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this); | ||||
|         sharedPreferences.edit().putInt(POPUP_SAVED_X, windowLayoutParams.x).apply(); | ||||
|         sharedPreferences.edit().putInt(POPUP_SAVED_Y, windowLayoutParams.y).apply(); | ||||
|         sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, windowLayoutParams.width).apply(); | ||||
|         sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply(); | ||||
|         sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply(); | ||||
|         sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply(); | ||||
|     } | ||||
|  | ||||
|     private float getMinimumVideoHeight(float width) { | ||||
| @@ -340,13 +450,13 @@ public final class PopupVideoPlayer extends Service { | ||||
|         if (height == -1) height = (int) getMinimumVideoHeight(width); | ||||
|         else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height); | ||||
|  | ||||
|         windowLayoutParams.width = width; | ||||
|         windowLayoutParams.height = height; | ||||
|         popupLayoutParams.width = width; | ||||
|         popupLayoutParams.height = height; | ||||
|         popupWidth = width; | ||||
|         popupHeight = height; | ||||
|  | ||||
|         if (DEBUG) Log.d(TAG, "updatePopupSize() updated values:  width = [" + width + "], height = [" + height + "]"); | ||||
|         windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams); | ||||
|         windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); | ||||
|     } | ||||
|  | ||||
|     protected void setRepeatModeRemote(final RemoteViews remoteViews, final int repeatMode) { | ||||
| @@ -367,6 +477,12 @@ public final class PopupVideoPlayer extends Service { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void updateWindowFlags(final int flags) { | ||||
|         if (popupLayoutParams == null || windowManager == null || playerImpl == null) return; | ||||
|  | ||||
|         popupLayoutParams.flags = flags; | ||||
|         windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); | ||||
|     } | ||||
|     /////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener { | ||||
| @@ -375,6 +491,7 @@ public final class PopupVideoPlayer extends Service { | ||||
|         private ImageView videoPlayPause; | ||||
|  | ||||
|         private View extraOptionsView; | ||||
|         private View closingOverlayView; | ||||
|  | ||||
|         @Override | ||||
|         public void handleIntent(Intent intent) { | ||||
| @@ -395,12 +512,18 @@ public final class PopupVideoPlayer extends Service { | ||||
|             fullScreenButton = rootView.findViewById(R.id.fullScreenButton); | ||||
|             fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked()); | ||||
|             videoPlayPause = rootView.findViewById(R.id.videoPlayPause); | ||||
|             videoPlayPause.setOnClickListener(this::onPlayPauseButtonPressed); | ||||
|  | ||||
|             extraOptionsView = rootView.findViewById(R.id.extraOptionsView); | ||||
|             closingOverlayView = rootView.findViewById(R.id.closingOverlay); | ||||
|             rootView.addOnLayoutChangeListener(this); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void initListeners() { | ||||
|             super.initListeners(); | ||||
|             videoPlayPause.setOnClickListener(v -> onPlayPause()); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void setupSubtitleView(@NonNull SubtitleView view, | ||||
|                                          final float captionScale, | ||||
| @@ -411,10 +534,6 @@ public final class PopupVideoPlayer extends Service { | ||||
|             view.setStyle(captionStyle); | ||||
|         } | ||||
|  | ||||
|         private void onPlayPauseButtonPressed(View ib) { | ||||
|             onPlayPause(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onLayoutChange(final View view, int left, int top, int right, int bottom, | ||||
|                                    int oldLeft, int oldTop, int oldRight, int oldBottom) { | ||||
| @@ -429,21 +548,6 @@ public final class PopupVideoPlayer extends Service { | ||||
|             super.destroy(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | ||||
|             super.onLoadingComplete(imageUri, view, loadedImage); | ||||
|             if (loadedImage != null) { | ||||
|                 // rebuild notification here since remote view does not release bitmaps, causing memory leaks | ||||
|                 notBuilder = createNotification(); | ||||
|  | ||||
|                 if (notRemoteView != null) { | ||||
|                     notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); | ||||
|                 } | ||||
|  | ||||
|                 updateNotification(-1); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onFullScreenButtonClicked() { | ||||
|             super.onFullScreenButtonClicked(); | ||||
| @@ -460,6 +564,7 @@ public final class PopupVideoPlayer extends Service { | ||||
|                         this.getRepeatMode(), | ||||
|                         this.getPlaybackSpeed(), | ||||
|                         this.getPlaybackPitch(), | ||||
|                         this.getPlaybackSkipSilence(), | ||||
|                         this.getPlaybackQuality() | ||||
|                 ); | ||||
|                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
| @@ -472,7 +577,7 @@ public final class PopupVideoPlayer extends Service { | ||||
|                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             } | ||||
|             context.startActivity(intent); | ||||
|             onClose(); | ||||
|             closePopup(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -511,14 +616,47 @@ public final class PopupVideoPlayer extends Service { | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) { | ||||
|             return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); | ||||
|         protected VideoPlaybackResolver.QualityResolver getQualityResolver() { | ||||
|             return new VideoPlaybackResolver.QualityResolver() { | ||||
|                 @Override | ||||
|                 public int getDefaultResolutionIndex(List<VideoStream> sortedVideos) { | ||||
|                     return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); | ||||
|                 } | ||||
|  | ||||
|                 @Override | ||||
|                 public int getOverrideResolutionIndex(List<VideoStream> sortedVideos, | ||||
|                                                       String playbackQuality) { | ||||
|                     return ListHelper.getPopupResolutionIndex(context, sortedVideos, | ||||
|                             playbackQuality); | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
|         // Thumbnail Loading | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|         @Override | ||||
|         public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | ||||
|             super.onLoadingComplete(imageUri, view, loadedImage); | ||||
|             // rebuild notification here since remote view does not release bitmaps, | ||||
|             // causing memory leaks | ||||
|             resetNotification(); | ||||
|             updateNotification(-1); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, | ||||
|                                                  final String playbackQuality) { | ||||
|             return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality); | ||||
|         public void onLoadingFailed(String imageUri, View view, FailReason failReason) { | ||||
|             super.onLoadingFailed(imageUri, view, failReason); | ||||
|             resetNotification(); | ||||
|             updateNotification(-1); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onLoadingCancelled(String imageUri, View view) { | ||||
|             super.onLoadingCancelled(imageUri, view); | ||||
|             resetNotification(); | ||||
|             updateNotification(-1); | ||||
|         } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -539,8 +677,8 @@ public final class PopupVideoPlayer extends Service { | ||||
|         } | ||||
|  | ||||
|         private void updateMetadata() { | ||||
|             if (activityListener != null && currentInfo != null) { | ||||
|                 activityListener.onMetadataUpdate(currentInfo); | ||||
|             if (activityListener != null && getCurrentMetadata() != null) { | ||||
|                 activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -572,8 +710,9 @@ public final class PopupVideoPlayer extends Service { | ||||
|         public void onRepeatModeChanged(int i) { | ||||
|             super.onRepeatModeChanged(i); | ||||
|             setRepeatModeRemote(notRemoteView, i); | ||||
|             updateNotification(-1); | ||||
|             updatePlayback(); | ||||
|             resetNotification(); | ||||
|             updateNotification(-1); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -586,18 +725,17 @@ public final class PopupVideoPlayer extends Service { | ||||
|         // Playback Listener | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|         protected void onMetadataChanged(@NonNull final PlayQueueItem item, | ||||
|                                          @Nullable final StreamInfo info, | ||||
|                                          final int newPlayQueueIndex, | ||||
|                                          final boolean hasPlayQueueItemChanged) { | ||||
|             super.onMetadataChanged(item, info, newPlayQueueIndex, false); | ||||
|         protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { | ||||
|             super.onMetadataChanged(tag); | ||||
|             resetNotification(); | ||||
|             updateNotification(-1); | ||||
|             updateMetadata(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onPlaybackShutdown() { | ||||
|             super.onPlaybackShutdown(); | ||||
|             onClose(); | ||||
|             closePopup(); | ||||
|         } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -623,7 +761,7 @@ public final class PopupVideoPlayer extends Service { | ||||
|             if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); | ||||
|             switch (intent.getAction()) { | ||||
|                 case ACTION_CLOSE: | ||||
|                     onClose(); | ||||
|                     closePopup(); | ||||
|                     break; | ||||
|                 case ACTION_PLAY_PAUSE: | ||||
|                     onPlayPause(); | ||||
| @@ -653,49 +791,70 @@ public final class PopupVideoPlayer extends Service { | ||||
|         @Override | ||||
|         public void onBlocked() { | ||||
|             super.onBlocked(); | ||||
|             resetNotification(); | ||||
|             updateNotification(R.drawable.ic_play_arrow_white); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onPlaying() { | ||||
|             super.onPlaying(); | ||||
|             updateNotification(R.drawable.ic_pause_white); | ||||
|             videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); | ||||
|             lockManager.acquireWifiAndCpu(); | ||||
|  | ||||
|             updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); | ||||
|  | ||||
|             resetNotification(); | ||||
|             updateNotification(R.drawable.ic_pause_white); | ||||
|  | ||||
|             videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); | ||||
|             hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); | ||||
|  | ||||
|             // Check for new version | ||||
|             //new CheckForNewAppVersionTask().execute(); | ||||
|             startForeground(NOTIFICATION_ID, notBuilder.build()); | ||||
|             lockManager.acquireWifiAndCpu(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onBuffering() { | ||||
|             super.onBuffering(); | ||||
|             resetNotification(); | ||||
|             updateNotification(R.drawable.ic_play_arrow_white); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onPaused() { | ||||
|             super.onPaused(); | ||||
|  | ||||
|             updateWindowFlags(IDLE_WINDOW_FLAGS); | ||||
|  | ||||
|             resetNotification(); | ||||
|             updateNotification(R.drawable.ic_play_arrow_white); | ||||
|  | ||||
|             videoPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_white); | ||||
|             lockManager.releaseWifiAndCpu(); | ||||
|  | ||||
|             stopForeground(false); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onPausedSeek() { | ||||
|             super.onPausedSeek(); | ||||
|             videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); | ||||
|             resetNotification(); | ||||
|             updateNotification(R.drawable.ic_play_arrow_white); | ||||
|  | ||||
|             videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onCompleted() { | ||||
|             super.onCompleted(); | ||||
|  | ||||
|             updateWindowFlags(IDLE_WINDOW_FLAGS); | ||||
|  | ||||
|             resetNotification(); | ||||
|             updateNotification(R.drawable.ic_replay_white); | ||||
|  | ||||
|             videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white); | ||||
|             lockManager.releaseWifiAndCpu(); | ||||
|  | ||||
|             stopForeground(false); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -713,16 +872,15 @@ public final class PopupVideoPlayer extends Service { | ||||
|             super.hideControlsAndButton(duration, delay, videoPlayPause); | ||||
|         } | ||||
|  | ||||
|  | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
|         // Utils | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|         /*package-private*/ void enableVideoRenderer(final boolean enable) { | ||||
|             final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); | ||||
|             if (trackSelector != null && videoRendererIndex != RENDERER_UNAVAILABLE) { | ||||
|                 trackSelector.setRendererDisabled(videoRendererIndex, !enable); | ||||
|             if (videoRendererIndex != RENDERER_UNAVAILABLE) { | ||||
|                 trackSelector.setParameters(trackSelector.buildUponParameters() | ||||
|                         .setRendererDisabled(videoRendererIndex, !enable)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -734,12 +892,15 @@ public final class PopupVideoPlayer extends Service { | ||||
|         public TextView getResizingIndicator() { | ||||
|             return resizingIndicator; | ||||
|         } | ||||
|  | ||||
|         public View getClosingOverlayView() { | ||||
|             return closingOverlayView; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { | ||||
|     private class PopupWindowGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { | ||||
|         private int initialPopupX, initialPopupY; | ||||
|         private boolean isMoving; | ||||
|  | ||||
|         private boolean isResizing; | ||||
|  | ||||
|         @Override | ||||
| @@ -775,10 +936,15 @@ public final class PopupVideoPlayer extends Service { | ||||
|         @Override | ||||
|         public boolean onDown(MotionEvent e) { | ||||
|             if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); | ||||
|             initialPopupX = windowLayoutParams.x; | ||||
|             initialPopupY = windowLayoutParams.y; | ||||
|             popupWidth = windowLayoutParams.width; | ||||
|             popupHeight = windowLayoutParams.height; | ||||
|  | ||||
|             // Fix popup position when the user touch it, it may have the wrong one | ||||
|             // because the soft input is visible (the draggable area is currently resized). | ||||
|             checkPopupPositionBounds(closeOverlayView.getWidth(), closeOverlayView.getHeight()); | ||||
|  | ||||
|             initialPopupX = popupLayoutParams.x; | ||||
|             initialPopupY = popupLayoutParams.y; | ||||
|             popupWidth = popupLayoutParams.width; | ||||
|             popupHeight = popupLayoutParams.height; | ||||
|             return super.onDown(e); | ||||
|         } | ||||
|  | ||||
| @@ -786,20 +952,22 @@ public final class PopupVideoPlayer extends Service { | ||||
|         public void onLongPress(MotionEvent e) { | ||||
|             if (DEBUG) Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); | ||||
|             updateScreenSize(); | ||||
|             checkPositionBounds(); | ||||
|             checkPopupPositionBounds(); | ||||
|             updatePopupSize((int) screenWidth, -1); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { | ||||
|             if (isResizing || playerImpl == null) return super.onScroll(e1, e2, distanceX, distanceY); | ||||
|         public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) { | ||||
|             if (isResizing || playerImpl == null) return super.onScroll(initialEvent, movingEvent, distanceX, distanceY); | ||||
|  | ||||
|             if (!isMoving) { | ||||
|                 animateView(closeOverlayButton, true, 200); | ||||
|             } | ||||
|  | ||||
|             if (playerImpl.getCurrentState() != BasePlayer.STATE_BUFFERING | ||||
|                     && (!isMoving || playerImpl.getControlsRoot().getAlpha() != 1f)) playerImpl.showControls(0); | ||||
|             isMoving = true; | ||||
|  | ||||
|             float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX); | ||||
|             float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY); | ||||
|             float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()), posX = (int) (initialPopupX + diffX); | ||||
|             float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()), posY = (int) (initialPopupY + diffY); | ||||
|  | ||||
|             if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth); | ||||
|             else if (posX < 0) posX = 0; | ||||
| @@ -807,26 +975,49 @@ public final class PopupVideoPlayer extends Service { | ||||
|             if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight); | ||||
|             else if (posY < 0) posY = 0; | ||||
|  | ||||
|             windowLayoutParams.x = (int) posX; | ||||
|             windowLayoutParams.y = (int) posY; | ||||
|             popupLayoutParams.x = (int) posX; | ||||
|             popupLayoutParams.y = (int) posY; | ||||
|  | ||||
|             final View closingOverlayView = playerImpl.getClosingOverlayView(); | ||||
|             if (isInsideClosingRadius(movingEvent)) { | ||||
|                 if (closingOverlayView.getVisibility() == View.GONE) { | ||||
|                     animateView(closingOverlayView, true, 250); | ||||
|                 } | ||||
|             } else { | ||||
|                 if (closingOverlayView.getVisibility() == View.VISIBLE) { | ||||
|                     animateView(closingOverlayView, false, 0); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             //noinspection PointlessBooleanExpression | ||||
|             if (DEBUG && false) Log.d(TAG, "PopupVideoPlayer.onScroll = " + | ||||
|                     ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + | ||||
|                     ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + | ||||
|                     ", distanceXy = [" + distanceX + ", " + distanceY + "]" + | ||||
|                     ", posXy = [" + posX + ", " + posY + "]" + | ||||
|                     ", popupWh = [" + popupWidth + " x " + popupHeight + "]"); | ||||
|             windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams); | ||||
|             if (DEBUG && false) { | ||||
|                 Log.d(TAG, "PopupVideoPlayer.onScroll = " + | ||||
|                         ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + ", e1.getX,Y = [" + initialEvent.getX() + ", " + initialEvent.getY() + "]" + | ||||
|                         ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "]" + | ||||
|                         ", distanceX,Y = [" + distanceX + ", " + distanceY + "]" + | ||||
|                         ", posX,Y = [" + posX + ", " + posY + "]" + | ||||
|                         ", popupW,H = [" + popupWidth + " x " + popupHeight + "]"); | ||||
|             } | ||||
|             windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         private void onScrollEnd() { | ||||
|         private void onScrollEnd(MotionEvent event) { | ||||
|             if (DEBUG) Log.d(TAG, "onScrollEnd() called"); | ||||
|             if (playerImpl == null) return; | ||||
|             if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { | ||||
|                 playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); | ||||
|             } | ||||
|  | ||||
|             if (isInsideClosingRadius(event)) { | ||||
|                 closePopup(); | ||||
|             } else { | ||||
|                 animateView(playerImpl.getClosingOverlayView(), false, 0); | ||||
|  | ||||
|                 if (!isPopupClosing) { | ||||
|                     animateView(closeOverlayButton, false, 200); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -836,14 +1027,11 @@ public final class PopupVideoPlayer extends Service { | ||||
|  | ||||
|             final float absVelocityX = Math.abs(velocityX); | ||||
|             final float absVelocityY = Math.abs(velocityY); | ||||
|             if (absVelocityX > shutdownFlingVelocity) { | ||||
|                 onClose(); | ||||
|                 return true; | ||||
|             } else if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { | ||||
|                 if (absVelocityX > tossFlingVelocity) windowLayoutParams.x = (int) velocityX; | ||||
|                 if (absVelocityY > tossFlingVelocity) windowLayoutParams.y = (int) velocityY; | ||||
|                 checkPositionBounds(); | ||||
|                 windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams); | ||||
|             if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { | ||||
|                 if (absVelocityX > tossFlingVelocity) popupLayoutParams.x = (int) velocityX; | ||||
|                 if (absVelocityY > tossFlingVelocity) popupLayoutParams.y = (int) velocityY; | ||||
|                 checkPopupPositionBounds(); | ||||
|                 windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); | ||||
|                 return true; | ||||
|             } | ||||
|             return false; | ||||
| @@ -851,7 +1039,7 @@ public final class PopupVideoPlayer extends Service { | ||||
|  | ||||
|         @Override | ||||
|         public boolean onTouch(View v, MotionEvent event) { | ||||
|             gestureDetector.onTouchEvent(event); | ||||
|             popupGestureDetector.onTouchEvent(event); | ||||
|             if (playerImpl == null) return false; | ||||
|             if (event.getPointerCount() == 2 && !isResizing) { | ||||
|                 if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); | ||||
| @@ -874,7 +1062,7 @@ public final class PopupVideoPlayer extends Service { | ||||
|                     Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "],  e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); | ||||
|                 if (isMoving) { | ||||
|                     isMoving = false; | ||||
|                     onScrollEnd(); | ||||
|                     onScrollEnd(event); | ||||
|                 } | ||||
|  | ||||
|                 if (isResizing) { | ||||
| @@ -882,7 +1070,10 @@ public final class PopupVideoPlayer extends Service { | ||||
|                     animateView(playerImpl.getResizingIndicator(), false, 100, 0); | ||||
|                     playerImpl.changeState(playerImpl.getCurrentState()); | ||||
|                 } | ||||
|                 savePositionAndSize(); | ||||
|  | ||||
|                 if (!isPopupClosing) { | ||||
|                     savePositionAndSize(); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             v.performClick(); | ||||
| @@ -898,13 +1089,13 @@ public final class PopupVideoPlayer extends Service { | ||||
|             final float diff = Math.abs(firstPointerX - secondPointerX); | ||||
|             if (firstPointerX > secondPointerX) { | ||||
|                 // second pointer is the anchor (the leftmost pointer) | ||||
|                 windowLayoutParams.x = (int) (event.getRawX() - diff); | ||||
|                 popupLayoutParams.x = (int) (event.getRawX() - diff); | ||||
|             } else { | ||||
|                 // first pointer is the anchor | ||||
|                 windowLayoutParams.x = (int) event.getRawX(); | ||||
|                 popupLayoutParams.x = (int) event.getRawX(); | ||||
|             } | ||||
|  | ||||
|             checkPositionBounds(); | ||||
|             checkPopupPositionBounds(); | ||||
|             updateScreenSize(); | ||||
|  | ||||
|             final int width = (int) Math.min(screenWidth, diff); | ||||
| @@ -912,5 +1103,29 @@ public final class PopupVideoPlayer extends Service { | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
|         // Utils | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|         private int distanceFromCloseButton(MotionEvent popupMotionEvent) { | ||||
|             final int closeOverlayButtonX = closeOverlayButton.getLeft() + closeOverlayButton.getWidth() / 2; | ||||
|             final int closeOverlayButtonY = closeOverlayButton.getTop() + closeOverlayButton.getHeight() / 2; | ||||
|  | ||||
|             float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); | ||||
|             float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); | ||||
|  | ||||
|             return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + Math.pow(closeOverlayButtonY - fingerY, 2)); | ||||
|         } | ||||
|  | ||||
|         private float getClosingRadius() { | ||||
|             final int buttonRadius = closeOverlayButton.getWidth() / 2; | ||||
|             // 20% wider than the button itself | ||||
|             return buttonRadius * 1.2f; | ||||
|         } | ||||
|  | ||||
|         private boolean isInsideClosingRadius(MotionEvent popupMotionEvent) { | ||||
|             return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -16,6 +16,7 @@ import android.util.Log; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageButton; | ||||
| import android.widget.LinearLayout; | ||||
| import android.widget.PopupMenu; | ||||
| @@ -187,6 +188,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | ||||
|                 this.player.getRepeatMode(), | ||||
|                 this.player.getPlaybackSpeed(), | ||||
|                 this.player.getPlaybackPitch(), | ||||
|                 this.player.getPlaybackSkipSilence(), | ||||
|                 null | ||||
|         ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|     } | ||||
| @@ -340,6 +342,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | ||||
|             return true; | ||||
|         }); | ||||
|  | ||||
|         final MenuItem share = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/3, | ||||
|                 Menu.NONE, R.string.share); | ||||
|         share.setOnMenuItemClickListener(menuItem -> { | ||||
|             shareUrl(item.getTitle(), item.getUrl()); | ||||
|             return true; | ||||
|         }); | ||||
|  | ||||
|         menu.show(); | ||||
|     } | ||||
|  | ||||
| @@ -459,13 +468,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | ||||
|  | ||||
|     private void openPlaybackParameterDialog() { | ||||
|         if (player == null) return; | ||||
|         PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), | ||||
|                 player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); | ||||
|         PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), | ||||
|                 player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { | ||||
|         if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch); | ||||
|     public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, | ||||
|                                            boolean playbackSkipSilence) { | ||||
|         if (player != null) { | ||||
|             player.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
| @@ -509,6 +521,18 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | ||||
|                 .show(getSupportFragmentManager(), getTag()); | ||||
|     } | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Share | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private void shareUrl(String subject, String url) { | ||||
|         Intent intent = new Intent(Intent.ACTION_SEND); | ||||
|         intent.setType("text/plain"); | ||||
|         intent.putExtra(Intent.EXTRA_SUBJECT, subject); | ||||
|         intent.putExtra(Intent.EXTRA_TEXT, url); | ||||
|         startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); | ||||
|     } | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Binding Service Listener | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
| @@ -539,6 +563,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | ||||
|         if (player != null) { | ||||
|             progressLiveSync.setClickable(!player.isLiveEdge()); | ||||
|         } | ||||
|  | ||||
|         // this will make shure progressCurrentTime has the same width as progressEndTime | ||||
|         final ViewGroup.LayoutParams endTimeParams = progressEndTime.getLayoutParams(); | ||||
|         final ViewGroup.LayoutParams currentTimeParams = progressCurrentTime.getLayoutParams(); | ||||
|         currentTimeParams.width = progressEndTime.getWidth(); | ||||
|         progressCurrentTime.setLayoutParams(currentTimeParams); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -29,7 +29,6 @@ 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.support.annotation.NonNull; | ||||
| @@ -47,11 +46,9 @@ 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.PlaybackParameters; | ||||
| import com.google.android.exoplayer2.Player; | ||||
| import com.google.android.exoplayer2.source.MediaSource; | ||||
| import com.google.android.exoplayer2.source.MergingMediaSource; | ||||
| import com.google.android.exoplayer2.source.TrackGroup; | ||||
| import com.google.android.exoplayer2.source.TrackGroupArray; | ||||
| import com.google.android.exoplayer2.text.CaptionStyleCompat; | ||||
| @@ -62,21 +59,17 @@ import com.google.android.exoplayer2.video.VideoListener; | ||||
|  | ||||
| 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.StreamType; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.player.resolver.MediaSourceTag; | ||||
| import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; | ||||
| import org.schabi.newpipe.util.AnimationUtils; | ||||
| 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; | ||||
| @@ -105,13 +98,12 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|     public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis | ||||
|     public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000;  // 2 Seconds | ||||
|  | ||||
|     private ArrayList<VideoStream> availableStreams; | ||||
|     private List<VideoStream> availableStreams; | ||||
|     private int selectedStreamIndex; | ||||
|  | ||||
|     protected String playbackQuality; | ||||
|  | ||||
|     protected boolean wasPlaying = false; | ||||
|  | ||||
|     @NonNull final private VideoPlaybackResolver resolver; | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Views | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -162,6 +154,7 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|     public VideoPlayer(String debugTag, Context context) { | ||||
|         super(context); | ||||
|         this.TAG = debugTag; | ||||
|         this.resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); | ||||
|     } | ||||
|  | ||||
|     public void setup(View rootView) { | ||||
| @@ -241,7 +234,8 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|  | ||||
|         // Setup audio session with onboard equalizer | ||||
|         if (Build.VERSION.SDK_INT >= 21) { | ||||
|             trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)); | ||||
|             trackSelector.setParameters(trackSelector.buildUponParameters() | ||||
|                     .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -297,8 +291,9 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|                 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); | ||||
|             if (textRendererIndex != RENDERER_UNAVAILABLE) { | ||||
|                 trackSelector.setParameters(trackSelector.buildUponParameters() | ||||
|                         .setRendererDisabled(textRendererIndex, true)); | ||||
|             } | ||||
|             return true; | ||||
|         }); | ||||
| @@ -310,68 +305,61 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|                     i + 1, Menu.NONE, captionLanguage); | ||||
|             captionItem.setOnMenuItemClickListener(menuItem -> { | ||||
|                 final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); | ||||
|                 if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { | ||||
|                 if (textRendererIndex != RENDERER_UNAVAILABLE) { | ||||
|                     trackSelector.setPreferredTextLanguage(captionLanguage); | ||||
|                     trackSelector.setRendererDisabled(textRendererIndex, false); | ||||
|                     trackSelector.setParameters(trackSelector.buildUponParameters() | ||||
|                             .setRendererDisabled(textRendererIndex, false)); | ||||
|                 } | ||||
|                 return true; | ||||
|             }); | ||||
|         } | ||||
|         captionPopupMenu.setOnDismissListener(this); | ||||
|     } | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Playback Listener | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     protected abstract int getDefaultResolutionIndex(final List<VideoStream> sortedVideos); | ||||
|  | ||||
|     protected abstract int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, final String playbackQuality); | ||||
|     private void updateStreamRelatedViews() { | ||||
|         if (getCurrentMetadata() == null) return; | ||||
|  | ||||
|         final MediaSourceTag tag = getCurrentMetadata(); | ||||
|         final StreamInfo metadata = tag.getMetadata(); | ||||
|  | ||||
|     protected void onMetadataChanged(@NonNull final PlayQueueItem item, | ||||
|                                      @Nullable final StreamInfo info, | ||||
|                                      final int newPlayQueueIndex, | ||||
|                                      final boolean hasPlayQueueItemChanged) { | ||||
|         qualityTextView.setVisibility(View.GONE); | ||||
|         playbackSpeedTextView.setVisibility(View.GONE); | ||||
|  | ||||
|         playbackEndTime.setVisibility(View.GONE); | ||||
|         playbackLiveSync.setVisibility(View.GONE); | ||||
|  | ||||
|         final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); | ||||
|  | ||||
|         switch (streamType) { | ||||
|         switch (metadata.getStreamType()) { | ||||
|             case AUDIO_STREAM: | ||||
|                 surfaceView.setVisibility(View.GONE); | ||||
|                 endScreen.setVisibility(View.VISIBLE); | ||||
|                 playbackEndTime.setVisibility(View.VISIBLE); | ||||
|                 break; | ||||
|  | ||||
|             case AUDIO_LIVE_STREAM: | ||||
|                 surfaceView.setVisibility(View.GONE); | ||||
|                 endScreen.setVisibility(View.VISIBLE); | ||||
|                 playbackLiveSync.setVisibility(View.VISIBLE); | ||||
|                 break; | ||||
|  | ||||
|             case LIVE_STREAM: | ||||
|                 surfaceView.setVisibility(View.VISIBLE); | ||||
|                 endScreen.setVisibility(View.GONE); | ||||
|                 playbackLiveSync.setVisibility(View.VISIBLE); | ||||
|                 break; | ||||
|  | ||||
|             case VIDEO_STREAM: | ||||
|                 if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) break; | ||||
|  | ||||
|                 final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, | ||||
|                         info.getVideoStreams(), info.getVideoOnlyStreams(), false); | ||||
|                 availableStreams = new ArrayList<>(videos); | ||||
|                 if (playbackQuality == null) { | ||||
|                     selectedStreamIndex = getDefaultResolutionIndex(videos); | ||||
|                 } else { | ||||
|                     selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality()); | ||||
|                 } | ||||
|                 if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size() == 0) | ||||
|                     break; | ||||
|  | ||||
|                 availableStreams = tag.getSortedAvailableVideoStreams(); | ||||
|                 selectedStreamIndex = tag.getSelectedVideoStreamIndex(); | ||||
|                 buildQualityMenu(); | ||||
|                 qualityTextView.setVisibility(View.VISIBLE); | ||||
|  | ||||
|                 qualityTextView.setVisibility(View.VISIBLE); | ||||
|                 surfaceView.setVisibility(View.VISIBLE); | ||||
|             default: | ||||
|                 endScreen.setVisibility(View.GONE); | ||||
|                 playbackEndTime.setVisibility(View.VISIBLE); | ||||
|                 break; | ||||
|         } | ||||
| @@ -379,69 +367,21 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         buildPlaybackSpeedMenu(); | ||||
|         playbackSpeedTextView.setVisibility(View.VISIBLE); | ||||
|     } | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Playback Listener | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     protected abstract VideoPlaybackResolver.QualityResolver getQualityResolver(); | ||||
|  | ||||
|     protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { | ||||
|         super.onMetadataChanged(tag); | ||||
|         updateStreamRelatedViews(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @Nullable | ||||
|     public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { | ||||
|         final MediaSource liveSource = super.sourceOf(item, info); | ||||
|         if (liveSource != null) return liveSource; | ||||
|  | ||||
|         List<MediaSource> mediaSources = new ArrayList<>(); | ||||
|  | ||||
|         // Create video stream source | ||||
|         final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, | ||||
|                 info.getVideoStreams(), info.getVideoOnlyStreams(), false); | ||||
|         final int index; | ||||
|         if (videos.isEmpty()) { | ||||
|             index = -1; | ||||
|         } else if (playbackQuality == null) { | ||||
|             index = getDefaultResolutionIndex(videos); | ||||
|         } else { | ||||
|             index = getOverrideResolutionIndex(videos, getPlaybackQuality()); | ||||
|         } | ||||
|         final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null; | ||||
|         if (video != null) { | ||||
|             final MediaSource streamSource = buildMediaSource(video.getUrl(), | ||||
|                     PlayerHelper.cacheKeyOf(info, video), | ||||
|                     MediaFormat.getSuffixById(video.getFormatId())); | ||||
|             mediaSources.add(streamSource); | ||||
|         } | ||||
|  | ||||
|         // Create optional audio stream source | ||||
|         final List<AudioStream> audioStreams = info.getAudioStreams(); | ||||
|         final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( | ||||
|                 ListHelper.getDefaultAudioFormat(context, audioStreams)); | ||||
|         // Use the audio stream if there is no video stream, or | ||||
|         // Merge with audio stream in case if video does not contain audio | ||||
|         if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { | ||||
|             final MediaSource audioSource = buildMediaSource(audio.getUrl(), | ||||
|                     PlayerHelper.cacheKeyOf(info, audio), | ||||
|                     MediaFormat.getSuffixById(audio.getFormatId())); | ||||
|             mediaSources.add(audioSource); | ||||
|         } | ||||
|  | ||||
|         // If there is no audio or video sources, then this media source cannot be played back | ||||
|         if (mediaSources.isEmpty()) return null; | ||||
|         // Below are auxiliary media sources | ||||
|  | ||||
|         // 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(context, subtitle)); | ||||
|             final MediaSource textSource = dataSource.getSampleMediaSourceFactory() | ||||
|                     .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); | ||||
|             mediaSources.add(textSource); | ||||
|         } | ||||
|  | ||||
|         if (mediaSources.size() == 1) { | ||||
|             return mediaSources.get(0); | ||||
|         } else { | ||||
|             return new MergingMediaSource(mediaSources.toArray( | ||||
|                     new MediaSource[mediaSources.size()])); | ||||
|         } | ||||
|         return resolver.resolve(info); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -460,7 +400,6 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) | ||||
|             playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); | ||||
|  | ||||
|         animateView(endScreen, false, 0); | ||||
|         loadingPanel.setBackgroundColor(Color.BLACK); | ||||
|         animateView(loadingPanel, true, 0); | ||||
|         animateView(surfaceForeground, true, 100); | ||||
| @@ -470,6 +409,8 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|     public void onPlaying() { | ||||
|         super.onPlaying(); | ||||
|  | ||||
|         updateStreamRelatedViews(); | ||||
|  | ||||
|         showAndAnimateControl(-1, true); | ||||
|  | ||||
|         playbackSeekBar.setEnabled(true); | ||||
| @@ -480,14 +421,12 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         loadingPanel.setVisibility(View.GONE); | ||||
|  | ||||
|         animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); | ||||
|         animateView(endScreen, false, 0); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBuffering() { | ||||
|         if (DEBUG) Log.d(TAG, "onBuffering() called"); | ||||
|         loadingPanel.setBackgroundColor(Color.TRANSPARENT); | ||||
|         animateView(loadingPanel, true, 500); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -552,8 +491,7 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|         final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT); | ||||
|  | ||||
|         if (captionTextView == null) return; | ||||
|         if (trackSelector == null || trackSelector.getCurrentMappedTrackInfo() == null || | ||||
|                 textRenderer == RENDERER_UNAVAILABLE) { | ||||
|         if (trackSelector.getCurrentMappedTrackInfo() == null || textRenderer == RENDERER_UNAVAILABLE) { | ||||
|             captionTextView.setVisibility(View.GONE); | ||||
|             return; | ||||
|         } | ||||
| @@ -575,8 +513,8 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|  | ||||
|         // Build UI | ||||
|         buildCaptionMenu(availableLanguages); | ||||
|         if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null || | ||||
|                 !availableLanguages.contains(preferredLanguage)) { | ||||
|         if (trackSelector.getParameters().getRendererDisabled(textRenderer) || | ||||
|                 preferredLanguage == null || !availableLanguages.contains(preferredLanguage)) { | ||||
|             captionTextView.setText(R.string.caption_none); | ||||
|         } else { | ||||
|             captionTextView.setText(preferredLanguage); | ||||
| @@ -905,11 +843,12 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public void setPlaybackQuality(final String quality) { | ||||
|         this.playbackQuality = quality; | ||||
|         this.resolver.setPlaybackQuality(quality); | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public String getPlaybackQuality() { | ||||
|         return playbackQuality; | ||||
|         return resolver.getPlaybackQuality(); | ||||
|     } | ||||
|  | ||||
|     public AspectRatioFrameLayout getAspectRatioFrameLayout() { | ||||
|   | ||||
| @@ -39,10 +39,13 @@ public class MediaSessionManager { | ||||
|         return MediaButtonReceiver.handleIntent(mediaSession, intent); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Should be called on player destruction to prevent leakage. | ||||
|      * */ | ||||
|     public void dispose() { | ||||
|         this.sessionConnector.setPlayer(null, null); | ||||
|         this.sessionConnector.setQueueNavigator(null); | ||||
|         this.mediaSession.setActive(false); | ||||
|         this.mediaSession.release(); | ||||
|     } | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -21,25 +21,34 @@ import static org.schabi.newpipe.player.BasePlayer.DEBUG; | ||||
| public class PlaybackParameterDialog extends DialogFragment { | ||||
|     @NonNull private static final String TAG = "PlaybackParameterDialog"; | ||||
|  | ||||
|     public static final double MINIMUM_PLAYBACK_VALUE = 0.25f; | ||||
|     // Minimum allowable range in ExoPlayer | ||||
|     public static final double MINIMUM_PLAYBACK_VALUE = 0.10f; | ||||
|     public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f; | ||||
|  | ||||
|     public static final char STEP_UP_SIGN = '+'; | ||||
|     public static final char STEP_DOWN_SIGN = '-'; | ||||
|     public static final double PLAYBACK_STEP_VALUE = 0.05f; | ||||
|  | ||||
|     public static final double NIGHTCORE_TEMPO = 1.20f; | ||||
|     public static final double NIGHTCORE_PITCH_LOWER = 1.15f; | ||||
|     public static final double NIGHTCORE_PITCH_UPPER = 1.25f; | ||||
|     public static final double STEP_ONE_PERCENT_VALUE = 0.01f; | ||||
|     public static final double STEP_FIVE_PERCENT_VALUE = 0.05f; | ||||
|     public static final double STEP_TEN_PERCENT_VALUE = 0.10f; | ||||
|     public static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f; | ||||
|     public static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f; | ||||
|  | ||||
|     public static final double DEFAULT_TEMPO = 1.00f; | ||||
|     public static final double DEFAULT_PITCH = 1.00f; | ||||
|     public static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE; | ||||
|     public static final boolean DEFAULT_SKIP_SILENCE = false; | ||||
|  | ||||
|     @NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; | ||||
|     @NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; | ||||
|  | ||||
|     @NonNull private static final String TEMPO_KEY = "tempo_key"; | ||||
|     @NonNull private static final String PITCH_KEY = "pitch_key"; | ||||
|     @NonNull private static final String STEP_SIZE_KEY = "step_size_key"; | ||||
|  | ||||
|     public interface Callback { | ||||
|         void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch); | ||||
|         void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, | ||||
|                                         final boolean playbackSkipSilence); | ||||
|     } | ||||
|  | ||||
|     @Nullable private Callback callback; | ||||
| @@ -50,6 +59,11 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|  | ||||
|     private double initialTempo = DEFAULT_TEMPO; | ||||
|     private double initialPitch = DEFAULT_PITCH; | ||||
|     private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE; | ||||
|  | ||||
|     private double tempo = DEFAULT_TEMPO; | ||||
|     private double pitch = DEFAULT_PITCH; | ||||
|     private double stepSize = DEFAULT_STEP; | ||||
|  | ||||
|     @Nullable private SeekBar tempoSlider; | ||||
|     @Nullable private TextView tempoMinimumText; | ||||
| @@ -65,16 +79,26 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|     @Nullable private TextView pitchStepDownText; | ||||
|     @Nullable private TextView pitchStepUpText; | ||||
|  | ||||
|     @Nullable private CheckBox unhookingCheckbox; | ||||
|     @Nullable private TextView stepSizeOnePercentText; | ||||
|     @Nullable private TextView stepSizeFivePercentText; | ||||
|     @Nullable private TextView stepSizeTenPercentText; | ||||
|     @Nullable private TextView stepSizeTwentyFivePercentText; | ||||
|     @Nullable private TextView stepSizeOneHundredPercentText; | ||||
|  | ||||
|     @Nullable private TextView nightCorePresetText; | ||||
|     @Nullable private TextView resetPresetText; | ||||
|     @Nullable private CheckBox unhookingCheckbox; | ||||
|     @Nullable private CheckBox skipSilenceCheckbox; | ||||
|  | ||||
|     public static PlaybackParameterDialog newInstance(final double playbackTempo, | ||||
|                                                       final double playbackPitch) { | ||||
|                                                       final double playbackPitch, | ||||
|                                                       final boolean playbackSkipSilence) { | ||||
|         PlaybackParameterDialog dialog = new PlaybackParameterDialog(); | ||||
|         dialog.initialTempo = playbackTempo; | ||||
|         dialog.initialPitch = playbackPitch; | ||||
|  | ||||
|         dialog.tempo = playbackTempo; | ||||
|         dialog.pitch = playbackPitch; | ||||
|  | ||||
|         dialog.initialSkipSilence = playbackSkipSilence; | ||||
|         return dialog; | ||||
|     } | ||||
|  | ||||
| @@ -98,6 +122,10 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|         if (savedInstanceState != null) { | ||||
|             initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); | ||||
|             initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH); | ||||
|  | ||||
|             tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO); | ||||
|             pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH); | ||||
|             stepSize = savedInstanceState.getDouble(STEP_SIZE_KEY, DEFAULT_STEP); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -106,6 +134,10 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|         super.onSaveInstanceState(outState); | ||||
|         outState.putDouble(INITIAL_TEMPO_KEY, initialTempo); | ||||
|         outState.putDouble(INITIAL_PITCH_KEY, initialPitch); | ||||
|  | ||||
|         outState.putDouble(TEMPO_KEY, getCurrentTempo()); | ||||
|         outState.putDouble(PITCH_KEY, getCurrentPitch()); | ||||
|         outState.putDouble(STEP_SIZE_KEY, getCurrentStepSize()); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -123,7 +155,9 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|                 .setView(view) | ||||
|                 .setCancelable(true) | ||||
|                 .setNegativeButton(R.string.cancel, (dialogInterface, i) -> | ||||
|                         setPlaybackParameters(initialTempo, initialPitch)) | ||||
|                         setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence)) | ||||
|                 .setNeutralButton(R.string.playback_reset, (dialogInterface, i) -> | ||||
|                         setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE)) | ||||
|                 .setPositiveButton(R.string.finish, (dialogInterface, i) -> | ||||
|                         setCurrentPlaybackParameters()); | ||||
|  | ||||
| @@ -136,9 +170,13 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|  | ||||
|     private void setupControlViews(@NonNull View rootView) { | ||||
|         setupHookingControl(rootView); | ||||
|         setupSkipSilenceControl(rootView); | ||||
|  | ||||
|         setupTempoControl(rootView); | ||||
|         setupPitchControl(rootView); | ||||
|         setupPresetControl(rootView); | ||||
|  | ||||
|         changeStepSize(stepSize); | ||||
|         setupStepSizeSelector(rootView); | ||||
|     } | ||||
|  | ||||
|     private void setupTempoControl(@NonNull View rootView) { | ||||
| @@ -150,31 +188,15 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|         tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); | ||||
|  | ||||
|         if (tempoCurrentText != null) | ||||
|             tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); | ||||
|             tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); | ||||
|         if (tempoMaximumText != null) | ||||
|             tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); | ||||
|         if (tempoMinimumText != null) | ||||
|             tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); | ||||
|  | ||||
|         if (tempoStepUpText != null) { | ||||
|             tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); | ||||
|             tempoStepUpText.setOnClickListener(view -> { | ||||
|                 onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE); | ||||
|                 setCurrentPlaybackParameters(); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (tempoStepDownText != null) { | ||||
|             tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); | ||||
|             tempoStepDownText.setOnClickListener(view -> { | ||||
|                 onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE); | ||||
|                 setCurrentPlaybackParameters(); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (tempoSlider != null) { | ||||
|             tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); | ||||
|             tempoSlider.setProgress(strategy.progressOf(initialTempo)); | ||||
|             tempoSlider.setProgress(strategy.progressOf(tempo)); | ||||
|             tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); | ||||
|         } | ||||
|     } | ||||
| @@ -188,31 +210,15 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|         pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); | ||||
|  | ||||
|         if (pitchCurrentText != null) | ||||
|             pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); | ||||
|             pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); | ||||
|         if (pitchMaximumText != null) | ||||
|             pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); | ||||
|         if (pitchMinimumText != null) | ||||
|             pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); | ||||
|  | ||||
|         if (pitchStepUpText != null) { | ||||
|             pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); | ||||
|             pitchStepUpText.setOnClickListener(view -> { | ||||
|                 onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE); | ||||
|                 setCurrentPlaybackParameters(); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (pitchStepDownText != null) { | ||||
|             pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); | ||||
|             pitchStepDownText.setOnClickListener(view -> { | ||||
|                 onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE); | ||||
|                 setCurrentPlaybackParameters(); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (pitchSlider != null) { | ||||
|             pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); | ||||
|             pitchSlider.setProgress(strategy.progressOf(initialPitch)); | ||||
|             pitchSlider.setProgress(strategy.progressOf(pitch)); | ||||
|             pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); | ||||
|         } | ||||
|     } | ||||
| @@ -220,7 +226,7 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|     private void setupHookingControl(@NonNull View rootView) { | ||||
|         unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); | ||||
|         if (unhookingCheckbox != null) { | ||||
|             unhookingCheckbox.setChecked(initialPitch != initialTempo); | ||||
|             unhookingCheckbox.setChecked(pitch != tempo); | ||||
|             unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { | ||||
|                 if (isChecked) return; | ||||
|                 // When unchecked, slide back to the minimum of current tempo or pitch | ||||
| @@ -231,24 +237,84 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void setupPresetControl(@NonNull View rootView) { | ||||
|         nightCorePresetText = rootView.findViewById(R.id.presetNightcore); | ||||
|         if (nightCorePresetText != null) { | ||||
|             nightCorePresetText.setOnClickListener(view -> { | ||||
|                 final double randomPitch = NIGHTCORE_PITCH_LOWER + | ||||
|                         Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); | ||||
|     private void setupSkipSilenceControl(@NonNull View rootView) { | ||||
|         skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox); | ||||
|         if (skipSilenceCheckbox != null) { | ||||
|             skipSilenceCheckbox.setChecked(initialSkipSilence); | ||||
|             skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> | ||||
|                     setCurrentPlaybackParameters()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|                 setTempoSlider(NIGHTCORE_TEMPO); | ||||
|                 setPitchSlider(randomPitch); | ||||
|     private void setupStepSizeSelector(@NonNull final View rootView) { | ||||
|         stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent); | ||||
|         stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent); | ||||
|         stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent); | ||||
|         stepSizeTwentyFivePercentText = rootView.findViewById(R.id.stepSizeTwentyFivePercent); | ||||
|         stepSizeOneHundredPercentText = rootView.findViewById(R.id.stepSizeOneHundredPercent); | ||||
|  | ||||
|         if (stepSizeOnePercentText != null) { | ||||
|             stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE)); | ||||
|             stepSizeOnePercentText.setOnClickListener(view -> | ||||
|                     changeStepSize(STEP_ONE_PERCENT_VALUE)); | ||||
|         } | ||||
|  | ||||
|         if (stepSizeFivePercentText != null) { | ||||
|             stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE)); | ||||
|             stepSizeFivePercentText.setOnClickListener(view -> | ||||
|                     changeStepSize(STEP_FIVE_PERCENT_VALUE)); | ||||
|         } | ||||
|  | ||||
|         if (stepSizeTenPercentText != null) { | ||||
|             stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE)); | ||||
|             stepSizeTenPercentText.setOnClickListener(view -> | ||||
|                     changeStepSize(STEP_TEN_PERCENT_VALUE)); | ||||
|         } | ||||
|  | ||||
|         if (stepSizeTwentyFivePercentText != null) { | ||||
|             stepSizeTwentyFivePercentText.setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE)); | ||||
|             stepSizeTwentyFivePercentText.setOnClickListener(view -> | ||||
|                     changeStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE)); | ||||
|         } | ||||
|  | ||||
|         if (stepSizeOneHundredPercentText != null) { | ||||
|             stepSizeOneHundredPercentText.setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE)); | ||||
|             stepSizeOneHundredPercentText.setOnClickListener(view -> | ||||
|                     changeStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void changeStepSize(final double stepSize) { | ||||
|         this.stepSize = stepSize; | ||||
|  | ||||
|         if (tempoStepUpText != null) { | ||||
|             tempoStepUpText.setText(getStepUpPercentString(stepSize)); | ||||
|             tempoStepUpText.setOnClickListener(view -> { | ||||
|                 onTempoSliderUpdated(getCurrentTempo() + stepSize); | ||||
|                 setCurrentPlaybackParameters(); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         resetPresetText = rootView.findViewById(R.id.presetReset); | ||||
|         if (resetPresetText != null) { | ||||
|             resetPresetText.setOnClickListener(view -> { | ||||
|                 setTempoSlider(DEFAULT_TEMPO); | ||||
|                 setPitchSlider(DEFAULT_PITCH); | ||||
|         if (tempoStepDownText != null) { | ||||
|             tempoStepDownText.setText(getStepDownPercentString(stepSize)); | ||||
|             tempoStepDownText.setOnClickListener(view -> { | ||||
|                 onTempoSliderUpdated(getCurrentTempo() - stepSize); | ||||
|                 setCurrentPlaybackParameters(); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (pitchStepUpText != null) { | ||||
|             pitchStepUpText.setText(getStepUpPercentString(stepSize)); | ||||
|             pitchStepUpText.setOnClickListener(view -> { | ||||
|                 onPitchSliderUpdated(getCurrentPitch() + stepSize); | ||||
|                 setCurrentPlaybackParameters(); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (pitchStepDownText != null) { | ||||
|             pitchStepDownText.setText(getStepDownPercentString(stepSize)); | ||||
|             pitchStepDownText.setOnClickListener(view -> { | ||||
|                 onPitchSliderUpdated(getCurrentPitch() - stepSize); | ||||
|                 setCurrentPlaybackParameters(); | ||||
|             }); | ||||
|         } | ||||
| @@ -342,10 +408,11 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void setCurrentPlaybackParameters() { | ||||
|         setPlaybackParameters(getCurrentTempo(), getCurrentPitch()); | ||||
|         setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence()); | ||||
|     } | ||||
|  | ||||
|     private void setPlaybackParameters(final double tempo, final double pitch) { | ||||
|     private void setPlaybackParameters(final double tempo, final double pitch, | ||||
|                                        final boolean skipSilence) { | ||||
|         if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { | ||||
|             if (DEBUG) Log.d(TAG, "Setting playback parameters to " + | ||||
|                     "tempo=[" + tempo + "], " + | ||||
| @@ -353,27 +420,40 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|  | ||||
|             tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); | ||||
|             pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); | ||||
|             callback.onPlaybackParameterChanged((float) tempo, (float) pitch); | ||||
|             callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private double getCurrentTempo() { | ||||
|         return tempoSlider == null ? initialTempo : strategy.valueOf( | ||||
|         return tempoSlider == null ? tempo : strategy.valueOf( | ||||
|                 tempoSlider.getProgress()); | ||||
|     } | ||||
|  | ||||
|     private double getCurrentPitch() { | ||||
|         return pitchSlider == null ? initialPitch : strategy.valueOf( | ||||
|         return pitchSlider == null ? pitch : strategy.valueOf( | ||||
|                 pitchSlider.getProgress()); | ||||
|     } | ||||
|  | ||||
|     private double getCurrentStepSize() { | ||||
|         return stepSize; | ||||
|     } | ||||
|  | ||||
|     private boolean getCurrentSkipSilence() { | ||||
|         return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked(); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     private static String getStepUpPercentString(final double percent) { | ||||
|         return STEP_UP_SIGN + PlayerHelper.formatPitch(percent); | ||||
|         return STEP_UP_SIGN + getPercentString(percent); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     private static String getStepDownPercentString(final double percent) { | ||||
|         return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent); | ||||
|         return STEP_DOWN_SIGN + getPercentString(percent); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     private static String getPercentString(final double percent) { | ||||
|         return PlayerHelper.formatPitch(percent); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.os.Build; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.IntDef; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.view.accessibility.CaptioningManager; | ||||
| @@ -28,6 +29,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||
|  | ||||
| import java.lang.annotation.Retention; | ||||
| import java.text.DecimalFormat; | ||||
| import java.text.NumberFormat; | ||||
| import java.util.ArrayList; | ||||
| @@ -42,6 +44,8 @@ import java.util.concurrent.TimeUnit; | ||||
| 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_ZOOM; | ||||
| import static java.lang.annotation.RetentionPolicy.SOURCE; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*; | ||||
|  | ||||
| public class PlayerHelper { | ||||
|     private PlayerHelper() {} | ||||
| @@ -51,6 +55,14 @@ public class PlayerHelper { | ||||
|     private static final NumberFormat speedFormatter = new DecimalFormat("0.##x"); | ||||
|     private static final NumberFormat pitchFormatter = new DecimalFormat("##%"); | ||||
|  | ||||
|     @Retention(SOURCE) | ||||
|     @IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND, | ||||
|             MINIMIZE_ON_EXIT_MODE_POPUP}) | ||||
|     public @interface MinimizeMode { | ||||
|         int MINIMIZE_ON_EXIT_MODE_NONE = 0; | ||||
|         int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1; | ||||
|         int MINIMIZE_ON_EXIT_MODE_POPUP = 2; | ||||
|     } | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Exposed helpers | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
| @@ -173,6 +185,22 @@ public class PlayerHelper { | ||||
|         return isAutoQueueEnabled(context, false); | ||||
|     } | ||||
|  | ||||
|     @MinimizeMode | ||||
|     public static int getMinimizeOnExitAction(@NonNull final Context context) { | ||||
|         final String defaultAction = context.getString(R.string.minimize_on_exit_none_key); | ||||
|         final String popupAction = context.getString(R.string.minimize_on_exit_popup_key); | ||||
|         final String backgroundAction = context.getString(R.string.minimize_on_exit_background_key); | ||||
|  | ||||
|         final String action = getMinimizeOnExitAction(context, defaultAction); | ||||
|         if (action.equals(popupAction)) { | ||||
|             return MINIMIZE_ON_EXIT_MODE_POPUP; | ||||
|         } else if (action.equals(backgroundAction)) { | ||||
|             return MINIMIZE_ON_EXIT_MODE_BACKGROUND; | ||||
|         } else { | ||||
|             return MINIMIZE_ON_EXIT_MODE_NONE; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public static SeekParameters getSeekParameters(@NonNull final Context context) { | ||||
|         return isUsingInexactSeek(context, false) ? | ||||
| @@ -213,7 +241,6 @@ public class PlayerHelper { | ||||
|     public static TrackSelection.Factory getQualitySelector(@NonNull final Context context, | ||||
|                                                             @NonNull final BandwidthMeter meter) { | ||||
|         return new AdaptiveTrackSelection.Factory(meter, | ||||
|                 AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE, | ||||
|                 /*bufferDurationRequiredForQualityIncrease=*/1000, | ||||
|                 AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, | ||||
|                 AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, | ||||
| @@ -224,10 +251,6 @@ public class PlayerHelper { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public static int getShutdownFlingVelocity(@NonNull final Context context) { | ||||
|         return 10000; | ||||
|     } | ||||
|  | ||||
|     public static int getTossFlingVelocity(@NonNull final Context context) { | ||||
|         return 2500; | ||||
|     } | ||||
| @@ -249,7 +272,6 @@ public class PlayerHelper { | ||||
|      * System font scaling: | ||||
|      * Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f | ||||
|      * */ | ||||
|     @NonNull | ||||
|     public static float getCaptionScale(@NonNull final Context context) { | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1f; | ||||
|  | ||||
| @@ -322,4 +344,10 @@ public class PlayerHelper { | ||||
|             return sp.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static String getMinimizeOnExitAction(@NonNull final Context context, | ||||
|                                                   final String key) { | ||||
|         return getPreferences(context).getString(context.getString(R.string.minimize_on_exit_key), | ||||
|                 key); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import android.support.annotation.NonNull; | ||||
| import android.util.Log; | ||||
|  | ||||
| import com.google.android.exoplayer2.ExoPlayer; | ||||
| import com.google.android.exoplayer2.source.BaseMediaSource; | ||||
| import com.google.android.exoplayer2.source.MediaPeriod; | ||||
| import com.google.android.exoplayer2.upstream.Allocator; | ||||
|  | ||||
| @@ -11,7 +12,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| public class FailedMediaSource implements ManagedMediaSource { | ||||
| public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource { | ||||
|     private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); | ||||
|  | ||||
|     public static class FailedMediaSourceException extends Exception { | ||||
| @@ -72,11 +73,6 @@ public class FailedMediaSource implements ManagedMediaSource { | ||||
|         return System.currentTimeMillis() >= retryTimestamp; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { | ||||
|         Log.e(TAG, "Loading failed source: ", error); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void maybeThrowSourceInfoRefreshError() throws IOException { | ||||
|         throw new IOException(error); | ||||
| @@ -90,8 +86,14 @@ public class FailedMediaSource implements ManagedMediaSource { | ||||
|     @Override | ||||
|     public void releasePeriod(MediaPeriod mediaPeriod) {} | ||||
|  | ||||
|  | ||||
|     @Override | ||||
|     public void releaseSource() {} | ||||
|     protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { | ||||
|         Log.e(TAG, "Loading failed source: ", error); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void releaseSourceInternal() {} | ||||
|  | ||||
|     @Override | ||||
|     public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| package org.schabi.newpipe.player.mediasource; | ||||
|  | ||||
| import android.os.Handler; | ||||
| import android.support.annotation.NonNull; | ||||
|  | ||||
| import com.google.android.exoplayer2.ExoPlayer; | ||||
| import com.google.android.exoplayer2.source.MediaPeriod; | ||||
| import com.google.android.exoplayer2.source.MediaSource; | ||||
| import com.google.android.exoplayer2.source.MediaSourceEventListener; | ||||
| import com.google.android.exoplayer2.upstream.Allocator; | ||||
|  | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| @@ -34,7 +36,8 @@ public class LoadedMediaSource implements ManagedMediaSource { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { | ||||
|     public void prepareSource(ExoPlayer player, boolean isTopLevelSource, | ||||
|                               SourceInfoRefreshListener listener) { | ||||
|         source.prepareSource(player, isTopLevelSource, listener); | ||||
|     } | ||||
|  | ||||
| @@ -54,8 +57,18 @@ public class LoadedMediaSource implements ManagedMediaSource { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void releaseSource() { | ||||
|         source.releaseSource(); | ||||
|     public void releaseSource(SourceInfoRefreshListener listener) { | ||||
|         source.releaseSource(listener); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void addEventListener(Handler handler, MediaSourceEventListener eventListener) { | ||||
|         source.addEventListener(handler, eventListener); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void removeEventListener(MediaSourceEventListener eventListener) { | ||||
|         source.removeEventListener(eventListener); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -3,14 +3,14 @@ package org.schabi.newpipe.player.mediasource; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
|  | ||||
| import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; | ||||
| import com.google.android.exoplayer2.source.ConcatenatingMediaSource; | ||||
| import com.google.android.exoplayer2.source.ShuffleOrder; | ||||
|  | ||||
| public class ManagedMediaSourcePlaylist { | ||||
|     @NonNull private final DynamicConcatenatingMediaSource internalSource; | ||||
|     @NonNull private final ConcatenatingMediaSource internalSource; | ||||
|  | ||||
|     public ManagedMediaSourcePlaylist() { | ||||
|         internalSource = new DynamicConcatenatingMediaSource(/*isPlaylistAtomic=*/false, | ||||
|         internalSource = new ConcatenatingMediaSource(/*isPlaylistAtomic=*/false, | ||||
|                 new ShuffleOrder.UnshuffledShuffleOrder(0)); | ||||
|     } | ||||
|  | ||||
| @@ -32,12 +32,8 @@ public class ManagedMediaSourcePlaylist { | ||||
|                 null : (ManagedMediaSource) internalSource.getMediaSource(index); | ||||
|     } | ||||
|  | ||||
|     public void dispose() { | ||||
|         internalSource.releaseSource(); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public DynamicConcatenatingMediaSource getParentMediaSource() { | ||||
|     public ConcatenatingMediaSource getParentMediaSource() { | ||||
|         return internalSource; | ||||
|     } | ||||
|  | ||||
| @@ -46,7 +42,7 @@ public class ManagedMediaSourcePlaylist { | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     /** | ||||
|      * Expands the {@link DynamicConcatenatingMediaSource} by appending it with a | ||||
|      * Expands the {@link ConcatenatingMediaSource} by appending it with a | ||||
|      * {@link PlaceholderMediaSource}. | ||||
|      * | ||||
|      * @see #append(ManagedMediaSource) | ||||
| @@ -56,17 +52,17 @@ public class ManagedMediaSourcePlaylist { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Appends a {@link ManagedMediaSource} to the end of {@link DynamicConcatenatingMediaSource}. | ||||
|      * @see DynamicConcatenatingMediaSource#addMediaSource | ||||
|      * Appends a {@link ManagedMediaSource} to the end of {@link ConcatenatingMediaSource}. | ||||
|      * @see ConcatenatingMediaSource#addMediaSource | ||||
|      * */ | ||||
|     public synchronized void append(@NonNull final ManagedMediaSource source) { | ||||
|         internalSource.addMediaSource(source); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Removes a {@link ManagedMediaSource} from {@link DynamicConcatenatingMediaSource} | ||||
|      * Removes a {@link ManagedMediaSource} from {@link ConcatenatingMediaSource} | ||||
|      * at the given index. If this index is out of bound, then the removal is ignored. | ||||
|      * @see DynamicConcatenatingMediaSource#removeMediaSource(int) | ||||
|      * @see ConcatenatingMediaSource#removeMediaSource(int) | ||||
|      * */ | ||||
|     public synchronized void remove(final int index) { | ||||
|         if (index < 0 || index > internalSource.getSize()) return; | ||||
| @@ -75,10 +71,10 @@ public class ManagedMediaSourcePlaylist { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Moves a {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource} | ||||
|      * Moves a {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} | ||||
|      * from the given source index to the target index. If either index is out of bound, | ||||
|      * then the call is ignored. | ||||
|      * @see DynamicConcatenatingMediaSource#moveMediaSource(int, int) | ||||
|      * @see ConcatenatingMediaSource#moveMediaSource(int, int) | ||||
|      * */ | ||||
|     public synchronized void move(final int source, final int target) { | ||||
|         if (source < 0 || target < 0) return; | ||||
| @@ -99,7 +95,7 @@ public class ManagedMediaSourcePlaylist { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource} | ||||
|      * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} | ||||
|      * at the given index with a given {@link ManagedMediaSource}. | ||||
|      * @see #update(int, ManagedMediaSource, Runnable) | ||||
|      * */ | ||||
| @@ -108,11 +104,11 @@ public class ManagedMediaSourcePlaylist { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource} | ||||
|      * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} | ||||
|      * at the given index with a given {@link ManagedMediaSource}. If the index is out of bound, | ||||
|      * then the replacement is ignored. | ||||
|      * @see DynamicConcatenatingMediaSource#addMediaSource | ||||
|      * @see DynamicConcatenatingMediaSource#removeMediaSource(int, Runnable) | ||||
|      * @see ConcatenatingMediaSource#addMediaSource | ||||
|      * @see ConcatenatingMediaSource#removeMediaSource(int, Runnable) | ||||
|      * */ | ||||
|     public synchronized void update(final int index, @NonNull final ManagedMediaSource source, | ||||
|                                     @Nullable final Runnable finalizingAction) { | ||||
|   | ||||
| @@ -3,20 +3,19 @@ package org.schabi.newpipe.player.mediasource; | ||||
| import android.support.annotation.NonNull; | ||||
|  | ||||
| import com.google.android.exoplayer2.ExoPlayer; | ||||
| import com.google.android.exoplayer2.source.BaseMediaSource; | ||||
| import com.google.android.exoplayer2.source.MediaPeriod; | ||||
| import com.google.android.exoplayer2.upstream.Allocator; | ||||
|  | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| public class PlaceholderMediaSource implements ManagedMediaSource { | ||||
| public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource { | ||||
|     // Do nothing, so this will stall the playback | ||||
|     @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {} | ||||
|     @Override public void maybeThrowSourceInfoRefreshError() throws IOException {} | ||||
|     @Override public void maybeThrowSourceInfoRefreshError() {} | ||||
|     @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; } | ||||
|     @Override public void releasePeriod(MediaPeriod mediaPeriod) {} | ||||
|     @Override public void releaseSource() {} | ||||
|     @Override protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {} | ||||
|     @Override protected void releaseSourceInternal() {} | ||||
|  | ||||
|     @Override | ||||
|     public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, | ||||
|   | ||||
| @@ -5,12 +5,10 @@ import android.support.annotation.Nullable; | ||||
| import android.support.v4.util.ArraySet; | ||||
| import android.util.Log; | ||||
|  | ||||
| import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; | ||||
| import com.google.android.exoplayer2.source.MediaSource; | ||||
|  | ||||
| import org.reactivestreams.Subscriber; | ||||
| import org.reactivestreams.Subscription; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.player.mediasource.FailedMediaSource; | ||||
| import org.schabi.newpipe.player.mediasource.LoadedMediaSource; | ||||
| import org.schabi.newpipe.player.mediasource.ManagedMediaSource; | ||||
| @@ -24,10 +22,8 @@ import org.schabi.newpipe.player.playqueue.events.RemoveEvent; | ||||
| import org.schabi.newpipe.player.playqueue.events.ReorderEvent; | ||||
| import org.schabi.newpipe.util.ServiceHelper; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import java.util.concurrent.atomic.AtomicBoolean; | ||||
| @@ -37,8 +33,6 @@ import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import io.reactivex.disposables.SerialDisposable; | ||||
| import io.reactivex.functions.Consumer; | ||||
| import io.reactivex.internal.subscriptions.EmptySubscription; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import io.reactivex.subjects.PublishSubject; | ||||
| @@ -104,7 +98,6 @@ public class MediaSourceManager { | ||||
|     private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; | ||||
|     @NonNull private final CompositeDisposable loaderReactor; | ||||
|     @NonNull private final Set<PlayQueueItem> loadingItems; | ||||
|     @NonNull private final SerialDisposable syncReactor; | ||||
|  | ||||
|     @NonNull private final AtomicBoolean isBlocked; | ||||
|  | ||||
| @@ -144,7 +137,6 @@ public class MediaSourceManager { | ||||
|  | ||||
|         this.playQueueReactor = EmptySubscription.INSTANCE; | ||||
|         this.loaderReactor = new CompositeDisposable(); | ||||
|         this.syncReactor = new SerialDisposable(); | ||||
|  | ||||
|         this.isBlocked = new AtomicBoolean(false); | ||||
|  | ||||
| @@ -171,8 +163,6 @@ public class MediaSourceManager { | ||||
|  | ||||
|         playQueueReactor.cancel(); | ||||
|         loaderReactor.dispose(); | ||||
|         syncReactor.dispose(); | ||||
|         playlist.dispose(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -311,21 +301,7 @@ public class MediaSourceManager { | ||||
|         final PlayQueueItem currentItem = playQueue.getItem(); | ||||
|         if (isBlocked.get() || currentItem == null) return; | ||||
|  | ||||
|         final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); | ||||
|         final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null); | ||||
|  | ||||
|         final Disposable sync = currentItem.getStream() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(onSuccess, onError); | ||||
|         syncReactor.set(sync); | ||||
|     } | ||||
|  | ||||
|     private void syncInternal(@NonNull final PlayQueueItem item, | ||||
|                               @Nullable final StreamInfo info) { | ||||
|         // Ensure the current item is up to date with the play queue | ||||
|         if (playQueue.getItem() == item) { | ||||
|             playbackListener.onPlaybackSynchronize(item, info); | ||||
|         } | ||||
|         playbackListener.onPlaybackSynchronize(currentItem); | ||||
|     } | ||||
|  | ||||
|     private synchronized void maybeSynchronizePlayer() { | ||||
| @@ -424,7 +400,8 @@ public class MediaSourceManager { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource} | ||||
|      * Checks if the corresponding MediaSource in | ||||
|      * {@link com.google.android.exoplayer2.source.ConcatenatingMediaSource} | ||||
|      * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback | ||||
|      * readiness or playlist desynchronization. | ||||
|      * <br><br> | ||||
| @@ -481,8 +458,6 @@ public class MediaSourceManager { | ||||
|  | ||||
|     private void resetSources() { | ||||
|         if (DEBUG) Log.d(TAG, "resetSources() called."); | ||||
|  | ||||
|         playlist.dispose(); | ||||
|         playlist = new ManagedMediaSourcePlaylist(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -45,7 +45,7 @@ public interface PlaybackListener { | ||||
|      * | ||||
|      * May be called anytime at any amount once unblock is called. | ||||
|      * */ | ||||
|     void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); | ||||
|     void onPlaybackSynchronize(@NonNull final PlayQueueItem item); | ||||
|  | ||||
|     /** | ||||
|      * Requests the listener to resolve a stream info into a media source | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import android.util.Log; | ||||
|  | ||||
| import org.reactivestreams.Subscriber; | ||||
| import org.reactivestreams.Subscription; | ||||
| import org.schabi.newpipe.BuildConfig; | ||||
| import org.schabi.newpipe.player.playqueue.events.AppendEvent; | ||||
| import org.schabi.newpipe.player.playqueue.events.ErrorEvent; | ||||
| import org.schabi.newpipe.player.playqueue.events.InitEvent; | ||||
| @@ -41,7 +42,7 @@ import io.reactivex.subjects.BehaviorSubject; | ||||
| public abstract class PlayQueue implements Serializable { | ||||
|     private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode()); | ||||
|  | ||||
|     public static final boolean DEBUG = true; | ||||
|     public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); | ||||
|  | ||||
|     private ArrayList<PlayQueueItem> backup; | ||||
|     private ArrayList<PlayQueueItem> streams; | ||||
|   | ||||
| @@ -0,0 +1,41 @@ | ||||
| package org.schabi.newpipe.player.resolver; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
|  | ||||
| import com.google.android.exoplayer2.source.MediaSource; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.MediaFormat; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.player.helper.PlayerDataSource; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
|  | ||||
| public class AudioPlaybackResolver implements PlaybackResolver { | ||||
|  | ||||
|     @NonNull private final Context context; | ||||
|     @NonNull private final PlayerDataSource dataSource; | ||||
|  | ||||
|     public AudioPlaybackResolver(@NonNull final Context context, | ||||
|                                  @NonNull final PlayerDataSource dataSource) { | ||||
|         this.context = context; | ||||
|         this.dataSource = dataSource; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @Nullable | ||||
|     public MediaSource resolve(@NonNull StreamInfo info) { | ||||
|         final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); | ||||
|         if (liveSource != null) return liveSource; | ||||
|  | ||||
|         final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); | ||||
|         if (index < 0 || index >= info.getAudioStreams().size()) return null; | ||||
|  | ||||
|         final AudioStream audio = info.getAudioStreams().get(index); | ||||
|         final MediaSourceTag tag = new MediaSourceTag(info); | ||||
|         return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), | ||||
|                 MediaFormat.getSuffixById(audio.getFormatId()), tag); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,51 @@ | ||||
| package org.schabi.newpipe.player.resolver; | ||||
|  | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| public class MediaSourceTag implements Serializable { | ||||
|     @NonNull private final StreamInfo metadata; | ||||
|  | ||||
|     @NonNull private final List<VideoStream> sortedAvailableVideoStreams; | ||||
|     private final int selectedVideoStreamIndex; | ||||
|  | ||||
|     public MediaSourceTag(@NonNull final StreamInfo metadata, | ||||
|                           @NonNull final List<VideoStream> sortedAvailableVideoStreams, | ||||
|                           final int selectedVideoStreamIndex) { | ||||
|         this.metadata = metadata; | ||||
|         this.sortedAvailableVideoStreams = sortedAvailableVideoStreams; | ||||
|         this.selectedVideoStreamIndex = selectedVideoStreamIndex; | ||||
|     } | ||||
|  | ||||
|     public MediaSourceTag(@NonNull final StreamInfo metadata) { | ||||
|         this(metadata, Collections.emptyList(), /*indexNotAvailable=*/-1); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public StreamInfo getMetadata() { | ||||
|         return metadata; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public List<VideoStream> getSortedAvailableVideoStreams() { | ||||
|         return sortedAvailableVideoStreams; | ||||
|     } | ||||
|  | ||||
|     public int getSelectedVideoStreamIndex() { | ||||
|         return selectedVideoStreamIndex; | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public VideoStream getSelectedVideoStream() { | ||||
|         return selectedVideoStreamIndex < 0 || | ||||
|                 selectedVideoStreamIndex >= sortedAvailableVideoStreams.size() ? null : | ||||
|                 sortedAvailableVideoStreams.get(selectedVideoStreamIndex); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,84 @@ | ||||
| package org.schabi.newpipe.player.resolver; | ||||
|  | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.TextUtils; | ||||
|  | ||||
| import com.google.android.exoplayer2.C; | ||||
| import com.google.android.exoplayer2.source.MediaSource; | ||||
| import com.google.android.exoplayer2.util.Util; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
| import org.schabi.newpipe.player.helper.PlayerDataSource; | ||||
|  | ||||
| public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> { | ||||
|  | ||||
|     @Nullable | ||||
|     default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource, | ||||
|                                                   @NonNull final StreamInfo info) { | ||||
|         final StreamType streamType = info.getStreamType(); | ||||
|         if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         final MediaSourceTag tag = new MediaSourceTag(info); | ||||
|         if (!info.getHlsUrl().isEmpty()) { | ||||
|             return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag); | ||||
|         } else if (!info.getDashMpdUrl().isEmpty()) { | ||||
|             return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag); | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource, | ||||
|                                              @NonNull final String sourceUrl, | ||||
|                                              @C.ContentType final int type, | ||||
|                                              @NonNull final MediaSourceTag metadata) { | ||||
|         final Uri uri = Uri.parse(sourceUrl); | ||||
|         switch (type) { | ||||
|             case C.TYPE_SS: | ||||
|                 return dataSource.getLiveSsMediaSourceFactory().setTag(metadata) | ||||
|                         .createMediaSource(uri); | ||||
|             case C.TYPE_DASH: | ||||
|                 return dataSource.getLiveDashMediaSourceFactory().setTag(metadata) | ||||
|                         .createMediaSource(uri); | ||||
|             case C.TYPE_HLS: | ||||
|                 return dataSource.getLiveHlsMediaSourceFactory().setTag(metadata) | ||||
|                         .createMediaSource(uri); | ||||
|             default: | ||||
|                 throw new IllegalStateException("Unsupported type: " + type); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource, | ||||
|                                          @NonNull final String sourceUrl, | ||||
|                                          @NonNull final String cacheKey, | ||||
|                                          @NonNull final String overrideExtension, | ||||
|                                          @NonNull final MediaSourceTag metadata) { | ||||
|         final Uri uri = Uri.parse(sourceUrl); | ||||
|         @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? | ||||
|                 Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); | ||||
|  | ||||
|         switch (type) { | ||||
|             case C.TYPE_SS: | ||||
|                 return dataSource.getLiveSsMediaSourceFactory().setTag(metadata) | ||||
|                         .createMediaSource(uri); | ||||
|             case C.TYPE_DASH: | ||||
|                 return dataSource.getDashMediaSourceFactory().setTag(metadata) | ||||
|                         .createMediaSource(uri); | ||||
|             case C.TYPE_HLS: | ||||
|                 return dataSource.getHlsMediaSourceFactory().setTag(metadata) | ||||
|                         .createMediaSource(uri); | ||||
|             case C.TYPE_OTHER: | ||||
|                 return dataSource.getExtractorMediaSourceFactory(cacheKey).setTag(metadata) | ||||
|                         .createMediaSource(uri); | ||||
|             default: | ||||
|                 throw new IllegalStateException("Unsupported type: " + type); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,8 @@ | ||||
| package org.schabi.newpipe.player.resolver; | ||||
|  | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
|  | ||||
| public interface Resolver<Source, Product> { | ||||
|     @Nullable Product resolve(@NonNull Source source); | ||||
| } | ||||
| @@ -0,0 +1,123 @@ | ||||
| package org.schabi.newpipe.player.resolver; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
|  | ||||
| import com.google.android.exoplayer2.Format; | ||||
| import com.google.android.exoplayer2.source.MediaSource; | ||||
| import com.google.android.exoplayer2.source.MergingMediaSource; | ||||
|  | ||||
| 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.PlayerDataSource; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| 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; | ||||
|  | ||||
| public class VideoPlaybackResolver implements PlaybackResolver { | ||||
|  | ||||
|     public interface QualityResolver { | ||||
|         int getDefaultResolutionIndex(final List<VideoStream> sortedVideos); | ||||
|         int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, | ||||
|                                        final String playbackQuality); | ||||
|     } | ||||
|  | ||||
|     @NonNull private final Context context; | ||||
|     @NonNull private final PlayerDataSource dataSource; | ||||
|     @NonNull private final QualityResolver qualityResolver; | ||||
|  | ||||
|     @Nullable private String playbackQuality; | ||||
|  | ||||
|     public VideoPlaybackResolver(@NonNull final Context context, | ||||
|                                  @NonNull final PlayerDataSource dataSource, | ||||
|                                  @NonNull final QualityResolver qualityResolver) { | ||||
|         this.context = context; | ||||
|         this.dataSource = dataSource; | ||||
|         this.qualityResolver = qualityResolver; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @Nullable | ||||
|     public MediaSource resolve(@NonNull StreamInfo info) { | ||||
|         final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); | ||||
|         if (liveSource != null) return liveSource; | ||||
|  | ||||
|         List<MediaSource> mediaSources = new ArrayList<>(); | ||||
|  | ||||
|         // Create video stream source | ||||
|         final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, | ||||
|                 info.getVideoStreams(), info.getVideoOnlyStreams(), false); | ||||
|         final int index; | ||||
|         if (videos.isEmpty()) { | ||||
|             index = -1; | ||||
|         } else if (playbackQuality == null) { | ||||
|             index = qualityResolver.getDefaultResolutionIndex(videos); | ||||
|         } else { | ||||
|             index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality()); | ||||
|         } | ||||
|         final MediaSourceTag tag = new MediaSourceTag(info, videos, index); | ||||
|         @Nullable final VideoStream video = tag.getSelectedVideoStream(); | ||||
|  | ||||
|         if (video != null) { | ||||
|             final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(), | ||||
|                     PlayerHelper.cacheKeyOf(info, video), | ||||
|                     MediaFormat.getSuffixById(video.getFormatId()), tag); | ||||
|             mediaSources.add(streamSource); | ||||
|         } | ||||
|  | ||||
|         // Create optional audio stream source | ||||
|         final List<AudioStream> audioStreams = info.getAudioStreams(); | ||||
|         final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( | ||||
|                 ListHelper.getDefaultAudioFormat(context, audioStreams)); | ||||
|         // Use the audio stream if there is no video stream, or | ||||
|         // Merge with audio stream in case if video does not contain audio | ||||
|         if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { | ||||
|             final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(), | ||||
|                     PlayerHelper.cacheKeyOf(info, audio), | ||||
|                     MediaFormat.getSuffixById(audio.getFormatId()), tag); | ||||
|             mediaSources.add(audioSource); | ||||
|         } | ||||
|  | ||||
|         // If there is no audio or video sources, then this media source cannot be played back | ||||
|         if (mediaSources.isEmpty()) return null; | ||||
|         // Below are auxiliary media sources | ||||
|  | ||||
|         // 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(context, subtitle)); | ||||
|             final MediaSource textSource = dataSource.getSampleMediaSourceFactory() | ||||
|                     .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); | ||||
|             mediaSources.add(textSource); | ||||
|         } | ||||
|  | ||||
|         if (mediaSources.size() == 1) { | ||||
|             return mediaSources.get(0); | ||||
|         } else { | ||||
|             return new MergingMediaSource(mediaSources.toArray( | ||||
|                     new MediaSource[mediaSources.size()])); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public String getPlaybackQuality() { | ||||
|         return playbackQuality; | ||||
|     } | ||||
|  | ||||
|     public void setPlaybackQuality(@Nullable String playbackQuality) { | ||||
|         this.playbackQuality = playbackQuality; | ||||
|     } | ||||
| } | ||||
| @@ -15,7 +15,8 @@ public enum UserAction { | ||||
|     REQUESTED_CHANNEL("requested channel"), | ||||
|     REQUESTED_PLAYLIST("requested playlist"), | ||||
|     REQUESTED_KIOSK("requested kiosk"), | ||||
|     DELETE_FROM_HISTORY("delete from history"); | ||||
|     DELETE_FROM_HISTORY("delete from history"), | ||||
|     PLAY_STREAM("Play stream"); | ||||
|  | ||||
|  | ||||
|     private final String message; | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import android.os.Bundle; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.Fragment; | ||||
| import android.support.v7.preference.ListPreference; | ||||
| import android.support.v7.preference.Preference; | ||||
| import android.util.Log; | ||||
| @@ -20,15 +21,12 @@ import com.nostra13.universalimageloader.core.ImageLoader; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.Constants; | ||||
| import org.schabi.newpipe.util.FilePickerActivityHelper; | ||||
| import org.schabi.newpipe.util.KioskTranslator; | ||||
| import org.schabi.newpipe.util.ZipHelper; | ||||
|  | ||||
| import java.io.BufferedInputStream; | ||||
| import java.io.BufferedOutputStream; | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| @@ -42,11 +40,8 @@ import java.util.Date; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| import java.util.zip.ZipFile; | ||||
| import java.util.zip.ZipInputStream; | ||||
| import java.util.zip.ZipOutputStream; | ||||
|  | ||||
| import static android.content.Context.MODE_PRIVATE; | ||||
|  | ||||
| public class ContentSettingsFragment extends BasePreferenceFragment { | ||||
|  | ||||
|     private static final int REQUEST_IMPORT_PATH = 8945; | ||||
| @@ -56,6 +51,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | ||||
|     private File databasesDir; | ||||
|     private File newpipe_db; | ||||
|     private File newpipe_db_journal; | ||||
|     private File newpipe_db_shm; | ||||
|     private File newpipe_db_wal; | ||||
|     private File newpipe_settings; | ||||
|  | ||||
|     private String thumbnailLoadToggleKey; | ||||
| @@ -88,73 +85,14 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | ||||
|         databasesDir = new File(homeDir + "/databases"); | ||||
|         newpipe_db = new File(homeDir + "/databases/newpipe.db"); | ||||
|         newpipe_db_journal = new File(homeDir + "/databases/newpipe.db-journal"); | ||||
|         newpipe_db_shm = new File(homeDir + "/databases/newpipe.db-shm"); | ||||
|         newpipe_db_wal = new File(homeDir + "/databases/newpipe.db-wal"); | ||||
|  | ||||
|         newpipe_settings = new File(homeDir + "/databases/newpipe.settings"); | ||||
|         newpipe_settings.delete(); | ||||
|  | ||||
|         addPreferencesFromResource(R.xml.content_settings); | ||||
|  | ||||
|         final ListPreference mainPageContentPref =  (ListPreference) findPreference(getString(R.string.main_page_content_key)); | ||||
|         mainPageContentPref.setOnPreferenceChangeListener((Preference preference, Object newValueO) -> { | ||||
|             final String newValue = newValueO.toString(); | ||||
|  | ||||
|             final String mainPrefOldValue = | ||||
|                     defaultPreferences.getString(getString(R.string.main_page_content_key), "blank_page"); | ||||
|             final String mainPrefOldSummary = getMainPagePrefSummery(mainPrefOldValue, mainPageContentPref); | ||||
|  | ||||
|             if(newValue.equals(getString(R.string.kiosk_page_key))) { | ||||
|                 SelectKioskFragment selectKioskFragment = new SelectKioskFragment(); | ||||
|                 selectKioskFragment.setOnSelectedLisener((String kioskId, int service_id) -> { | ||||
|                     defaultPreferences.edit() | ||||
|                             .putInt(getString(R.string.main_page_selected_service), service_id).apply(); | ||||
|                     defaultPreferences.edit() | ||||
|                             .putString(getString(R.string.main_page_selectd_kiosk_id), kioskId).apply(); | ||||
|                     String serviceName = ""; | ||||
|                     try { | ||||
|                         serviceName = NewPipe.getService(service_id).getServiceInfo().getName(); | ||||
|                     } catch (ExtractionException e) { | ||||
|                         onError(e); | ||||
|                     } | ||||
|                     String kioskName = KioskTranslator.getTranslatedKioskName(kioskId, | ||||
|                             getContext()); | ||||
|  | ||||
|                     String summary = | ||||
|                             String.format(getString(R.string.service_kiosk_string), | ||||
|                                     serviceName, | ||||
|                                     kioskName); | ||||
|  | ||||
|                     mainPageContentPref.setSummary(summary); | ||||
|                 }); | ||||
|                 selectKioskFragment.setOnCancelListener(() -> { | ||||
|                     mainPageContentPref.setSummary(mainPrefOldSummary); | ||||
|                     mainPageContentPref.setValue(mainPrefOldValue); | ||||
|                 }); | ||||
|                 selectKioskFragment.show(getFragmentManager(), "select_kiosk"); | ||||
|             } else if(newValue.equals(getString(R.string.channel_page_key))) { | ||||
|                 SelectChannelFragment selectChannelFragment = new SelectChannelFragment(); | ||||
|                 selectChannelFragment.setOnSelectedLisener((String url, String name, int service) -> { | ||||
|                     defaultPreferences.edit() | ||||
|                             .putInt(getString(R.string.main_page_selected_service), service).apply(); | ||||
|                     defaultPreferences.edit() | ||||
|                             .putString(getString(R.string.main_page_selected_channel_url), url).apply(); | ||||
|                     defaultPreferences.edit() | ||||
|                             .putString(getString(R.string.main_page_selected_channel_name), name).apply(); | ||||
|  | ||||
|                     mainPageContentPref.setSummary(name); | ||||
|                 }); | ||||
|                 selectChannelFragment.setOnCancelListener(() -> { | ||||
|                     mainPageContentPref.setSummary(mainPrefOldSummary); | ||||
|                     mainPageContentPref.setValue(mainPrefOldValue); | ||||
|                 }); | ||||
|                 selectChannelFragment.show(getFragmentManager(), "select_channel"); | ||||
|             } else { | ||||
|                 mainPageContentPref.setSummary(getMainPageSummeryByKey(newValue)); | ||||
|             } | ||||
|  | ||||
|             defaultPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); | ||||
|  | ||||
|             return true; | ||||
|         }); | ||||
|  | ||||
|         Preference importDataPreference = findPreference(getString(R.string.import_data)); | ||||
|         importDataPreference.setOnPreferenceClickListener((Preference p) -> { | ||||
|             Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) | ||||
| @@ -207,7 +145,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | ||||
|                     new BufferedOutputStream( | ||||
|                             new FileOutputStream(path))); | ||||
|             ZipHelper.addFileToZip(outZip, newpipe_db.getPath(), "newpipe.db"); | ||||
|             ZipHelper.addFileToZip(outZip, newpipe_db_journal.getPath(), "newpipe.db-journal"); | ||||
|  | ||||
|             saveSharedPreferencesToFile(newpipe_settings); | ||||
|             ZipHelper.addFileToZip(outZip, newpipe_settings.getPath(), "newpipe.settings"); | ||||
|  | ||||
| @@ -263,8 +201,16 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | ||||
|                 throw new Exception("Could not create databases dir"); | ||||
|             } | ||||
|  | ||||
|             if(!(ZipHelper.extractFileFromZip(filePath, newpipe_db.getPath(), "newpipe.db") | ||||
|                     && ZipHelper.extractFileFromZip(filePath, newpipe_db_journal.getPath(), "newpipe.db-journal"))) { | ||||
|             final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, | ||||
|                     newpipe_db.getPath(), "newpipe.db"); | ||||
|  | ||||
|             if (isDbFileExtracted) { | ||||
|                 newpipe_db_journal.delete(); | ||||
|                 newpipe_db_wal.delete(); | ||||
|                 newpipe_db_shm.delete(); | ||||
|  | ||||
|             } else { | ||||
|  | ||||
|                 Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG) | ||||
|                         .show(); | ||||
|             } | ||||
| @@ -336,66 +282,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|  | ||||
|         final String mainPageContentKey = getString(R.string.main_page_content_key); | ||||
|         final Preference mainPagePref = findPreference(getString(R.string.main_page_content_key)); | ||||
|         final String bpk = getString(R.string.blank_page_key); | ||||
|         if(defaultPreferences.getString(mainPageContentKey, bpk) | ||||
|                 .equals(getString(R.string.channel_page_key))) { | ||||
|             mainPagePref.setSummary(defaultPreferences.getString(getString(R.string.main_page_selected_channel_name), "error")); | ||||
|         } else if(defaultPreferences.getString(mainPageContentKey, bpk) | ||||
|                 .equals(getString(R.string.kiosk_page_key))) { | ||||
|             try { | ||||
|                 StreamingService service = NewPipe.getService( | ||||
|                         defaultPreferences.getInt( | ||||
|                                 getString(R.string.main_page_selected_service), 0)); | ||||
|  | ||||
|                 String kioskName = KioskTranslator.getTranslatedKioskName( | ||||
|                         defaultPreferences.getString( | ||||
|                                 getString(R.string.main_page_selectd_kiosk_id), "Trending"), | ||||
|                         getContext()); | ||||
|  | ||||
|                 String summary = | ||||
|                         String.format(getString(R.string.service_kiosk_string), | ||||
|                                 service.getServiceInfo().getName(), | ||||
|                                 kioskName); | ||||
|  | ||||
|                 mainPagePref.setSummary(summary); | ||||
|             } catch (Exception e) { | ||||
|                 onError(e); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     private String getMainPagePrefSummery(final String mainPrefOldValue, final ListPreference mainPageContentPref) { | ||||
|         if(mainPrefOldValue.equals(getString(R.string.channel_page_key))) { | ||||
|             return defaultPreferences.getString(getString(R.string.main_page_selected_channel_name), "error"); | ||||
|         } else { | ||||
|             return mainPageContentPref.getSummary().toString(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private int getMainPageSummeryByKey(final String key) { | ||||
|         if(key.equals(getString(R.string.blank_page_key))) { | ||||
|             return R.string.blank_page_summary; | ||||
|         } else if(key.equals(getString(R.string.kiosk_page_key))) { | ||||
|             return R.string.kiosk_page_summary; | ||||
|         } else if(key.equals(getString(R.string.feed_page_key))) { | ||||
|             return R.string.feed_page_summary; | ||||
|         } else if(key.equals(getString(R.string.subscription_page_key))) { | ||||
|             return R.string.subscription_page_summary; | ||||
|         } else if(key.equals(getString(R.string.channel_page_key))) { | ||||
|             return R.string.channel_page_summary; | ||||
|         } | ||||
|         return R.string.blank_page_summary; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Error | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|   | ||||
| @@ -71,7 +71,7 @@ public class NewPipeSettings { | ||||
|     } | ||||
|  | ||||
|     public static File getVideoDownloadFolder(Context context) { | ||||
|         return getFolder(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES); | ||||
|         return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES); | ||||
|     } | ||||
|  | ||||
|     public static String getVideoDownloadPath(Context context) { | ||||
| @@ -81,7 +81,7 @@ public class NewPipeSettings { | ||||
|     } | ||||
|  | ||||
|     public static File getAudioDownloadFolder(Context context) { | ||||
|         return getFolder(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); | ||||
|         return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); | ||||
|     } | ||||
|  | ||||
|     public static String getAudioDownloadPath(Context context) { | ||||
| @@ -90,21 +90,37 @@ public class NewPipeSettings { | ||||
|         return prefs.getString(key, Environment.DIRECTORY_MUSIC); | ||||
|     } | ||||
|  | ||||
|     private static File getFolder(Context context, int keyID, String defaultDirectoryName) { | ||||
|     private static File getDir(Context context, int keyID, String defaultDirectoryName) { | ||||
|         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|         final String key = context.getString(keyID); | ||||
|         String downloadPath = prefs.getString(key, null); | ||||
|         if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim()); | ||||
|  | ||||
|         final File folder = getFolder(defaultDirectoryName); | ||||
|         final File dir = getDir(defaultDirectoryName); | ||||
|         SharedPreferences.Editor spEditor = prefs.edit(); | ||||
|         spEditor.putString(key, new File(folder, "NewPipe").getAbsolutePath()); | ||||
|         spEditor.putString(key, getNewPipeChildFolderPathForDir(dir)); | ||||
|         spEditor.apply(); | ||||
|         return folder; | ||||
|         return dir; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     private static File getFolder(String defaultDirectoryName) { | ||||
|     private static File getDir(String defaultDirectoryName) { | ||||
|         return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName); | ||||
|     } | ||||
|  | ||||
|     public static void resetDownloadFolders(Context context) { | ||||
|         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|         resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC); | ||||
|         resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES); | ||||
|     } | ||||
|  | ||||
|     private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) { | ||||
|         SharedPreferences.Editor spEditor = prefs.edit(); | ||||
|         spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); | ||||
|         spEditor.apply(); | ||||
|     } | ||||
|  | ||||
|     private static String getNewPipeChildFolderPathForDir(File dir) { | ||||
|         return new File(dir, "NewPipe").getAbsolutePath(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -66,7 +66,7 @@ public class SelectChannelFragment extends DialogFragment { | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public interface OnSelectedLisener { | ||||
|         void onChannelSelected(String url, String name, int service); | ||||
|         void onChannelSelected(int serviceId, String url, String name); | ||||
|     } | ||||
|     OnSelectedLisener onSelectedLisener = null; | ||||
|     public void setOnSelectedLisener(OnSelectedLisener listener) { | ||||
| @@ -126,7 +126,7 @@ public class SelectChannelFragment extends DialogFragment { | ||||
|     private void clickedItem(int position) { | ||||
|         if(onSelectedLisener != null) { | ||||
|             SubscriptionEntity entry = subscriptions.get(position); | ||||
|             onSelectedLisener.onChannelSelected(entry.getUrl(), entry.getName(), entry.getServiceId()); | ||||
|             onSelectedLisener.onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName()); | ||||
|         } | ||||
|         dismiss(); | ||||
|     } | ||||
|   | ||||
| @@ -56,7 +56,7 @@ public class SelectKioskFragment extends DialogFragment { | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public interface OnSelectedLisener { | ||||
|         void onKioskSelected(String kioskId, int service_id); | ||||
|         void onKioskSelected(int serviceId, String kioskId, String kioskName); | ||||
|     } | ||||
|  | ||||
|     OnSelectedLisener onSelectedLisener = null; | ||||
| @@ -101,7 +101,7 @@ public class SelectKioskFragment extends DialogFragment { | ||||
|  | ||||
|     private void clickedItem(SelectKioskAdapter.Entry entry) { | ||||
|         if(onSelectedLisener != null) { | ||||
|             onSelectedLisener.onKioskSelected(entry.kioskId, entry.serviceId); | ||||
|             onSelectedLisener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName); | ||||
|         } | ||||
|         dismiss(); | ||||
|     } | ||||
|   | ||||
| @@ -77,7 +77,8 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc | ||||
|                 finish(); | ||||
|             } else getSupportFragmentManager().popBackStack(); | ||||
|         } | ||||
|         return true; | ||||
|  | ||||
|         return super.onOptionsItemSelected(item); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -0,0 +1,94 @@ | ||||
| package org.schabi.newpipe.settings.tabs; | ||||
|  | ||||
| import android.app.AlertDialog; | ||||
| import android.content.Context; | ||||
| import android.content.DialogInterface; | ||||
| import android.support.annotation.DrawableRes; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.v7.widget.AppCompatImageView; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.BaseAdapter; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| public class AddTabDialog { | ||||
|     private final AlertDialog dialog; | ||||
|  | ||||
|     AddTabDialog(@NonNull final Context context, | ||||
|                  @NonNull final ChooseTabListItem[] items, | ||||
|                  @NonNull final DialogInterface.OnClickListener actions) { | ||||
|  | ||||
|         dialog = new AlertDialog.Builder(context) | ||||
|                 .setTitle(context.getString(R.string.tab_choose)) | ||||
|                 .setAdapter(new DialogListAdapter(context, items), actions) | ||||
|                 .create(); | ||||
|     } | ||||
|  | ||||
|     public void show() { | ||||
|         dialog.show(); | ||||
|     } | ||||
|  | ||||
|     public static final class ChooseTabListItem { | ||||
|         final int tabId; | ||||
|         final String itemName; | ||||
|         @DrawableRes final int itemIcon; | ||||
|  | ||||
|         ChooseTabListItem(Context context, Tab tab) { | ||||
|             this(tab.getTabId(), tab.getTabName(context), tab.getTabIconRes(context)); | ||||
|         } | ||||
|  | ||||
|         ChooseTabListItem(int tabId, String itemName, @DrawableRes int itemIcon) { | ||||
|             this.tabId = tabId; | ||||
|             this.itemName = itemName; | ||||
|             this.itemIcon = itemIcon; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class DialogListAdapter extends BaseAdapter { | ||||
|         private final LayoutInflater inflater; | ||||
|         private final ChooseTabListItem[] items; | ||||
|  | ||||
|         @DrawableRes private final int fallbackIcon; | ||||
|  | ||||
|         private DialogListAdapter(Context context, ChooseTabListItem[] items) { | ||||
|             this.inflater = LayoutInflater.from(context); | ||||
|             this.items = items; | ||||
|             this.fallbackIcon = ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public int getCount() { | ||||
|             return items.length; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public ChooseTabListItem getItem(int position) { | ||||
|             return items[position]; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public long getItemId(int position) { | ||||
|             return getItem(position).tabId; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public View getView(int position, View convertView, ViewGroup parent) { | ||||
|             if (convertView == null) { | ||||
|                 convertView = inflater.inflate(R.layout.list_choose_tabs_dialog, parent, false); | ||||
|             } | ||||
|  | ||||
|             final ChooseTabListItem item = getItem(position); | ||||
|             final AppCompatImageView tabIconView = convertView.findViewById(R.id.tabIcon); | ||||
|             final TextView tabNameView = convertView.findViewById(R.id.tabName); | ||||
|  | ||||
|             tabIconView.setImageResource(item.itemIcon > 0 ? item.itemIcon : fallbackIcon); | ||||
|             tabNameView.setText(item.itemName); | ||||
|  | ||||
|             return convertView; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,386 @@ | ||||
| package org.schabi.newpipe.settings.tabs; | ||||
|  | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.Dialog; | ||||
| import android.content.Context; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.design.widget.FloatingActionButton; | ||||
| import android.support.v4.app.Fragment; | ||||
| import android.support.v7.app.ActionBar; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.support.v7.content.res.AppCompatResources; | ||||
| import android.support.v7.widget.AppCompatImageView; | ||||
| import android.support.v7.widget.LinearLayoutManager; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.support.v7.widget.helper.ItemTouchHelper; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.settings.SelectChannelFragment; | ||||
| import org.schabi.newpipe.settings.SelectKioskFragment; | ||||
| import org.schabi.newpipe.settings.tabs.AddTabDialog.ChooseTabListItem; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| import static org.schabi.newpipe.settings.tabs.Tab.typeFrom; | ||||
|  | ||||
| public class ChooseTabsFragment extends Fragment { | ||||
|  | ||||
|     private TabsManager tabsManager; | ||||
|     private List<Tab> tabList = new ArrayList<>(); | ||||
|     public ChooseTabsFragment.SelectedTabsAdapter selectedTabsAdapter; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Lifecycle | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|  | ||||
|         tabsManager = TabsManager.getManager(requireContext()); | ||||
|         updateTabList(); | ||||
|  | ||||
|         setHasOptionsMenu(true); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||
|         return inflater.inflate(R.layout.fragment_choose_tabs, container, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) { | ||||
|         super.onViewCreated(rootView, savedInstanceState); | ||||
|  | ||||
|         initButton(rootView); | ||||
|  | ||||
|         RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs); | ||||
|         listSelectedTabs.setLayoutManager(new LinearLayoutManager(requireContext())); | ||||
|  | ||||
|         ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); | ||||
|         itemTouchHelper.attachToRecyclerView(listSelectedTabs); | ||||
|  | ||||
|         selectedTabsAdapter = new SelectedTabsAdapter(requireContext(), itemTouchHelper); | ||||
|         listSelectedTabs.setAdapter(selectedTabsAdapter); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         updateTitle(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPause() { | ||||
|         super.onPause(); | ||||
|         saveChanges(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Menu | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private final int MENU_ITEM_RESTORE_ID = 123456; | ||||
|  | ||||
|     @Override | ||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||
|         super.onCreateOptionsMenu(menu, inflater); | ||||
|  | ||||
|         final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults); | ||||
|         restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); | ||||
|  | ||||
|         final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults); | ||||
|         restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), restoreIcon)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         if (item.getItemId() == MENU_ITEM_RESTORE_ID) { | ||||
|             restoreDefaults(); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         return super.onOptionsItemSelected(item); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void updateTabList() { | ||||
|         tabList.clear(); | ||||
|         tabList.addAll(tabsManager.getTabs()); | ||||
|     } | ||||
|  | ||||
|     private void updateTitle() { | ||||
|         if (getActivity() instanceof AppCompatActivity) { | ||||
|             ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); | ||||
|             if (actionBar != null) actionBar.setTitle(R.string.main_page_content); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void saveChanges() { | ||||
|         tabsManager.saveTabs(tabList); | ||||
|     } | ||||
|  | ||||
|     private void restoreDefaults() { | ||||
|         new AlertDialog.Builder(requireContext(), ThemeHelper.getDialogTheme(requireContext())) | ||||
|                 .setTitle(R.string.restore_defaults) | ||||
|                 .setMessage(R.string.restore_defaults_confirmation) | ||||
|                 .setNegativeButton(R.string.cancel, null) | ||||
|                 .setPositiveButton(R.string.yes, (dialog, which) -> { | ||||
|                     tabsManager.resetTabs(); | ||||
|                     updateTabList(); | ||||
|                     selectedTabsAdapter.notifyDataSetChanged(); | ||||
|                 }) | ||||
|                 .show(); | ||||
|     } | ||||
|  | ||||
|     private void initButton(View rootView) { | ||||
|         final FloatingActionButton fab = rootView.findViewById(R.id.addTabsButton); | ||||
|         fab.setOnClickListener(v -> { | ||||
|             final ChooseTabListItem[] availableTabs = getAvailableTabs(requireContext()); | ||||
|  | ||||
|             if (availableTabs.length == 0) { | ||||
|                 //Toast.makeText(requireContext(), "No available tabs", Toast.LENGTH_SHORT).show(); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             Dialog.OnClickListener actionListener = (dialog, which) -> { | ||||
|                 final ChooseTabListItem selected = availableTabs[which]; | ||||
|                 addTab(selected.tabId); | ||||
|             }; | ||||
|  | ||||
|             new AddTabDialog(requireContext(), availableTabs, actionListener) | ||||
|                     .show(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private void addTab(final Tab tab) { | ||||
|         tabList.add(tab); | ||||
|         selectedTabsAdapter.notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     private void addTab(int tabId) { | ||||
|         final Tab.Type type = typeFrom(tabId); | ||||
|  | ||||
|         if (type == null) { | ||||
|             ErrorActivity.reportError(requireContext(), new IllegalStateException("Tab id not found: " + tabId), null, null, | ||||
|                     ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", "Choosing tabs on settings", 0)); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         switch (type) { | ||||
|             case KIOSK: { | ||||
|                 SelectKioskFragment selectFragment = new SelectKioskFragment(); | ||||
|                 selectFragment.setOnSelectedLisener((serviceId, kioskId, kioskName) -> | ||||
|                         addTab(new Tab.KioskTab(serviceId, kioskId))); | ||||
|                 selectFragment.show(requireFragmentManager(), "select_kiosk"); | ||||
|                 return; | ||||
|             } | ||||
|             case CHANNEL: { | ||||
|                 SelectChannelFragment selectFragment = new SelectChannelFragment(); | ||||
|                 selectFragment.setOnSelectedLisener((serviceId, url, name) -> | ||||
|                         addTab(new Tab.ChannelTab(serviceId, url, name))); | ||||
|                 selectFragment.show(requireFragmentManager(), "select_channel"); | ||||
|                 return; | ||||
|             } | ||||
|             default: | ||||
|                 addTab(type.getTab()); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public ChooseTabListItem[] getAvailableTabs(Context context) { | ||||
|         final ArrayList<ChooseTabListItem> returnList = new ArrayList<>(); | ||||
|  | ||||
|         for (Tab.Type type : Tab.Type.values()) { | ||||
|             final Tab tab = type.getTab(); | ||||
|             switch (type) { | ||||
|                 case BLANK: | ||||
|                     if (!tabList.contains(tab)) { | ||||
|                         returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.blank_page_summary), | ||||
|                                 tab.getTabIconRes(context))); | ||||
|                     } | ||||
|                     break; | ||||
|                 case KIOSK: | ||||
|                     returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.kiosk_page_summary), | ||||
|                             ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot))); | ||||
|                     break; | ||||
|                 case CHANNEL: | ||||
|                     returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.channel_page_summary), | ||||
|                             tab.getTabIconRes(context))); | ||||
|                     break; | ||||
|                 default: | ||||
|                     if (!tabList.contains(tab)) { | ||||
|                         returnList.add(new ChooseTabListItem(context, tab)); | ||||
|                     } | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return returnList.toArray(new ChooseTabListItem[0]); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // List Handling | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private class SelectedTabsAdapter extends RecyclerView.Adapter<ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder> { | ||||
|         private ItemTouchHelper itemTouchHelper; | ||||
|         private final LayoutInflater inflater; | ||||
|  | ||||
|         SelectedTabsAdapter(Context context, ItemTouchHelper itemTouchHelper) { | ||||
|             this.itemTouchHelper = itemTouchHelper; | ||||
|             this.inflater = LayoutInflater.from(context); | ||||
|         } | ||||
|  | ||||
|         public void swapItems(int fromPosition, int toPosition) { | ||||
|             Collections.swap(tabList, fromPosition, toPosition); | ||||
|             notifyItemMoved(fromPosition, toPosition); | ||||
|         } | ||||
|  | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { | ||||
|             View view = inflater.inflate(R.layout.list_choose_tabs, parent, false); | ||||
|             return new ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder(view); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onBindViewHolder(@NonNull ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder holder, int position) { | ||||
|             holder.bind(position, holder); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public int getItemCount() { | ||||
|             return tabList.size(); | ||||
|         } | ||||
|  | ||||
|         class TabViewHolder extends RecyclerView.ViewHolder { | ||||
|             private AppCompatImageView tabIconView; | ||||
|             private TextView tabNameView; | ||||
|             private ImageView handle; | ||||
|  | ||||
|             TabViewHolder(View itemView) { | ||||
|                 super(itemView); | ||||
|  | ||||
|                 tabNameView = itemView.findViewById(R.id.tabName); | ||||
|                 tabIconView = itemView.findViewById(R.id.tabIcon); | ||||
|                 handle = itemView.findViewById(R.id.handle); | ||||
|             } | ||||
|  | ||||
|             @SuppressLint("ClickableViewAccessibility") | ||||
|             void bind(int position, TabViewHolder holder) { | ||||
|                 handle.setOnTouchListener(getOnTouchListener(holder)); | ||||
|  | ||||
|                 final Tab tab = tabList.get(position); | ||||
|                 final Tab.Type type = Tab.typeFrom(tab.getTabId()); | ||||
|  | ||||
|                 if (type == null) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 String tabName = tab.getTabName(requireContext()); | ||||
|                 switch (type) { | ||||
|                     case BLANK: | ||||
|                         tabName = requireContext().getString(R.string.blank_page_summary); | ||||
|                         break; | ||||
|                     case KIOSK: | ||||
|                         tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab).getKioskServiceId()) + "/" + tabName; | ||||
|                         break; | ||||
|                     case CHANNEL: | ||||
|                         tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tabName; | ||||
|                         break; | ||||
|                 } | ||||
|  | ||||
|  | ||||
|                 tabNameView.setText(tabName); | ||||
|                 tabIconView.setImageResource(tab.getTabIconRes(requireContext())); | ||||
|             } | ||||
|  | ||||
|             @SuppressLint("ClickableViewAccessibility") | ||||
|             private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) { | ||||
|                 return (view, motionEvent) -> { | ||||
|                     if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { | ||||
|                         if (itemTouchHelper != null && getItemCount() > 1) { | ||||
|                             itemTouchHelper.startDrag(item); | ||||
|                             return true; | ||||
|                         } | ||||
|                     } | ||||
|                     return false; | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private ItemTouchHelper.SimpleCallback getItemTouchCallback() { | ||||
|         return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, | ||||
|                 ItemTouchHelper.START | ItemTouchHelper.END) { | ||||
|             @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(12, | ||||
|                         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() || | ||||
|                         selectedTabsAdapter == null) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 final int sourceIndex = source.getAdapterPosition(); | ||||
|                 final int targetIndex = target.getAdapterPosition(); | ||||
|                 selectedTabsAdapter.swapItems(sourceIndex, targetIndex); | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean isLongPressDragEnabled() { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean isItemViewSwipeEnabled() { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { | ||||
|                 int position = viewHolder.getAdapterPosition(); | ||||
|                 tabList.remove(position); | ||||
|                 selectedTabsAdapter.notifyItemRemoved(position); | ||||
|  | ||||
|                 if (tabList.isEmpty()) { | ||||
|                     tabList.add(Tab.Type.BLANK.getTab()); | ||||
|                     selectedTabsAdapter.notifyItemInserted(0); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										416
									
								
								app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,416 @@ | ||||
| package org.schabi.newpipe.settings.tabs; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.support.annotation.DrawableRes; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.Fragment; | ||||
|  | ||||
| import com.grack.nanojson.JsonObject; | ||||
| import com.grack.nanojson.JsonSink; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
| import org.schabi.newpipe.fragments.BlankFragment; | ||||
| import org.schabi.newpipe.fragments.list.channel.ChannelFragment; | ||||
| import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; | ||||
| import org.schabi.newpipe.local.bookmark.BookmarkFragment; | ||||
| import org.schabi.newpipe.local.feed.FeedFragment; | ||||
| import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; | ||||
| import org.schabi.newpipe.local.subscription.SubscriptionFragment; | ||||
| import org.schabi.newpipe.util.KioskTranslator; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| public abstract class Tab { | ||||
|     Tab() { | ||||
|     } | ||||
|  | ||||
|     Tab(@NonNull JsonObject jsonObject) { | ||||
|         readDataFromJson(jsonObject); | ||||
|     } | ||||
|  | ||||
|     public abstract int getTabId(); | ||||
|     public abstract String getTabName(Context context); | ||||
|     @DrawableRes public abstract int getTabIconRes(Context context); | ||||
|  | ||||
|     /** | ||||
|      * Return a instance of the fragment that this tab represent. | ||||
|      */ | ||||
|     public abstract Fragment getFragment() throws ExtractionException; | ||||
|  | ||||
|     @Override | ||||
|     public boolean equals(Object obj) { | ||||
|         return obj instanceof Tab && obj.getClass().equals(this.getClass()) | ||||
|                 && ((Tab) obj).getTabId() == this.getTabId(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // JSON Handling | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private static final String JSON_TAB_ID_KEY = "tab_id"; | ||||
|  | ||||
|     public void writeJsonOn(JsonSink jsonSink) { | ||||
|         jsonSink.object(); | ||||
|  | ||||
|         jsonSink.value(JSON_TAB_ID_KEY, getTabId()); | ||||
|         writeDataToJson(jsonSink); | ||||
|  | ||||
|         jsonSink.end(); | ||||
|     } | ||||
|  | ||||
|     protected void writeDataToJson(JsonSink writerSink) { | ||||
|         // No-op | ||||
|     } | ||||
|  | ||||
|     protected void readDataFromJson(JsonObject jsonObject) { | ||||
|         // No-op | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Tab Handling | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Nullable | ||||
|     public static Tab from(@NonNull JsonObject jsonObject) { | ||||
|         final int tabId = jsonObject.getInt(Tab.JSON_TAB_ID_KEY, -1); | ||||
|  | ||||
|         if (tabId == -1) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         return from(tabId, jsonObject); | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public static Tab from(final int tabId) { | ||||
|         return from(tabId, null); | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public static Type typeFrom(int tabId) { | ||||
|         for (Type available : Type.values()) { | ||||
|             if (available.getTabId() == tabId) { | ||||
|                 return available; | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     private static Tab from(final int tabId, @Nullable JsonObject jsonObject) { | ||||
|         final Type type = typeFrom(tabId); | ||||
|  | ||||
|         if (type == null) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         if (jsonObject != null) { | ||||
|             switch (type) { | ||||
|                 case KIOSK: | ||||
|                     return new KioskTab(jsonObject); | ||||
|                 case CHANNEL: | ||||
|                     return new ChannelTab(jsonObject); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return type.getTab(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Implementations | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public enum Type { | ||||
|         BLANK(new BlankTab()), | ||||
|         SUBSCRIPTIONS(new SubscriptionsTab()), | ||||
|         FEED(new FeedTab()), | ||||
|         BOOKMARKS(new BookmarksTab()), | ||||
|         HISTORY(new HistoryTab()), | ||||
|         KIOSK(new KioskTab()), | ||||
|         CHANNEL(new ChannelTab()); | ||||
|  | ||||
|         private Tab tab; | ||||
|  | ||||
|         Type(Tab tab) { | ||||
|             this.tab = tab; | ||||
|         } | ||||
|  | ||||
|         public int getTabId() { | ||||
|             return tab.getTabId(); | ||||
|         } | ||||
|  | ||||
|         public Tab getTab() { | ||||
|             return tab; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static class BlankTab extends Tab { | ||||
|         public static final int ID = 0; | ||||
|  | ||||
|         @Override | ||||
|         public int getTabId() { | ||||
|             return ID; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String getTabName(Context context) { | ||||
|             return "NewPipe"; //context.getString(R.string.blank_page_summary); | ||||
|         } | ||||
|  | ||||
|         @DrawableRes | ||||
|         @Override | ||||
|         public int getTabIconRes(Context context) { | ||||
|             return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_blank_page); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public BlankFragment getFragment() { | ||||
|             return new BlankFragment(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static class SubscriptionsTab extends Tab { | ||||
|         public static final int ID = 1; | ||||
|  | ||||
|         @Override | ||||
|         public int getTabId() { | ||||
|             return ID; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String getTabName(Context context) { | ||||
|             return context.getString(R.string.tab_subscriptions); | ||||
|         } | ||||
|  | ||||
|         @DrawableRes | ||||
|         @Override | ||||
|         public int getTabIconRes(Context context) { | ||||
|             return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_channel); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public SubscriptionFragment getFragment() { | ||||
|             return new SubscriptionFragment(); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public static class FeedTab extends Tab { | ||||
|         public static final int ID = 2; | ||||
|  | ||||
|         @Override | ||||
|         public int getTabId() { | ||||
|             return ID; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String getTabName(Context context) { | ||||
|             return context.getString(R.string.fragment_whats_new); | ||||
|         } | ||||
|  | ||||
|         @DrawableRes | ||||
|         @Override | ||||
|         public int getTabIconRes(Context context) { | ||||
|             return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.rss); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public FeedFragment getFragment() { | ||||
|             return new FeedFragment(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static class BookmarksTab extends Tab { | ||||
|         public static final int ID = 3; | ||||
|  | ||||
|         @Override | ||||
|         public int getTabId() { | ||||
|             return ID; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String getTabName(Context context) { | ||||
|             return context.getString(R.string.tab_bookmarks); | ||||
|         } | ||||
|  | ||||
|         @DrawableRes | ||||
|         @Override | ||||
|         public int getTabIconRes(Context context) { | ||||
|             return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_bookmark); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public BookmarkFragment getFragment() { | ||||
|             return new BookmarkFragment(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static class HistoryTab extends Tab { | ||||
|         public static final int ID = 4; | ||||
|  | ||||
|         @Override | ||||
|         public int getTabId() { | ||||
|             return ID; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String getTabName(Context context) { | ||||
|             return context.getString(R.string.title_activity_history); | ||||
|         } | ||||
|  | ||||
|         @DrawableRes | ||||
|         @Override | ||||
|         public int getTabIconRes(Context context) { | ||||
|             return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.history); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public StatisticsPlaylistFragment getFragment() { | ||||
|             return new StatisticsPlaylistFragment(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static class KioskTab extends Tab { | ||||
|         public static final int ID = 5; | ||||
|  | ||||
|         private int kioskServiceId; | ||||
|         private String kioskId; | ||||
|  | ||||
|         private static final String JSON_KIOSK_SERVICE_ID_KEY = "service_id"; | ||||
|         private static final String JSON_KIOSK_ID_KEY = "kiosk_id"; | ||||
|  | ||||
|         private KioskTab() { | ||||
|             this(-1, "<no-id>"); | ||||
|         } | ||||
|  | ||||
|         public KioskTab(int kioskServiceId, String kioskId) { | ||||
|             this.kioskServiceId = kioskServiceId; | ||||
|             this.kioskId = kioskId; | ||||
|         } | ||||
|  | ||||
|         public KioskTab(JsonObject jsonObject) { | ||||
|             super(jsonObject); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public int getTabId() { | ||||
|             return ID; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String getTabName(Context context) { | ||||
|             return KioskTranslator.getTranslatedKioskName(kioskId, context); | ||||
|         } | ||||
|  | ||||
|         @DrawableRes | ||||
|         @Override | ||||
|         public int getTabIconRes(Context context) { | ||||
|             final int kioskIcon = KioskTranslator.getKioskIcons(kioskId, context); | ||||
|  | ||||
|             if (kioskIcon <= 0) { | ||||
|                 throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\""); | ||||
|             } | ||||
|  | ||||
|             return kioskIcon; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public KioskFragment getFragment() throws ExtractionException { | ||||
|             return KioskFragment.getInstance(kioskServiceId, kioskId); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void writeDataToJson(JsonSink writerSink) { | ||||
|             writerSink.value(JSON_KIOSK_SERVICE_ID_KEY, kioskServiceId) | ||||
|                     .value(JSON_KIOSK_ID_KEY, kioskId); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void readDataFromJson(JsonObject jsonObject) { | ||||
|             kioskServiceId = jsonObject.getInt(JSON_KIOSK_SERVICE_ID_KEY, -1); | ||||
|             kioskId = jsonObject.getString(JSON_KIOSK_ID_KEY, "<no-id>"); | ||||
|         } | ||||
|  | ||||
|         public int getKioskServiceId() { | ||||
|             return kioskServiceId; | ||||
|         } | ||||
|  | ||||
|         public String getKioskId() { | ||||
|             return kioskId; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static class ChannelTab extends Tab { | ||||
|         public static final int ID = 6; | ||||
|  | ||||
|         private int channelServiceId; | ||||
|         private String channelUrl; | ||||
|         private String channelName; | ||||
|  | ||||
|         private static final String JSON_CHANNEL_SERVICE_ID_KEY = "channel_service_id"; | ||||
|         private static final String JSON_CHANNEL_URL_KEY = "channel_url"; | ||||
|         private static final String JSON_CHANNEL_NAME_KEY = "channel_name"; | ||||
|  | ||||
|         private ChannelTab() { | ||||
|             this(-1, "<no-url>", "<no-name>"); | ||||
|         } | ||||
|  | ||||
|         public ChannelTab(int channelServiceId, String channelUrl, String channelName) { | ||||
|             this.channelServiceId = channelServiceId; | ||||
|             this.channelUrl = channelUrl; | ||||
|             this.channelName = channelName; | ||||
|         } | ||||
|  | ||||
|         public ChannelTab(JsonObject jsonObject) { | ||||
|             super(jsonObject); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public int getTabId() { | ||||
|             return ID; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String getTabName(Context context) { | ||||
|             return channelName; | ||||
|         } | ||||
|  | ||||
|         @DrawableRes | ||||
|         @Override | ||||
|         public int getTabIconRes(Context context) { | ||||
|             return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_channel); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public ChannelFragment getFragment() { | ||||
|             return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void writeDataToJson(JsonSink writerSink) { | ||||
|             writerSink.value(JSON_CHANNEL_SERVICE_ID_KEY, channelServiceId) | ||||
|                     .value(JSON_CHANNEL_URL_KEY, channelUrl) | ||||
|                     .value(JSON_CHANNEL_NAME_KEY, channelName); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void readDataFromJson(JsonObject jsonObject) { | ||||
|             channelServiceId = jsonObject.getInt(JSON_CHANNEL_SERVICE_ID_KEY, -1); | ||||
|             channelUrl = jsonObject.getString(JSON_CHANNEL_URL_KEY, "<no-url>"); | ||||
|             channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, "<no-name>"); | ||||
|         } | ||||
|  | ||||
|         public int getChannelServiceId() { | ||||
|             return channelServiceId; | ||||
|         } | ||||
|  | ||||
|         public String getChannelUrl() { | ||||
|             return channelUrl; | ||||
|         } | ||||
|  | ||||
|         public String getChannelName() { | ||||
|             return channelName; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,114 @@ | ||||
| package org.schabi.newpipe.settings.tabs; | ||||
|  | ||||
| import android.support.annotation.Nullable; | ||||
|  | ||||
| import com.grack.nanojson.JsonArray; | ||||
| import com.grack.nanojson.JsonObject; | ||||
| import com.grack.nanojson.JsonParser; | ||||
| import com.grack.nanojson.JsonParserException; | ||||
| import com.grack.nanojson.JsonStringWriter; | ||||
| import com.grack.nanojson.JsonWriter; | ||||
|  | ||||
| import org.schabi.newpipe.settings.tabs.Tab.Type; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| import static org.schabi.newpipe.extractor.ServiceList.YouTube; | ||||
|  | ||||
| /** | ||||
|  * Class to get a JSON representation of a list of tabs, and the other way around. | ||||
|  */ | ||||
| public class TabsJsonHelper { | ||||
|     private static final String JSON_TABS_ARRAY_KEY = "tabs"; | ||||
|  | ||||
|     protected static final List<Tab> FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList(Arrays.asList( | ||||
|             new Tab.KioskTab(YouTube.getServiceId(), "Trending"), | ||||
|             Type.SUBSCRIPTIONS.getTab(), | ||||
|             Type.BOOKMARKS.getTab() | ||||
|     )); | ||||
|  | ||||
|     public static class InvalidJsonException extends Exception { | ||||
|         private InvalidJsonException() { | ||||
|             super(); | ||||
|         } | ||||
|  | ||||
|         private InvalidJsonException(String message) { | ||||
|             super(message); | ||||
|         } | ||||
|  | ||||
|         private InvalidJsonException(Throwable cause) { | ||||
|             super(cause); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Try to reads the passed JSON and returns the list of tabs if no error were encountered. | ||||
|      * <p> | ||||
|      * If the JSON is null or empty, or the list of tabs that it represents is empty, the | ||||
|      * {@link #FALLBACK_INITIAL_TABS_LIST fallback list} will be returned. | ||||
|      * <p> | ||||
|      * Tabs with invalid ids (i.e. not in the {@link Tab.Type} enum) will be ignored. | ||||
|      * | ||||
|      * @param tabsJson a JSON string got from {@link #getJsonToSave(List)}. | ||||
|      * @return a list of {@link Tab tabs}. | ||||
|      * @throws InvalidJsonException if the JSON string is not valid | ||||
|      */ | ||||
|     public static List<Tab> getTabsFromJson(@Nullable String tabsJson) throws InvalidJsonException { | ||||
|         if (tabsJson == null || tabsJson.isEmpty()) { | ||||
|             return FALLBACK_INITIAL_TABS_LIST; | ||||
|         } | ||||
|  | ||||
|         final List<Tab> returnTabs = new ArrayList<>(); | ||||
|  | ||||
|         final JsonObject outerJsonObject; | ||||
|         try { | ||||
|             outerJsonObject = JsonParser.object().from(tabsJson); | ||||
|             final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY); | ||||
|  | ||||
|             if (tabsArray == null) { | ||||
|                 throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY + "\" array"); | ||||
|             } | ||||
|  | ||||
|             for (Object o : tabsArray) { | ||||
|                 if (!(o instanceof JsonObject)) continue; | ||||
|  | ||||
|                 final Tab tab = Tab.from((JsonObject) o); | ||||
|  | ||||
|                 if (tab != null) { | ||||
|                     returnTabs.add(tab); | ||||
|                 } | ||||
|             } | ||||
|         } catch (JsonParserException e) { | ||||
|             throw new InvalidJsonException(e); | ||||
|         } | ||||
|  | ||||
|         if (returnTabs.isEmpty()) { | ||||
|             return FALLBACK_INITIAL_TABS_LIST; | ||||
|         } | ||||
|  | ||||
|         return returnTabs; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a JSON representation from a list of tabs. | ||||
|      * | ||||
|      * @param tabList a list of {@link Tab tabs}. | ||||
|      * @return a JSON string representing the list of tabs | ||||
|      */ | ||||
|     public static String getJsonToSave(@Nullable List<Tab> tabList) { | ||||
|         final JsonStringWriter jsonWriter = JsonWriter.string(); | ||||
|         jsonWriter.object(); | ||||
|  | ||||
|         jsonWriter.array(JSON_TABS_ARRAY_KEY); | ||||
|         if (tabList != null) for (Tab tab : tabList) { | ||||
|             tab.writeJsonOn(jsonWriter); | ||||
|         } | ||||
|         jsonWriter.end(); | ||||
|  | ||||
|         jsonWriter.end(); | ||||
|         return jsonWriter.done(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,93 @@ | ||||
| package org.schabi.newpipe.settings.tabs; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| public class TabsManager { | ||||
|     private final SharedPreferences sharedPreferences; | ||||
|     private final String savedTabsKey; | ||||
|     private final Context context; | ||||
|  | ||||
|     public static TabsManager getManager(Context context) { | ||||
|         return new TabsManager(context); | ||||
|     } | ||||
|  | ||||
|     private TabsManager(Context context) { | ||||
|         this.context = context; | ||||
|         this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|         this.savedTabsKey = context.getString(R.string.saved_tabs_key); | ||||
|     } | ||||
|  | ||||
|     public List<Tab> getTabs() { | ||||
|         final String savedJson = sharedPreferences.getString(savedTabsKey, null); | ||||
|         try { | ||||
|             return TabsJsonHelper.getTabsFromJson(savedJson); | ||||
|         } catch (TabsJsonHelper.InvalidJsonException e) { | ||||
|             Toast.makeText(context, R.string.saved_tabs_invalid_json, Toast.LENGTH_SHORT).show(); | ||||
|             return getDefaultTabs(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void saveTabs(List<Tab> tabList) { | ||||
|         final String jsonToSave = TabsJsonHelper.getJsonToSave(tabList); | ||||
|         sharedPreferences.edit().putString(savedTabsKey, jsonToSave).apply(); | ||||
|     } | ||||
|  | ||||
|     public void resetTabs() { | ||||
|         sharedPreferences.edit().remove(savedTabsKey).apply(); | ||||
|     } | ||||
|  | ||||
|     public List<Tab> getDefaultTabs() { | ||||
|         return TabsJsonHelper.FALLBACK_INITIAL_TABS_LIST; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Listener | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public interface SavedTabsChangeListener { | ||||
|         void onTabsChanged(); | ||||
|     } | ||||
|  | ||||
|     private SavedTabsChangeListener savedTabsChangeListener; | ||||
|     private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; | ||||
|  | ||||
|     public void setSavedTabsListener(SavedTabsChangeListener listener) { | ||||
|         if (preferenceChangeListener != null) { | ||||
|             sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener); | ||||
|         } | ||||
|         savedTabsChangeListener = listener; | ||||
|         preferenceChangeListener = getPreferenceChangeListener(); | ||||
|         sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener); | ||||
|     } | ||||
|  | ||||
|     public void unsetSavedTabsListener() { | ||||
|         if (preferenceChangeListener != null) { | ||||
|             sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener); | ||||
|         } | ||||
|         preferenceChangeListener = null; | ||||
|         savedTabsChangeListener = null; | ||||
|     } | ||||
|  | ||||
|     private SharedPreferences.OnSharedPreferenceChangeListener getPreferenceChangeListener() { | ||||
|         return (sharedPreferences, key) -> { | ||||
|             if (key.equals(savedTabsKey)) { | ||||
|                 if (savedTabsChangeListener != null) savedTabsChangeListener.onTabsChanged(); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -6,7 +6,7 @@ public class Constants { | ||||
|     public static final String KEY_TITLE = "key_title"; | ||||
|     public static final String KEY_LINK_TYPE = "key_link_type"; | ||||
|     public static final String KEY_OPEN_SEARCH = "key_open_search"; | ||||
|     public static final String KEY_QUERY = "key_query"; | ||||
|     public static final String KEY_SEARCH_STRING = "key_search_string"; | ||||
|  | ||||
|     public static final String KEY_THEME_CHANGE = "key_theme_change"; | ||||
|     public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; | ||||
|   | ||||
| @@ -37,9 +37,8 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException; | ||||
| import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; | ||||
| import org.schabi.newpipe.extractor.kiosk.KioskInfo; | ||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | ||||
| import org.schabi.newpipe.extractor.search.SearchEngine; | ||||
| import org.schabi.newpipe.extractor.search.SearchResult; | ||||
| import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; | ||||
| import org.schabi.newpipe.extractor.search.SearchInfo; | ||||
| import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| @@ -50,7 +49,6 @@ import java.util.List; | ||||
|  | ||||
| import io.reactivex.Maybe; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.annotations.NonNull; | ||||
|  | ||||
| public final class ExtractorHelper { | ||||
|     private static final String TAG = ExtractorHelper.class.getSimpleName(); | ||||
| @@ -66,29 +64,35 @@ public final class ExtractorHelper { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static Single<SearchResult> searchFor(final int serviceId, | ||||
|                                                  final String query, | ||||
|                                                  final int pageNumber, | ||||
|                                                  final String contentCountry, | ||||
|                                                  final SearchEngine.Filter filter) { | ||||
|     public static Single<SearchInfo> searchFor(final int serviceId, | ||||
|                                                final String searchString, | ||||
|                                                final List<String> contentFilter, | ||||
|                                                final String sortFilter, | ||||
|                                                final String contentCountry) { | ||||
|         checkServiceId(serviceId); | ||||
|         return Single.fromCallable(() -> | ||||
|             SearchResult.getSearchResult(NewPipe.getService(serviceId).getSearchEngine(), | ||||
|                     query, pageNumber, contentCountry, filter) | ||||
|         ); | ||||
|             SearchInfo.getInfo(NewPipe.getService(serviceId), | ||||
|                     NewPipe.getService(serviceId) | ||||
|                         .getSearchQHFactory() | ||||
|                         .fromQuery(searchString, contentFilter, sortFilter), | ||||
|                     contentCountry)); | ||||
|     } | ||||
|  | ||||
|     public static Single<InfoItemsPage> getMoreSearchItems(final int serviceId, | ||||
|                                                              final String query, | ||||
|                                                              final int nextPageNumber, | ||||
|                                                              final String searchLanguage, | ||||
|                                                              final SearchEngine.Filter filter) { | ||||
|                                                            final String searchString, | ||||
|                                                            final List<String> contentFilter, | ||||
|                                                            final String sortFilter, | ||||
|                                                            final String pageUrl, | ||||
|                                                            final String contentCountry) { | ||||
|         checkServiceId(serviceId); | ||||
|         return searchFor(serviceId, query, nextPageNumber, searchLanguage, filter) | ||||
|                 .map((@NonNull SearchResult searchResult) -> | ||||
|                         new InfoItemsPage(searchResult.resultList, | ||||
|                                 nextPageNumber + "", | ||||
|                                 searchResult.errors)); | ||||
|         return Single.fromCallable(() -> | ||||
|                 SearchInfo.getMoreItems(NewPipe.getService(serviceId), | ||||
|                         NewPipe.getService(serviceId) | ||||
|                             .getSearchQHFactory() | ||||
|                             .fromQuery(searchString, contentFilter, sortFilter), | ||||
|                         contentCountry, | ||||
|                         pageUrl)); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public static Single<List<String>> suggestionsFor(final int serviceId, | ||||
| @@ -233,7 +237,6 @@ public final class ExtractorHelper { | ||||
|                         serviceId == -1 ? "none" : NewPipe.getNameOfService(serviceId), url + (optionalErrorMessage == null ? "" : optionalErrorMessage), errorId)); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -24,7 +24,7 @@ import org.schabi.newpipe.R; | ||||
|  | ||||
| public class KioskTranslator { | ||||
|     public static String getTranslatedKioskName(String kioskId, Context c) { | ||||
|         switch(kioskId) { | ||||
|         switch (kioskId) { | ||||
|             case "Trending": | ||||
|                 return c.getString(R.string.trending); | ||||
|             case "Top 50": | ||||
| @@ -35,4 +35,17 @@ public class KioskTranslator { | ||||
|                 return kioskId; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static int getKioskIcons(String kioskId, Context c) { | ||||
|         switch(kioskId) { | ||||
|             case "Trending": | ||||
|                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); | ||||
|             case "Top 50": | ||||
|                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); | ||||
|             case "New & hot": | ||||
|                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); | ||||
|             default: | ||||
|                 return 0; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -443,11 +443,11 @@ public final class ListHelper { | ||||
|     /** | ||||
|      * Are we connected to wifi? | ||||
|      * @param context App context | ||||
|      * @return True if connected to wifi | ||||
|      * @return {@code true} if connected to wifi | ||||
|      */ | ||||
|     private static boolean isWifiActive(Context context) | ||||
|     { | ||||
|         ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); | ||||
|         return manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI; | ||||
|         return manager != null && manager.getActiveNetworkInfo() != null && manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,7 @@ import org.schabi.newpipe.download.DownloadActivity; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.search.SearchExtractor; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.Stream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| @@ -33,12 +34,14 @@ import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.fragments.MainFragment; | ||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||
| import org.schabi.newpipe.fragments.list.channel.ChannelFragment; | ||||
| import org.schabi.newpipe.local.bookmark.BookmarkFragment; | ||||
| import org.schabi.newpipe.local.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.local.history.StatisticsPlaylistFragment; | ||||
| import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; | ||||
| import org.schabi.newpipe.local.subscription.SubscriptionFragment; | ||||
| import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; | ||||
| import org.schabi.newpipe.player.BackgroundPlayer; | ||||
| import org.schabi.newpipe.player.BackgroundPlayerActivity; | ||||
| @@ -100,11 +103,13 @@ public class NavigationHelper { | ||||
|                                          final int repeatMode, | ||||
|                                          final float playbackSpeed, | ||||
|                                          final float playbackPitch, | ||||
|                                          final boolean playbackSkipSilence, | ||||
|                                          @Nullable final String playbackQuality) { | ||||
|         return getPlayerIntent(context, targetClazz, playQueue, playbackQuality) | ||||
|                 .putExtra(BasePlayer.REPEAT_MODE, repeatMode) | ||||
|                 .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) | ||||
|                 .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch); | ||||
|                 .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch) | ||||
|                 .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence); | ||||
|     } | ||||
|  | ||||
|     public static void playOnMainPlayer(final Context context, final PlayQueue queue) { | ||||
| @@ -281,9 +286,11 @@ public class NavigationHelper { | ||||
|         return fragmentManager.popBackStackImmediate(SEARCH_FRAGMENT_TAG, 0); | ||||
|     } | ||||
|  | ||||
|     public static void openSearchFragment(FragmentManager fragmentManager, int serviceId, String query) { | ||||
|     public static void openSearchFragment(FragmentManager fragmentManager, | ||||
|                                           int serviceId, | ||||
|                                           String searchString) { | ||||
|         defaultTransaction(fragmentManager) | ||||
|                 .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, query)) | ||||
|                 .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, searchString)) | ||||
|                 .addToBackStack(SEARCH_FRAGMENT_TAG) | ||||
|                 .commit(); | ||||
|     } | ||||
| @@ -312,7 +319,11 @@ public class NavigationHelper { | ||||
|                 .commit(); | ||||
|     } | ||||
|  | ||||
|     public static void openChannelFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { | ||||
|     public static void openChannelFragment( | ||||
|             FragmentManager fragmentManager, | ||||
|             int serviceId, | ||||
|             String url, | ||||
|             String name) { | ||||
|         if (name == null) name = ""; | ||||
|         defaultTransaction(fragmentManager) | ||||
|                 .replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name)) | ||||
| @@ -320,7 +331,10 @@ public class NavigationHelper { | ||||
|                 .commit(); | ||||
|     } | ||||
|  | ||||
|     public static void openPlaylistFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { | ||||
|     public static void openPlaylistFragment(FragmentManager fragmentManager, | ||||
|                                             int serviceId, | ||||
|                                             String url, | ||||
|                                             String name) { | ||||
|         if (name == null) name = ""; | ||||
|         defaultTransaction(fragmentManager) | ||||
|                 .replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name)) | ||||
| @@ -335,6 +349,20 @@ public class NavigationHelper { | ||||
|                 .commit(); | ||||
|     } | ||||
|  | ||||
|     public static void openBookmarksFragment(FragmentManager fragmentManager) { | ||||
|         defaultTransaction(fragmentManager) | ||||
|                 .replace(R.id.fragment_holder, new BookmarkFragment()) | ||||
|                 .addToBackStack(null) | ||||
|                 .commit(); | ||||
|     } | ||||
|  | ||||
|     public static void openSubscriptionFragment(FragmentManager fragmentManager) { | ||||
|         defaultTransaction(fragmentManager) | ||||
|                 .replace(R.id.fragment_holder, new SubscriptionFragment()) | ||||
|                 .addToBackStack(null) | ||||
|                 .commit(); | ||||
|     } | ||||
|  | ||||
|     public static void openKioskFragment(FragmentManager fragmentManager, int serviceId, String kioskId) throws ExtractionException { | ||||
|         defaultTransaction(fragmentManager) | ||||
|                 .replace(R.id.fragment_holder, KioskFragment.getInstance(serviceId, kioskId)) | ||||
| @@ -368,10 +396,10 @@ public class NavigationHelper { | ||||
|     // Through Intents | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public static void openSearch(Context context, int serviceId, String query) { | ||||
|     public static void openSearch(Context context, int serviceId, String searchString) { | ||||
|         Intent mIntent = new Intent(context, MainActivity.class); | ||||
|         mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); | ||||
|         mIntent.putExtra(Constants.KEY_QUERY, query); | ||||
|         mIntent.putExtra(Constants.KEY_SEARCH_STRING, searchString); | ||||
|         mIntent.putExtra(Constants.KEY_OPEN_SEARCH, true); | ||||
|         context.startActivity(mIntent); | ||||
|     } | ||||
| @@ -465,7 +493,8 @@ public class NavigationHelper { | ||||
|  | ||||
|         switch (linkType) { | ||||
|             case STREAM: | ||||
|                 rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context) | ||||
|                 rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, | ||||
|                         PreferenceManager.getDefaultSharedPreferences(context) | ||||
|                         .getBoolean(context.getString(R.string.autoplay_through_intent_key), false)); | ||||
|                 break; | ||||
|         } | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import android.preference.PreferenceManager; | ||||
| import android.support.annotation.DrawableRes; | ||||
| import android.support.annotation.StringRes; | ||||
|  | ||||
| import org.schabi.newpipe.BuildConfig; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| @@ -31,6 +30,18 @@ public class ServiceHelper { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static String getTranslatedFilterString(String filter, Context c) { | ||||
|         switch(filter) { | ||||
|             case "all": return c.getString(R.string.all); | ||||
|             case "videos": return c.getString(R.string.videos); | ||||
|             case "channels": return c.getString(R.string.channels); | ||||
|             case "playlists": return c.getString(R.string.playlists); | ||||
|             case "tracks": return c.getString(R.string.tracks); | ||||
|             case "users": return c.getString(R.string.users); | ||||
|             default: return filter; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a resource string with instructions for importing subscriptions for each service. | ||||
|      * | ||||
|   | ||||
| @@ -1,10 +1,17 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.os.Handler; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.util.Log; | ||||
|  | ||||
| import org.schabi.newpipe.download.ExtSDDownloadFailedActivity; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FilenameFilter; | ||||
| import java.io.IOException; | ||||
| import java.io.RandomAccessFile; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| @@ -23,7 +30,9 @@ public class DownloadManagerImpl implements DownloadManager { | ||||
|     private static final String TAG = DownloadManagerImpl.class.getSimpleName(); | ||||
|     private final DownloadDataSource mDownloadDataSource; | ||||
|  | ||||
|     private final ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>(); | ||||
|     private final ArrayList<DownloadMission> mMissions = new ArrayList<>(); | ||||
|     @NonNull | ||||
|     private final Context context; | ||||
|  | ||||
|     /** | ||||
|      * Create a new instance | ||||
| @@ -33,6 +42,13 @@ public class DownloadManagerImpl implements DownloadManager { | ||||
|      */ | ||||
|     public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource) { | ||||
|         mDownloadDataSource = downloadDataSource; | ||||
|         this.context = null; | ||||
|         loadMissions(searchLocations); | ||||
|     } | ||||
|  | ||||
|     public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource, Context context) { | ||||
|         mDownloadDataSource = downloadDataSource; | ||||
|         this.context = context; | ||||
|         loadMissions(searchLocations); | ||||
|     } | ||||
|  | ||||
| @@ -277,10 +293,12 @@ public class DownloadManagerImpl implements DownloadManager { | ||||
|     } | ||||
|  | ||||
|     private class Initializer extends Thread { | ||||
|         private DownloadMission mission; | ||||
|         private final DownloadMission mission; | ||||
|         private final Handler handler; | ||||
|  | ||||
|         public Initializer(DownloadMission mission) { | ||||
|             this.mission = mission; | ||||
|             this.handler = new Handler(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -335,6 +353,13 @@ public class DownloadManagerImpl implements DownloadManager { | ||||
|                 af.close(); | ||||
|  | ||||
|                 mission.start(); | ||||
|             } catch (IOException ie) { | ||||
|                 if(context == null) throw new RuntimeException(ie); | ||||
|  | ||||
|                 if(ie.getMessage().contains("Permission denied")) { | ||||
|                     handler.post(() -> | ||||
|                         context.startActivity(new Intent(context, ExtSDDownloadFailedActivity.class))); | ||||
|                 } else throw new RuntimeException(ie); | ||||
|             } catch (Exception e) { | ||||
|                 // TODO Notify | ||||
|                 throw new RuntimeException(e); | ||||
|   | ||||
| @@ -81,7 +81,7 @@ public class DownloadManagerService extends Service { | ||||
|             ArrayList<String> paths = new ArrayList<>(2); | ||||
|             paths.add(NewPipeSettings.getVideoDownloadPath(this)); | ||||
|             paths.add(NewPipeSettings.getAudioDownloadPath(this)); | ||||
|             mManager = new DownloadManagerImpl(paths, mDataSource); | ||||
|             mManager = new DownloadManagerImpl(paths, mDataSource, this); | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "mManager == null"); | ||||
|                 Log.d(TAG, "Download directory: " + paths); | ||||
|   | ||||
| @@ -25,10 +25,13 @@ import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.download.DeleteDownloadManager; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
|  | ||||
| @@ -52,18 +55,34 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|  | ||||
|     private Activity mContext; | ||||
|     private LayoutInflater mInflater; | ||||
|     private DownloadManager mManager; | ||||
|     private DownloadManager mDownloadManager; | ||||
|     private DeleteDownloadManager mDeleteDownloadManager; | ||||
|     private List<DownloadMission> mItemList; | ||||
|     private DownloadManagerService.DMBinder mBinder; | ||||
|     private int mLayout; | ||||
|  | ||||
|     public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager manager, boolean isLinear) { | ||||
|     public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager downloadManager, DeleteDownloadManager deleteDownloadManager, boolean isLinear) { | ||||
|         mContext = context; | ||||
|         mManager = manager; | ||||
|         mDownloadManager = downloadManager; | ||||
|         mDeleteDownloadManager = deleteDownloadManager; | ||||
|         mBinder = binder; | ||||
|  | ||||
|         mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); | ||||
|  | ||||
|         mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item; | ||||
|  | ||||
|         mItemList = new ArrayList<>(); | ||||
|         updateItemList(); | ||||
|     } | ||||
|  | ||||
|     public void updateItemList() { | ||||
|         mItemList.clear(); | ||||
|  | ||||
|         for (int i = 0; i < mDownloadManager.getCount(); i++) { | ||||
|             DownloadMission mission = mDownloadManager.getMission(i); | ||||
|             if (!mDeleteDownloadManager.contains(mission)) { | ||||
|                 mItemList.add(mDownloadManager.getMission(i)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -102,7 +121,7 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|  | ||||
|     @Override | ||||
|     public void onBindViewHolder(MissionAdapter.ViewHolder h, int pos) { | ||||
|         DownloadMission ms = mManager.getMission(pos); | ||||
|         DownloadMission ms = mItemList.get(pos); | ||||
|         h.mission = ms; | ||||
|         h.position = pos; | ||||
|  | ||||
| @@ -123,7 +142,7 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|  | ||||
|     @Override | ||||
|     public int getItemCount() { | ||||
|         return mManager.getCount(); | ||||
|         return mItemList.size(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -214,12 +233,12 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|                 int id = item.getItemId(); | ||||
|                 switch (id) { | ||||
|                     case R.id.start: | ||||
|                         mManager.resumeMission(h.position); | ||||
|                         mBinder.onMissionAdded(mManager.getMission(h.position)); | ||||
|                         mDownloadManager.resumeMission(h.position); | ||||
|                         mBinder.onMissionAdded(mItemList.get(h.position)); | ||||
|                         return true; | ||||
|                     case R.id.pause: | ||||
|                         mManager.pauseMission(h.position); | ||||
|                         mBinder.onMissionRemoved(mManager.getMission(h.position)); | ||||
|                         mDownloadManager.pauseMission(h.position); | ||||
|                         mBinder.onMissionRemoved(mItemList.get(h.position)); | ||||
|                         h.lastTimeStamp = -1; | ||||
|                         h.lastDone = -1; | ||||
|                         return true; | ||||
| @@ -245,12 +264,13 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|  | ||||
|                         return true; | ||||
|                     case R.id.delete: | ||||
|                         mManager.deleteMission(h.position); | ||||
|                         mDeleteDownloadManager.add(h.mission); | ||||
|                         updateItemList(); | ||||
|                         notifyDataSetChanged(); | ||||
|                         return true; | ||||
|                     case R.id.md5: | ||||
|                     case R.id.sha1: | ||||
|                         DownloadMission mission = mManager.getMission(h.position); | ||||
|                         DownloadMission mission = mItemList.get(h.position); | ||||
|                         new ChecksumTask(mContext).execute(mission.location + "/" + mission.name, ALGORITHMS.get(id)); | ||||
|                         return true; | ||||
|                     default: | ||||
| @@ -262,19 +282,6 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|         popup.show(); | ||||
|     } | ||||
|  | ||||
|     private void viewFile(File file, String mimetype) { | ||||
|         Intent intent = new Intent(); | ||||
|         intent.setAction(Intent.ACTION_VIEW); | ||||
|         intent.setDataAndType(Uri.fromFile(file), mimetype); | ||||
|         intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|             intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); | ||||
|         } | ||||
|         //mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||||
|         Log.v(TAG, "Starting intent: " + intent); | ||||
|         mContext.startActivity(intent); | ||||
|     } | ||||
|  | ||||
|     private void viewFileWithFileProvider(File file, String mimetype) { | ||||
|         String ourPackage = mContext.getApplicationContext().getPackageName(); | ||||
|         Uri uri = FileProvider.getUriForFile(mContext, ourPackage + ".provider", file); | ||||
|   | ||||
| @@ -10,6 +10,8 @@ import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| import android.os.IBinder; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v7.widget.GridLayoutManager; | ||||
| import android.support.v7.widget.LinearLayoutManager; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| @@ -19,13 +21,15 @@ import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.download.DeleteDownloadManager; | ||||
|  | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import us.shandian.giga.get.DownloadManager; | ||||
| import us.shandian.giga.service.DownloadManagerService; | ||||
| import us.shandian.giga.ui.adapter.MissionAdapter; | ||||
|  | ||||
| public abstract class MissionsFragment extends Fragment { | ||||
|     private DownloadManager mManager; | ||||
|     private DownloadManager mDownloadManager; | ||||
|     private DownloadManagerService.DMBinder mBinder; | ||||
|  | ||||
|     private SharedPreferences mPrefs; | ||||
| @@ -37,14 +41,19 @@ public abstract class MissionsFragment extends Fragment { | ||||
|     private GridLayoutManager mGridManager; | ||||
|     private LinearLayoutManager mLinearManager; | ||||
|     private Context mActivity; | ||||
|     private DeleteDownloadManager mDeleteDownloadManager; | ||||
|     private Disposable mDeleteDisposable; | ||||
|  | ||||
|     private ServiceConnection mConnection = new ServiceConnection() { | ||||
|  | ||||
|         @Override | ||||
|         public void onServiceConnected(ComponentName name, IBinder binder) { | ||||
|             mBinder = (DownloadManagerService.DMBinder) binder; | ||||
|             mManager = setupDownloadManager(mBinder); | ||||
|             updateList(); | ||||
|             mDownloadManager = setupDownloadManager(mBinder); | ||||
|             if (mDeleteDownloadManager != null) { | ||||
|                 mDeleteDownloadManager.setDownloadManager(mDownloadManager); | ||||
|                 updateList(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -55,6 +64,14 @@ public abstract class MissionsFragment extends Fragment { | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     public void setDeleteManager(@NonNull DeleteDownloadManager deleteDownloadManager) { | ||||
|         mDeleteDownloadManager = deleteDownloadManager; | ||||
|         if (mDownloadManager != null) { | ||||
|             mDeleteDownloadManager.setDownloadManager(mDownloadManager); | ||||
|             updateList(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||
|         View v = inflater.inflate(R.layout.missions, container, false); | ||||
| @@ -104,10 +121,26 @@ public abstract class MissionsFragment extends Fragment { | ||||
|         mActivity = activity; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
|         if (mDeleteDownloadManager != null) { | ||||
|             mDeleteDisposable = mDeleteDownloadManager.getUndoObservable().subscribe(mission -> { | ||||
|                 if (mAdapter != null) { | ||||
|                     mAdapter.updateItemList(); | ||||
|                     mAdapter.notifyDataSetChanged(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         getActivity().unbindService(mConnection); | ||||
|         if (mDeleteDisposable != null) { | ||||
|             mDeleteDisposable.dispose(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -129,7 +162,7 @@ public abstract class MissionsFragment extends Fragment { | ||||
|     } | ||||
|  | ||||
|     private void updateList() { | ||||
|         mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mManager, mLinear); | ||||
|         mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mDownloadManager, mDeleteDownloadManager, mLinear); | ||||
|  | ||||
|         if (mLinear) { | ||||
|             mList.setLayoutManager(mLinearManager); | ||||
| @@ -143,7 +176,7 @@ public abstract class MissionsFragment extends Fragment { | ||||
|             mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list); | ||||
|         } | ||||
|  | ||||
|         mPrefs.edit().putBoolean("linear", mLinear).commit(); | ||||
|         mPrefs.edit().putBoolean("linear", mLinear).apply(); | ||||
|     } | ||||
|  | ||||
|     protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder); | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_add.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 223 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_arrow_down_white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 267 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_arrow_up_white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 261 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_remove.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 363 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_add.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 152 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_arrow_down_white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 210 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_arrow_up_white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 201 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_remove.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 230 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_add.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 197 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_arrow_down_white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 425 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_arrow_up_white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 415 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_remove.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 380 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_add.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 351 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_arrow_down_white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 470 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_arrow_up_white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 458 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_remove.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 611 B | 
| @@ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <shape xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:shape="oval"> | ||||
|     <solid android:color="#64000000" /> | ||||
| </shape> | ||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_add_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="24dp" | ||||
|         android:height="24dp" | ||||
|         android:viewportWidth="24.0" | ||||
|         android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> | ||||
| </vector> | ||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_add_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| <vector android:height="24dp" android:tint="#FFFFFF" | ||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" | ||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> | ||||
| </vector> | ||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_blank_page_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="24dp" | ||||
|         android:height="24dp" | ||||
|         android:viewportWidth="24.0" | ||||
|         android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M17,3L7,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,5c0,-1.1 -0.9,-2 -2,-2zM17,19L7,19L7,5h10v14z"/> | ||||
| </vector> | ||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_blank_page_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="24dp" | ||||
|         android:height="24dp" | ||||
|         android:viewportWidth="24.0" | ||||
|         android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFFFF" | ||||
|         android:pathData="M17,3L7,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,5c0,-1.1 -0.9,-2 -2,-2zM17,19L7,19L7,5h10v14z"/> | ||||
| </vector> | ||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_brightness_high_white_72dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="72dp" | ||||
|     android:height="72dp" | ||||
|     android:tint="#FFFFFF" | ||||
|     android:viewportWidth="24.0" | ||||
|     android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M20,8.69L20,4h-4.69L12,0.69 8.69,4L4,4v4.69L0.69,12 4,15.31L4,20h4.69L12,23.31 15.31,20L20,20v-4.69L23.31,12 20,8.69zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4z" /> | ||||
| </vector> | ||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_brightness_low_white_72dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="72dp" | ||||
|     android:height="72dp" | ||||
|     android:tint="#FFFFFF" | ||||
|     android:viewportWidth="24.0" | ||||
|     android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M20,15.31L23.31,12 20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z" /> | ||||
| </vector> | ||||
| @@ -0,0 +1,10 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="72dp" | ||||
|     android:height="72dp" | ||||
|     android:tint="#FFFFFF" | ||||
|     android:viewportWidth="24.0" | ||||
|     android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M20,15.31L23.31,12 20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69zM12,18V6c3.31,0 6,2.69 6,6s-2.69,6 -6,6z" /> | ||||
| </vector> | ||||
| @@ -0,0 +1,9 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="24dp" | ||||
|         android:height="24dp" | ||||
|         android:viewportWidth="24.0" | ||||
|         android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M14,12c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2 0.9,2 2,2 2,-0.9 2,-2zM12,3c-4.97,0 -9,4.03 -9,9L0,12l4,4 4,-4L5,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.51,0 -2.91,-0.49 -4.06,-1.3l-1.42,1.44C8.04,20.3 9.94,21 12,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9z"/> | ||||
| </vector> | ||||
| @@ -0,0 +1,9 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="24dp" | ||||
|         android:height="24dp" | ||||
|         android:viewportWidth="24.0" | ||||
|         android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFFFF" | ||||
|         android:pathData="M14,12c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2 0.9,2 2,2 2,-0.9 2,-2zM12,3c-4.97,0 -9,4.03 -9,9L0,12l4,4 4,-4L5,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.51,0 -2.91,-0.49 -4.06,-1.3l-1.42,1.44C8.04,20.3 9.94,21 12,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9z"/> | ||||
| </vector> | ||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_volume_down_white_72dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="72dp" | ||||
|     android:height="72dp" | ||||
|     android:tint="#FFFFFF" | ||||
|     android:viewportWidth="24.0" | ||||
|     android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M18.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM5,9v6h4l5,5V4L9,9H5z" /> | ||||
| </vector> | ||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_volume_mute_white_72dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="72dp" | ||||
|     android:height="72dp" | ||||
|     android:tint="#FFFFFF" | ||||
|     android:viewportWidth="24.0" | ||||
|     android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M7,9v6h4l5,5V4l-5,5H7z" /> | ||||
| </vector> | ||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_volume_off_white_72dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="72dp" | ||||
|     android:height="72dp" | ||||
|     android:tint="#FFFFFF" | ||||
|     android:viewportWidth="24.0" | ||||
|     android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z" /> | ||||
| </vector> | ||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_volume_up_white_72dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="72dp" | ||||
|     android:height="72dp" | ||||
|     android:tint="#FFFFFF" | ||||
|     android:viewportWidth="24.0" | ||||
|     android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z" /> | ||||
| </vector> | ||||
							
								
								
									
										12
									
								
								app/src/main/res/drawable/progress_circular_white.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <rotate xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:fromDegrees="-90" | ||||
|     android:toDegrees="-90"> | ||||
|     <shape xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:innerRadiusRatio="2.25" | ||||
|         android:shape="ring" | ||||
|         android:thicknessRatio="17.75" | ||||
|         android:useLevel="true"> | ||||
|         <solid android:color="@android:color/white" /> | ||||
|     </shape> | ||||
| </rotate> | ||||
 Kartikey Kushwaha
					Kartikey Kushwaha