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));
 | 
			
		||||
        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 {
 | 
			
		||||
    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();
 | 
			
		||||
@@ -333,9 +515,6 @@ public class MainActivity extends AppCompatActivity {
 | 
			
		||||
            case R.id.action_history:
 | 
			
		||||
                    NavigationHelper.openStatisticFragment(getSupportFragmentManager());
 | 
			
		||||
                    return true;
 | 
			
		||||
            case R.id.action_about:
 | 
			
		||||
                NavigationHelper.openAbout(this);
 | 
			
		||||
                return true;
 | 
			
		||||
            case R.id.action_settings:
 | 
			
		||||
                    NavigationHelper.openSettings(this);
 | 
			
		||||
                    return true;
 | 
			
		||||
@@ -382,6 +561,7 @@ public class MainActivity extends AppCompatActivity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handleIntent(Intent intent) {
 | 
			
		||||
        try {
 | 
			
		||||
            if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
 | 
			
		||||
 | 
			
		||||
            if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
 | 
			
		||||
@@ -394,19 +574,32 @@ public class MainActivity extends AppCompatActivity {
 | 
			
		||||
                        NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case CHANNEL:
 | 
			
		||||
                    NavigationHelper.openChannelFragment(getSupportFragmentManager(), serviceId, url, title);
 | 
			
		||||
                        NavigationHelper.openChannelFragment(getSupportFragmentManager(),
 | 
			
		||||
                                serviceId,
 | 
			
		||||
                                url,
 | 
			
		||||
                                title);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case PLAYLIST:
 | 
			
		||||
                    NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), serviceId, url, title);
 | 
			
		||||
                        NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
 | 
			
		||||
                                serviceId,
 | 
			
		||||
                                url,
 | 
			
		||||
                                title);
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
            } else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
 | 
			
		||||
            String searchQuery = intent.getStringExtra(Constants.KEY_QUERY);
 | 
			
		||||
            if (searchQuery == null) searchQuery = "";
 | 
			
		||||
                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, searchQuery);
 | 
			
		||||
                NavigationHelper.openSearchFragment(
 | 
			
		||||
                        getSupportFragmentManager(),
 | 
			
		||||
                        serviceId,
 | 
			
		||||
                        searchString);
 | 
			
		||||
 | 
			
		||||
            } 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);
 | 
			
		||||
@@ -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,7 +47,13 @@ public class DownloadActivity extends AppCompatActivity {
 | 
			
		||||
            actionBar.setDisplayShowTitleEnabled(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Fragment
 | 
			
		||||
        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() {
 | 
			
		||||
@@ -51,12 +62,20 @@ public class DownloadActivity extends AppCompatActivity {
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @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");
 | 
			
		||||
            }
 | 
			
		||||
        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,28 +72,32 @@ 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);
 | 
			
		||||
 | 
			
		||||
        int channelIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_channel);
 | 
			
		||||
        int whatsHotIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_hot);
 | 
			
		||||
        int bookmarkIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_bookmark);
 | 
			
		||||
 | 
			
		||||
        if (isSubscriptionsPageOnlySelected()) {
 | 
			
		||||
            tabLayout.getTabAt(0).setIcon(channelIcon);
 | 
			
		||||
            tabLayout.getTabAt(1).setIcon(bookmarkIcon);
 | 
			
		||||
        } else {
 | 
			
		||||
            tabLayout.getTabAt(0).setIcon(whatsHotIcon);
 | 
			
		||||
            tabLayout.getTabAt(1).setIcon(channelIcon);
 | 
			
		||||
            tabLayout.getTabAt(2).setIcon(bookmarkIcon);
 | 
			
		||||
        tabLayout.addOnTabSelectedListener(this);
 | 
			
		||||
        updateTabs();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onResume() {
 | 
			
		||||
        super.onResume();
 | 
			
		||||
 | 
			
		||||
        if (hasTabsChanged) {
 | 
			
		||||
            hasTabsChanged = false;
 | 
			
		||||
            updateTabs();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onDestroy() {
 | 
			
		||||
        super.onDestroy();
 | 
			
		||||
        tabsManager.unsetSavedTabsListener();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*//////////////////////////////////////////////////////////////////////////
 | 
			
		||||
@@ -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();
 | 
			
		||||
            final Tab tab = tabsList.get(position);
 | 
			
		||||
 | 
			
		||||
            Throwable throwable = null;
 | 
			
		||||
            Fragment fragment = null;
 | 
			
		||||
            try {
 | 
			
		||||
                fragment = tab.getFragment();
 | 
			
		||||
            } catch (ExtractionException e) {
 | 
			
		||||
                throwable = e;
 | 
			
		||||
            }
 | 
			
		||||
                case 2:
 | 
			
		||||
                    return new BookmarkFragment();
 | 
			
		||||
                default:
 | 
			
		||||
 | 
			
		||||
            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++;
 | 
			
		||||
        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 {
 | 
			
		||||
                    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) {
 | 
			
		||||
                try {
 | 
			
		||||
                    onItemSelected(selectedItem);
 | 
			
		||||
                NavigationHelper.openChannelFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
 | 
			
		||||
                        selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
 | 
			
		||||
                    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) {
 | 
			
		||||
                try {
 | 
			
		||||
                    onItemSelected(selectedItem);
 | 
			
		||||
                NavigationHelper.openPlaylistFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
 | 
			
		||||
                        selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
 | 
			
		||||
                    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,12 +164,11 @@ 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 DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
 | 
			
		||||
            final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
 | 
			
		||||
            switch (i) {
 | 
			
		||||
                case 0:
 | 
			
		||||
@@ -190,10 +192,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
 | 
			
		||||
                                .show(getFragmentManager(), TAG);
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 6:
 | 
			
		||||
                    shareUrl(item.getName(), item.getUrl());
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        new InfoItemDialog(getActivity(), item, commands, actions).show();
 | 
			
		||||
@@ -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 {
 | 
			
		||||
        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 {
 | 
			
		||||
        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,9 +336,7 @@ 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 {
 | 
			
		||||
        return (List<SubscriptionEntity> subscriptionEntities) -> {
 | 
			
		||||
            if (DEBUG)
 | 
			
		||||
                Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
 | 
			
		||||
            if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
 | 
			
		||||
@@ -359,14 +346,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
 | 
			
		||||
                SubscriptionEntity channel = new SubscriptionEntity();
 | 
			
		||||
                channel.setServiceId(info.getServiceId());
 | 
			
		||||
                channel.setUrl(info.getUrl());
 | 
			
		||||
                    channel.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
 | 
			
		||||
                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.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
 | 
			
		||||
        headerSubscribersTextView.setVisibility(View.VISIBLE);
 | 
			
		||||
        } else headerSubscribersTextView.setVisibility(View.GONE);
 | 
			
		||||
        if (result.getSubscriberCount() >= 0) {
 | 
			
		||||
            headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
 | 
			
		||||
        } 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;
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
@@ -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()));
 | 
			
		||||
 | 
			
		||||
        List<String> contentFilter = new ArrayList<>(1);
 | 
			
		||||
        contentFilter.add(menuItemToFilterName.get(item.getItemId()));
 | 
			
		||||
        changeContentFilter(item, contentFilter);
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
            default:
 | 
			
		||||
                return super.onOptionsItemSelected(item);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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,9 +431,7 @@ public class SearchFragment
 | 
			
		||||
 | 
			
		||||
    private void initSearchListeners() {
 | 
			
		||||
        if (DEBUG) Log.d(TAG, "initSearchListeners() called");
 | 
			
		||||
        searchClear.setOnClickListener(new View.OnClickListener() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onClick(View v) {
 | 
			
		||||
        searchClear.setOnClickListener(v -> {
 | 
			
		||||
            if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
 | 
			
		||||
            if (TextUtils.isEmpty(searchEditText.getText())) {
 | 
			
		||||
                NavigationHelper.gotoMainFragment(getFragmentManager());
 | 
			
		||||
@@ -406,37 +439,30 @@ public class SearchFragment
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            searchEditText.setText("");
 | 
			
		||||
                suggestionListAdapter.setItems(new ArrayList<SuggestionItem>());
 | 
			
		||||
            suggestionListAdapter.setItems(new ArrayList<>());
 | 
			
		||||
            showKeyboardSearch();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));
 | 
			
		||||
 | 
			
		||||
        searchEditText.setOnClickListener(new View.OnClickListener() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onClick(View v) {
 | 
			
		||||
        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) {
 | 
			
		||||
        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) {
 | 
			
		||||
        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());
 | 
			
		||||
                    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 {
 | 
			
		||||
                        .subscribe(intent -> {
 | 
			
		||||
                            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);
 | 
			
		||||
                            }
 | 
			
		||||
                        }));
 | 
			
		||||
                        }, 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,8 +112,10 @@ public class StatisticsPlaylistFragment
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void initViews(View rootView, Bundle savedInstanceState) {
 | 
			
		||||
        super.initViews(rootView, savedInstanceState);
 | 
			
		||||
        if(!useAsFrontPage) {
 | 
			
		||||
            setTitle(getString(R.string.title_last_played));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected View getListHeader() {
 | 
			
		||||
@@ -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,29 +291,40 @@ 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);
 | 
			
		||||
 | 
			
		||||
            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) {
 | 
			
		||||
@@ -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) {
 | 
			
		||||
        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;
 | 
			
		||||
 | 
			
		||||
            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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (!info.getHlsUrl().isEmpty()) {
 | 
			
		||||
            return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS);
 | 
			
		||||
        } else if (!info.getDashMpdUrl().isEmpty()) {
 | 
			
		||||
            return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH);
 | 
			
		||||
    protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
 | 
			
		||||
        final StreamInfo info = tag.getMetadata();
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
@@ -152,8 +158,11 @@ public final class MainVideoPlayer extends AppCompatActivity
 | 
			
		||||
    protected void onNewIntent(Intent intent) {
 | 
			
		||||
        if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
 | 
			
		||||
        super.onNewIntent(intent);
 | 
			
		||||
        if (intent != null) {
 | 
			
		||||
            playerState = null;
 | 
			
		||||
            playerImpl.handleIntent(intent);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onResume() {
 | 
			
		||||
@@ -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,15 +716,20 @@ public final class MainVideoPlayer extends AppCompatActivity
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        protected int getDefaultResolutionIndex(final List<VideoStream> 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) {
 | 
			
		||||
                public int getOverrideResolutionIndex(List<VideoStream> sortedVideos,
 | 
			
		||||
                                                      String playbackQuality) {
 | 
			
		||||
                    return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /*//////////////////////////////////////////////////////////////////////////
 | 
			
		||||
        // States
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
        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) {
 | 
			
		||||
        protected VideoPlaybackResolver.QualityResolver getQualityResolver() {
 | 
			
		||||
            return new VideoPlaybackResolver.QualityResolver() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public int getDefaultResolutionIndex(List<VideoStream> sortedVideos) {
 | 
			
		||||
                    return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
        protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
 | 
			
		||||
                                                 final String playbackQuality) {
 | 
			
		||||
            return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality);
 | 
			
		||||
                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
 | 
			
		||||
        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,8 +1070,11 @@ public final class PopupVideoPlayer extends Service {
 | 
			
		||||
                    animateView(playerImpl.getResizingIndicator(), false, 100, 0);
 | 
			
		||||
                    playerImpl.changeState(playerImpl.getCurrentState());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!isPopupClosing) {
 | 
			
		||||
                    savePositionAndSize();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            v.performClick();
 | 
			
		||||
            return true;
 | 
			
		||||
@@ -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,6 +39,9 @@ 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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -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,15 +41,20 @@ 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);
 | 
			
		||||
            mDownloadManager = setupDownloadManager(mBinder);
 | 
			
		||||
            if (mDeleteDownloadManager != null) {
 | 
			
		||||
                mDeleteDownloadManager.setDownloadManager(mDownloadManager);
 | 
			
		||||
                updateList();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onServiceDisconnected(ComponentName name) {
 | 
			
		||||
@@ -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>
 | 
			
		||||