Conflict resolution.

This commit is contained in:
Kartikey Kushwaha 2018-09-15 13:22:13 +05:30
commit 395c9587b6
182 changed files with 7944 additions and 2562 deletions

View file

@ -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 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 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.

View file

@ -8,8 +8,8 @@ android {
applicationId "org.schabi.newpipe" applicationId "org.schabi.newpipe"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 27 targetSdkVersion 27
versionCode 64 versionCode 68
versionName "0.13.5" versionName "0.14.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@ -59,22 +59,23 @@ android {
ext { ext {
supportLibVersion = '27.1.1' supportLibVersion = '27.1.1'
exoPlayerLibVersion = '2.7.3' exoPlayerLibVersion = '2.8.2'
roomDbLibVersion = '1.0.0' roomDbLibVersion = '1.1.1'
leakCanaryLibVersion = '1.5.4' leakCanaryLibVersion = '1.5.4'
okHttpLibVersion = '1.5.0' okHttpLibVersion = '3.10.0'
icepickLibVersion = '3.2.0' icepickLibVersion = '3.2.0'
stethoLibVersion = '1.5.0' stethoLibVersion = '1.5.0'
} }
dependencies { dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
exclude module: 'support-annotations' exclude module: 'support-annotations'
} }
implementation 'com.github.TeamNewPipe:NewPipeExtractor:bf1c771' implementation 'com.github.TeamNewPipe:NewPipeExtractor:66c3c3f45241d4b0c909'
testImplementation 'junit:junit:4.12' 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:appcompat-v7:$supportLibVersion"
implementation "com.android.support:support-v4:$supportLibVersion" implementation "com.android.support:support-v4:$supportLibVersion"
@ -96,7 +97,7 @@ dependencies {
debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion" debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
debugImplementation 'com.android.support:multidex:1.0.3' 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 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
@ -110,6 +111,9 @@ dependencies {
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion" debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$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'
} }

View file

@ -76,10 +76,6 @@
android:name=".about.AboutActivity" android:name=".about.AboutActivity"
android:label="@string/title_activity_about"/> 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.SubscriptionsImportService"/>
<service android:name=".local.subscription.services.SubscriptionsExportService"/> <service android:name=".local.subscription.services.SubscriptionsExportService"/>
@ -122,6 +118,7 @@
<activity <activity
android:name=".ReCaptchaActivity" android:name=".ReCaptchaActivity"
android:label="@string/reCaptchaActivity"/> android:label="@string/reCaptchaActivity"/>
<activity android:name=".download.ExtSDDownloadFailedActivity" />
<provider <provider
android:name="android.support.v4.content.FileProvider" android:name="android.support.v4.content.FileProvider"

View file

@ -4,6 +4,7 @@ import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
@ -11,7 +12,10 @@ import android.view.View;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import com.squareup.leakcanary.RefWatcher; import com.squareup.leakcanary.RefWatcher;
import org.schabi.newpipe.report.UserAction;
import icepick.Icepick; import icepick.Icepick;
import icepick.State;
public abstract class BaseFragment extends Fragment { public abstract class BaseFragment extends Fragment {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
@ -20,6 +24,15 @@ public abstract class BaseFragment extends Fragment {
protected AppCompatActivity activity; protected AppCompatActivity activity;
public static final ImageLoader imageLoader = ImageLoader.getInstance(); 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 // Fragment's Lifecycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -72,6 +85,12 @@ public abstract class BaseFragment extends Fragment {
if (refWatcher != null) refWatcher.watch(this); if (refWatcher != null) refWatcher.watch(this);
} }
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Init // Init
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -88,8 +107,15 @@ public abstract class BaseFragment extends Fragment {
public void setTitle(String title) { public void setTitle(String title) {
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + 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); activity.getSupportActionBar().setTitle(title);
} }
} }
protected FragmentManager getFM() {
return getParentFragment() == null
? getFragmentManager()
: getParentFragment().getFragmentManager();
}
} }

View file

@ -24,6 +24,7 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@ -43,24 +44,30 @@ import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageButton; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.extractor.InfoItem.InfoType.PLAYLIST;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity"; private static final String TAG = "MainActivity";
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
@ -70,6 +77,19 @@ public class MainActivity extends AppCompatActivity {
private NavigationView drawerItems = null; private NavigationView drawerItems = null;
private TextView headerServiceView = 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 // Activity's LifeCycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -83,28 +103,64 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); 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) { if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) {
initFragments(); initFragments();
} }
setSupportActionBar(findViewById(R.id.toolbar)); setSupportActionBar(findViewById(R.id.toolbar));
setupDrawer(); try {
setupDrawer();
} catch (Exception e) {
ErrorActivity.reportUiError(this, e);
}
} }
private void setupDrawer() { private void setupDrawer() throws Exception {
final Toolbar toolbar = findViewById(R.id.toolbar); final Toolbar toolbar = findViewById(R.id.toolbar);
drawer = findViewById(R.id.drawer_layout); drawer = findViewById(R.id.drawer_layout);
drawerItems = findViewById(R.id.navigation); drawerItems = findViewById(R.id.navigation);
for(StreamingService s : NewPipe.getServices()) { //Tabs
final String title = s.getServiceInfo().getName() + int currentServiceId = ServiceHelper.getSelectedServiceId(this);
(ServiceHelper.isBeta(s) ? " (beta)" : ""); StreamingService service = NewPipe.getService(currentServiceId);
final MenuItem item = drawerItems.getMenu()
.add(R.id.menu_services_group, s.getServiceId(), 0, title); int kioskId = 0;
item.setIcon(ServiceHelper.getIcon(s.getServiceId()));
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 = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close);
toggle.syncState(); toggle.syncState();
@ -119,53 +175,179 @@ public class MainActivity extends AppCompatActivity {
@Override @Override
public void onDrawerClosed(View drawerView) { public void onDrawerClosed(View drawerView) {
if(servicesShown) {
toggleServices();
}
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) { if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate); new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
} }
} }
}); });
drawerItems.setNavigationItemSelectedListener(this::changeService); drawerItems.setNavigationItemSelectedListener(this::drawerItemSelected);
setupDrawerFooter();
setupDrawerHeader(); setupDrawerHeader();
} }
private boolean drawerItemSelected(MenuItem item) {
private boolean changeService(MenuItem item) { switch (item.getGroupId()) {
if (item.getGroupId() == R.id.menu_services_group) { case R.id.menu_services_group:
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false); changeService(item);
ServiceHelper.setSelectedServiceId(this, item.getItemId()); break;
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); case R.id.menu_tabs_group:
} else { try {
return false; 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(); drawer.closeDrawers();
return true; return true;
} }
private void setupDrawerFooter() { private void changeService(MenuItem item) {
ImageButton settings = findViewById(R.id.drawer_settings); drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
ImageButton downloads = findViewById(R.id.drawer_downloads); ServiceHelper.setSelectedServiceId(this, item.getItemId());
ImageButton history = findViewById(R.id.drawer_history); drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
}
settings.setOnClickListener(view -> NavigationHelper.openSettings(this)); private void tabSelected(MenuItem item) throws ExtractionException {
downloads.setOnClickListener(view ->NavigationHelper.openDownloads(this)); switch(item.getItemId()) {
history.setOnClickListener(view -> case ITEM_ID_SUBSCRIPTIONS:
NavigationHelper.openStatisticFragment(getSupportFragmentManager())); 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() { private void setupDrawerHeader() {
headerServiceView = findViewById(R.id.drawer_header_service_view); NavigationView navigationView = findViewById(R.id.navigation);
Button action = findViewById(R.id.drawer_header_action_button); 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 -> { action.setOnClickListener(view -> {
Intent intent = new Intent(Intent.ACTION_VIEW); toggleServices();
intent.setData(Uri.parse("https://newpipe.schabi.org/blog/"));
startActivity(intent);
drawer.closeDrawers();
}); });
} }
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 @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
@ -329,16 +511,13 @@ public class MainActivity extends AppCompatActivity {
onHomeButtonPressed(); onHomeButtonPressed();
return true; return true;
case R.id.action_show_downloads: case R.id.action_show_downloads:
return NavigationHelper.openDownloads(this); return NavigationHelper.openDownloads(this);
case R.id.action_history: case R.id.action_history:
NavigationHelper.openStatisticFragment(getSupportFragmentManager()); NavigationHelper.openStatisticFragment(getSupportFragmentManager());
return true; return true;
case R.id.action_about:
NavigationHelper.openAbout(this);
return true;
case R.id.action_settings: case R.id.action_settings:
NavigationHelper.openSettings(this); NavigationHelper.openSettings(this);
return true; return true;
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -382,31 +561,45 @@ public class MainActivity extends AppCompatActivity {
} }
private void handleIntent(Intent intent) { private void handleIntent(Intent intent) {
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); try {
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) { if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
String url = intent.getStringExtra(Constants.KEY_URL); String url = intent.getStringExtra(Constants.KEY_URL);
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
String title = intent.getStringExtra(Constants.KEY_TITLE); String title = intent.getStringExtra(Constants.KEY_TITLE);
switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) { switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
case STREAM: case STREAM:
boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay); NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay);
break; break;
case CHANNEL: case CHANNEL:
NavigationHelper.openChannelFragment(getSupportFragmentManager(), serviceId, url, title); NavigationHelper.openChannelFragment(getSupportFragmentManager(),
break; serviceId,
case PLAYLIST: url,
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), serviceId, url, title); title);
break; break;
case PLAYLIST:
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
serviceId,
url,
title);
break;
}
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING);
if (searchString == null) searchString = "";
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
NavigationHelper.openSearchFragment(
getSupportFragmentManager(),
serviceId,
searchString);
} else {
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
} }
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) { } catch (Exception e) {
String searchQuery = intent.getStringExtra(Constants.KEY_QUERY); ErrorActivity.reportUiError(this, e);
if (searchQuery == null) searchQuery = "";
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
NavigationHelper.openSearchFragment(getSupportFragmentManager(), serviceId, searchQuery);
} else {
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
} }
} }
} }

View file

@ -1,14 +1,19 @@
package org.schabi.newpipe; package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.FragmentManager;
import android.app.IntentService; import android.app.IntentService;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
@ -23,6 +28,7 @@ import android.widget.RadioButton;
import android.widget.RadioGroup; import android.widget.RadioGroup;
import android.widget.Toast; import android.widget.Toast;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; 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.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo; 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.helper.PlayerHelper;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; 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.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Observer;
import icepick.Icepick; import icepick.Icepick;
import icepick.State; import icepick.State;
@ -77,6 +88,8 @@ public class RouterActivity extends AppCompatActivity {
protected String currentUrl; protected String currentUrl;
protected CompositeDisposable disposables = new CompositeDisposable(); protected CompositeDisposable disposables = new CompositeDisposable();
private boolean selectionIsDownload = false;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -165,6 +178,7 @@ public class RouterActivity extends AppCompatActivity {
final String videoPlayerKey = getString(R.string.video_player_key); final String videoPlayerKey = getString(R.string.video_player_key);
final String backgroundPlayerKey = getString(R.string.background_player_key); final String backgroundPlayerKey = getString(R.string.background_player_key);
final String popupPlayerKey = getString(R.string.popup_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); final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
if (selectedChoiceKey.equals(alwaysAskKey)) { if (selectedChoiceKey.equals(alwaysAskKey)) {
@ -179,6 +193,8 @@ public class RouterActivity extends AppCompatActivity {
} }
} else if (selectedChoiceKey.equals(showInfoKey)) { } else if (selectedChoiceKey.equals(showInfoKey)) {
handleChoice(showInfoKey); handleChoice(showInfoKey);
} else if (selectedChoiceKey.equals(downloadKey)) {
handleChoice(downloadKey);
} else { } else {
final boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); 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); 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) .setCancelable(true)
.setNegativeButton(R.string.just_once, dialogButtonsClickListener) .setNegativeButton(R.string.just_once, dialogButtonsClickListener)
.setPositiveButton(R.string.always, dialogButtonsClickListener) .setPositiveButton(R.string.always, dialogButtonsClickListener)
.setOnDismissListener((dialog) -> finish()) .setOnDismissListener((dialog) -> {
if(!selectionIsDownload) finish();
})
.create(); .create();
//noinspection CodeBlock2Expr //noinspection CodeBlock2Expr
@ -316,6 +334,9 @@ public class RouterActivity extends AppCompatActivity {
resolveResourceIdFromAttr(context, R.attr.audio))); 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; return returnList;
} }
@ -347,6 +368,14 @@ public class RouterActivity extends AppCompatActivity {
return; 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 // stop and bypass FetcherService if InfoScreen was selected since
// StreamDetailFragment can fetch data itself // StreamDetailFragment can fetch data itself
if (selectedChoiceKey.equals(getString(R.string.show_info_key))) { if (selectedChoiceKey.equals(getString(R.string.show_info_key))) {
@ -373,6 +402,47 @@ public class RouterActivity extends AppCompatActivity {
finish(); 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 { private static class AdapterChoiceItem {
final String description, key; final String description, key;
@DrawableRes final int icon; @DrawableRes final int icon;

View file

@ -71,6 +71,14 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
info.getUploaderName(), info.getStreamCount()); 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() { public long getUid() {
return uid; return uid;
} }

View file

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

View file

@ -15,12 +15,17 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ThemeHelper; 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.service.DownloadManagerService;
import us.shandian.giga.ui.fragment.AllMissionsFragment; import us.shandian.giga.ui.fragment.AllMissionsFragment;
import us.shandian.giga.ui.fragment.MissionsFragment; import us.shandian.giga.ui.fragment.MissionsFragment;
public class DownloadActivity extends AppCompatActivity { public class DownloadActivity extends AppCompatActivity {
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
private DeleteDownloadManager mDeleteDownloadManager;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
// Service // Service
@ -42,21 +47,35 @@ public class DownloadActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true); actionBar.setDisplayShowTitleEnabled(true);
} }
// Fragment mDeleteDownloadManager = new DeleteDownloadManager(this);
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { mDeleteDownloadManager.restoreState(savedInstanceState);
@Override
public void onGlobalLayout() { MissionsFragment fragment = (MissionsFragment) getFragmentManager().findFragmentByTag(MISSIONS_FRAGMENT_TAG);
updateFragments(); if (fragment != null) {
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this); fragment.setDeleteManager(mDeleteDownloadManager);
} } else {
}); getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
updateFragments();
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
mDeleteDownloadManager.saveState(outState);
super.onSaveInstanceState(outState);
} }
private void updateFragments() { private void updateFragments() {
MissionsFragment fragment = new AllMissionsFragment(); MissionsFragment fragment = new AllMissionsFragment();
fragment.setDeleteManager(mDeleteDownloadManager);
getFragmentManager().beginTransaction() getFragmentManager().beginTransaction()
.replace(R.id.frame, fragment) .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit(); .commit();
} }
@ -80,6 +99,7 @@ public class DownloadActivity extends AppCompatActivity {
case R.id.action_settings: { case R.id.action_settings: {
Intent intent = new Intent(this, SettingsActivity.class); Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent); startActivity(intent);
deletePending();
return true; return true;
} }
default: 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();
}
} }

View file

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

View file

@ -51,9 +51,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
protected Button errorButtonRetry; protected Button errorButtonRetry;
protected TextView errorTextView; protected TextView errorTextView;
@State
protected boolean useAsFrontPage = false;
@Override @Override
public void onViewCreated(View rootView, Bundle savedInstanceState) { public void onViewCreated(View rootView, Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState); super.onViewCreated(rootView, savedInstanceState);
@ -66,9 +63,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
wasLoading.set(isLoading.get()); wasLoading.set(isLoading.get());
} }
public void useAsFrontPage(boolean value) {
useAsFrontPage = value;
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Init // Init
@ -93,12 +87,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
RxView.clicks(errorButtonRetry) RxView.clicks(errorButtonRetry)
.debounce(300, TimeUnit.MILLISECONDS) .debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Object>() { .subscribe(o -> onRetryButtonClicked());
@Override
public void accept(Object o) throws Exception {
onRetryButtonClicked();
}
});
} }
protected void onRetryButtonClicked() { protected void onRetryButtonClicked() {

View file

@ -14,24 +14,16 @@ public class BlankFragment extends BaseFragment {
@Nullable @Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
if(activity != null && activity.getSupportActionBar() != null) { setTitle("NewPipe");
activity.getSupportActionBar()
.setTitle("NewPipe");
}
return inflater.inflate(R.layout.fragment_blank, container, false); return inflater.inflate(R.layout.fragment_blank, container, false);
} }
@Override @Override
public void setUserVisibleHint(boolean isVisibleToUser) { public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser); super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser) { setTitle("NewPipe");
if(activity != null && activity.getSupportActionBar() != null) { // leave this inline. Will make it harder for copy cats.
activity.getSupportActionBar() // If you are a Copy cat FUCK YOU.
.setTitle("NewPipe"); // I WILL FIND YOU, AND I WILL ...
}
// 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 ...
}
} }
} }

View file

@ -1,6 +1,5 @@
package org.schabi.newpipe.fragments; package org.schabi.newpipe.fragments;
import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; 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.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.preference.PreferenceManager; import android.support.v7.app.AppCompatActivity;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
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.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; 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.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper; 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 class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
public int currentServiceId = -1;
private ViewPager viewPager; private ViewPager viewPager;
private SelectedTabsPagerAdapter pagerAdapter;
private TabLayout tabLayout;
/*////////////////////////////////////////////////////////////////////////// private List<Tab> tabsList = new ArrayList<>();
// Constants private TabsManager tabsManager;
//////////////////////////////////////////////////////////////////////////*/
private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getServiceId(); private boolean hasTabsChanged = false;
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;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Fragment's LifeCycle // Fragment's LifeCycle
@ -61,11 +49,22 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setHasOptionsMenu(true); 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 @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 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); return inflater.inflate(R.layout.fragment_main, container, false);
} }
@ -73,30 +72,34 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
protected void initViews(View rootView, Bundle savedInstanceState) { protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, 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); viewPager = rootView.findViewById(R.id.pager);
/* Nested fragment, use child fragment here to maintain backstack in view pager. */ /* Nested fragment, use child fragment here to maintain backstack in view pager. */
PagerAdapter adapter = new PagerAdapter(getChildFragmentManager()); pagerAdapter = new SelectedTabsPagerAdapter(getChildFragmentManager());
viewPager.setAdapter(adapter); viewPager.setAdapter(pagerAdapter);
viewPager.setOffscreenPageLimit(adapter.getCount());
tabLayout.setupWithViewPager(viewPager); tabLayout.setupWithViewPager(viewPager);
tabLayout.addOnTabSelectedListener(this);
updateTabs();
}
int channelIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_channel); @Override
int whatsHotIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_hot); public void onResume() {
int bookmarkIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_bookmark); super.onResume();
if (isSubscriptionsPageOnlySelected()) { if (hasTabsChanged) {
tabLayout.getTabAt(0).setIcon(channelIcon); hasTabsChanged = false;
tabLayout.getTabAt(1).setIcon(bookmarkIcon); updateTabs();
} else {
tabLayout.getTabAt(0).setIcon(whatsHotIcon);
tabLayout.getTabAt(1).setIcon(channelIcon);
tabLayout.getTabAt(2).setIcon(bookmarkIcon);
} }
} }
@Override
public void onDestroy() {
super.onDestroy();
tabsManager.unsetSavedTabsListener();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Menu // Menu
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -106,16 +109,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
super.onCreateOptionsMenu(menu, inflater); super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
inflater.inflate(R.menu.main_fragment_menu, menu); 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(); ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) { if (supportActionBar != null) {
@ -127,7 +120,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_search: 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 true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
@ -137,9 +137,33 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
// Tabs // 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 @Override
public void onTabSelected(TabLayout.Tab tab) { public void onTabSelected(TabLayout.Tab selectedTab) {
viewPager.setCurrentItem(tab.getPosition()); if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
updateCurrentTitle();
} }
@Override @Override
@ -148,129 +172,58 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
@Override @Override
public void onTabReselected(TabLayout.Tab tab) { public void onTabReselected(TabLayout.Tab tab) {
if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
updateCurrentTitle();
} }
private class PagerAdapter extends FragmentPagerAdapter { private class SelectedTabsPagerAdapter extends FragmentPagerAdapter {
PagerAdapter(FragmentManager fm) { private SelectedTabsPagerAdapter(FragmentManager fragmentManager) {
super(fm); super(fragmentManager);
} }
@Override @Override
public Fragment getItem(int position) { public Fragment getItem(int position) {
switch (position) { final Tab tab = tabsList.get(position);
case 0:
return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment(); Throwable throwable = null;
case 1: Fragment fragment = null;
if(PreferenceManager.getDefaultSharedPreferences(getActivity()) try {
.getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key)) fragment = tab.getFragment();
.equals(getString(R.string.subscription_page_key))) { } catch (ExtractionException e) {
return new BookmarkFragment(); throwable = e;
} else {
return new SubscriptionFragment();
}
case 2:
return new BookmarkFragment();
default:
return new BlankFragment();
} }
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 @Override
public CharSequence getPageTitle(int position) { public int getItemPosition(Object object) {
//return getString(this.tabTitles[position]); // Causes adapter to reload all Fragments when
return ""; // notifyDataSetChanged is called
return POSITION_NONE;
} }
@Override @Override
public int getCount() { public int getCount() {
return isSubscriptionsPageOnlySelected() ? 2 : 3; return tabsList.size();
} }
}
/*////////////////////////////////////////////////////////////////////////// @Override
// Main page content public void destroyItem(ViewGroup container, int position, Object object) {
//////////////////////////////////////////////////////////////////////////*/ getChildFragmentManager()
.beginTransaction()
private boolean isSubscriptionsPageOnlySelected() { .remove((Fragment) object)
return PreferenceManager.getDefaultSharedPreferences(activity) .commitNowAllowingStateLoss();
.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++;
} }
} }
} }

View file

@ -17,6 +17,7 @@ import android.support.v4.content.ContextCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.Html; import android.text.Html;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; 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.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ParsingException; 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.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.BaseStateFragment; 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;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
@ -128,7 +132,7 @@ public class VideoDetailFragment
private StreamInfo currentInfo; private StreamInfo currentInfo;
private Disposable currentWorker; private Disposable currentWorker;
private CompositeDisposable disposables = new CompositeDisposable(); @NonNull private CompositeDisposable disposables = new CompositeDisposable();
private List<VideoStream> sortedVideoStreams; private List<VideoStream> sortedVideoStreams;
private int selectedVideoStreamIndex = -1; private int selectedVideoStreamIndex = -1;
@ -363,11 +367,15 @@ public class VideoDetailFragment
if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) {
Log.w(TAG, "Can't open channel because we got no channel URL"); Log.w(TAG, "Can't open channel because we got no channel URL");
} else { } else {
NavigationHelper.openChannelFragment( try {
getFragmentManager(), NavigationHelper.openChannelFragment(
currentInfo.getServiceId(), getFragmentManager(),
currentInfo.getUploaderUrl(), currentInfo.getServiceId(),
currentInfo.getUploaderName()); currentInfo.getUploaderUrl(),
currentInfo.getUploaderName());
} catch (Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
}
} }
break; break;
case R.id.detail_thumbnail_root_layout: case R.id.detail_thumbnail_root_layout:
@ -540,7 +548,8 @@ public class VideoDetailFragment
final String[] commands = new String[]{ final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup), context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.append_playlist) context.getResources().getString(R.string.append_playlist),
context.getResources().getString(R.string.share)
}; };
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
@ -557,6 +566,9 @@ public class VideoDetailFragment
.show(getFragmentManager(), TAG); .show(getFragmentManager(), TAG);
} }
break; break;
case 3:
shareUrl(item.getName(), item.getUrl());
break;
default: default:
break; break;
} }
@ -872,10 +884,7 @@ public class VideoDetailFragment
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) {
openNormalBackgroundPlayer(append); openNormalBackgroundPlayer(append);
} else { } else {
NavigationHelper.playOnExternalPlayer(activity, startOnExternalPlayer(activity, currentInfo, audioStream);
currentInfo.getName(),
currentInfo.getUploaderName(),
audioStream);
} }
} }
@ -902,10 +911,7 @@ public class VideoDetailFragment
if (PreferenceManager.getDefaultSharedPreferences(activity) if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) { .getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
NavigationHelper.playOnExternalPlayer(activity, startOnExternalPlayer(activity, currentInfo, selectedVideoStream);
currentInfo.getName(),
currentInfo.getUploaderName(),
selectedVideoStream);
} else { } else {
openNormalPlayer(selectedVideoStream); openNormalPlayer(selectedVideoStream);
} }
@ -949,6 +955,20 @@ public class VideoDetailFragment
this.autoPlayEnabled = autoplay; 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 @Nullable
private VideoStream getSelectedVideoStream() { private VideoStream getSelectedVideoStream() {
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
@ -1207,10 +1227,10 @@ public class VideoDetailFragment
spinnerToolbar.setVisibility(View.GONE); spinnerToolbar.setVisibility(View.GONE);
break; break;
default: default:
if(info.getAudioStreams().isEmpty()) detailControlsBackground.setVisibility(View.GONE);
if (!info.getVideoStreams().isEmpty() if (!info.getVideoStreams().isEmpty()
|| !info.getVideoOnlyStreams().isEmpty()) break; || !info.getVideoOnlyStreams().isEmpty()) break;
detailControlsBackground.setVisibility(View.GONE);
detailControlsPopup.setVisibility(View.GONE); detailControlsPopup.setVisibility(View.GONE);
spinnerToolbar.setVisibility(View.GONE); spinnerToolbar.setVisibility(View.GONE);
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp); thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);

View file

@ -6,6 +6,7 @@ import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log; 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.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver; 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>() { infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
@Override @Override
public void selected(ChannelInfoItem selectedItem) { public void selected(ChannelInfoItem selectedItem) {
onItemSelected(selectedItem); try {
NavigationHelper.openChannelFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), onItemSelected(selectedItem);
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>() { infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() {
@Override @Override
public void selected(PlaylistInfoItem selectedItem) { public void selected(PlaylistInfoItem selectedItem) {
onItemSelected(selectedItem); try {
NavigationHelper.openPlaylistFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), onItemSelected(selectedItem);
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) { private void onStreamSelected(StreamInfoItem selectedItem) {
onItemSelected(selectedItem); onItemSelected(selectedItem);
NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), NavigationHelper.openVideoDetailFragment(getFM(),
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); 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[]{ final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup), context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.append_playlist) context.getResources().getString(R.string.append_playlist),
context.getResources().getString(R.string.share)
}; };
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
@ -213,6 +228,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
.show(getFragmentManager(), TAG); .show(getFragmentManager(), TAG);
} }
break; break;
case 3:
shareUrl(item.getName(), item.getUrl());
break;
default: default:
break; break;
} }

View file

@ -8,6 +8,9 @@ import android.view.View;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo; 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 org.schabi.newpipe.util.Constants;
import java.util.Queue; import java.util.Queue;
@ -166,7 +169,6 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
public void handleResult(@NonNull I result) { public void handleResult(@NonNull I result) {
super.handleResult(result); super.handleResult(result);
url = result.getUrl();
name = result.getName(); name = result.getName();
setTitle(name); setTitle(name);

View file

@ -36,11 +36,11 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; 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.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.local.subscription.SubscriptionService;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.ImageDisplayConstants;
@ -90,6 +90,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
private MenuItem menuRssButton; private MenuItem menuRssButton;
private boolean mIsVisibleToUser = false;
public static ChannelFragment getInstance(int serviceId, String url, String name) { public static ChannelFragment getInstance(int serviceId, String url, String name) {
ChannelFragment instance = new ChannelFragment(); ChannelFragment instance = new ChannelFragment();
instance.setInitialData(serviceId, url, name); instance.setInitialData(serviceId, url, name);
@ -103,6 +105,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
@Override @Override
public void setUserVisibleHint(boolean isVisibleToUser) { public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser); super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
if(activity != null if(activity != null
&& useAsFrontPage && useAsFrontPage
&& isVisibleToUser) { && isVisibleToUser) {
@ -161,38 +164,39 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
context.getResources().getString(R.string.start_here_on_main), context.getResources().getString(R.string.start_here_on_main),
context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_background),
context.getResources().getString(R.string.start_here_on_popup), 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() { final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
@Override final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
public void onClick(DialogInterface dialogInterface, int i) { switch (i) {
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); case 0:
switch (i) { NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
case 0: break;
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); case 1:
break; NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
case 1: break;
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); case 2:
break; NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
case 2: break;
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); case 3:
break; NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
case 3: break;
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); case 4:
break; NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
case 4: break;
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); case 5:
break; if (getFragmentManager() != null) {
case 5: PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
if (getFragmentManager() != null) { .show(getFragmentManager(), TAG);
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) }
.show(getFragmentManager(), TAG); break;
} case 6:
break; shareUrl(item.getName(), item.getUrl());
default: break;
break; default:
} break;
} }
}; };
@ -250,12 +254,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100; private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private void monitorSubscription(final ChannelInfo info) { private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = new Consumer<Throwable>() { final Consumer<Throwable> onError = (Throwable throwable) -> {
@Override
public void accept(Throwable throwable) throws Exception {
animateView(headerSubscribeButton, false, 100); 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() 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) // so only update the UI for the latest emission ("sync" the subscribe button's state)
.debounce(100, TimeUnit.MILLISECONDS) .debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<SubscriptionEntity>>() { .subscribe((List<SubscriptionEntity> subscriptionEntities) ->
@Override updateSubscribeButton(!subscriptionEntities.isEmpty())
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception { , onError));
updateSubscribeButton(!subscriptionEntities.isEmpty());
}
}, onError));
} }
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) { private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
return new Function<Object, Object>() { return (@NonNull Object o) -> {
@Override subscriptionService.subscriptionTable().insert(subscription);
public Object apply(@NonNull Object o) throws Exception { return o;
subscriptionService.subscriptionTable().insert(subscription);
return o;
}
}; };
} }
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) { private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
return new Function<Object, Object>() { return (@NonNull Object o) -> {
@Override subscriptionService.subscriptionTable().delete(subscription);
public Object apply(@NonNull Object o) throws Exception { return o;
subscriptionService.subscriptionTable().delete(subscription);
return o;
}
}; };
} }
private void updateSubscription(final ChannelInfo info) { private void updateSubscription(final ChannelInfo info) {
if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]"); if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
final Action onComplete = new Action() { final Action onComplete = () -> {
@Override
public void run() throws Exception {
if (DEBUG) Log.d(TAG, "Updated subscription: " + info.getUrl()); if (DEBUG) Log.d(TAG, "Updated subscription: " + info.getUrl());
}
}; };
final Consumer<Throwable> onError = new Consumer<Throwable>() { final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
@Override onUnrecoverableError(throwable,
public void accept(@NonNull Throwable throwable) throws Exception { UserAction.SUBSCRIPTION,
onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(info.getServiceId()), "Updating Subscription for " + info.getUrl(), R.string.subscription_update_failed); NewPipe.getNameOfService(info.getServiceId()),
} "Updating Subscription for " + info.getUrl(),
}; R.string.subscription_update_failed);
disposables.add(subscriptionService.updateChannelInfo(info) disposables.add(subscriptionService.updateChannelInfo(info)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -323,19 +315,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
} }
private Disposable monitorSubscribeButton(final Button subscribeButton, final Function<Object, Object> action) { private Disposable monitorSubscribeButton(final Button subscribeButton, final Function<Object, Object> action) {
final Consumer<Object> onNext = new Consumer<Object>() { final Consumer<Object> onNext = (@NonNull Object o) -> {
@Override
public void accept(@NonNull Object o) throws Exception {
if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!"); if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!");
}
}; };
final Consumer<Throwable> onError = new Consumer<Throwable>() { final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
@Override onUnrecoverableError(throwable,
public void accept(@NonNull Throwable throwable) throws Exception { UserAction.SUBSCRIPTION,
onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.getServiceId()), "Subscription Change", R.string.subscription_change_failed); NewPipe.getNameOfService(currentInfo.getServiceId()),
} "Subscription Change",
}; R.string.subscription_change_failed);
/* Emit clicks from main thread unto io thread */ /* Emit clicks from main thread unto io thread */
return RxView.clicks(subscribeButton) return RxView.clicks(subscribeButton)
@ -347,25 +336,25 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
} }
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) { private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
return new Consumer<List<SubscriptionEntity>>() { return (List<SubscriptionEntity> subscriptionEntities) -> {
@Override if (DEBUG)
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception { Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
if (DEBUG) if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
if (subscriptionEntities.isEmpty()) { if (subscriptionEntities.isEmpty()) {
if (DEBUG) Log.d(TAG, "No subscription to this channel!"); if (DEBUG) Log.d(TAG, "No subscription to this channel!");
SubscriptionEntity channel = new SubscriptionEntity(); SubscriptionEntity channel = new SubscriptionEntity();
channel.setServiceId(info.getServiceId()); channel.setServiceId(info.getServiceId());
channel.setUrl(info.getUrl()); channel.setUrl(info.getUrl());
channel.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount()); channel.setData(info.getName(),
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel)); info.getAvatarUrl(),
} else { info.getDescription(),
if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); info.getSubscriberCount());
final SubscriptionEntity subscription = subscriptionEntities.get(0); subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel));
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); } 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, imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
if (result.getSubscriberCount() != -1) { headerSubscribersTextView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) {
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount())); headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
headerSubscribersTextView.setVisibility(View.VISIBLE); } else {
} else headerSubscribersTextView.setVisibility(View.GONE); headerSubscribersTextView.setText(R.string.subscribers_count_not_available);
}
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
@ -483,8 +474,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
super.handleNextItems(result); super.handleNextItems(result);
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), showSnackBarError(result.getErrors(),
"Get next page of: " + url, R.string.general_error); 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; if (super.onError(exception)) return true;
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; 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; return true;
} }
@ -508,6 +506,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
@Override @Override
public void setTitle(String title) { public void setTitle(String title) {
super.setTitle(title); super.setTitle(title);
headerTitleView.setText(title); if (!useAsFrontPage) headerTitleView.setText(title);
} }
} }

View file

@ -11,22 +11,20 @@ import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskInfo; 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.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.NavigationHelper;
import icepick.State; import icepick.State;
import io.reactivex.Single; import io.reactivex.Single;
@ -59,6 +57,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
protected String kioskId = ""; protected String kioskId = "";
protected String kioskTranslatedName; protected String kioskTranslatedName;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Views // Views
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -74,10 +73,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
throws ExtractionException { throws ExtractionException {
KioskFragment instance = new KioskFragment(); KioskFragment instance = new KioskFragment();
StreamingService service = NewPipe.getService(serviceId); StreamingService service = NewPipe.getService(serviceId);
UrlIdHandler kioskTypeUrlIdHandler = service.getKioskList() ListLinkHandlerFactory kioskLinkHandlerFactory = service.getKioskList()
.getUrlIdHandlerByType(kioskId); .getListLinkHandlerFactoryByType(kioskId);
instance.setInitialData(serviceId, instance.setInitialData(serviceId,
kioskTypeUrlIdHandler.getUrl(kioskId), kioskId); kioskLinkHandlerFactory.fromId(kioskId).getUrl(), kioskId);
instance.kioskId = kioskId; instance.kioskId = kioskId;
return instance; return instance;
} }
@ -136,7 +135,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
.getDefaultSharedPreferences(activity) .getDefaultSharedPreferences(activity)
.getString(getString(R.string.content_country_key), .getString(getString(R.string.content_country_key),
getString(R.string.default_country_value)); getString(R.string.default_country_value));
return ExtractorHelper.getKioskInfo(serviceId, url, contentCountry, forceReload); return ExtractorHelper.getKioskInfo(serviceId,
url,
contentCountry,
forceReload);
} }
@Override @Override
@ -145,7 +147,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
.getDefaultSharedPreferences(activity) .getDefaultSharedPreferences(activity)
.getString(getString(R.string.content_country_key), .getString(getString(R.string.content_country_key),
getString(R.string.default_country_value)); 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); super.handleResult(result);
name = kioskTranslatedName; name = kioskTranslatedName;
setTitle(kioskTranslatedName); if(!useAsFrontPage) {
setTitle(kioskTranslatedName);
}
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), showSnackBarError(result.getErrors(),

View file

@ -6,6 +6,7 @@ import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -19,6 +20,7 @@ import android.widget.TextView;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.App;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; 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.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; 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.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.info_list.InfoItemDialog; 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.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.ImageDisplayConstants;
@ -44,6 +48,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.Flowable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
@ -93,7 +98,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
disposables = new CompositeDisposable(); disposables = new CompositeDisposable();
isBookmarkButtonReady = new AtomicBoolean(false); isBookmarkButtonReady = new AtomicBoolean(false);
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext())); remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(
requireContext()));
} }
@Override @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_main),
context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_background),
context.getResources().getString(R.string.start_here_on_popup), context.getResources().getString(R.string.start_here_on_popup),
context.getResources().getString(R.string.share)
}; };
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
@ -162,6 +169,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
case 4: case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break; break;
case 5:
shareUrl(item.getName(), item.getUrl());
break;
default: default:
break; break;
} }
@ -261,11 +271,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
if (!TextUtils.isEmpty(result.getUploaderName())) { if (!TextUtils.isEmpty(result.getUploaderName())) {
headerUploaderName.setText(result.getUploaderName()); headerUploaderName.setText(result.getUploaderName());
if (!TextUtils.isEmpty(result.getUploaderUrl())) { if (!TextUtils.isEmpty(result.getUploaderUrl())) {
headerUploaderLayout.setOnClickListener(v -> headerUploaderLayout.setOnClickListener(v -> {
try {
NavigationHelper.openChannelFragment(getFragmentManager(), NavigationHelper.openChannelFragment(getFragmentManager(),
result.getServiceId(), result.getUploaderUrl(), result.getServiceId(),
result.getUploaderName()) 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) remotePlaylistManager.getPlaylist(result)
.flatMap(lists -> getUpdateProcessor(lists, result), (lists, id) -> lists)
.onBackpressureLatest() .onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistBookmarkSubscriber()); .subscribe(getPlaylistBookmarkSubscriber());
remotePlaylistManager.onUpdate(result)
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(integer -> {/* Do nothing*/}, this::onError);
headerPlayAllButton.setOnClickListener(view -> headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view -> headerPopupButton.setOnClickListener(view ->
@ -336,7 +348,11 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
if (super.onError(exception)) return true; if (super.onError(exception)) return true;
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; 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; return true;
} }
@ -344,6 +360,17 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
// Utils // 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() { private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
return new Subscriber<List<PlaylistRemoteEntity>>() { return new Subscriber<List<PlaylistRemoteEntity>>() {
@Override @Override

View file

@ -37,26 +37,30 @@ import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchResult; import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.LayoutManagerSmoothScroller; import org.schabi.newpipe.util.LayoutManagerSmoothScroller;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.net.SocketException; import java.net.SocketException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import icepick.State; import icepick.State;
@ -65,14 +69,15 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.PublishSubject;
import static java.util.Arrays.asList;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SearchFragment public class SearchFragment
extends BaseListFragment<SearchResult, ListExtractor.InfoItemsPage> extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage>
implements BackPressable { implements BackPressable {
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -92,19 +97,29 @@ public class SearchFragment
@State @State
protected int filterItemCheckedId = -1; protected int filterItemCheckedId = -1;
private SearchEngine.Filter filter = SearchEngine.Filter.ANY;
@State @State
protected int serviceId = Constants.NO_SERVICE_ID; protected int serviceId = Constants.NO_SERVICE_ID;
// this three represet the current search query
@State @State
protected String searchQuery; protected String searchString;
@State @State
protected String lastSearchedQuery; protected String[] contentFilter;
@State
protected String sortFilter;
// these represtent the last search
@State
protected String lastSearchedString;
@State @State
protected boolean wasSearchFocused = false; protected boolean wasSearchFocused = false;
private int currentPage = 0; private Map<Integer, String> menuItemToFilterName;
private int currentNextPage = 0; private StreamingService service;
private String currentPageUrl;
private String nextPageUrl;
private String contentCountry; private String contentCountry;
private boolean isSuggestionsEnabled = true; private boolean isSuggestionsEnabled = true;
private boolean isSearchHistoryEnabled = 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 searchFragment = new SearchFragment();
searchFragment.setQuery(serviceId, query); searchFragment.setQuery(serviceId, searchString, new String[0], "");
if (!TextUtils.isEmpty(query)) { if (!TextUtils.isEmpty(searchString)) {
searchFragment.setSearchOnResume(); searchFragment.setSearchOnResume();
} }
@ -202,13 +217,22 @@ public class SearchFragment
if (DEBUG) Log.d(TAG, "onResume() called"); if (DEBUG) Log.d(TAG, "onResume() called");
super.onResume(); 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 (wasLoading.getAndSet(false)) {
if (currentNextPage > currentPage) loadMoreItems(); search(searchString, contentFilter, sortFilter);
else search(searchQuery);
} else if (infoListAdapter.getItemsList().size() == 0) { } else if (infoListAdapter.getItemsList().size() == 0) {
if (savedState == null) { if (savedState == null) {
search(searchQuery); search(searchString, contentFilter, sortFilter);
} else if (!isLoading.get() && !wasSearchFocused) { } else if (!isLoading.get() && !wasSearchFocused) {
infoListAdapter.clearStreamItemList(); infoListAdapter.clearStreamItemList();
showEmptyState(); showEmptyState();
@ -218,7 +242,7 @@ public class SearchFragment
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver(); if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) { if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
showKeyboardSearch(); showKeyboardSearch();
showSuggestionsPanel(); showSuggestionsPanel();
} else { } else {
@ -247,8 +271,9 @@ public class SearchFragment
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) { switch (requestCode) {
case ReCaptchaActivity.RECAPTCHA_REQUEST: case ReCaptchaActivity.RECAPTCHA_REQUEST:
if (resultCode == Activity.RESULT_OK && !TextUtils.isEmpty(searchQuery)) { if (resultCode == Activity.RESULT_OK
search(searchQuery); && !TextUtils.isEmpty(searchString)) {
search(searchString, contentFilter, sortFilter);
} else Log.e(TAG, "ReCaptcha failed"); } else Log.e(TAG, "ReCaptcha failed");
break; break;
@ -282,20 +307,22 @@ public class SearchFragment
@Override @Override
public void writeTo(Queue<Object> objectsToSave) { public void writeTo(Queue<Object> objectsToSave) {
super.writeTo(objectsToSave); super.writeTo(objectsToSave);
objectsToSave.add(currentPage); objectsToSave.add(currentPageUrl);
objectsToSave.add(currentNextPage); objectsToSave.add(nextPageUrl);
} }
@Override @Override
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception { public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
super.readFrom(savedObjects); super.readFrom(savedObjects);
currentPage = (int) savedObjects.poll(); currentPageUrl = (String) savedObjects.poll();
currentNextPage = (int) savedObjects.poll(); nextPageUrl = (String) savedObjects.poll();
} }
@Override @Override
public void onSaveInstanceState(Bundle bundle) { public void onSaveInstanceState(Bundle bundle) {
searchQuery = searchEditText != null ? searchEditText.getText().toString() : searchQuery; searchString = searchEditText != null
? searchEditText.getText().toString()
: searchString;
super.onSaveInstanceState(bundle); super.onSaveInstanceState(bundle);
} }
@ -305,8 +332,11 @@ public class SearchFragment
@Override @Override
public void reloadContent() { public void reloadContent() {
if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { if (!TextUtils.isEmpty(searchString)
search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString()); || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
search(!TextUtils.isEmpty(searchString)
? searchString
: searchEditText.getText().toString(), new String[0], "");
} else { } else {
if (searchEditText != null) { if (searchEditText != null) {
searchEditText.setText(""); searchEditText.setText("");
@ -330,22 +360,35 @@ public class SearchFragment
supportActionBar.setDisplayHomeAsUpEnabled(true); 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); restoreFilterChecked(menu, filterItemCheckedId);
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_filter_all: List<String> contentFilter = new ArrayList<>(1);
case R.id.menu_filter_video: contentFilter.add(menuItemToFilterName.get(item.getItemId()));
case R.id.menu_filter_channel: changeContentFilter(item, contentFilter);
case R.id.menu_filter_playlist:
changeFilter(item, getFilterFromMenuId(item.getItemId())); return true;
return true;
default:
return super.onOptionsItemSelected(item);
}
} }
private void restoreFilterChecked(Menu menu, int itemId) { private void restoreFilterChecked(Menu menu, int itemId) {
@ -354,21 +397,6 @@ public class SearchFragment
if (item == null) return; if (item == null) return;
item.setChecked(true); 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 TextWatcher textWatcher;
private void showSearchOnStart() { private void showSearchOnStart() {
if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " + searchQuery+", lastSearchedQuery → " + lastSearchedQuery); if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → "
searchEditText.setText(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.setTranslationX(100);
searchToolbarContainer.setAlpha(0f); searchToolbarContainer.setAlpha(0f);
searchToolbarContainer.setVisibility(View.VISIBLE); 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 { } else {
searchToolbarContainer.setTranslationX(0); searchToolbarContainer.setTranslationX(0);
searchToolbarContainer.setAlpha(1f); searchToolbarContainer.setAlpha(1f);
@ -396,47 +431,38 @@ public class SearchFragment
private void initSearchListeners() { private void initSearchListeners() {
if (DEBUG) Log.d(TAG, "initSearchListeners() called"); if (DEBUG) Log.d(TAG, "initSearchListeners() called");
searchClear.setOnClickListener(new View.OnClickListener() { searchClear.setOnClickListener(v -> {
@Override if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
public void onClick(View v) { if (TextUtils.isEmpty(searchEditText.getText())) {
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); NavigationHelper.gotoMainFragment(getFragmentManager());
if (TextUtils.isEmpty(searchEditText.getText())) { return;
NavigationHelper.gotoMainFragment(getFragmentManager());
return;
}
searchEditText.setText("");
suggestionListAdapter.setItems(new ArrayList<SuggestionItem>());
showKeyboardSearch();
} }
searchEditText.setText("");
suggestionListAdapter.setItems(new ArrayList<>());
showKeyboardSearch();
}); });
TooltipCompat.setTooltipText(searchClear, getString(R.string.clear)); TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));
searchEditText.setOnClickListener(new View.OnClickListener() { searchEditText.setOnClickListener(v -> {
@Override if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
public void onClick(View v) { if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); showSuggestionsPanel();
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
showSuggestionsPanel();
}
} }
}); });
searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
@Override if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
public void onFocusChange(View v, boolean hasFocus) { if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) {
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]"); showSuggestionsPanel();
if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) {
showSuggestionsPanel();
}
} }
}); });
suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() { suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() {
@Override @Override
public void onSuggestionItemSelected(SuggestionItem item) { public void onSuggestionItemSelected(SuggestionItem item) {
search(item.query); search(item.query, new String[0], "");
searchEditText.setText(item.query); searchEditText.setText(item.query);
} }
@ -469,21 +495,22 @@ public class SearchFragment
} }
}; };
searchEditText.addTextChangedListener(textWatcher); searchEditText.addTextChangedListener(textWatcher);
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { searchEditText.setOnEditorActionListener(
@Override (TextView v, int actionId, KeyEvent event) -> {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (DEBUG) {
if (DEBUG) { Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); }
} if (event != null
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
search(searchEditText.getText().toString()); || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
return true; search(searchEditText.getText().toString(), new String[0], "");
} return true;
return false; }
} return false;
}); });
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver(); if (suggestionDisposable == null || suggestionDisposable.isDisposed())
initSuggestionObserver();
} }
private void unsetSearchListeners() { private void unsetSearchListeners() {
@ -513,7 +540,8 @@ public class SearchFragment
if (searchEditText == null) return; if (searchEditText == null) return;
if (searchEditText.requestFocus()) { 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); imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT);
} }
} }
@ -522,8 +550,10 @@ public class SearchFragment
if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called"); if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called");
if (searchEditText == null) return; if (searchEditText == null) return;
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); InputMethodManager imm = (InputMethodManager) activity.getSystemService(
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
searchEditText.clearFocus(); searchEditText.clearFocus();
} }
@ -545,8 +575,7 @@ public class SearchFragment
.onNext(searchEditText.getText().toString()), .onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable, throwable -> showSnackBarError(throwable,
UserAction.DELETE_FROM_HISTORY, "none", UserAction.DELETE_FROM_HISTORY, "none",
"Deleting item failed", R.string.general_error) "Deleting item failed", R.string.general_error));
);
disposables.add(onDelete); disposables.add(onDelete);
}) })
.show(); .show();
@ -554,10 +583,12 @@ public class SearchFragment
@Override @Override
public boolean onBackPressed() { 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(); hideSuggestionsPanel();
hideKeyboardSearch(); hideKeyboardSearch();
searchEditText.setText(lastSearchedQuery); searchEditText.setText(lastSearchedString);
return true; return true;
} }
return false; return false;
@ -573,8 +604,10 @@ public class SearchFragment
final Observable<String> observable = suggestionPublisher final Observable<String> observable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWith(searchQuery != null ? searchQuery : "") .startWith(searchString != null
.filter(query -> isSuggestionsEnabled); ? searchString
: "")
.filter(searchString -> isSuggestionsEnabled);
suggestionDisposable = observable suggestionDisposable = observable
.switchMap(query -> { .switchMap(query -> {
@ -645,56 +678,44 @@ public class SearchFragment
// no-op // no-op
} }
private void search(final String query) { private void search(final String searchString, String[] contentFilter, String sortFilter) {
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]"); if (DEBUG) Log.d(TAG, "search() called with: query = [" + searchString + "]");
if (query.isEmpty()) return; if (searchString.isEmpty()) return;
try { try {
final StreamingService service = NewPipe.getServiceByUrl(query); final StreamingService service = NewPipe.getServiceByUrl(searchString);
if (service != null) { if (service != null) {
showLoading(); showLoading();
disposables.add(Observable disposables.add(Observable
.fromCallable(new Callable<Intent>() { .fromCallable(() ->
@Override NavigationHelper.getIntentByLink(activity, service, searchString))
public Intent call() throws Exception {
return NavigationHelper.getIntentByLink(activity, service, query);
}
})
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Intent>() { .subscribe(intent -> {
@Override getFragmentManager().popBackStackImmediate();
public void accept(Intent intent) throws Exception { activity.startActivity(intent);
getFragmentManager().popBackStackImmediate(); }, throwable ->
activity.startActivity(intent); showError(getString(R.string.url_not_supported_toast), false)));
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
showError(getString(R.string.url_not_supported_toast), false);
}
}));
return; return;
} }
} catch (Exception e) { } catch (Exception e) {
// Exception occurred, it's not a url // Exception occurred, it's not a url
} }
lastSearchedQuery = query; lastSearchedString = this.searchString;
searchQuery = query; this.searchString = searchString;
currentPage = 0;
infoListAdapter.clearStreamItemList(); infoListAdapter.clearStreamItemList();
hideSuggestionsPanel(); hideSuggestionsPanel();
hideKeyboardSearch(); hideKeyboardSearch();
historyRecordManager.onSearched(serviceId, query) historyRecordManager.onSearched(serviceId, searchString)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
ignored -> {}, ignored -> {},
error -> showSnackBarError(error, UserAction.SEARCHED, error -> showSnackBarError(error, UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), query, 0) NewPipe.getNameOfService(serviceId), searchString, 0)
); );
suggestionPublisher.onNext(query); suggestionPublisher.onNext(searchString);
startLoading(false); startLoading(false);
} }
@ -703,11 +724,16 @@ public class SearchFragment
super.startLoading(forceLoad); super.startLoading(forceLoad);
if (disposables != null) disposables.clear(); if (disposables != null) disposables.clear();
if (searchDisposable != null) searchDisposable.dispose(); 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()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnEvent((searchResult, throwable) -> isLoading.set(false)) .doOnEvent((searchResult, throwable) -> isLoading.set(false))
.subscribe(this::handleResult, this::onError); .subscribe(this::handleResult, this::onError);
} }
@Override @Override
@ -715,8 +741,13 @@ public class SearchFragment
isLoading.set(true); isLoading.set(true);
showListFooter(true); showListFooter(true);
if (searchDisposable != null) searchDisposable.dispose(); if (searchDisposable != null) searchDisposable.dispose();
currentNextPage = currentPage + 1; searchDisposable = ExtractorHelper.getMoreSearchItems(
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter) serviceId,
searchString,
asList(contentFilter),
sortFilter,
nextPageUrl,
contentCountry)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
@ -739,19 +770,22 @@ public class SearchFragment
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void changeFilter(MenuItem item, SearchEngine.Filter filter) { private void changeContentFilter(MenuItem item, List<String> contentFilter) {
this.filter = filter;
this.filterItemCheckedId = item.getItemId(); this.filterItemCheckedId = item.getItemId();
item.setChecked(true); item.setChecked(true);
if (!TextUtils.isEmpty(searchQuery)) { this.contentFilter = new String[] {contentFilter.get(0)};
search(searchQuery);
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.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 (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
if (super.onError(exception)) return; if (super.onError(exception)) return;
int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; int errorId = exception instanceof ParsingException
onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, NewPipe.getNameOfService(serviceId), searchQuery, errorId); ? 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 @Override
public void handleResult(@NonNull SearchResult result) { public void handleResult(@NonNull SearchInfo result) {
if (!result.errors.isEmpty()) { final List<Throwable> exceptions = result.getErrors();
showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0); 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 (infoListAdapter.getItemsList().size() == 0) {
if (!result.getResults().isEmpty()) { if (!result.getRelatedItems().isEmpty()) {
infoListAdapter.addInfoItemList(result.getResults()); infoListAdapter.addInfoItemList(result.getRelatedItems());
} else { } else {
infoListAdapter.clearStreamItemList(); infoListAdapter.clearStreamItemList();
showEmptyState(); showEmptyState();
@ -821,12 +864,14 @@ public class SearchFragment
@Override @Override
public void handleNextItems(ListExtractor.InfoItemsPage result) { public void handleNextItems(ListExtractor.InfoItemsPage result) {
showListFooter(false); showListFooter(false);
currentPage = Integer.parseInt(result.getNextPageUrl()); currentPageUrl = result.getNextPageUrl();
infoListAdapter.addInfoItemList(result.getItems()); infoListAdapter.addInfoItemList(result.getItems());
nextPageUrl = result.getNextPageUrl();
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId) showSnackBarError(result.getErrors(), UserAction.SEARCHED,
, "\"" + searchQuery + "\" → page " + currentPage, 0); NewPipe.getNameOfService(serviceId)
, "\"" + searchString + "\" → page: " + nextPageUrl, 0);
} }
super.handleNextItems(result); super.handleNextItems(result);
} }
@ -835,12 +880,15 @@ public class SearchFragment
protected boolean onError(Throwable exception) { protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true; if (super.onError(exception)) return true;
if (exception instanceof SearchEngine.NothingFoundException) { if (exception instanceof SearchExtractor.NothingFoundException) {
infoListAdapter.clearStreamItemList(); infoListAdapter.clearStreamItemList();
showEmptyState(); showEmptyState();
} else { } else {
int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; int errorId = exception instanceof ParsingException
onUnrecoverableError(exception, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, errorId); ? R.string.parsing_error
: R.string.general_error;
onUnrecoverableError(exception, UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), searchString, errorId);
} }
return true; return true;

View file

@ -66,11 +66,10 @@ public final class BookmarkFragment
public View onCreateView(@NonNull LayoutInflater inflater, public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
Bundle savedInstanceState) { 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); return inflater.inflate(R.layout.fragment_bookmarks, container, false);
} }
@ -99,9 +98,7 @@ public final class BookmarkFragment
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() { itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
@Override @Override
public void selected(LocalItem selectedItem) { public void selected(LocalItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement final FragmentManager fragmentManager = getFM();
if (getParentFragment() == null) return;
final FragmentManager fragmentManager = getParentFragment().getFragmentManager();
if (selectedItem instanceof PlaylistMetadataEntry) { if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
@ -110,8 +107,11 @@ public final class BookmarkFragment
} else if (selectedItem instanceof PlaylistRemoteEntity) { } else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(), NavigationHelper.openPlaylistFragment(
entry.getUrl(), entry.getName()); fragmentManager,
entry.getServiceId(),
entry.getUrl(),
entry.getName());
} }
} }

View file

@ -71,6 +71,10 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
@Override @Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { 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); return inflater.inflate(R.layout.fragment_feed, container, false);
} }
@ -105,20 +109,19 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
super.onDestroyView(); super.onDestroyView();
} }
/*@Override @Override
protected RecyclerView.LayoutManager getListLayoutManager() { public void setUserVisibleHint(boolean isVisibleToUser) {
boolean isPortrait = getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels; super.setUserVisibleHint(isVisibleToUser);
return new GridLayoutManager(activity, isPortrait ? 1 : 2); if (activity != null && isVisibleToUser) {
}*/ setTitle(activity.getString(R.string.fragment_whats_new));
}
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater); super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar(); ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setTitle(R.string.fragment_whats_new);
}
if(useAsFrontPage) { if(useAsFrontPage) {
supportActionBar.setDisplayShowTitleEnabled(true); supportActionBar.setDisplayShowTitleEnabled(true);
@ -176,19 +179,9 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
showLoading(); showLoading();
showListFooter(true); showListFooter(true);
subscriptionObserver = subscriptionService.getSubscription() subscriptionObserver = subscriptionService.getSubscription()
.onErrorReturnItem(Collections.<SubscriptionEntity>emptyList()) .onErrorReturnItem(Collections.emptyList())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<SubscriptionEntity>>() { .subscribe(this::handleResult, this::onError);
@Override
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
handleResult(subscriptionEntities);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
onError(throwable);
}
});
} }
@Override @Override
@ -239,13 +232,12 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) { if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) {
subscriptionService.getChannelInfo(subscriptionEntity) subscriptionService.getChannelInfo(subscriptionEntity)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.onErrorComplete(new Predicate<Throwable>() { .onErrorComplete(
@Override (@io.reactivex.annotations.NonNull Throwable throwable) ->
public boolean test(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception { FeedFragment.super.onError(throwable))
return FeedFragment.super.onError(throwable); .subscribe(
} getChannelInfoObserver(subscriptionEntity.getServiceId(),
}) subscriptionEntity.getUrl()));
.subscribe(getChannelInfoObserver(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl()));
} else { } else {
requestFeed(1); requestFeed(1);
} }
@ -316,7 +308,10 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
@Override @Override
public void onError(Throwable exception) { 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); requestFeed(1);
onDone(); onDone();
} }
@ -361,12 +356,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
delayHandler.removeCallbacksAndMessages(null); delayHandler.removeCallbacksAndMessages(null);
// Add a little of a delay when requesting more items because the cache is so fast, // 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 // that the view seems stuck to the user when he scroll to the bottom
delayHandler.postDelayed(new Runnable() { delayHandler.postDelayed(() -> requestFeed(FEED_LOAD_COUNT), 300);
@Override
public void run() {
requestFeed(FEED_LOAD_COUNT);
}
}, 300);
} }
@Override @Override
@ -423,7 +413,9 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
int heightPixels = getResources().getDisplayMetrics().heightPixels; int heightPixels = getResources().getDisplayMetrics().heightPixels;
int itemHeightPixels = activity.getResources().getDimensionPixelSize(R.dimen.video_item_search_height); 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); 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) { protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true; if (super.onError(exception)) return true;
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; int errorId = exception instanceof ExtractionException
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Requesting feed", errorId); ? R.string.parsing_error
: R.string.general_error;
onUnrecoverableError(exception,
UserAction.SOMETHING_ELSE,
"none",
"Requesting feed",
errorId);
return true; return true;
} }
} }

View file

@ -21,6 +21,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -73,7 +74,7 @@ public class StatisticsPlaylistFragment
return results; return results;
case MOST_PLAYED: case MOST_PLAYED:
Collections.sort(results, (left, right) -> Collections.sort(results, (left, right) ->
((Long) right.watchCount).compareTo(left.watchCount)); Long.compare(right.watchCount, left.watchCount));
return results; return results;
default: return null; default: return null;
} }
@ -96,6 +97,14 @@ public class StatisticsPlaylistFragment
return inflater.inflate(R.layout.fragment_playlist, container, false); 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 // Fragment LifeCycle - Views
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -103,7 +112,9 @@ public class StatisticsPlaylistFragment
@Override @Override
protected void initViews(View rootView, Bundle savedInstanceState) { protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
setTitle(getString(R.string.title_last_played)); if(!useAsFrontPage) {
setTitle(getString(R.string.title_last_played));
}
} }
@Override @Override
@ -129,8 +140,10 @@ public class StatisticsPlaylistFragment
public void selected(LocalItem selectedItem) { public void selected(LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) { if (selectedItem instanceof StreamStatisticsEntry) {
final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem; final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem;
NavigationHelper.openVideoDetailFragment(getFragmentManager(), NavigationHelper.openVideoDetailFragment(getFM(),
item.serviceId, item.url, item.title); 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_background),
context.getResources().getString(R.string.start_here_on_popup), context.getResources().getString(R.string.start_here_on_popup),
context.getResources().getString(R.string.delete), context.getResources().getString(R.string.delete),
context.getResources().getString(R.string.share)
}; };
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
@ -321,6 +335,9 @@ public class StatisticsPlaylistFragment
case 5: case 5:
deleteEntry(index); deleteEntry(index);
break; break;
case 6:
shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
break;
default: default:
break; break;
} }
@ -337,7 +354,7 @@ public class StatisticsPlaylistFragment
final Disposable onDelete = recordManager.deleteStreamHistory(entry.streamId) final Disposable onDelete = recordManager.deleteStreamHistory(entry.streamId)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
howManyDelted -> { howManyDeleted -> {
if(getView() != null) { if(getView() != null) {
Snackbar.make(getView(), R.string.one_item_deleted, Snackbar.make(getView(), R.string.one_item_deleted,
Snackbar.LENGTH_SHORT).show(); Snackbar.LENGTH_SHORT).show();

View file

@ -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_background),
context.getResources().getString(R.string.start_here_on_popup), context.getResources().getString(R.string.start_here_on_popup),
context.getResources().getString(R.string.set_as_playlist_thumbnail), 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) -> { final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
@ -549,6 +550,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
case 6: case 6:
deleteItem(item); deleteItem(item);
break; break;
case 7:
shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
break;
default: default:
break; break;
} }

View file

@ -40,8 +40,11 @@ public class RemotePlaylistManager {
}).subscribeOn(Schedulers.io()); }).subscribeOn(Schedulers.io());
} }
public Single<Integer> onUpdate(final PlaylistInfo playlistInfo) { public Single<Integer> onUpdate(final long playlistId, final PlaylistInfo playlistInfo) {
return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo))) return Single.fromCallable(() -> {
.subscribeOn(Schedulers.io()); PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
playlist.setUid(playlistId);
return playlistRemoteTable.update(playlist);
}).subscribeOn(Schedulers.io());
} }
} }

View file

@ -13,8 +13,10 @@ import android.os.Parcelable;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; 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.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService; import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService;
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; 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) { 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()); previousBackupItem.setOnClickListener(item -> onImportPreviousSelected());
final int iconColor = ThemeHelper.isLightThemeSelected(getContext()) ? Color.BLACK : Color.WHITE; 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) { private void onImportFromServiceSelected(int serviceId) {
if (getParentFragment() == null) return; FragmentManager fragmentManager = getFM();
NavigationHelper.openSubscriptionsImportFragment(getParentFragment().getFragmentManager(), serviceId); NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId);
} }
private void onImportPreviousSelected() { private void onImportPreviousSelected() {
@ -318,15 +322,19 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() { infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
@Override @Override
public void selected(ChannelInfoItem selectedItem) { public void selected(ChannelInfoItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement final FragmentManager fragmentManager = getFM();
NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), NavigationHelper.openChannelFragment(fragmentManager,
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); selectedItem.getServiceId(),
selectedItem.getUrl(),
selectedItem.getName());
} }
}); });
//noinspection ConstantConditions //noinspection ConstantConditions
whatsNewItemListHeader.setOnClickListener(v -> whatsNewItemListHeader.setOnClickListener(v -> {
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager())); FragmentManager fragmentManager = getFM();
NavigationHelper.openWhatsNewFragment(fragmentManager);
});
importExportListHeader.setOnClickListener(v -> importExportOptions.switchState()); importExportListHeader.setOnClickListener(v -> importExportOptions.switchState());
} }
@ -397,10 +405,13 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
private List<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) { private List<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) {
List<InfoItem> items = new ArrayList<>(); 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, Collections.sort(items,
(InfoItem o1, InfoItem o2) -> o1.getName().compareToIgnoreCase(o2.getName())); (InfoItem o1, InfoItem o2) ->
o1.getName().compareToIgnoreCase(o2.getName()));
return items; return items;
} }
@ -429,7 +440,11 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
resetFragment(); resetFragment();
if (super.onError(exception)) return true; 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; return true;
} }
} }

View file

@ -28,7 +28,6 @@ import android.content.IntentFilter;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat; 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.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.nostra13.universalimageloader.core.assist.FailReason;
import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R; 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.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.LockManager; 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.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.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -94,7 +92,6 @@ public final class BackgroundPlayer extends Service {
private NotificationCompat.Builder notBuilder; private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView; private RemoteViews notRemoteView;
private RemoteViews bigNotRemoteView; private RemoteViews bigNotRemoteView;
private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha";
private boolean shouldUpdateOnProgress; private boolean shouldUpdateOnProgress;
@ -192,7 +189,9 @@ public final class BackgroundPlayer extends Service {
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCustomContentView(notRemoteView) .setCustomContentView(notRemoteView)
.setCustomBigContentView(bigNotRemoteView); .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; return builder;
} }
@ -249,15 +248,6 @@ public final class BackgroundPlayer extends Service {
notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); 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 // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -279,8 +269,16 @@ public final class BackgroundPlayer extends Service {
protected class BasePlayerImpl extends BasePlayer { protected class BasePlayerImpl extends BasePlayer {
@NonNull final private AudioPlaybackResolver resolver;
BasePlayerImpl(Context context) { BasePlayerImpl(Context context) {
super(context); super(context);
this.resolver = new AudioPlaybackResolver(context, dataSource);
}
@Override
public void initPlayer(boolean playOnReady) {
super.initPlayer(playOnReady);
} }
@Override @Override
@ -293,30 +291,41 @@ public final class BackgroundPlayer extends Service {
startForeground(NOTIFICATION_ID, notBuilder.build()); startForeground(NOTIFICATION_ID, notBuilder.build());
} }
@Override /*//////////////////////////////////////////////////////////////////////////
public void initThumbnail(final String url) { // Thumbnail Loading
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); private void updateNotificationThumbnail() {
updateNotification(-1); if (basePlayerImpl == null) return;
super.initThumbnail(url); if (notRemoteView != null) {
notRemoteView.setImageViewBitmap(R.id.notificationCover,
basePlayerImpl.getThumbnail());
}
if (bigNotRemoteView != null) {
bigNotRemoteView.setImageViewBitmap(R.id.notificationCover,
basePlayerImpl.getThumbnail());
}
} }
@Override @Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage); super.onLoadingComplete(imageUri, view, loadedImage);
resetNotification();
if (loadedImage != null) { updateNotificationThumbnail();
// rebuild notification here since remote view does not release bitmaps, causing memory leaks updateNotification(-1);
resetNotification();
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
updateNotification(-1);
}
} }
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
super.onLoadingFailed(imageUri, view, failReason);
resetNotification();
updateNotificationThumbnail();
updateNotification(-1);
}
/*//////////////////////////////////////////////////////////////////////////
// States Implementation
//////////////////////////////////////////////////////////////////////////*/
@Override @Override
public void onPrepared(boolean playWhenReady) { public void onPrepared(boolean playWhenReady) {
super.onPrepared(playWhenReady); super.onPrepared(playWhenReady);
@ -335,6 +344,7 @@ public final class BackgroundPlayer extends Service {
if (!shouldUpdateOnProgress) return; if (!shouldUpdateOnProgress) return;
resetNotification(); resetNotification();
if(Build.VERSION.SDK_INT >= 26 /*Oreo*/) updateNotificationThumbnail();
if (bigNotRemoteView != null) { if (bigNotRemoteView != null) {
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration)); bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration));
@ -390,29 +400,18 @@ public final class BackgroundPlayer extends Service {
// Playback Listener // Playback Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
protected void onMetadataChanged(@NonNull final PlayQueueItem item, protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
@Nullable final StreamInfo info, super.onMetadataChanged(tag);
final int newPlayQueueIndex, resetNotification();
final boolean hasPlayQueueItemChanged) { updateNotificationThumbnail();
if (shouldUpdateOnProgress || hasPlayQueueItemChanged) { updateNotification(-1);
resetNotification(); updateMetadata();
updateNotification(-1);
updateMetadata();
}
} }
@Override @Override
@Nullable @Nullable
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
final MediaSource liveSource = super.sourceOf(item, info); return resolver.resolve(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()));
} }
@Override @Override
@ -439,8 +438,8 @@ public final class BackgroundPlayer extends Service {
} }
private void updateMetadata() { private void updateMetadata() {
if (activityListener != null && currentInfo != null) { if (activityListener != null && getCurrentMetadata() != null) {
activityListener.onMetadataUpdate(currentInfo); activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata());
} }
} }
@ -531,44 +530,36 @@ public final class BackgroundPlayer extends Service {
updatePlayback(); updatePlayback();
} }
@Override
public void onBlocked() {
super.onBlocked();
setControlsOpacity(77);
updateNotification(-1);
}
@Override @Override
public void onPlaying() { public void onPlaying() {
super.onPlaying(); super.onPlaying();
resetNotification();
setControlsOpacity(255); updateNotificationThumbnail();
updateNotification(R.drawable.ic_pause_white); updateNotification(R.drawable.ic_pause_white);
lockManager.acquireWifiAndCpu(); lockManager.acquireWifiAndCpu();
} }
@Override @Override
public void onPaused() { public void onPaused() {
super.onPaused(); super.onPaused();
resetNotification();
updateNotificationThumbnail();
updateNotification(R.drawable.ic_play_arrow_white); updateNotification(R.drawable.ic_play_arrow_white);
lockManager.releaseWifiAndCpu(); lockManager.releaseWifiAndCpu();
} }
@Override @Override
public void onCompleted() { public void onCompleted() {
super.onCompleted(); super.onCompleted();
setControlsOpacity(255);
resetNotification(); resetNotification();
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); if (bigNotRemoteView != null) {
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); 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); updateNotification(R.drawable.ic_replay_white);
lockManager.releaseWifiAndCpu(); lockManager.releaseWifiAndCpu();
} }
} }

View file

@ -24,16 +24,14 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory; 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.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; 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.ImageLoader;
import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.Downloader; import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo; 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.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.LoadController; 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.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; 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 org.schabi.newpipe.util.SerializedCache;
import java.io.IOException; import java.io.IOException;
@ -82,12 +81,12 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; 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_INTERNAL;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; 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;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; 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 * 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 public abstract class BasePlayer implements
Player.EventListener, PlaybackListener, ImageLoadingListener { 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 public static final String TAG = "BasePlayer";
@NonNull final protected Context context; @NonNull final protected Context context;
@ -108,17 +107,26 @@ public abstract class BasePlayer implements
@NonNull final protected HistoryRecordManager recordManager; @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 // Intent
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static final String REPEAT_MODE = "repeat_mode"; @NonNull public static final String REPEAT_MODE = "repeat_mode";
public static final String PLAYBACK_PITCH = "playback_pitch"; @NonNull public static final String PLAYBACK_PITCH = "playback_pitch";
public static final String PLAYBACK_SPEED = "playback_speed"; @NonNull public static final String PLAYBACK_SPEED = "playback_speed";
public static final String PLAYBACK_QUALITY = "playback_quality"; @NonNull public static final String PLAYBACK_SKIP_SILENCE = "playback_skip_silence";
public static final String PLAY_QUEUE_KEY = "play_queue_key"; @NonNull public static final String PLAYBACK_QUALITY = "playback_quality";
public static final String APPEND_ONLY = "append_only"; @NonNull public static final String PLAY_QUEUE_KEY = "play_queue_key";
public static final String SELECT_ON_APPEND = "select_on_append"; @NonNull public static final String APPEND_ONLY = "append_only";
@NonNull public static final String SELECT_ON_APPEND = "select_on_append";
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Playback // Playback
@ -129,12 +137,13 @@ public abstract class BasePlayer implements
protected PlayQueue playQueue; protected PlayQueue playQueue;
protected PlayQueueAdapter playQueueAdapter; protected PlayQueueAdapter playQueueAdapter;
protected MediaSourceManager playbackManager; @Nullable protected MediaSourceManager playbackManager;
protected StreamInfo currentInfo; @Nullable private PlayQueueItem currentItem;
protected PlayQueueItem currentItem; @Nullable private MediaSourceTag currentMetadata;
@Nullable private Bitmap currentThumbnail;
protected Toast errorToast; @Nullable protected Toast errorToast;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Player // Player
@ -145,18 +154,11 @@ public abstract class BasePlayer implements
protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500; protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds
protected CustomTrackSelector trackSelector;
protected PlayerDataSource dataSource;
protected SimpleExoPlayer simpleExoPlayer; protected SimpleExoPlayer simpleExoPlayer;
protected AudioReactor audioReactor; protected AudioReactor audioReactor;
protected MediaSessionManager mediaSessionManager; protected MediaSessionManager mediaSessionManager;
private boolean isPrepared = false; 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(); this.intentFilter = new IntentFilter();
setupBroadcastReceiver(intentFilter); setupBroadcastReceiver(intentFilter);
context.registerReceiver(broadcastReceiver, intentFilter);
this.recordManager = new HistoryRecordManager(context); 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() { public void setup() {
if (simpleExoPlayer == null) initPlayer(/*playOnInit=*/true); if (simpleExoPlayer == null) {
initPlayer(/*playOnInit=*/true);
}
initListeners(); initListeners();
} }
public void initPlayer(final boolean playOnReady) { public void initPlayer(final boolean playOnReady) {
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); 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 = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
simpleExoPlayer.addListener(this); simpleExoPlayer.addListener(this);
simpleExoPlayer.setPlayWhenReady(playOnReady); simpleExoPlayer.setPlayWhenReady(playOnReady);
@ -205,6 +209,8 @@ public abstract class BasePlayer implements
audioReactor = new AudioReactor(context, simpleExoPlayer); audioReactor = new AudioReactor(context, simpleExoPlayer);
mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer,
new BasePlayerMediaSession(this)); new BasePlayerMediaSession(this));
registerBroadcastReceiver();
} }
public void initListeners() {} public void initListeners() {}
@ -235,20 +241,24 @@ public abstract class BasePlayer implements
final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode());
final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()); final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed());
final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch());
final boolean playbackSkipSilence = intent.getBooleanExtra(PLAYBACK_SKIP_SILENCE,
getPlaybackSkipSilence());
// Good to go... // 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, protected void initPlayback(@NonNull final PlayQueue queue,
@Player.RepeatMode final int repeatMode, @Player.RepeatMode final int repeatMode,
final float playbackSpeed, final float playbackSpeed,
final float playbackPitch, final float playbackPitch,
final boolean playbackSkipSilence,
final boolean playOnReady) { final boolean playOnReady) {
destroyPlayer(); destroyPlayer();
initPlayer(playOnReady); initPlayer(playOnReady);
setRepeatMode(repeatMode); setRepeatMode(repeatMode);
setPlaybackParameters(playbackSpeed, playbackPitch); setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence);
playQueue = queue; playQueue = queue;
playQueue.init(); playQueue.init();
@ -270,7 +280,6 @@ public abstract class BasePlayer implements
if (playQueue != null) playQueue.dispose(); if (playQueue != null) playQueue.dispose();
if (audioReactor != null) audioReactor.dispose(); if (audioReactor != null) audioReactor.dispose();
if (playbackManager != null) playbackManager.dispose(); if (playbackManager != null) playbackManager.dispose();
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
if (mediaSessionManager != null) mediaSessionManager.dispose(); if (mediaSessionManager != null) mediaSessionManager.dispose();
if (playQueueAdapter != null) { if (playQueueAdapter != null) {
@ -284,20 +293,22 @@ public abstract class BasePlayer implements
destroyPlayer(); destroyPlayer();
unregisterBroadcastReceiver(); unregisterBroadcastReceiver();
trackSelector = null; databaseUpdateReactor.clear();
progressUpdateReactor.set(null);
simpleExoPlayer = null; simpleExoPlayer = null;
mediaSessionManager = null;
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Thumbnail Loading // Thumbnail Loading
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public void initThumbnail(final String url) { private void initThumbnail(final String url) {
if (DEBUG) Log.d(TAG, "Thumbnail - initThumbnail() called"); if (DEBUG) Log.d(TAG, "Thumbnail - initThumbnail() called");
if (url == null || url.isEmpty()) return; if (url == null || url.isEmpty()) return;
ImageLoader.getInstance().resume(); ImageLoader.getInstance().resume();
ImageLoader.getInstance().loadImage(url, this); ImageLoader.getInstance().loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS,
this);
} }
@Override @Override
@ -310,6 +321,7 @@ public abstract class BasePlayer implements
public void onLoadingFailed(String imageUri, View view, FailReason failReason) { public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]",
failReason.getCause()); failReason.getCause());
currentThumbnail = null;
} }
@Override @Override
@ -317,64 +329,14 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " +
"imageUri = [" + imageUri + "], view = [" + view + "], " + "imageUri = [" + imageUri + "], view = [" + view + "], " +
"loadedImage = [" + loadedImage + "]"); "loadedImage = [" + loadedImage + "]");
currentThumbnail = loadedImage;
} }
@Override @Override
public void onLoadingCancelled(String imageUri, View view) { public void onLoadingCancelled(String imageUri, View view) {
if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " +
"imageUri = [" + imageUri + "], view = [" + view + "]"); "imageUri = [" + imageUri + "], view = [" + view + "]");
} currentThumbnail = null;
/*//////////////////////////////////////////////////////////////////////////
// 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);
}
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -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 { try {
context.unregisterReceiver(broadcastReceiver); context.unregisterReceiver(broadcastReceiver);
} catch (final IllegalArgumentException unregisteredException) { } 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); public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent);
protected void startProgressLoop() { protected void startProgressLoop() {
if (progressUpdateReactor != null) progressUpdateReactor.dispose(); progressUpdateReactor.set(getProgressReactor());
progressUpdateReactor = getProgressReactor();
} }
protected void stopProgressLoop() { protected void stopProgressLoop() {
if (progressUpdateReactor != null) progressUpdateReactor.dispose(); progressUpdateReactor.set(null);
progressUpdateReactor = null;
} }
public void triggerProgressUpdate() { public void triggerProgressUpdate() {
@ -531,7 +497,8 @@ public abstract class BasePlayer implements
private Disposable getProgressReactor() { private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS) return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .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") + ", " + (manifest == null ? "no manifest" : "available manifest") + ", " +
"timeline size = [" + timeline.getWindowCount() + "], " + "timeline size = [" + timeline.getWindowCount() + "], " +
"reason = [" + reason + "]"); "reason = [" + reason + "]");
if (playQueue == null) return;
switch (reason) { maybeUpdateCurrentMetadata();
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;
}
} }
@Override @Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
if (DEBUG) Log.d(TAG, "ExoPlayer - onTracksChanged(), " + if (DEBUG) Log.d(TAG, "ExoPlayer - onTracksChanged(), " +
"track group size = " + trackGroups.length); "track group size = " + trackGroups.length);
maybeUpdateCurrentMetadata();
} }
@Override @Override
@ -586,6 +541,8 @@ public abstract class BasePlayer implements
} else if (isLoading && !isProgressLoopRunning()) { } else if (isLoading && !isProgressLoopRunning()) {
startProgressLoop(); startProgressLoop();
} }
maybeUpdateCurrentMetadata();
} }
@Override @Override
@ -609,6 +566,7 @@ public abstract class BasePlayer implements
} }
break; break;
case Player.STATE_READY: //3 case Player.STATE_READY: //3
maybeUpdateCurrentMetadata();
maybeCorrectSeekPosition(); maybeCorrectSeekPosition();
if (!isPrepared) { if (!isPrepared) {
isPrepared = true; isPrepared = true;
@ -625,38 +583,19 @@ public abstract class BasePlayer implements
} }
private void maybeCorrectSeekPosition() { 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(); final PlayQueueItem currentSourceItem = playQueue.getItem();
if (currentSourceItem == null) return; if (currentSourceItem == null) return;
final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition(); final StreamInfo currentInfo = currentMetadata.getMetadata();
final boolean isCurrentWindowCorrect =
simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex;
final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000;
if (presetStartPositionMillis > 0L) {
if (recoveryPositionMillis != PlayQueueItem.RECOVERY_UNSET && isCurrentWindowCorrect) { // Has another start position?
// 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 (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " +
"position=[" + presetStartPositionMillis + "]"); "position=[" + presetStartPositionMillis + "]");
// Has another start position?
seekTo(presetStartPositionMillis); seekTo(presetStartPositionMillis);
currentInfo.setStartPosition(0);
} }
isSynchronizing = false;
} }
/** /**
@ -708,7 +647,7 @@ public abstract class BasePlayer implements
setRecovery(); setRecovery();
final Throwable cause = error.getCause(); final Throwable cause = error.getCause();
if (cause instanceof BehindLiveWindowException) { if (error instanceof BehindLiveWindowException) {
reload(); reload();
} else if (cause instanceof UnknownHostException) { } else if (cause instanceof UnknownHostException) {
playQueue.error(/*isNetworkProblem=*/true); playQueue.error(/*isNetworkProblem=*/true);
@ -727,22 +666,29 @@ public abstract class BasePlayer implements
public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) {
if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " +
"reason = [" + reason + "]"); "reason = [" + reason + "]");
// Refresh the playback if there is a transition to the next video if (playQueue == null) return;
final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex();
/* 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) { switch (reason) {
case DISCONTINUITY_REASON_PERIOD_TRANSITION: 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(); registerView();
} else { break;
playQueue.offsetIndex(+1);
} }
case DISCONTINUITY_REASON_SEEK: case DISCONTINUITY_REASON_SEEK:
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
case DISCONTINUITY_REASON_INTERNAL: case DISCONTINUITY_REASON_INTERNAL:
if (playQueue.getIndex() != newWindowIndex) {
playQueue.setIndex(newWindowIndex);
}
break; break;
} }
maybeUpdateCurrentMetadata();
} }
@Override @Override
@ -788,7 +734,7 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called"); if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called");
currentItem = null; currentItem = null;
currentInfo = null; currentMetadata = null;
simpleExoPlayer.stop(); simpleExoPlayer.stop();
isPrepared = false; isPrepared = false;
@ -805,42 +751,21 @@ public abstract class BasePlayer implements
simpleExoPlayer.prepare(mediaSource); simpleExoPlayer.prepare(mediaSource);
} }
@Override public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) {
public void onPlaybackSynchronize(@NonNull final PlayQueueItem item,
@Nullable final StreamInfo info) {
if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " +
(info != null ? "available" : "null") + " info, " +
"item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
if (simpleExoPlayer == null || playQueue == null) return; if (simpleExoPlayer == null || playQueue == null) return;
final boolean onPlaybackInitial = currentItem == null; final boolean onPlaybackInitial = currentItem == null;
final boolean hasPlayQueueItemChanged = currentItem != item; final boolean hasPlayQueueItemChanged = currentItem != item;
final boolean hasStreamInfoChanged = currentInfo != info;
final int currentPlayQueueIndex = playQueue.indexOf(item); final int currentPlayQueueIndex = playQueue.indexOf(item);
final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex();
final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); 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 nothing to synchronize
if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { if (!hasPlayQueueItemChanged) return;
return;
}
currentItem = item; 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 // Check if on wrong window
if (currentPlayQueueIndex != playQueue.getIndex()) { if (currentPlayQueueIndex != playQueue.getIndex()) {
@ -855,39 +780,29 @@ public abstract class BasePlayer implements
"index=[" + currentPlayQueueIndex + "] with " + "index=[" + currentPlayQueueIndex + "] with " +
"playlist length=[" + currentPlaylistSize + "]"); "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 || } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial ||
!isPlaying()) { !isPlaying()) {
if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" +
" index=[" + currentPlayQueueIndex + "]," + " index=[" + currentPlayQueueIndex + "]," +
" from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "]."); " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "].");
isSynchronizing = true;
simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition());
playQueue.unsetRecovery(currentPlayQueueIndex);
} else {
simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex);
}
} }
} }
abstract protected void onMetadataChanged(@NonNull final PlayQueueItem item, protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
@Nullable final StreamInfo info, final StreamInfo info = tag.getMetadata();
final int newPlayQueueIndex, if (DEBUG) {
final boolean hasPlayQueueItemChanged); Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
@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()) { initThumbnail(info.getThumbnailUrl());
return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); registerView();
} else if (!info.getDashMpdUrl().isEmpty()) {
return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH);
}
return null;
} }
@Override @Override
@ -1020,9 +935,7 @@ public abstract class BasePlayer implements
public void seekTo(long positionMillis) { public void seekTo(long positionMillis) {
if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
if (simpleExoPlayer == null || positionMillis < 0 || if (simpleExoPlayer != null) simpleExoPlayer.seekTo(positionMillis);
positionMillis > simpleExoPlayer.getDuration()) return;
simpleExoPlayer.seekTo(positionMillis);
} }
public void seekBy(long offsetMillis) { public void seekBy(long offsetMillis) {
@ -1046,12 +959,14 @@ public abstract class BasePlayer implements
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void registerView() { private void registerView() {
if (databaseUpdateReactor == null || currentInfo == null) return; if (currentMetadata == null) return;
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() final StreamInfo currentInfo = currentMetadata.getMetadata();
final Disposable viewRegister = recordManager.onViewed(currentInfo).onErrorComplete()
.subscribe( .subscribe(
ignored -> {/* successful */}, ignored -> {/* successful */},
error -> Log.e(TAG, "Player onViewed() failure: ", error) error -> Log.e(TAG, "Player onViewed() failure: ", error)
)); );
databaseUpdateReactor.add(viewRegister);
} }
protected void reload() { protected void reload() {
@ -1065,7 +980,7 @@ public abstract class BasePlayer implements
} }
protected void savePlaybackState(final StreamInfo info, final long progress) { 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) final Disposable stateSaver = recordManager.saveStreamState(info, progress)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.onErrorComplete() .onErrorComplete()
@ -1077,7 +992,8 @@ public abstract class BasePlayer implements
} }
private void savePlaybackState() { 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 && if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS &&
simpleExoPlayer.getCurrentPosition() < simpleExoPlayer.getCurrentPosition() <
@ -1085,6 +1001,36 @@ public abstract class BasePlayer implements
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); 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 // Getters and Setters
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -1101,19 +1047,35 @@ public abstract class BasePlayer implements
return currentState; return currentState;
} }
@Nullable
public MediaSourceTag getCurrentMetadata() {
return currentMetadata;
}
@NonNull
public String getVideoUrl() { 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() { 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() { 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 */ /** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isLiveEdge() { public boolean isLiveEdge() {
if (simpleExoPlayer == null || !isLive()) return false; if (simpleExoPlayer == null || !isLive()) return false;
@ -1135,6 +1097,9 @@ public abstract class BasePlayer implements
return simpleExoPlayer.isCurrentWindowDynamic(); return simpleExoPlayer.isCurrentWindowDynamic();
} catch (@NonNull IndexOutOfBoundsException ignored) { } catch (@NonNull IndexOutOfBoundsException ignored) {
// Why would this even happen =( // 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; return false;
} }
} }
@ -1147,11 +1112,11 @@ public abstract class BasePlayer implements
@Player.RepeatMode @Player.RepeatMode
public int getRepeatMode() { public int getRepeatMode() {
return simpleExoPlayer.getRepeatMode(); return simpleExoPlayer == null ? Player.REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
} }
public void setRepeatMode(@Player.RepeatMode final int repeatMode) { public void setRepeatMode(@Player.RepeatMode final int repeatMode) {
simpleExoPlayer.setRepeatMode(repeatMode); if (simpleExoPlayer != null) simpleExoPlayer.setRepeatMode(repeatMode);
} }
public float getPlaybackSpeed() { public float getPlaybackSpeed() {
@ -1162,19 +1127,22 @@ public abstract class BasePlayer implements
return getPlaybackParameters().pitch; return getPlaybackParameters().pitch;
} }
public boolean getPlaybackSkipSilence() {
return getPlaybackParameters().skipSilence;
}
public void setPlaybackSpeed(float speed) { public void setPlaybackSpeed(float speed) {
setPlaybackParameters(speed, getPlaybackPitch()); setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence());
} }
public PlaybackParameters getPlaybackParameters() { public PlaybackParameters getPlaybackParameters() {
final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f); if (simpleExoPlayer == null) return PlaybackParameters.DEFAULT;
if (simpleExoPlayer == null) return defaultParameters;
final PlaybackParameters parameters = simpleExoPlayer.getPlaybackParameters(); final PlaybackParameters parameters = simpleExoPlayer.getPlaybackParameters();
return parameters == null ? defaultParameters : parameters; return parameters == null ? PlaybackParameters.DEFAULT : parameters;
} }
public void setPlaybackParameters(float speed, float pitch) { public void setPlaybackParameters(float speed, float pitch, boolean skipSilence) {
simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch)); simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch, skipSilence));
} }
public PlayQueue getPlayQueue() { public PlayQueue getPlayQueue() {
@ -1190,7 +1158,7 @@ public abstract class BasePlayer implements
} }
public boolean isProgressLoopRunning() { public boolean isProgressLoopRunning() {
return progressUpdateReactor != null && !progressUpdateReactor.isDisposed(); return progressUpdateReactor.get() != null;
} }
public void setRecovery() { public void setRecovery() {

View file

@ -36,6 +36,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.content.res.AppCompatResources;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper; import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@ -46,7 +47,9 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.SeekBar; import android.widget.SeekBar;
import android.widget.TextView; import android.widget.TextView;
@ -58,7 +61,6 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.ui.SubtitleView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog; 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.PlayQueueItemBuilder;
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; 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.AnimationUtils;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; 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.BasePlayer.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; 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.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.Type.SLIDE_AND_ALPHA;
import static org.schabi.newpipe.util.AnimationUtils.animateRotation; import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
@ -104,6 +109,7 @@ public final class MainVideoPlayer extends AppCompatActivity
@Nullable private PlayerState playerState; @Nullable private PlayerState playerState;
private boolean isInMultiWindow; private boolean isInMultiWindow;
private boolean isBackPressed;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Activity LifeCycle // Activity LifeCycle
@ -125,7 +131,7 @@ public final class MainVideoPlayer extends AppCompatActivity
hideSystemUi(); hideSystemUi();
setContentView(R.layout.activity_main_player); setContentView(R.layout.activity_main_player);
playerImpl = new VideoPlayerImpl(this); playerImpl = new VideoPlayerImpl(this);
playerImpl.setup(findViewById(android.R.id.content)); playerImpl.setup(findViewById(android.R.id.content));
if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) { if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) {
@ -152,7 +158,10 @@ public final class MainVideoPlayer extends AppCompatActivity
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
super.onNewIntent(intent); super.onNewIntent(intent);
playerImpl.handleIntent(intent); if (intent != null) {
playerState = null;
playerImpl.handleIntent(intent);
}
} }
@Override @Override
@ -177,7 +186,7 @@ public final class MainVideoPlayer extends AppCompatActivity
playerImpl.setPlaybackQuality(playerState.getPlaybackQuality()); playerImpl.setPlaybackQuality(playerState.getPlaybackQuality());
playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(), playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(),
playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(), 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 @Override
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called"); if (DEBUG) Log.d(TAG, "onSaveInstanceState() called");
@ -200,7 +215,8 @@ public final class MainVideoPlayer extends AppCompatActivity
playerImpl.setRecovery(); playerImpl.setRecovery();
playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(), playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(), playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
playerImpl.getPlaybackQuality(), playerImpl.isPlaying()); playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(),
playerImpl.isPlaying());
StateSaver.tryToSave(isChangingConfigurations(), null, outState, this); StateSaver.tryToSave(isChangingConfigurations(), null, outState, this);
} }
@ -208,10 +224,17 @@ public final class MainVideoPlayer extends AppCompatActivity
protected void onStop() { protected void onStop() {
if (DEBUG) Log.d(TAG, "onStop() called"); if (DEBUG) Log.d(TAG, "onStop() called");
super.onStop(); super.onStop();
playerImpl.destroy();
PlayerHelper.setScreenBrightness(getApplicationContext(), PlayerHelper.setScreenBrightness(getApplicationContext(),
getWindow().getAttributes().screenBrightness); 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 @Override
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch); boolean playbackSkipSilence) {
if (playerImpl != null) {
playerImpl.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence);
}
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@SuppressWarnings({"unused", "WeakerAccess"}) @SuppressWarnings({"unused", "WeakerAccess"})
private class VideoPlayerImpl extends VideoPlayer { private class VideoPlayerImpl extends VideoPlayer {
private final float MAX_GESTURE_LENGTH = 0.75f;
private TextView titleTextView; private TextView titleTextView;
private TextView channelTextView; private TextView channelTextView;
private TextView volumeTextView; private RelativeLayout volumeRelativeLayout;
private TextView brightnessTextView; private ProgressBar volumeProgressBar;
private ImageView volumeImageView;
private RelativeLayout brightnessRelativeLayout;
private ProgressBar brightnessProgressBar;
private ImageView brightnessImageView;
private ImageButton queueButton; private ImageButton queueButton;
private ImageButton repeatButton; private ImageButton repeatButton;
private ImageButton shuffleButton; private ImageButton shuffleButton;
@ -370,6 +402,8 @@ public final class MainVideoPlayer extends AppCompatActivity
private RelativeLayout windowRootLayout; private RelativeLayout windowRootLayout;
private View secondaryControls; private View secondaryControls;
private int maxGestureLength;
VideoPlayerImpl(final Context context) { VideoPlayerImpl(final Context context) {
super("VideoPlayerImpl" + MainVideoPlayer.TAG, context); super("VideoPlayerImpl" + MainVideoPlayer.TAG, context);
} }
@ -379,8 +413,12 @@ public final class MainVideoPlayer extends AppCompatActivity
super.initViews(rootView); super.initViews(rootView);
this.titleTextView = rootView.findViewById(R.id.titleTextView); this.titleTextView = rootView.findViewById(R.id.titleTextView);
this.channelTextView = rootView.findViewById(R.id.channelTextView); this.channelTextView = rootView.findViewById(R.id.channelTextView);
this.volumeTextView = rootView.findViewById(R.id.volumeTextView); this.volumeRelativeLayout = rootView.findViewById(R.id.volumeRelativeLayout);
this.brightnessTextView = rootView.findViewById(R.id.brightnessTextView); 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.queueButton = rootView.findViewById(R.id.queueButton);
this.repeatButton = rootView.findViewById(R.id.repeatButton); this.repeatButton = rootView.findViewById(R.id.repeatButton);
this.shuffleButton = rootView.findViewById(R.id.shuffleButton); this.shuffleButton = rootView.findViewById(R.id.shuffleButton);
@ -422,7 +460,7 @@ public final class MainVideoPlayer extends AppCompatActivity
public void initListeners() { public void initListeners() {
super.initListeners(); super.initListeners();
MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); PlayerGestureListener listener = new PlayerGestureListener();
gestureDetector = new GestureDetector(context, listener); gestureDetector = new GestureDetector(context, listener);
gestureDetector.setIsLongpressEnabled(false); gestureDetector.setIsLongpressEnabled(false);
getRootView().setOnTouchListener(listener); getRootView().setOnTouchListener(listener);
@ -439,6 +477,37 @@ public final class MainVideoPlayer extends AppCompatActivity
toggleOrientationButton.setOnClickListener(this); toggleOrientationButton.setOnClickListener(this);
switchBackgroundButton.setOnClickListener(this); switchBackgroundButton.setOnClickListener(this);
switchPopupButton.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 // Playback Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
protected void onMetadataChanged(@NonNull final PlayQueueItem item, protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
@Nullable final StreamInfo info, super.onMetadataChanged(tag);
final int newPlayQueueIndex,
final boolean hasPlayQueueItemChanged) {
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
titleTextView.setText(getVideoTitle()); titleTextView.setText(tag.getMetadata().getName());
channelTextView.setText(getUploaderName()); channelTextView.setText(tag.getMetadata().getUploaderName());
} }
@Override @Override
@ -501,6 +567,7 @@ public final class MainVideoPlayer extends AppCompatActivity
this.getRepeatMode(), this.getRepeatMode(),
this.getPlaybackSpeed(), this.getPlaybackSpeed(),
this.getPlaybackPitch(), this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality() this.getPlaybackQuality()
); );
context.startService(intent); context.startService(intent);
@ -522,6 +589,7 @@ public final class MainVideoPlayer extends AppCompatActivity
this.getRepeatMode(), this.getRepeatMode(),
this.getPlaybackSpeed(), this.getPlaybackSpeed(),
this.getPlaybackPitch(), this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality() this.getPlaybackQuality()
); );
context.startService(intent); context.startService(intent);
@ -617,7 +685,8 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override @Override
public void onPlaybackSpeedClicked() { public void onPlaybackSpeedClicked() {
PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch()) PlaybackParameterDialog
.newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence())
.show(getSupportFragmentManager(), TAG); .show(getSupportFragmentManager(), TAG);
} }
@ -647,14 +716,19 @@ public final class MainVideoPlayer extends AppCompatActivity
} }
@Override @Override
protected int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) { protected VideoPlaybackResolver.QualityResolver getQualityResolver() {
return ListHelper.getDefaultResolutionIndex(context, sortedVideos); return new VideoPlaybackResolver.QualityResolver() {
} @Override
public int getDefaultResolutionIndex(List<VideoStream> sortedVideos) {
return ListHelper.getDefaultResolutionIndex(context, sortedVideos);
}
@Override @Override
protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, public int getOverrideResolutionIndex(List<VideoStream> sortedVideos,
final String playbackQuality) { String playbackQuality) {
return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality);
}
};
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -678,7 +752,6 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override @Override
public void onBuffering() { public void onBuffering() {
super.onBuffering(); super.onBuffering();
animatePlayButtons(false, 100);
getRootView().setKeepScreenOn(true); getRootView().setKeepScreenOn(true);
} }
@ -728,6 +801,13 @@ public final class MainVideoPlayer extends AppCompatActivity
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void setInitialGestureValues() {
if (getAudioReactor() != null) {
final float currentVolumeNormalized = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume();
volumeProgressBar.setProgress((int) (volumeProgressBar.getMax() * currentVolumeNormalized));
}
}
@Override @Override
public void showControlsThenHide() { public void showControlsThenHide() {
if (queueVisible) return; if (queueVisible) return;
@ -831,12 +911,28 @@ public final class MainVideoPlayer extends AppCompatActivity
return channelTextView; return channelTextView;
} }
public TextView getVolumeTextView() { public RelativeLayout getVolumeRelativeLayout() {
return volumeTextView; return volumeRelativeLayout;
} }
public TextView getBrightnessTextView() { public ProgressBar getVolumeProgressBar() {
return brightnessTextView; 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() { public ImageButton getRepeatButton() {
@ -846,15 +942,18 @@ public final class MainVideoPlayer extends AppCompatActivity
public ImageButton getPlayPauseButton() { public ImageButton getPlayPauseButton() {
return playPauseButton; 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; private boolean isMoving;
@Override @Override
public boolean onDoubleTap(MotionEvent e) { 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 (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) { if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) {
playerImpl.onFastForward(); playerImpl.onFastForward();
@ -888,91 +987,91 @@ public final class MainVideoPlayer extends AppCompatActivity
return super.onDown(e); return super.onDown(e);
} }
private static final int MOVEMENT_THRESHOLD = 40;
private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext()); 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 @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; if (!isPlayerGestureEnabled) return false;
//noinspection PointlessBooleanExpression //noinspection PointlessBooleanExpression
if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " + if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " +
", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" +
", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" +
", distanceXy = [" + distanceX + ", " + distanceY + "]"); ", distanceXy = [" + distanceX + ", " + distanceY + "]");
float abs = Math.abs(e2.getY() - e1.getY());
if (!triggered) { final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD;
triggered = abs > MOVEMENT_THRESHOLD; if (!isMoving && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY))
|| playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
return false; return false;
} }
if (eventsNum++ % eventsThreshold != 0 || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) return false;
isMoving = true; isMoving = true;
// boolean up = !((e2.getY() - e1.getY()) > 0) && distanceY > 0; // Android's origin point is on top
boolean up = distanceY > 0;
if (initialEvent.getX() > playerImpl.getRootView().getWidth() / 2) {
if (e1.getX() > playerImpl.getRootView().getWidth() / 2) { playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
double floor = Math.floor(up ? stepVolume : -stepVolume); float currentProgressPercent =
currentVolume = (int) (playerImpl.getAudioReactor().getVolume() + floor); (float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
if (currentVolume >= maxVolume) currentVolume = maxVolume; int currentVolume = (int) (maxVolume * currentProgressPercent);
if (currentVolume <= minVolume) currentVolume = (int) minVolume;
playerImpl.getAudioReactor().setVolume(currentVolume); playerImpl.getAudioReactor().setVolume(currentVolume);
currentVolume = playerImpl.getAudioReactor().getVolume();
if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); 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); final int resId =
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE); 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 { } else {
WindowManager.LayoutParams lp = getWindow().getAttributes(); playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY);
currentBrightness += up ? stepBrightness : -stepBrightness; float currentProgressPercent =
if (currentBrightness >= 1f) currentBrightness = 1f; (float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength();
if (currentBrightness <= minBrightness) currentBrightness = minBrightness; WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
layoutParams.screenBrightness = currentProgressPercent;
getWindow().setAttributes(layoutParams);
lp.screenBrightness = currentBrightness; if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentProgressPercent);
getWindow().setAttributes(lp);
if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentBrightness);
int brightnessNormalized = Math.round(currentBrightness * 100);
final String brightnessText = brightnessUnicode + " " + (brightnessNormalized == 1 ? 0 : brightnessNormalized) + "%"; final int resId =
playerImpl.getBrightnessTextView().setText(brightnessText); 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); playerImpl.getBrightnessImageView().setImageDrawable(
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); 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; return true;
} }
private void onScrollEnd() { private void onScrollEnd() {
if (DEBUG) Log.d(TAG, "onScrollEnd() called"); if (DEBUG) Log.d(TAG, "onScrollEnd() called");
triggered = false;
eventsNum = 0; if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
/* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
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.getBrightnessTextView().getVisibility() == View.VISIBLE) { if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getBrightnessTextView(), false, 200, 200); animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
} }
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {

View file

@ -14,21 +14,26 @@ public class PlayerState implements Serializable {
private final float playbackSpeed; private final float playbackSpeed;
private final float playbackPitch; private final float playbackPitch;
@Nullable private final String playbackQuality; @Nullable private final String playbackQuality;
private final boolean playbackSkipSilence;
private final boolean wasPlaying; private final boolean wasPlaying;
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch, final boolean wasPlaying) { final float playbackSpeed, final float playbackPitch,
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, wasPlaying); final boolean playbackSkipSilence, final boolean wasPlaying) {
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null,
playbackSkipSilence, wasPlaying);
} }
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch, 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.playQueue = playQueue;
this.repeatMode = repeatMode; this.repeatMode = repeatMode;
this.playbackSpeed = playbackSpeed; this.playbackSpeed = playbackSpeed;
this.playbackPitch = playbackPitch; this.playbackPitch = playbackPitch;
this.playbackQuality = playbackQuality; this.playbackQuality = playbackQuality;
this.playbackSkipSilence = playbackSkipSilence;
this.wasPlaying = wasPlaying; this.wasPlaying = wasPlaying;
} }
@ -62,6 +67,10 @@ public class PlayerState implements Serializable {
return playbackQuality; return playbackQuality;
} }
public boolean isPlaybackSkipSilence() {
return playbackSkipSilence;
}
public boolean wasPlaying() { public boolean wasPlaying() {
return wasPlaying; return wasPlaying;
} }

View file

@ -19,6 +19,8 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
@ -34,7 +36,7 @@ import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
@ -42,7 +44,9 @@ import android.view.GestureDetector;
import android.view.Gravity; import android.view.Gravity;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.animation.AnticipateInterpolator;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.PopupMenu; 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.text.CaptionStyleCompat;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.ui.SubtitleView;
import com.nostra13.universalimageloader.core.assist.FailReason;
import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.CheckForNewAppVersionTask;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.old.PlayVideoActivity; 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.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper; 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 static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300;
private WindowManager windowManager; private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
private WindowManager.LayoutParams windowLayoutParams; WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
private GestureDetector gestureDetector; 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 int tossFlingVelocity;
private float screenWidth, screenHeight; private float screenWidth, screenHeight;
@ -118,6 +130,7 @@ public final class PopupVideoPlayer extends Service {
private VideoPlayerImpl playerImpl; private VideoPlayerImpl playerImpl;
private LockManager lockManager; private LockManager lockManager;
private boolean isPopupClosing = false;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder // Service-Activity Binder
@ -146,7 +159,10 @@ public final class PopupVideoPlayer extends Service {
public int onStartCommand(final Intent intent, int flags, int startId) { public int onStartCommand(final Intent intent, int flags, int startId) {
if (DEBUG) if (DEBUG)
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); 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); if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true);
playerImpl.handleIntent(intent); playerImpl.handleIntent(intent);
@ -156,15 +172,16 @@ public final class PopupVideoPlayer extends Service {
@Override @Override
public void onConfigurationChanged(Configuration newConfig) { public void onConfigurationChanged(Configuration newConfig) {
if (DEBUG) Log.d(TAG, "onConfigurationChanged() called with: newConfig = [" + newConfig + "]");
updateScreenSize(); updateScreenSize();
updatePopupSize(windowLayoutParams.width, -1); updatePopupSize(popupLayoutParams.width, -1);
checkPositionBounds(); checkPopupPositionBounds();
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy() called"); if (DEBUG) Log.d(TAG, "onDestroy() called");
onClose(); closePopup();
} }
@Override @Override
@ -182,7 +199,6 @@ public final class PopupVideoPlayer extends Service {
View rootView = View.inflate(this, R.layout.player_popup, null); View rootView = View.inflate(this, R.layout.player_popup, null);
playerImpl.setup(rootView); playerImpl.setup(rootView);
shutdownFlingVelocity = PlayerHelper.getShutdownFlingVelocity(this);
tossFlingVelocity = PlayerHelper.getTossFlingVelocity(this); tossFlingVelocity = PlayerHelper.getTossFlingVelocity(this);
updateScreenSize(); updateScreenSize();
@ -192,28 +208,56 @@ public final class PopupVideoPlayer extends Service {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize; 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), (int) popupWidth, (int) getMinimumVideoHeight(popupWidth),
layoutParamType, layoutParamType,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, IDLE_WINDOW_FLAGS,
PixelFormat.TRANSLUCENT); 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 centerX = (int) (screenWidth / 2f - popupWidth / 2f);
int centerY = (int) (screenHeight / 2f - popupHeight / 2f); int centerY = (int) (screenHeight / 2f - popupHeight / 2f);
windowLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; popupLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX;
windowLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; popupLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY;
checkPositionBounds(); checkPopupPositionBounds();
MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); PopupWindowGestureListener listener = new PopupWindowGestureListener();
gestureDetector = new GestureDetector(this, listener); popupGestureDetector = new GestureDetector(this, listener);
rootView.setOnTouchListener(listener); rootView.setOnTouchListener(listener);
playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width);
playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height); playerImpl.getLoadingPanel().setMinimumWidth(popupLayoutParams.width);
windowManager.addView(rootView, windowLayoutParams); 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.notificationSongName, playerImpl.getVideoTitle());
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName());
notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail());
notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); 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()); 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) .setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white) .setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContent(notRemoteView); .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 // Misc
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public void onClose() { public void closePopup() {
if (DEBUG) Log.d(TAG, "onClose() called"); if (DEBUG) Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
if (isPopupClosing) return;
isPopupClosing = true;
if (playerImpl != null) { if (playerImpl != null) {
if (playerImpl.getRootView() != null) { if (playerImpl.getRootView() != null) {
windowManager.removeView(playerImpl.getRootView()); windowManager.removeView(playerImpl.getRootView());
playerImpl.setRootView(null);
} }
playerImpl.setRootView(null);
playerImpl.stopActivityBinding(); playerImpl.stopActivityBinding();
playerImpl.destroy(); playerImpl.destroy();
playerImpl = null;
} }
mBinder = null;
if (lockManager != null) lockManager.releaseWifiAndCpu(); if (lockManager != null) lockManager.releaseWifiAndCpu();
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
mBinder = null;
playerImpl = null;
stopForeground(true); animateOverlayAndFinishService();
stopSelf(); }
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 // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void checkPositionBounds() { /**
if (windowLayoutParams.x > screenWidth - windowLayoutParams.width) * @see #checkPopupPositionBounds(float, float)
windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width); */
if (windowLayoutParams.x < 0) windowLayoutParams.x = 0; @SuppressWarnings("UnusedReturnValue")
if (windowLayoutParams.y > screenHeight - windowLayoutParams.height) private boolean checkPopupPositionBounds() {
windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height); return checkPopupPositionBounds(screenWidth, screenHeight);
if (windowLayoutParams.y < 0) windowLayoutParams.y = 0; }
/**
* 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() { private void savePositionAndSize() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this);
sharedPreferences.edit().putInt(POPUP_SAVED_X, windowLayoutParams.x).apply(); sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply();
sharedPreferences.edit().putInt(POPUP_SAVED_Y, windowLayoutParams.y).apply(); sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply();
sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, windowLayoutParams.width).apply(); sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply();
} }
private float getMinimumVideoHeight(float width) { private float getMinimumVideoHeight(float width) {
@ -340,13 +450,13 @@ public final class PopupVideoPlayer extends Service {
if (height == -1) height = (int) getMinimumVideoHeight(width); if (height == -1) height = (int) getMinimumVideoHeight(width);
else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height); else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height);
windowLayoutParams.width = width; popupLayoutParams.width = width;
windowLayoutParams.height = height; popupLayoutParams.height = height;
popupWidth = width; popupWidth = width;
popupHeight = height; popupHeight = height;
if (DEBUG) Log.d(TAG, "updatePopupSize() updated values: width = [" + width + "], height = [" + 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) { 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 { protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener {
@ -375,6 +491,7 @@ public final class PopupVideoPlayer extends Service {
private ImageView videoPlayPause; private ImageView videoPlayPause;
private View extraOptionsView; private View extraOptionsView;
private View closingOverlayView;
@Override @Override
public void handleIntent(Intent intent) { public void handleIntent(Intent intent) {
@ -395,12 +512,18 @@ public final class PopupVideoPlayer extends Service {
fullScreenButton = rootView.findViewById(R.id.fullScreenButton); fullScreenButton = rootView.findViewById(R.id.fullScreenButton);
fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked()); fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked());
videoPlayPause = rootView.findViewById(R.id.videoPlayPause); videoPlayPause = rootView.findViewById(R.id.videoPlayPause);
videoPlayPause.setOnClickListener(this::onPlayPauseButtonPressed);
extraOptionsView = rootView.findViewById(R.id.extraOptionsView); extraOptionsView = rootView.findViewById(R.id.extraOptionsView);
closingOverlayView = rootView.findViewById(R.id.closingOverlay);
rootView.addOnLayoutChangeListener(this); rootView.addOnLayoutChangeListener(this);
} }
@Override
public void initListeners() {
super.initListeners();
videoPlayPause.setOnClickListener(v -> onPlayPause());
}
@Override @Override
protected void setupSubtitleView(@NonNull SubtitleView view, protected void setupSubtitleView(@NonNull SubtitleView view,
final float captionScale, final float captionScale,
@ -411,10 +534,6 @@ public final class PopupVideoPlayer extends Service {
view.setStyle(captionStyle); view.setStyle(captionStyle);
} }
private void onPlayPauseButtonPressed(View ib) {
onPlayPause();
}
@Override @Override
public void onLayoutChange(final View view, int left, int top, int right, int bottom, public void onLayoutChange(final View view, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) { int oldLeft, int oldTop, int oldRight, int oldBottom) {
@ -429,21 +548,6 @@ public final class PopupVideoPlayer extends Service {
super.destroy(); 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 @Override
public void onFullScreenButtonClicked() { public void onFullScreenButtonClicked() {
super.onFullScreenButtonClicked(); super.onFullScreenButtonClicked();
@ -460,6 +564,7 @@ public final class PopupVideoPlayer extends Service {
this.getRepeatMode(), this.getRepeatMode(),
this.getPlaybackSpeed(), this.getPlaybackSpeed(),
this.getPlaybackPitch(), this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality() this.getPlaybackQuality()
); );
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@ -472,7 +577,7 @@ public final class PopupVideoPlayer extends Service {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} }
context.startActivity(intent); context.startActivity(intent);
onClose(); closePopup();
} }
@Override @Override
@ -511,14 +616,47 @@ public final class PopupVideoPlayer extends Service {
} }
@Override @Override
protected int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) { protected VideoPlaybackResolver.QualityResolver getQualityResolver() {
return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); return new VideoPlaybackResolver.QualityResolver() {
@Override
public int getDefaultResolutionIndex(List<VideoStream> sortedVideos) {
return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
}
@Override
public int getOverrideResolutionIndex(List<VideoStream> sortedVideos,
String playbackQuality) {
return ListHelper.getPopupResolutionIndex(context, sortedVideos,
playbackQuality);
}
};
}
/*//////////////////////////////////////////////////////////////////////////
// Thumbnail Loading
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
// rebuild notification here since remote view does not release bitmaps,
// causing memory leaks
resetNotification();
updateNotification(-1);
} }
@Override @Override
protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
final String playbackQuality) { super.onLoadingFailed(imageUri, view, failReason);
return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality); 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() { private void updateMetadata() {
if (activityListener != null && currentInfo != null) { if (activityListener != null && getCurrentMetadata() != null) {
activityListener.onMetadataUpdate(currentInfo); activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata());
} }
} }
@ -572,8 +710,9 @@ public final class PopupVideoPlayer extends Service {
public void onRepeatModeChanged(int i) { public void onRepeatModeChanged(int i) {
super.onRepeatModeChanged(i); super.onRepeatModeChanged(i);
setRepeatModeRemote(notRemoteView, i); setRepeatModeRemote(notRemoteView, i);
updateNotification(-1);
updatePlayback(); updatePlayback();
resetNotification();
updateNotification(-1);
} }
@Override @Override
@ -586,18 +725,17 @@ public final class PopupVideoPlayer extends Service {
// Playback Listener // Playback Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
protected void onMetadataChanged(@NonNull final PlayQueueItem item, protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
@Nullable final StreamInfo info, super.onMetadataChanged(tag);
final int newPlayQueueIndex, resetNotification();
final boolean hasPlayQueueItemChanged) { updateNotification(-1);
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
updateMetadata(); updateMetadata();
} }
@Override @Override
public void onPlaybackShutdown() { public void onPlaybackShutdown() {
super.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 + "]"); if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
switch (intent.getAction()) { switch (intent.getAction()) {
case ACTION_CLOSE: case ACTION_CLOSE:
onClose(); closePopup();
break; break;
case ACTION_PLAY_PAUSE: case ACTION_PLAY_PAUSE:
onPlayPause(); onPlayPause();
@ -653,49 +791,70 @@ public final class PopupVideoPlayer extends Service {
@Override @Override
public void onBlocked() { public void onBlocked() {
super.onBlocked(); super.onBlocked();
resetNotification();
updateNotification(R.drawable.ic_play_arrow_white); updateNotification(R.drawable.ic_play_arrow_white);
} }
@Override @Override
public void onPlaying() { public void onPlaying() {
super.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); hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
// Check for new version startForeground(NOTIFICATION_ID, notBuilder.build());
//new CheckForNewAppVersionTask().execute(); lockManager.acquireWifiAndCpu();
} }
@Override @Override
public void onBuffering() { public void onBuffering() {
super.onBuffering(); super.onBuffering();
resetNotification();
updateNotification(R.drawable.ic_play_arrow_white); updateNotification(R.drawable.ic_play_arrow_white);
} }
@Override @Override
public void onPaused() { public void onPaused() {
super.onPaused(); super.onPaused();
updateWindowFlags(IDLE_WINDOW_FLAGS);
resetNotification();
updateNotification(R.drawable.ic_play_arrow_white); updateNotification(R.drawable.ic_play_arrow_white);
videoPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_white); videoPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_white);
lockManager.releaseWifiAndCpu(); lockManager.releaseWifiAndCpu();
stopForeground(false);
} }
@Override @Override
public void onPausedSeek() { public void onPausedSeek() {
super.onPausedSeek(); super.onPausedSeek();
videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); resetNotification();
updateNotification(R.drawable.ic_play_arrow_white); updateNotification(R.drawable.ic_play_arrow_white);
videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
} }
@Override @Override
public void onCompleted() { public void onCompleted() {
super.onCompleted(); super.onCompleted();
updateWindowFlags(IDLE_WINDOW_FLAGS);
resetNotification();
updateNotification(R.drawable.ic_replay_white); updateNotification(R.drawable.ic_replay_white);
videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white); videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white);
lockManager.releaseWifiAndCpu(); lockManager.releaseWifiAndCpu();
stopForeground(false);
} }
@Override @Override
@ -713,16 +872,15 @@ public final class PopupVideoPlayer extends Service {
super.hideControlsAndButton(duration, delay, videoPlayPause); super.hideControlsAndButton(duration, delay, videoPlayPause);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
/*package-private*/ void enableVideoRenderer(final boolean enable) { /*package-private*/ void enableVideoRenderer(final boolean enable) {
final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO);
if (trackSelector != null && videoRendererIndex != RENDERER_UNAVAILABLE) { if (videoRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setRendererDisabled(videoRendererIndex, !enable); trackSelector.setParameters(trackSelector.buildUponParameters()
.setRendererDisabled(videoRendererIndex, !enable));
} }
} }
@ -734,12 +892,15 @@ public final class PopupVideoPlayer extends Service {
public TextView getResizingIndicator() { public TextView getResizingIndicator() {
return resizingIndicator; 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 int initialPopupX, initialPopupY;
private boolean isMoving; private boolean isMoving;
private boolean isResizing; private boolean isResizing;
@Override @Override
@ -775,10 +936,15 @@ public final class PopupVideoPlayer extends Service {
@Override @Override
public boolean onDown(MotionEvent e) { public boolean onDown(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
initialPopupX = windowLayoutParams.x;
initialPopupY = windowLayoutParams.y; // Fix popup position when the user touch it, it may have the wrong one
popupWidth = windowLayoutParams.width; // because the soft input is visible (the draggable area is currently resized).
popupHeight = windowLayoutParams.height; checkPopupPositionBounds(closeOverlayView.getWidth(), closeOverlayView.getHeight());
initialPopupX = popupLayoutParams.x;
initialPopupY = popupLayoutParams.y;
popupWidth = popupLayoutParams.width;
popupHeight = popupLayoutParams.height;
return super.onDown(e); return super.onDown(e);
} }
@ -786,20 +952,22 @@ public final class PopupVideoPlayer extends Service {
public void onLongPress(MotionEvent e) { public void onLongPress(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); if (DEBUG) Log.d(TAG, "onLongPress() called with: e = [" + e + "]");
updateScreenSize(); updateScreenSize();
checkPositionBounds(); checkPopupPositionBounds();
updatePopupSize((int) screenWidth, -1); updatePopupSize((int) screenWidth, -1);
} }
@Override @Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) {
if (isResizing || playerImpl == null) return super.onScroll(e1, e2, distanceX, 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; isMoving = true;
float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX); float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()), posX = (int) (initialPopupX + diffX);
float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY); float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()), posY = (int) (initialPopupY + diffY);
if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth); if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth);
else if (posX < 0) posX = 0; else if (posX < 0) posX = 0;
@ -807,26 +975,49 @@ public final class PopupVideoPlayer extends Service {
if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight); if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight);
else if (posY < 0) posY = 0; else if (posY < 0) posY = 0;
windowLayoutParams.x = (int) posX; popupLayoutParams.x = (int) posX;
windowLayoutParams.y = (int) posY; 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 //noinspection PointlessBooleanExpression
if (DEBUG && false) Log.d(TAG, "PopupVideoPlayer.onScroll = " + if (DEBUG && false) {
", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + Log.d(TAG, "PopupVideoPlayer.onScroll = " +
", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + ", e1.getX,Y = [" + initialEvent.getX() + ", " + initialEvent.getY() + "]" +
", distanceXy = [" + distanceX + ", " + distanceY + "]" + ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "]" +
", posXy = [" + posX + ", " + posY + "]" + ", distanceX,Y = [" + distanceX + ", " + distanceY + "]" +
", popupWh = [" + popupWidth + " x " + popupHeight + "]"); ", posX,Y = [" + posX + ", " + posY + "]" +
windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams); ", popupW,H = [" + popupWidth + " x " + popupHeight + "]");
}
windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
return true; return true;
} }
private void onScrollEnd() { private void onScrollEnd(MotionEvent event) {
if (DEBUG) Log.d(TAG, "onScrollEnd() called"); if (DEBUG) Log.d(TAG, "onScrollEnd() called");
if (playerImpl == null) return; if (playerImpl == null) return;
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); 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 @Override
@ -836,14 +1027,11 @@ public final class PopupVideoPlayer extends Service {
final float absVelocityX = Math.abs(velocityX); final float absVelocityX = Math.abs(velocityX);
final float absVelocityY = Math.abs(velocityY); final float absVelocityY = Math.abs(velocityY);
if (absVelocityX > shutdownFlingVelocity) { if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) {
onClose(); if (absVelocityX > tossFlingVelocity) popupLayoutParams.x = (int) velocityX;
return true; if (absVelocityY > tossFlingVelocity) popupLayoutParams.y = (int) velocityY;
} else if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { checkPopupPositionBounds();
if (absVelocityX > tossFlingVelocity) windowLayoutParams.x = (int) velocityX; windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
if (absVelocityY > tossFlingVelocity) windowLayoutParams.y = (int) velocityY;
checkPositionBounds();
windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
return true; return true;
} }
return false; return false;
@ -851,7 +1039,7 @@ public final class PopupVideoPlayer extends Service {
@Override @Override
public boolean onTouch(View v, MotionEvent event) { public boolean onTouch(View v, MotionEvent event) {
gestureDetector.onTouchEvent(event); popupGestureDetector.onTouchEvent(event);
if (playerImpl == null) return false; if (playerImpl == null) return false;
if (event.getPointerCount() == 2 && !isResizing) { if (event.getPointerCount() == 2 && !isResizing) {
if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); 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() + "]"); Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
if (isMoving) { if (isMoving) {
isMoving = false; isMoving = false;
onScrollEnd(); onScrollEnd(event);
} }
if (isResizing) { if (isResizing) {
@ -882,7 +1070,10 @@ public final class PopupVideoPlayer extends Service {
animateView(playerImpl.getResizingIndicator(), false, 100, 0); animateView(playerImpl.getResizingIndicator(), false, 100, 0);
playerImpl.changeState(playerImpl.getCurrentState()); playerImpl.changeState(playerImpl.getCurrentState());
} }
savePositionAndSize();
if (!isPopupClosing) {
savePositionAndSize();
}
} }
v.performClick(); v.performClick();
@ -898,13 +1089,13 @@ public final class PopupVideoPlayer extends Service {
final float diff = Math.abs(firstPointerX - secondPointerX); final float diff = Math.abs(firstPointerX - secondPointerX);
if (firstPointerX > secondPointerX) { if (firstPointerX > secondPointerX) {
// second pointer is the anchor (the leftmost pointer) // second pointer is the anchor (the leftmost pointer)
windowLayoutParams.x = (int) (event.getRawX() - diff); popupLayoutParams.x = (int) (event.getRawX() - diff);
} else { } else {
// first pointer is the anchor // first pointer is the anchor
windowLayoutParams.x = (int) event.getRawX(); popupLayoutParams.x = (int) event.getRawX();
} }
checkPositionBounds(); checkPopupPositionBounds();
updateScreenSize(); updateScreenSize();
final int width = (int) Math.min(screenWidth, diff); final int width = (int) Math.min(screenWidth, diff);
@ -912,5 +1103,29 @@ public final class PopupVideoPlayer extends Service {
return true; 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();
}
} }
} }

View file

@ -16,6 +16,7 @@ import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.PopupMenu; import android.widget.PopupMenu;
@ -187,6 +188,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
this.player.getRepeatMode(), this.player.getRepeatMode(),
this.player.getPlaybackSpeed(), this.player.getPlaybackSpeed(),
this.player.getPlaybackPitch(), this.player.getPlaybackPitch(),
this.player.getPlaybackSkipSilence(),
null null
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} }
@ -340,6 +342,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true; 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(); menu.show();
} }
@ -459,13 +468,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void openPlaybackParameterDialog() { private void openPlaybackParameterDialog() {
if (player == null) return; if (player == null) return;
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag());
} }
@Override @Override
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch); boolean playbackSkipSilence) {
if (player != null) {
player.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence);
}
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -509,6 +521,18 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
.show(getSupportFragmentManager(), getTag()); .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 // Binding Service Listener
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -539,6 +563,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (player != null) { if (player != null) {
progressLiveSync.setClickable(!player.isLiveEdge()); 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 @Override

View file

@ -29,7 +29,6 @@ import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@ -47,11 +46,9 @@ import android.widget.SeekBar;
import android.widget.TextView; import android.widget.TextView;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource; 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.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.CaptionStyleCompat; 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.R;
import org.schabi.newpipe.extractor.MediaFormat; 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.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; 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.AnimationUtils;
import org.schabi.newpipe.util.ListHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.formatSpeed;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.util.AnimationUtils.animateView; 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_DURATION = 300; // 300 millis
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
private ArrayList<VideoStream> availableStreams; private List<VideoStream> availableStreams;
private int selectedStreamIndex; private int selectedStreamIndex;
protected String playbackQuality;
protected boolean wasPlaying = false; protected boolean wasPlaying = false;
@NonNull final private VideoPlaybackResolver resolver;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Views // Views
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -162,6 +154,7 @@ public abstract class VideoPlayer extends BasePlayer
public VideoPlayer(String debugTag, Context context) { public VideoPlayer(String debugTag, Context context) {
super(context); super(context);
this.TAG = debugTag; this.TAG = debugTag;
this.resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
} }
public void setup(View rootView) { public void setup(View rootView) {
@ -241,7 +234,8 @@ public abstract class VideoPlayer extends BasePlayer
// Setup audio session with onboard equalizer // Setup audio session with onboard equalizer
if (Build.VERSION.SDK_INT >= 21) { 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); 0, Menu.NONE, R.string.caption_none);
captionOffItem.setOnMenuItemClickListener(menuItem -> { captionOffItem.setOnMenuItemClickListener(menuItem -> {
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { if (textRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setRendererDisabled(textRendererIndex, true); trackSelector.setParameters(trackSelector.buildUponParameters()
.setRendererDisabled(textRendererIndex, true));
} }
return true; return true;
}); });
@ -310,68 +305,61 @@ public abstract class VideoPlayer extends BasePlayer
i + 1, Menu.NONE, captionLanguage); i + 1, Menu.NONE, captionLanguage);
captionItem.setOnMenuItemClickListener(menuItem -> { captionItem.setOnMenuItemClickListener(menuItem -> {
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { if (textRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setPreferredTextLanguage(captionLanguage); trackSelector.setPreferredTextLanguage(captionLanguage);
trackSelector.setRendererDisabled(textRendererIndex, false); trackSelector.setParameters(trackSelector.buildUponParameters()
.setRendererDisabled(textRendererIndex, false));
} }
return true; return true;
}); });
} }
captionPopupMenu.setOnDismissListener(this); 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); qualityTextView.setVisibility(View.GONE);
playbackSpeedTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE);
playbackEndTime.setVisibility(View.GONE); playbackEndTime.setVisibility(View.GONE);
playbackLiveSync.setVisibility(View.GONE); playbackLiveSync.setVisibility(View.GONE);
final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); switch (metadata.getStreamType()) {
switch (streamType) {
case AUDIO_STREAM: case AUDIO_STREAM:
surfaceView.setVisibility(View.GONE); surfaceView.setVisibility(View.GONE);
endScreen.setVisibility(View.VISIBLE);
playbackEndTime.setVisibility(View.VISIBLE); playbackEndTime.setVisibility(View.VISIBLE);
break; break;
case AUDIO_LIVE_STREAM: case AUDIO_LIVE_STREAM:
surfaceView.setVisibility(View.GONE); surfaceView.setVisibility(View.GONE);
endScreen.setVisibility(View.VISIBLE);
playbackLiveSync.setVisibility(View.VISIBLE); playbackLiveSync.setVisibility(View.VISIBLE);
break; break;
case LIVE_STREAM: case LIVE_STREAM:
surfaceView.setVisibility(View.VISIBLE); surfaceView.setVisibility(View.VISIBLE);
endScreen.setVisibility(View.GONE);
playbackLiveSync.setVisibility(View.VISIBLE); playbackLiveSync.setVisibility(View.VISIBLE);
break; break;
case VIDEO_STREAM: case VIDEO_STREAM:
if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) break; if (metadata.getVideoStreams().size() + metadata.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());
}
availableStreams = tag.getSortedAvailableVideoStreams();
selectedStreamIndex = tag.getSelectedVideoStreamIndex();
buildQualityMenu(); buildQualityMenu();
qualityTextView.setVisibility(View.VISIBLE);
qualityTextView.setVisibility(View.VISIBLE);
surfaceView.setVisibility(View.VISIBLE); surfaceView.setVisibility(View.VISIBLE);
default: default:
endScreen.setVisibility(View.GONE);
playbackEndTime.setVisibility(View.VISIBLE); playbackEndTime.setVisibility(View.VISIBLE);
break; break;
} }
@ -379,69 +367,21 @@ public abstract class VideoPlayer extends BasePlayer
buildPlaybackSpeedMenu(); buildPlaybackSpeedMenu();
playbackSpeedTextView.setVisibility(View.VISIBLE); playbackSpeedTextView.setVisibility(View.VISIBLE);
} }
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
protected abstract VideoPlaybackResolver.QualityResolver getQualityResolver();
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
super.onMetadataChanged(tag);
updateStreamRelatedViews();
}
@Override @Override
@Nullable @Nullable
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
final MediaSource liveSource = super.sourceOf(item, info); return resolver.resolve(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()]));
}
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -460,7 +400,6 @@ public abstract class VideoPlayer extends BasePlayer
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
animateView(endScreen, false, 0);
loadingPanel.setBackgroundColor(Color.BLACK); loadingPanel.setBackgroundColor(Color.BLACK);
animateView(loadingPanel, true, 0); animateView(loadingPanel, true, 0);
animateView(surfaceForeground, true, 100); animateView(surfaceForeground, true, 100);
@ -470,6 +409,8 @@ public abstract class VideoPlayer extends BasePlayer
public void onPlaying() { public void onPlaying() {
super.onPlaying(); super.onPlaying();
updateStreamRelatedViews();
showAndAnimateControl(-1, true); showAndAnimateControl(-1, true);
playbackSeekBar.setEnabled(true); playbackSeekBar.setEnabled(true);
@ -480,14 +421,12 @@ public abstract class VideoPlayer extends BasePlayer
loadingPanel.setVisibility(View.GONE); loadingPanel.setVisibility(View.GONE);
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
animateView(endScreen, false, 0);
} }
@Override @Override
public void onBuffering() { public void onBuffering() {
if (DEBUG) Log.d(TAG, "onBuffering() called"); if (DEBUG) Log.d(TAG, "onBuffering() called");
loadingPanel.setBackgroundColor(Color.TRANSPARENT); loadingPanel.setBackgroundColor(Color.TRANSPARENT);
animateView(loadingPanel, true, 500);
} }
@Override @Override
@ -552,8 +491,7 @@ public abstract class VideoPlayer extends BasePlayer
final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT); final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT);
if (captionTextView == null) return; if (captionTextView == null) return;
if (trackSelector == null || trackSelector.getCurrentMappedTrackInfo() == null || if (trackSelector.getCurrentMappedTrackInfo() == null || textRenderer == RENDERER_UNAVAILABLE) {
textRenderer == RENDERER_UNAVAILABLE) {
captionTextView.setVisibility(View.GONE); captionTextView.setVisibility(View.GONE);
return; return;
} }
@ -575,8 +513,8 @@ public abstract class VideoPlayer extends BasePlayer
// Build UI // Build UI
buildCaptionMenu(availableLanguages); buildCaptionMenu(availableLanguages);
if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null || if (trackSelector.getParameters().getRendererDisabled(textRenderer) ||
!availableLanguages.contains(preferredLanguage)) { preferredLanguage == null || !availableLanguages.contains(preferredLanguage)) {
captionTextView.setText(R.string.caption_none); captionTextView.setText(R.string.caption_none);
} else { } else {
captionTextView.setText(preferredLanguage); captionTextView.setText(preferredLanguage);
@ -905,11 +843,12 @@ public abstract class VideoPlayer extends BasePlayer
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public void setPlaybackQuality(final String quality) { public void setPlaybackQuality(final String quality) {
this.playbackQuality = quality; this.resolver.setPlaybackQuality(quality);
} }
@Nullable
public String getPlaybackQuality() { public String getPlaybackQuality() {
return playbackQuality; return resolver.getPlaybackQuality();
} }
public AspectRatioFrameLayout getAspectRatioFrameLayout() { public AspectRatioFrameLayout getAspectRatioFrameLayout() {

View file

@ -39,10 +39,13 @@ public class MediaSessionManager {
return MediaButtonReceiver.handleIntent(mediaSession, intent); return MediaButtonReceiver.handleIntent(mediaSession, intent);
} }
/**
* Should be called on player destruction to prevent leakage.
* */
public void dispose() { public void dispose() {
this.sessionConnector.setPlayer(null, null); this.sessionConnector.setPlayer(null, null);
this.sessionConnector.setQueueNavigator(null); this.sessionConnector.setQueueNavigator(null);
this.mediaSession.setActive(false); this.mediaSession.setActive(false);
this.mediaSession.release(); this.mediaSession.release();
} }
} }

View file

@ -21,25 +21,34 @@ import static org.schabi.newpipe.player.BasePlayer.DEBUG;
public class PlaybackParameterDialog extends DialogFragment { public class PlaybackParameterDialog extends DialogFragment {
@NonNull private static final String TAG = "PlaybackParameterDialog"; @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 double MAXIMUM_PLAYBACK_VALUE = 3.00f;
public static final char STEP_UP_SIGN = '+'; public static final char STEP_UP_SIGN = '+';
public static final char STEP_DOWN_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 STEP_ONE_PERCENT_VALUE = 0.01f;
public static final double NIGHTCORE_PITCH_LOWER = 1.15f; public static final double STEP_FIVE_PERCENT_VALUE = 0.05f;
public static final double NIGHTCORE_PITCH_UPPER = 1.25f; 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_TEMPO = 1.00f;
public static final double DEFAULT_PITCH = 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_TEMPO_KEY = "initial_tempo_key";
@NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_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 { 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; @Nullable private Callback callback;
@ -50,6 +59,11 @@ public class PlaybackParameterDialog extends DialogFragment {
private double initialTempo = DEFAULT_TEMPO; private double initialTempo = DEFAULT_TEMPO;
private double initialPitch = DEFAULT_PITCH; 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 SeekBar tempoSlider;
@Nullable private TextView tempoMinimumText; @Nullable private TextView tempoMinimumText;
@ -65,16 +79,26 @@ public class PlaybackParameterDialog extends DialogFragment {
@Nullable private TextView pitchStepDownText; @Nullable private TextView pitchStepDownText;
@Nullable private TextView pitchStepUpText; @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 CheckBox unhookingCheckbox;
@Nullable private TextView resetPresetText; @Nullable private CheckBox skipSilenceCheckbox;
public static PlaybackParameterDialog newInstance(final double playbackTempo, public static PlaybackParameterDialog newInstance(final double playbackTempo,
final double playbackPitch) { final double playbackPitch,
final boolean playbackSkipSilence) {
PlaybackParameterDialog dialog = new PlaybackParameterDialog(); PlaybackParameterDialog dialog = new PlaybackParameterDialog();
dialog.initialTempo = playbackTempo; dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch; dialog.initialPitch = playbackPitch;
dialog.tempo = playbackTempo;
dialog.pitch = playbackPitch;
dialog.initialSkipSilence = playbackSkipSilence;
return dialog; return dialog;
} }
@ -98,6 +122,10 @@ public class PlaybackParameterDialog extends DialogFragment {
if (savedInstanceState != null) { if (savedInstanceState != null) {
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH); 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); super.onSaveInstanceState(outState);
outState.putDouble(INITIAL_TEMPO_KEY, initialTempo); outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
outState.putDouble(INITIAL_PITCH_KEY, initialPitch); 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) .setView(view)
.setCancelable(true) .setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) -> .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) -> .setPositiveButton(R.string.finish, (dialogInterface, i) ->
setCurrentPlaybackParameters()); setCurrentPlaybackParameters());
@ -136,9 +170,13 @@ public class PlaybackParameterDialog extends DialogFragment {
private void setupControlViews(@NonNull View rootView) { private void setupControlViews(@NonNull View rootView) {
setupHookingControl(rootView); setupHookingControl(rootView);
setupSkipSilenceControl(rootView);
setupTempoControl(rootView); setupTempoControl(rootView);
setupPitchControl(rootView); setupPitchControl(rootView);
setupPresetControl(rootView);
changeStepSize(stepSize);
setupStepSizeSelector(rootView);
} }
private void setupTempoControl(@NonNull View rootView) { private void setupTempoControl(@NonNull View rootView) {
@ -150,31 +188,15 @@ public class PlaybackParameterDialog extends DialogFragment {
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
if (tempoCurrentText != null) if (tempoCurrentText != null)
tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
if (tempoMaximumText != null) if (tempoMaximumText != null)
tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
if (tempoMinimumText != null) if (tempoMinimumText != null)
tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); 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) { if (tempoSlider != null) {
tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
tempoSlider.setProgress(strategy.progressOf(initialTempo)); tempoSlider.setProgress(strategy.progressOf(tempo));
tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
} }
} }
@ -188,31 +210,15 @@ public class PlaybackParameterDialog extends DialogFragment {
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
if (pitchCurrentText != null) if (pitchCurrentText != null)
pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
if (pitchMaximumText != null) if (pitchMaximumText != null)
pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
if (pitchMinimumText != null) if (pitchMinimumText != null)
pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); 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) { if (pitchSlider != null) {
pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
pitchSlider.setProgress(strategy.progressOf(initialPitch)); pitchSlider.setProgress(strategy.progressOf(pitch));
pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
} }
} }
@ -220,7 +226,7 @@ public class PlaybackParameterDialog extends DialogFragment {
private void setupHookingControl(@NonNull View rootView) { private void setupHookingControl(@NonNull View rootView) {
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
if (unhookingCheckbox != null) { if (unhookingCheckbox != null) {
unhookingCheckbox.setChecked(initialPitch != initialTempo); unhookingCheckbox.setChecked(pitch != tempo);
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
if (isChecked) return; if (isChecked) return;
// When unchecked, slide back to the minimum of current tempo or pitch // 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) { private void setupSkipSilenceControl(@NonNull View rootView) {
nightCorePresetText = rootView.findViewById(R.id.presetNightcore); skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox);
if (nightCorePresetText != null) { if (skipSilenceCheckbox != null) {
nightCorePresetText.setOnClickListener(view -> { skipSilenceCheckbox.setChecked(initialSkipSilence);
final double randomPitch = NIGHTCORE_PITCH_LOWER + skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) ->
Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); setCurrentPlaybackParameters());
}
}
setTempoSlider(NIGHTCORE_TEMPO); private void setupStepSizeSelector(@NonNull final View rootView) {
setPitchSlider(randomPitch); 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(); setCurrentPlaybackParameters();
}); });
} }
resetPresetText = rootView.findViewById(R.id.presetReset); if (tempoStepDownText != null) {
if (resetPresetText != null) { tempoStepDownText.setText(getStepDownPercentString(stepSize));
resetPresetText.setOnClickListener(view -> { tempoStepDownText.setOnClickListener(view -> {
setTempoSlider(DEFAULT_TEMPO); onTempoSliderUpdated(getCurrentTempo() - stepSize);
setPitchSlider(DEFAULT_PITCH); 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(); setCurrentPlaybackParameters();
}); });
} }
@ -342,10 +408,11 @@ public class PlaybackParameterDialog extends DialogFragment {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void setCurrentPlaybackParameters() { 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 (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
if (DEBUG) Log.d(TAG, "Setting playback parameters to " + if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
"tempo=[" + tempo + "], " + "tempo=[" + tempo + "], " +
@ -353,27 +420,40 @@ public class PlaybackParameterDialog extends DialogFragment {
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
callback.onPlaybackParameterChanged((float) tempo, (float) pitch); callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence);
} }
} }
private double getCurrentTempo() { private double getCurrentTempo() {
return tempoSlider == null ? initialTempo : strategy.valueOf( return tempoSlider == null ? tempo : strategy.valueOf(
tempoSlider.getProgress()); tempoSlider.getProgress());
} }
private double getCurrentPitch() { private double getCurrentPitch() {
return pitchSlider == null ? initialPitch : strategy.valueOf( return pitchSlider == null ? pitch : strategy.valueOf(
pitchSlider.getProgress()); pitchSlider.getProgress());
} }
private double getCurrentStepSize() {
return stepSize;
}
private boolean getCurrentSkipSilence() {
return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked();
}
@NonNull @NonNull
private static String getStepUpPercentString(final double percent) { private static String getStepUpPercentString(final double percent) {
return STEP_UP_SIGN + PlayerHelper.formatPitch(percent); return STEP_UP_SIGN + getPercentString(percent);
} }
@NonNull @NonNull
private static String getStepDownPercentString(final double percent) { 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);
} }
} }

View file

@ -4,6 +4,7 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.view.accessibility.CaptioningManager; 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.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import java.lang.annotation.Retention;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.ArrayList; 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_FILL;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; 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 { public class PlayerHelper {
private PlayerHelper() {} private PlayerHelper() {}
@ -51,6 +55,14 @@ public class PlayerHelper {
private static final NumberFormat speedFormatter = new DecimalFormat("0.##x"); private static final NumberFormat speedFormatter = new DecimalFormat("0.##x");
private static final NumberFormat pitchFormatter = new DecimalFormat("##%"); 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 // Exposed helpers
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -173,6 +185,22 @@ public class PlayerHelper {
return isAutoQueueEnabled(context, false); 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 @NonNull
public static SeekParameters getSeekParameters(@NonNull final Context context) { public static SeekParameters getSeekParameters(@NonNull final Context context) {
return isUsingInexactSeek(context, false) ? return isUsingInexactSeek(context, false) ?
@ -213,7 +241,6 @@ public class PlayerHelper {
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context, public static TrackSelection.Factory getQualitySelector(@NonNull final Context context,
@NonNull final BandwidthMeter meter) { @NonNull final BandwidthMeter meter) {
return new AdaptiveTrackSelection.Factory(meter, return new AdaptiveTrackSelection.Factory(meter,
AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE,
/*bufferDurationRequiredForQualityIncrease=*/1000, /*bufferDurationRequiredForQualityIncrease=*/1000,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
@ -224,10 +251,6 @@ public class PlayerHelper {
return true; return true;
} }
public static int getShutdownFlingVelocity(@NonNull final Context context) {
return 10000;
}
public static int getTossFlingVelocity(@NonNull final Context context) { public static int getTossFlingVelocity(@NonNull final Context context) {
return 2500; return 2500;
} }
@ -249,7 +272,6 @@ public class PlayerHelper {
* System font scaling: * System font scaling:
* Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f * 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) { public static float getCaptionScale(@NonNull final Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1f; 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); 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);
}
} }

View file

@ -4,6 +4,7 @@ import android.support.annotation.NonNull;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.ExoPlayer; 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.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
@ -11,7 +12,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.io.IOException; import java.io.IOException;
public class FailedMediaSource implements ManagedMediaSource { public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource {
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
public static class FailedMediaSourceException extends Exception { public static class FailedMediaSourceException extends Exception {
@ -72,11 +73,6 @@ public class FailedMediaSource implements ManagedMediaSource {
return System.currentTimeMillis() >= retryTimestamp; return System.currentTimeMillis() >= retryTimestamp;
} }
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
Log.e(TAG, "Loading failed source: ", error);
}
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() throws IOException {
throw new IOException(error); throw new IOException(error);
@ -90,8 +86,14 @@ public class FailedMediaSource implements ManagedMediaSource {
@Override @Override
public void releasePeriod(MediaPeriod mediaPeriod) {} public void releasePeriod(MediaPeriod mediaPeriod) {}
@Override @Override
public void releaseSource() {} protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
Log.e(TAG, "Loading failed source: ", error);
}
@Override
protected void releaseSourceInternal() {}
@Override @Override
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,

View file

@ -1,10 +1,12 @@
package org.schabi.newpipe.player.mediasource; package org.schabi.newpipe.player.mediasource;
import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
@ -34,7 +36,8 @@ public class LoadedMediaSource implements ManagedMediaSource {
} }
@Override @Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { public void prepareSource(ExoPlayer player, boolean isTopLevelSource,
SourceInfoRefreshListener listener) {
source.prepareSource(player, isTopLevelSource, listener); source.prepareSource(player, isTopLevelSource, listener);
} }
@ -54,8 +57,18 @@ public class LoadedMediaSource implements ManagedMediaSource {
} }
@Override @Override
public void releaseSource() { public void releaseSource(SourceInfoRefreshListener listener) {
source.releaseSource(); 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 @Override

View file

@ -3,14 +3,14 @@ package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; 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; import com.google.android.exoplayer2.source.ShuffleOrder;
public class ManagedMediaSourcePlaylist { public class ManagedMediaSourcePlaylist {
@NonNull private final DynamicConcatenatingMediaSource internalSource; @NonNull private final ConcatenatingMediaSource internalSource;
public ManagedMediaSourcePlaylist() { public ManagedMediaSourcePlaylist() {
internalSource = new DynamicConcatenatingMediaSource(/*isPlaylistAtomic=*/false, internalSource = new ConcatenatingMediaSource(/*isPlaylistAtomic=*/false,
new ShuffleOrder.UnshuffledShuffleOrder(0)); new ShuffleOrder.UnshuffledShuffleOrder(0));
} }
@ -32,12 +32,8 @@ public class ManagedMediaSourcePlaylist {
null : (ManagedMediaSource) internalSource.getMediaSource(index); null : (ManagedMediaSource) internalSource.getMediaSource(index);
} }
public void dispose() {
internalSource.releaseSource();
}
@NonNull @NonNull
public DynamicConcatenatingMediaSource getParentMediaSource() { public ConcatenatingMediaSource getParentMediaSource() {
return internalSource; 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}. * {@link PlaceholderMediaSource}.
* *
* @see #append(ManagedMediaSource) * @see #append(ManagedMediaSource)
@ -56,17 +52,17 @@ public class ManagedMediaSourcePlaylist {
} }
/** /**
* Appends a {@link ManagedMediaSource} to the end of {@link DynamicConcatenatingMediaSource}. * Appends a {@link ManagedMediaSource} to the end of {@link ConcatenatingMediaSource}.
* @see DynamicConcatenatingMediaSource#addMediaSource * @see ConcatenatingMediaSource#addMediaSource
* */ * */
public synchronized void append(@NonNull final ManagedMediaSource source) { public synchronized void append(@NonNull final ManagedMediaSource source) {
internalSource.addMediaSource(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. * 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) { public synchronized void remove(final int index) {
if (index < 0 || index > internalSource.getSize()) return; 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, * from the given source index to the target index. If either index is out of bound,
* then the call is ignored. * 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) { public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return; 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}. * at the given index with a given {@link ManagedMediaSource}.
* @see #update(int, ManagedMediaSource, Runnable) * @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, * at the given index with a given {@link ManagedMediaSource}. If the index is out of bound,
* then the replacement is ignored. * then the replacement is ignored.
* @see DynamicConcatenatingMediaSource#addMediaSource * @see ConcatenatingMediaSource#addMediaSource
* @see DynamicConcatenatingMediaSource#removeMediaSource(int, Runnable) * @see ConcatenatingMediaSource#removeMediaSource(int, Runnable)
* */ * */
public synchronized void update(final int index, @NonNull final ManagedMediaSource source, public synchronized void update(final int index, @NonNull final ManagedMediaSource source,
@Nullable final Runnable finalizingAction) { @Nullable final Runnable finalizingAction) {

View file

@ -3,20 +3,19 @@ package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import com.google.android.exoplayer2.ExoPlayer; 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.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.io.IOException; public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource {
public class PlaceholderMediaSource implements ManagedMediaSource {
// Do nothing, so this will stall the playback // Do nothing, so this will stall the playback
@Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {} @Override public void maybeThrowSourceInfoRefreshError() {}
@Override public void maybeThrowSourceInfoRefreshError() throws IOException {}
@Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; } @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; }
@Override public void releasePeriod(MediaPeriod mediaPeriod) {} @Override public void releasePeriod(MediaPeriod mediaPeriod) {}
@Override public void releaseSource() {} @Override protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {}
@Override protected void releaseSourceInternal() {}
@Override @Override
public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,

View file

@ -5,12 +5,10 @@ import android.support.annotation.Nullable;
import android.support.v4.util.ArraySet; import android.support.v4.util.ArraySet;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.FailedMediaSource; import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.mediasource.LoadedMediaSource; import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSource; 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.player.playqueue.events.ReorderEvent;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -37,8 +33,6 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.SerialDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.internal.subscriptions.EmptySubscription;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.PublishSubject;
@ -104,7 +98,6 @@ public class MediaSourceManager {
private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1;
@NonNull private final CompositeDisposable loaderReactor; @NonNull private final CompositeDisposable loaderReactor;
@NonNull private final Set<PlayQueueItem> loadingItems; @NonNull private final Set<PlayQueueItem> loadingItems;
@NonNull private final SerialDisposable syncReactor;
@NonNull private final AtomicBoolean isBlocked; @NonNull private final AtomicBoolean isBlocked;
@ -144,7 +137,6 @@ public class MediaSourceManager {
this.playQueueReactor = EmptySubscription.INSTANCE; this.playQueueReactor = EmptySubscription.INSTANCE;
this.loaderReactor = new CompositeDisposable(); this.loaderReactor = new CompositeDisposable();
this.syncReactor = new SerialDisposable();
this.isBlocked = new AtomicBoolean(false); this.isBlocked = new AtomicBoolean(false);
@ -171,8 +163,6 @@ public class MediaSourceManager {
playQueueReactor.cancel(); playQueueReactor.cancel();
loaderReactor.dispose(); loaderReactor.dispose();
syncReactor.dispose();
playlist.dispose();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -311,21 +301,7 @@ public class MediaSourceManager {
final PlayQueueItem currentItem = playQueue.getItem(); final PlayQueueItem currentItem = playQueue.getItem();
if (isBlocked.get() || currentItem == null) return; if (isBlocked.get() || currentItem == null) return;
final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); playbackListener.onPlaybackSynchronize(currentItem);
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);
}
} }
private synchronized void maybeSynchronizePlayer() { 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 * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback
* readiness or playlist desynchronization. * readiness or playlist desynchronization.
* <br><br> * <br><br>
@ -481,8 +458,6 @@ public class MediaSourceManager {
private void resetSources() { private void resetSources() {
if (DEBUG) Log.d(TAG, "resetSources() called."); if (DEBUG) Log.d(TAG, "resetSources() called.");
playlist.dispose();
playlist = new ManagedMediaSourcePlaylist(); playlist = new ManagedMediaSourcePlaylist();
} }

View file

@ -45,7 +45,7 @@ public interface PlaybackListener {
* *
* May be called anytime at any amount once unblock is called. * 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 * Requests the listener to resolve a stream info into a media source

View file

@ -6,6 +6,7 @@ import android.util.Log;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.player.playqueue.events.AppendEvent; import org.schabi.newpipe.player.playqueue.events.AppendEvent;
import org.schabi.newpipe.player.playqueue.events.ErrorEvent; import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
import org.schabi.newpipe.player.playqueue.events.InitEvent; import org.schabi.newpipe.player.playqueue.events.InitEvent;
@ -41,7 +42,7 @@ import io.reactivex.subjects.BehaviorSubject;
public abstract class PlayQueue implements Serializable { public abstract class PlayQueue implements Serializable {
private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode()); 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> backup;
private ArrayList<PlayQueueItem> streams; private ArrayList<PlayQueueItem> streams;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,8 @@ public enum UserAction {
REQUESTED_CHANNEL("requested channel"), REQUESTED_CHANNEL("requested channel"),
REQUESTED_PLAYLIST("requested playlist"), REQUESTED_PLAYLIST("requested playlist"),
REQUESTED_KIOSK("requested kiosk"), REQUESTED_KIOSK("requested kiosk"),
DELETE_FROM_HISTORY("delete from history"); DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream");
private final String message; private final String message;

View file

@ -9,6 +9,7 @@ import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.preference.ListPreference; import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference; import android.support.v7.preference.Preference;
import android.util.Log; import android.util.Log;
@ -20,15 +21,12 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ZipHelper; import org.schabi.newpipe.util.ZipHelper;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -42,11 +40,8 @@ import java.util.Date;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import static android.content.Context.MODE_PRIVATE;
public class ContentSettingsFragment extends BasePreferenceFragment { public class ContentSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_IMPORT_PATH = 8945; private static final int REQUEST_IMPORT_PATH = 8945;
@ -56,6 +51,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private File databasesDir; private File databasesDir;
private File newpipe_db; private File newpipe_db;
private File newpipe_db_journal; private File newpipe_db_journal;
private File newpipe_db_shm;
private File newpipe_db_wal;
private File newpipe_settings; private File newpipe_settings;
private String thumbnailLoadToggleKey; private String thumbnailLoadToggleKey;
@ -88,73 +85,14 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
databasesDir = new File(homeDir + "/databases"); databasesDir = new File(homeDir + "/databases");
newpipe_db = new File(homeDir + "/databases/newpipe.db"); newpipe_db = new File(homeDir + "/databases/newpipe.db");
newpipe_db_journal = new File(homeDir + "/databases/newpipe.db-journal"); 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 = new File(homeDir + "/databases/newpipe.settings");
newpipe_settings.delete(); newpipe_settings.delete();
addPreferencesFromResource(R.xml.content_settings); 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)); Preference importDataPreference = findPreference(getString(R.string.import_data));
importDataPreference.setOnPreferenceClickListener((Preference p) -> { importDataPreference.setOnPreferenceClickListener((Preference p) -> {
Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
@ -207,7 +145,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
new BufferedOutputStream( new BufferedOutputStream(
new FileOutputStream(path))); new FileOutputStream(path)));
ZipHelper.addFileToZip(outZip, newpipe_db.getPath(), "newpipe.db"); ZipHelper.addFileToZip(outZip, newpipe_db.getPath(), "newpipe.db");
ZipHelper.addFileToZip(outZip, newpipe_db_journal.getPath(), "newpipe.db-journal");
saveSharedPreferencesToFile(newpipe_settings); saveSharedPreferencesToFile(newpipe_settings);
ZipHelper.addFileToZip(outZip, newpipe_settings.getPath(), "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"); throw new Exception("Could not create databases dir");
} }
if(!(ZipHelper.extractFileFromZip(filePath, newpipe_db.getPath(), "newpipe.db") final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath,
&& ZipHelper.extractFileFromZip(filePath, newpipe_db_journal.getPath(), "newpipe.db-journal"))) { 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) Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG)
.show(); .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 // Error
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View file

@ -71,7 +71,7 @@ public class NewPipeSettings {
} }
public static File getVideoDownloadFolder(Context context) { 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) { public static String getVideoDownloadPath(Context context) {
@ -81,7 +81,7 @@ public class NewPipeSettings {
} }
public static File getAudioDownloadFolder(Context context) { 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) { public static String getAudioDownloadPath(Context context) {
@ -90,21 +90,37 @@ public class NewPipeSettings {
return prefs.getString(key, Environment.DIRECTORY_MUSIC); 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); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(keyID); final String key = context.getString(keyID);
String downloadPath = prefs.getString(key, null); String downloadPath = prefs.getString(key, null);
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim()); 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(); SharedPreferences.Editor spEditor = prefs.edit();
spEditor.putString(key, new File(folder, "NewPipe").getAbsolutePath()); spEditor.putString(key, getNewPipeChildFolderPathForDir(dir));
spEditor.apply(); spEditor.apply();
return folder; return dir;
} }
@NonNull @NonNull
private static File getFolder(String defaultDirectoryName) { private static File getDir(String defaultDirectoryName) {
return new File(Environment.getExternalStorageDirectory(), 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();
}
} }

View file

@ -66,7 +66,7 @@ public class SelectChannelFragment extends DialogFragment {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public interface OnSelectedLisener { public interface OnSelectedLisener {
void onChannelSelected(String url, String name, int service); void onChannelSelected(int serviceId, String url, String name);
} }
OnSelectedLisener onSelectedLisener = null; OnSelectedLisener onSelectedLisener = null;
public void setOnSelectedLisener(OnSelectedLisener listener) { public void setOnSelectedLisener(OnSelectedLisener listener) {
@ -126,7 +126,7 @@ public class SelectChannelFragment extends DialogFragment {
private void clickedItem(int position) { private void clickedItem(int position) {
if(onSelectedLisener != null) { if(onSelectedLisener != null) {
SubscriptionEntity entry = subscriptions.get(position); SubscriptionEntity entry = subscriptions.get(position);
onSelectedLisener.onChannelSelected(entry.getUrl(), entry.getName(), entry.getServiceId()); onSelectedLisener.onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName());
} }
dismiss(); dismiss();
} }

View file

@ -56,7 +56,7 @@ public class SelectKioskFragment extends DialogFragment {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public interface OnSelectedLisener { public interface OnSelectedLisener {
void onKioskSelected(String kioskId, int service_id); void onKioskSelected(int serviceId, String kioskId, String kioskName);
} }
OnSelectedLisener onSelectedLisener = null; OnSelectedLisener onSelectedLisener = null;
@ -101,7 +101,7 @@ public class SelectKioskFragment extends DialogFragment {
private void clickedItem(SelectKioskAdapter.Entry entry) { private void clickedItem(SelectKioskAdapter.Entry entry) {
if(onSelectedLisener != null) { if(onSelectedLisener != null) {
onSelectedLisener.onKioskSelected(entry.kioskId, entry.serviceId); onSelectedLisener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName);
} }
dismiss(); dismiss();
} }

View file

@ -77,7 +77,8 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc
finish(); finish();
} else getSupportFragmentManager().popBackStack(); } else getSupportFragmentManager().popBackStack();
} }
return true;
return super.onOptionsItemSelected(item);
} }
@Override @Override

View file

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

View file

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

View 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;
}
}
}

View file

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

View file

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

View file

@ -6,7 +6,7 @@ public class Constants {
public static final String KEY_TITLE = "key_title"; public static final String KEY_TITLE = "key_title";
public static final String KEY_LINK_TYPE = "key_link_type"; 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_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_THEME_CHANGE = "key_theme_change";
public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change";

View file

@ -37,9 +37,8 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.search.SearchResult; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
@ -50,7 +49,6 @@ import java.util.List;
import io.reactivex.Maybe; import io.reactivex.Maybe;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.annotations.NonNull;
public final class ExtractorHelper { public final class ExtractorHelper {
private static final String TAG = ExtractorHelper.class.getSimpleName(); private static final String TAG = ExtractorHelper.class.getSimpleName();
@ -66,29 +64,35 @@ public final class ExtractorHelper {
} }
} }
public static Single<SearchResult> searchFor(final int serviceId, public static Single<SearchInfo> searchFor(final int serviceId,
final String query, final String searchString,
final int pageNumber, final List<String> contentFilter,
final String contentCountry, final String sortFilter,
final SearchEngine.Filter filter) { final String contentCountry) {
checkServiceId(serviceId); checkServiceId(serviceId);
return Single.fromCallable(() -> return Single.fromCallable(() ->
SearchResult.getSearchResult(NewPipe.getService(serviceId).getSearchEngine(), SearchInfo.getInfo(NewPipe.getService(serviceId),
query, pageNumber, contentCountry, filter) NewPipe.getService(serviceId)
); .getSearchQHFactory()
.fromQuery(searchString, contentFilter, sortFilter),
contentCountry));
} }
public static Single<InfoItemsPage> getMoreSearchItems(final int serviceId, public static Single<InfoItemsPage> getMoreSearchItems(final int serviceId,
final String query, final String searchString,
final int nextPageNumber, final List<String> contentFilter,
final String searchLanguage, final String sortFilter,
final SearchEngine.Filter filter) { final String pageUrl,
final String contentCountry) {
checkServiceId(serviceId); checkServiceId(serviceId);
return searchFor(serviceId, query, nextPageNumber, searchLanguage, filter) return Single.fromCallable(() ->
.map((@NonNull SearchResult searchResult) -> SearchInfo.getMoreItems(NewPipe.getService(serviceId),
new InfoItemsPage(searchResult.resultList, NewPipe.getService(serviceId)
nextPageNumber + "", .getSearchQHFactory()
searchResult.errors)); .fromQuery(searchString, contentFilter, sortFilter),
contentCountry,
pageUrl));
} }
public static Single<List<String>> suggestionsFor(final int serviceId, 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)); serviceId == -1 ? "none" : NewPipe.getNameOfService(serviceId), url + (optionalErrorMessage == null ? "" : optionalErrorMessage), errorId));
} }
}); });
} }
/** /**

View file

@ -24,7 +24,7 @@ import org.schabi.newpipe.R;
public class KioskTranslator { public class KioskTranslator {
public static String getTranslatedKioskName(String kioskId, Context c) { public static String getTranslatedKioskName(String kioskId, Context c) {
switch(kioskId) { switch (kioskId) {
case "Trending": case "Trending":
return c.getString(R.string.trending); return c.getString(R.string.trending);
case "Top 50": case "Top 50":
@ -35,4 +35,17 @@ public class KioskTranslator {
return kioskId; 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;
}
}
} }

View file

@ -443,11 +443,11 @@ public final class ListHelper {
/** /**
* Are we connected to wifi? * Are we connected to wifi?
* @param context App context * @param context App context
* @return True if connected to wifi * @return {@code true} if connected to wifi
*/ */
private static boolean isWifiActive(Context context) private static boolean isWifiActive(Context context)
{ {
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); 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;
} }
} }

View file

@ -26,6 +26,7 @@ import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; 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.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo; 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.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment; 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.local.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; 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.local.subscription.SubscriptionsImportFragment;
import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.BackgroundPlayerActivity; import org.schabi.newpipe.player.BackgroundPlayerActivity;
@ -100,11 +103,13 @@ public class NavigationHelper {
final int repeatMode, final int repeatMode,
final float playbackSpeed, final float playbackSpeed,
final float playbackPitch, final float playbackPitch,
final boolean playbackSkipSilence,
@Nullable final String playbackQuality) { @Nullable final String playbackQuality) {
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality) return getPlayerIntent(context, targetClazz, playQueue, playbackQuality)
.putExtra(BasePlayer.REPEAT_MODE, repeatMode) .putExtra(BasePlayer.REPEAT_MODE, repeatMode)
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) .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) { public static void playOnMainPlayer(final Context context, final PlayQueue queue) {
@ -281,9 +286,11 @@ public class NavigationHelper {
return fragmentManager.popBackStackImmediate(SEARCH_FRAGMENT_TAG, 0); 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) defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, query)) .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, searchString))
.addToBackStack(SEARCH_FRAGMENT_TAG) .addToBackStack(SEARCH_FRAGMENT_TAG)
.commit(); .commit();
} }
@ -312,7 +319,11 @@ public class NavigationHelper {
.commit(); .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 = ""; if (name == null) name = "";
defaultTransaction(fragmentManager) defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name)) .replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name))
@ -320,7 +331,10 @@ public class NavigationHelper {
.commit(); .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 = ""; if (name == null) name = "";
defaultTransaction(fragmentManager) defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name)) .replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name))
@ -335,6 +349,20 @@ public class NavigationHelper {
.commit(); .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 { public static void openKioskFragment(FragmentManager fragmentManager, int serviceId, String kioskId) throws ExtractionException {
defaultTransaction(fragmentManager) defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, KioskFragment.getInstance(serviceId, kioskId)) .replace(R.id.fragment_holder, KioskFragment.getInstance(serviceId, kioskId))
@ -368,10 +396,10 @@ public class NavigationHelper {
// Through Intents // 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); Intent mIntent = new Intent(context, MainActivity.class);
mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); 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); mIntent.putExtra(Constants.KEY_OPEN_SEARCH, true);
context.startActivity(mIntent); context.startActivity(mIntent);
} }
@ -465,7 +493,8 @@ public class NavigationHelper {
switch (linkType) { switch (linkType) {
case STREAM: 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)); .getBoolean(context.getString(R.string.autoplay_through_intent_key), false));
break; break;
} }

View file

@ -5,7 +5,6 @@ import android.preference.PreferenceManager;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList; 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. * Get a resource string with instructions for importing subscriptions for each service.
* *

View file

@ -1,10 +1,17 @@
package us.shandian.giga.get; 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.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import org.schabi.newpipe.download.ExtSDDownloadFailedActivity;
import java.io.File; import java.io.File;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
@ -23,7 +30,9 @@ public class DownloadManagerImpl implements DownloadManager {
private static final String TAG = DownloadManagerImpl.class.getSimpleName(); private static final String TAG = DownloadManagerImpl.class.getSimpleName();
private final DownloadDataSource mDownloadDataSource; 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 * Create a new instance
@ -33,6 +42,13 @@ public class DownloadManagerImpl implements DownloadManager {
*/ */
public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource) { public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource) {
mDownloadDataSource = downloadDataSource; mDownloadDataSource = downloadDataSource;
this.context = null;
loadMissions(searchLocations);
}
public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource, Context context) {
mDownloadDataSource = downloadDataSource;
this.context = context;
loadMissions(searchLocations); loadMissions(searchLocations);
} }
@ -277,10 +293,12 @@ public class DownloadManagerImpl implements DownloadManager {
} }
private class Initializer extends Thread { private class Initializer extends Thread {
private DownloadMission mission; private final DownloadMission mission;
private final Handler handler;
public Initializer(DownloadMission mission) { public Initializer(DownloadMission mission) {
this.mission = mission; this.mission = mission;
this.handler = new Handler();
} }
@Override @Override
@ -335,6 +353,13 @@ public class DownloadManagerImpl implements DownloadManager {
af.close(); af.close();
mission.start(); 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) { } catch (Exception e) {
// TODO Notify // TODO Notify
throw new RuntimeException(e); throw new RuntimeException(e);

View file

@ -81,7 +81,7 @@ public class DownloadManagerService extends Service {
ArrayList<String> paths = new ArrayList<>(2); ArrayList<String> paths = new ArrayList<>(2);
paths.add(NewPipeSettings.getVideoDownloadPath(this)); paths.add(NewPipeSettings.getVideoDownloadPath(this));
paths.add(NewPipeSettings.getAudioDownloadPath(this)); paths.add(NewPipeSettings.getAudioDownloadPath(this));
mManager = new DownloadManagerImpl(paths, mDataSource); mManager = new DownloadManagerImpl(paths, mDataSource, this);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "mManager == null"); Log.d(TAG, "mManager == null");
Log.d(TAG, "Download directory: " + paths); Log.d(TAG, "Download directory: " + paths);

View file

@ -25,10 +25,13 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DeleteDownloadManager;
import java.io.File; import java.io.File;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -52,18 +55,34 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
private Activity mContext; private Activity mContext;
private LayoutInflater mInflater; private LayoutInflater mInflater;
private DownloadManager mManager; private DownloadManager mDownloadManager;
private DeleteDownloadManager mDeleteDownloadManager;
private List<DownloadMission> mItemList;
private DownloadManagerService.DMBinder mBinder; private DownloadManagerService.DMBinder mBinder;
private int mLayout; 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; mContext = context;
mManager = manager; mDownloadManager = downloadManager;
mDeleteDownloadManager = deleteDownloadManager;
mBinder = binder; mBinder = binder;
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item; 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 @Override
@ -102,7 +121,7 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
@Override @Override
public void onBindViewHolder(MissionAdapter.ViewHolder h, int pos) { public void onBindViewHolder(MissionAdapter.ViewHolder h, int pos) {
DownloadMission ms = mManager.getMission(pos); DownloadMission ms = mItemList.get(pos);
h.mission = ms; h.mission = ms;
h.position = pos; h.position = pos;
@ -123,7 +142,7 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
@Override @Override
public int getItemCount() { public int getItemCount() {
return mManager.getCount(); return mItemList.size();
} }
@Override @Override
@ -214,12 +233,12 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
int id = item.getItemId(); int id = item.getItemId();
switch (id) { switch (id) {
case R.id.start: case R.id.start:
mManager.resumeMission(h.position); mDownloadManager.resumeMission(h.position);
mBinder.onMissionAdded(mManager.getMission(h.position)); mBinder.onMissionAdded(mItemList.get(h.position));
return true; return true;
case R.id.pause: case R.id.pause:
mManager.pauseMission(h.position); mDownloadManager.pauseMission(h.position);
mBinder.onMissionRemoved(mManager.getMission(h.position)); mBinder.onMissionRemoved(mItemList.get(h.position));
h.lastTimeStamp = -1; h.lastTimeStamp = -1;
h.lastDone = -1; h.lastDone = -1;
return true; return true;
@ -245,12 +264,13 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
return true; return true;
case R.id.delete: case R.id.delete:
mManager.deleteMission(h.position); mDeleteDownloadManager.add(h.mission);
updateItemList();
notifyDataSetChanged(); notifyDataSetChanged();
return true; return true;
case R.id.md5: case R.id.md5:
case R.id.sha1: 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)); new ChecksumTask(mContext).execute(mission.location + "/" + mission.name, ALGORITHMS.get(id));
return true; return true;
default: default:
@ -262,19 +282,6 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
popup.show(); 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) { private void viewFileWithFileProvider(File file, String mimetype) {
String ourPackage = mContext.getApplicationContext().getPackageName(); String ourPackage = mContext.getApplicationContext().getPackageName();
Uri uri = FileProvider.getUriForFile(mContext, ourPackage + ".provider", file); Uri uri = FileProvider.getUriForFile(mContext, ourPackage + ".provider", file);

View file

@ -10,6 +10,8 @@ import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.preference.PreferenceManager; 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.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@ -19,13 +21,15 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.schabi.newpipe.R; 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.get.DownloadManager;
import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.adapter.MissionAdapter; import us.shandian.giga.ui.adapter.MissionAdapter;
public abstract class MissionsFragment extends Fragment { public abstract class MissionsFragment extends Fragment {
private DownloadManager mManager; private DownloadManager mDownloadManager;
private DownloadManagerService.DMBinder mBinder; private DownloadManagerService.DMBinder mBinder;
private SharedPreferences mPrefs; private SharedPreferences mPrefs;
@ -37,14 +41,19 @@ public abstract class MissionsFragment extends Fragment {
private GridLayoutManager mGridManager; private GridLayoutManager mGridManager;
private LinearLayoutManager mLinearManager; private LinearLayoutManager mLinearManager;
private Context mActivity; private Context mActivity;
private DeleteDownloadManager mDeleteDownloadManager;
private Disposable mDeleteDisposable;
private ServiceConnection mConnection = new ServiceConnection() { private ServiceConnection mConnection = new ServiceConnection() {
@Override @Override
public void onServiceConnected(ComponentName name, IBinder binder) { public void onServiceConnected(ComponentName name, IBinder binder) {
mBinder = (DownloadManagerService.DMBinder) binder; mBinder = (DownloadManagerService.DMBinder) binder;
mManager = setupDownloadManager(mBinder); mDownloadManager = setupDownloadManager(mBinder);
updateList(); if (mDeleteDownloadManager != null) {
mDeleteDownloadManager.setDownloadManager(mDownloadManager);
updateList();
}
} }
@Override @Override
@ -55,6 +64,14 @@ public abstract class MissionsFragment extends Fragment {
}; };
public void setDeleteManager(@NonNull DeleteDownloadManager deleteDownloadManager) {
mDeleteDownloadManager = deleteDownloadManager;
if (mDownloadManager != null) {
mDeleteDownloadManager.setDownloadManager(mDownloadManager);
updateList();
}
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.missions, container, false); View v = inflater.inflate(R.layout.missions, container, false);
@ -104,10 +121,26 @@ public abstract class MissionsFragment extends Fragment {
mActivity = activity; 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 @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
getActivity().unbindService(mConnection); getActivity().unbindService(mConnection);
if (mDeleteDisposable != null) {
mDeleteDisposable.dispose();
}
} }
@Override @Override
@ -129,7 +162,7 @@ public abstract class MissionsFragment extends Fragment {
} }
private void updateList() { private void updateList() {
mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mManager, mLinear); mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mDownloadManager, mDeleteDownloadManager, mLinear);
if (mLinear) { if (mLinear) {
mList.setLayoutManager(mLinearManager); mList.setLayoutManager(mLinearManager);
@ -143,7 +176,7 @@ public abstract class MissionsFragment extends Fragment {
mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list); 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); protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder);

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

View file

@ -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>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View 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>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="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>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="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>

View 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>

View 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>

View 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,18V6c3.31,0 6,2.69 6,6s-2.69,6 -6,6z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#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>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="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>

View 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>

View 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>

View 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>

View 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>

View 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>

Some files were not shown because too many files have changed in this diff Show more