diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 985223881..8073503ad 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,2 +1,3 @@ - [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them. - [ ] I checked if the issue/feature exists in the latest version. +- [ ] I did use the [incredible bugreport to markdown converter](https://teamnewpipe.github.io/CrashReportToMarkdown/) to paste bug reports. diff --git a/app/build.gradle b/app/build.gradle index 7264c0ab9..19d00196c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 15 targetSdkVersion 27 - versionCode 64 - versionName "0.13.5" + versionCode 68 + versionName "0.14.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -59,22 +59,23 @@ android { ext { supportLibVersion = '27.1.1' - exoPlayerLibVersion = '2.7.3' - roomDbLibVersion = '1.0.0' + exoPlayerLibVersion = '2.8.2' + roomDbLibVersion = '1.1.1' leakCanaryLibVersion = '1.5.4' - okHttpLibVersion = '1.5.0' + okHttpLibVersion = '3.10.0' icepickLibVersion = '3.2.0' stethoLibVersion = '1.5.0' } + dependencies { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:bf1c771' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:66c3c3f45241d4b0c909' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:1.10.19' + testImplementation 'org.mockito:mockito-core:2.8.9' implementation "com.android.support:appcompat-v7:$supportLibVersion" implementation "com.android.support:support-v4:$supportLibVersion" @@ -96,7 +97,7 @@ dependencies { debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion" debugImplementation 'com.android.support:multidex:1.0.3' - implementation 'io.reactivex.rxjava2:rxjava:2.1.10' + implementation 'io.reactivex.rxjava2:rxjava:2.1.14' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' @@ -110,6 +111,9 @@ dependencies { debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion" - implementation 'com.squareup.okhttp3:okhttp:3.9.1' - debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion" + + implementation "com.squareup.okhttp3:okhttp:$okHttpLibVersion" + debugImplementation "com.facebook.stetho:stetho-okhttp3:$stethoLibVersion" + implementation 'com.android.support.constraint:constraint-layout:1.1.2' + implementation 'com.android.support:cardview-v7:27.1.1' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 33e0651e5..e4d448184 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -76,10 +76,6 @@ android:name=".about.AboutActivity" android:label="@string/title_activity_about"/> - - @@ -122,6 +118,7 @@ + = Build.VERSION_CODES.LOLLIPOP) { + Window w = getWindow(); + w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) { initFragments(); } setSupportActionBar(findViewById(R.id.toolbar)); - setupDrawer(); + try { + setupDrawer(); + } catch (Exception e) { + ErrorActivity.reportUiError(this, e); + } } - private void setupDrawer() { + private void setupDrawer() throws Exception { final Toolbar toolbar = findViewById(R.id.toolbar); drawer = findViewById(R.id.drawer_layout); drawerItems = findViewById(R.id.navigation); - for(StreamingService s : NewPipe.getServices()) { - final String title = s.getServiceInfo().getName() + - (ServiceHelper.isBeta(s) ? " (beta)" : ""); - final MenuItem item = drawerItems.getMenu() - .add(R.id.menu_services_group, s.getServiceId(), 0, title); - item.setIcon(ServiceHelper.getIcon(s.getServiceId())); + //Tabs + int currentServiceId = ServiceHelper.getSelectedServiceId(this); + StreamingService service = NewPipe.getService(currentServiceId); + + int kioskId = 0; + + for (final String ks : service.getKioskList().getAvailableKiosks()) { + drawerItems.getMenu() + .add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator.getTranslatedKioskName(ks, this)) + .setIcon(KioskTranslator.getKioskIcons(ks, this)); + kioskId ++; } - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel)); + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss)); + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark)); + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download)); + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history)); + + //Settings and About + drawerItems.getMenu() + .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings)); + drawerItems.getMenu() + .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info)); toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close); toggle.syncState(); @@ -119,53 +175,179 @@ public class MainActivity extends AppCompatActivity { @Override public void onDrawerClosed(View drawerView) { + if(servicesShown) { + toggleServices(); + } if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) { new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate); } } }); - drawerItems.setNavigationItemSelectedListener(this::changeService); - - setupDrawerFooter(); + drawerItems.setNavigationItemSelectedListener(this::drawerItemSelected); setupDrawerHeader(); } - - private boolean changeService(MenuItem item) { - if (item.getGroupId() == R.id.menu_services_group) { - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false); - ServiceHelper.setSelectedServiceId(this, item.getItemId()); - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); - } else { - return false; + private boolean drawerItemSelected(MenuItem item) { + switch (item.getGroupId()) { + case R.id.menu_services_group: + changeService(item); + break; + case R.id.menu_tabs_group: + try { + tabSelected(item); + } catch (Exception e) { + ErrorActivity.reportUiError(this, e); + } + break; + case R.id.menu_options_about_group: + optionsAboutSelected(item); + break; + default: + return false; } + drawer.closeDrawers(); return true; } - private void setupDrawerFooter() { - ImageButton settings = findViewById(R.id.drawer_settings); - ImageButton downloads = findViewById(R.id.drawer_downloads); - ImageButton history = findViewById(R.id.drawer_history); + private void changeService(MenuItem item) { + drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false); + ServiceHelper.setSelectedServiceId(this, item.getItemId()); + drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); + } - settings.setOnClickListener(view -> NavigationHelper.openSettings(this)); - downloads.setOnClickListener(view ->NavigationHelper.openDownloads(this)); - history.setOnClickListener(view -> - NavigationHelper.openStatisticFragment(getSupportFragmentManager())); + private void tabSelected(MenuItem item) throws ExtractionException { + switch(item.getItemId()) { + case ITEM_ID_SUBSCRIPTIONS: + NavigationHelper.openSubscriptionFragment(getSupportFragmentManager()); + break; + case ITEM_ID_FEED: + NavigationHelper.openWhatsNewFragment(getSupportFragmentManager()); + break; + case ITEM_ID_BOOKMARKS: + NavigationHelper.openBookmarksFragment(getSupportFragmentManager()); + break; + case ITEM_ID_DOWNLOADS: + NavigationHelper.openDownloads(this); + break; + case ITEM_ID_HISTORY: + NavigationHelper.openStatisticFragment(getSupportFragmentManager()); + break; + default: + int currentServiceId = ServiceHelper.getSelectedServiceId(this); + StreamingService service = NewPipe.getService(currentServiceId); + String serviceName = ""; + + int kioskId = 0; + for (final String ks : service.getKioskList().getAvailableKiosks()) { + if(kioskId == item.getItemId()) { + serviceName = ks; + } + kioskId ++; + } + + NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId, serviceName); + break; + } + } + + private void optionsAboutSelected(MenuItem item) { + switch(item.getItemId()) { + case ITEM_ID_SETTINGS: + NavigationHelper.openSettings(this); + break; + case ITEM_ID_ABOUT: + NavigationHelper.openAbout(this); + break; + } } private void setupDrawerHeader() { - headerServiceView = findViewById(R.id.drawer_header_service_view); - Button action = findViewById(R.id.drawer_header_action_button); + NavigationView navigationView = findViewById(R.id.navigation); + View hView = navigationView.getHeaderView(0); + + serviceArrow = hView.findViewById(R.id.drawer_arrow); + headerServiceView = hView.findViewById(R.id.drawer_header_service_view); + Button action = hView.findViewById(R.id.drawer_header_action_button); action.setOnClickListener(view -> { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("https://newpipe.schabi.org/blog/")); - startActivity(intent); - drawer.closeDrawers(); + toggleServices(); }); } + private void toggleServices() { + servicesShown = !servicesShown; + + drawerItems.getMenu().removeGroup(R.id.menu_services_group); + drawerItems.getMenu().removeGroup(R.id.menu_tabs_group); + drawerItems.getMenu().removeGroup(R.id.menu_options_about_group); + + if(servicesShown) { + showServices(); + } else { + try { + showTabs(); + } catch (Exception e) { + ErrorActivity.reportUiError(this, e); + } + } + } + + private void showServices() { + serviceArrow.setImageResource(R.drawable.ic_arrow_up_white); + + for(StreamingService s : NewPipe.getServices()) { + final String title = s.getServiceInfo().getName() + + (ServiceHelper.isBeta(s) ? " (beta)" : ""); + + drawerItems.getMenu() + .add(R.id.menu_services_group, s.getServiceId(), ORDER, title) + .setIcon(ServiceHelper.getIcon(s.getServiceId())); + } + drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); + } + + private void showTabs() throws ExtractionException { + serviceArrow.setImageResource(R.drawable.ic_arrow_down_white); + + //Tabs + int currentServiceId = ServiceHelper.getSelectedServiceId(this); + StreamingService service = NewPipe.getService(currentServiceId); + + int kioskId = 0; + + for (final String ks : service.getKioskList().getAvailableKiosks()) { + drawerItems.getMenu() + .add(R.id.menu_tabs_group, kioskId, ORDER, KioskTranslator.getTranslatedKioskName(ks, this)) + .setIcon(KioskTranslator.getKioskIcons(ks, this)); + kioskId ++; + } + + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel)); + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss)); + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark)); + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download)); + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history)); + + //Settings and About + drawerItems.getMenu() + .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings)); + drawerItems.getMenu() + .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info)); + } + @Override protected void onDestroy() { super.onDestroy(); @@ -329,16 +511,13 @@ public class MainActivity extends AppCompatActivity { onHomeButtonPressed(); return true; case R.id.action_show_downloads: - return NavigationHelper.openDownloads(this); + return NavigationHelper.openDownloads(this); case R.id.action_history: - NavigationHelper.openStatisticFragment(getSupportFragmentManager()); - return true; - case R.id.action_about: - NavigationHelper.openAbout(this); - return true; + NavigationHelper.openStatisticFragment(getSupportFragmentManager()); + return true; case R.id.action_settings: - NavigationHelper.openSettings(this); - return true; + NavigationHelper.openSettings(this); + return true; default: return super.onOptionsItemSelected(item); } @@ -382,31 +561,45 @@ public class MainActivity extends AppCompatActivity { } private void handleIntent(Intent intent) { - if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); + try { + if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); - if (intent.hasExtra(Constants.KEY_LINK_TYPE)) { - String url = intent.getStringExtra(Constants.KEY_URL); - int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); - String title = intent.getStringExtra(Constants.KEY_TITLE); - switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) { - case STREAM: - boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); - NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay); - break; - case CHANNEL: - NavigationHelper.openChannelFragment(getSupportFragmentManager(), serviceId, url, title); - break; - case PLAYLIST: - NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), serviceId, url, title); - break; + if (intent.hasExtra(Constants.KEY_LINK_TYPE)) { + String url = intent.getStringExtra(Constants.KEY_URL); + int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); + String title = intent.getStringExtra(Constants.KEY_TITLE); + switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) { + case STREAM: + boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); + NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay); + break; + case CHANNEL: + NavigationHelper.openChannelFragment(getSupportFragmentManager(), + serviceId, + url, + title); + break; + case PLAYLIST: + NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), + serviceId, + url, + title); + break; + } + } else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) { + String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING); + if (searchString == null) searchString = ""; + int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); + NavigationHelper.openSearchFragment( + getSupportFragmentManager(), + serviceId, + searchString); + + } else { + NavigationHelper.gotoMainFragment(getSupportFragmentManager()); } - } else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) { - String searchQuery = intent.getStringExtra(Constants.KEY_QUERY); - if (searchQuery == null) searchQuery = ""; - int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); - NavigationHelper.openSearchFragment(getSupportFragmentManager(), serviceId, searchQuery); - } else { - NavigationHelper.gotoMainFragment(getSupportFragmentManager()); + } catch (Exception e) { + ErrorActivity.reportUiError(this, e); } } } diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index a862384cf..4f1fdeab2 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -1,14 +1,19 @@ package org.schabi.newpipe; +import android.annotation.SuppressLint; +import android.app.FragmentManager; import android.app.IntentService; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; import android.support.v4.app.NotificationCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; @@ -23,6 +28,7 @@ import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Toast; +import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; @@ -31,6 +37,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -38,16 +46,19 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; +import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Observer; import icepick.Icepick; import icepick.State; @@ -77,6 +88,8 @@ public class RouterActivity extends AppCompatActivity { protected String currentUrl; protected CompositeDisposable disposables = new CompositeDisposable(); + private boolean selectionIsDownload = false; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -104,7 +117,7 @@ public class RouterActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); - + handleUrl(currentUrl); } @@ -165,6 +178,7 @@ public class RouterActivity extends AppCompatActivity { final String videoPlayerKey = getString(R.string.video_player_key); final String backgroundPlayerKey = getString(R.string.background_player_key); final String popupPlayerKey = getString(R.string.popup_player_key); + final String downloadKey = getString(R.string.download_key); final String alwaysAskKey = getString(R.string.always_ask_open_action_key); if (selectedChoiceKey.equals(alwaysAskKey)) { @@ -179,6 +193,8 @@ public class RouterActivity extends AppCompatActivity { } } else if (selectedChoiceKey.equals(showInfoKey)) { handleChoice(showInfoKey); + } else if (selectedChoiceKey.equals(downloadKey)) { + handleChoice(downloadKey); } else { final boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); final boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); @@ -236,7 +252,9 @@ public class RouterActivity extends AppCompatActivity { .setCancelable(true) .setNegativeButton(R.string.just_once, dialogButtonsClickListener) .setPositiveButton(R.string.always, dialogButtonsClickListener) - .setOnDismissListener((dialog) -> finish()) + .setOnDismissListener((dialog) -> { + if(!selectionIsDownload) finish(); + }) .create(); //noinspection CodeBlock2Expr @@ -316,6 +334,9 @@ public class RouterActivity extends AppCompatActivity { resolveResourceIdFromAttr(context, R.attr.audio))); } + returnList.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download), + resolveResourceIdFromAttr(context, R.attr.download))); + return returnList; } @@ -347,6 +368,14 @@ public class RouterActivity extends AppCompatActivity { return; } + if (selectedChoiceKey.equals(getString(R.string.download_key))) { + if (PermissionHelper.checkStoragePermissions(this, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { + selectionIsDownload = true; + openDownloadDialog(); + } + return; + } + // stop and bypass FetcherService if InfoScreen was selected since // StreamDetailFragment can fetch data itself if (selectedChoiceKey.equals(getString(R.string.show_info_key))) { @@ -373,6 +402,47 @@ public class RouterActivity extends AppCompatActivity { finish(); } + @SuppressLint("CheckResult") + private void openDownloadDialog() { + ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((@NonNull StreamInfo result) -> { + List sortedVideoStreams = ListHelper.getSortedStreamVideosList(this, + result.getVideoStreams(), + result.getVideoOnlyStreams(), + false); + int selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(this, + sortedVideoStreams); + + android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); + DownloadDialog downloadDialog = DownloadDialog.newInstance(result); + downloadDialog.setVideoStreams(sortedVideoStreams); + downloadDialog.setAudioStreams(result.getAudioStreams()); + downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); + downloadDialog.show(fm, "downloadDialog"); + fm.executePendingTransactions(); + downloadDialog.getDialog().setOnDismissListener(dialog -> { + finish(); + }); + }, (@NonNull Throwable throwable) -> { + onError(); + }); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + for (int i: grantResults){ + if (i == PackageManager.PERMISSION_DENIED){ + finish(); + return; + } + } + if (requestCode == PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE) { + openDownloadDialog(); + } + } + private static class AdapterChoiceItem { final String description, key; @DrawableRes final int icon; diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java index 486350fc9..e2f2c8b08 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java @@ -71,6 +71,14 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { info.getUploaderName(), info.getStreamCount()); } + @Ignore + public boolean isIdenticalTo(final PlaylistInfo info) { + return getServiceId() == info.getServiceId() && getName().equals(info.getName()) && + getStreamCount() == info.getStreamCount() && getUrl().equals(info.getUrl()) && + getThumbnailUrl().equals(info.getThumbnailUrl()) && + getUploader().equals(info.getUploaderName()); + } + public long getUid() { return uid; } diff --git a/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java b/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java new file mode 100644 index 000000000..f2912a6fa --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java @@ -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 mPendingMap; + private List mDisposableList; + private DownloadManager mDownloadManager; + private PublishSubject publishSubject = PublishSubject.create(); + + DeleteDownloadManager(Activity activity) { + mPendingMap = new HashSet<>(); + mDisposableList = new ArrayList<>(); + mView = activity.findViewById(android.R.id.content); + } + + public Observable 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 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() { + @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 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; + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index 6512f5270..4a2c85149 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -15,12 +15,17 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.util.ThemeHelper; +import io.reactivex.Completable; +import io.reactivex.schedulers.Schedulers; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.ui.fragment.AllMissionsFragment; import us.shandian.giga.ui.fragment.MissionsFragment; public class DownloadActivity extends AppCompatActivity { + private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag"; + private DeleteDownloadManager mDeleteDownloadManager; + @Override protected void onCreate(Bundle savedInstanceState) { // Service @@ -42,21 +47,35 @@ public class DownloadActivity extends AppCompatActivity { actionBar.setDisplayShowTitleEnabled(true); } - // Fragment - getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - updateFragments(); - getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this); - } - }); + mDeleteDownloadManager = new DeleteDownloadManager(this); + mDeleteDownloadManager.restoreState(savedInstanceState); + + MissionsFragment fragment = (MissionsFragment) getFragmentManager().findFragmentByTag(MISSIONS_FRAGMENT_TAG); + if (fragment != null) { + fragment.setDeleteManager(mDeleteDownloadManager); + } else { + getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + updateFragments(); + getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + }); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + mDeleteDownloadManager.saveState(outState); + super.onSaveInstanceState(outState); } private void updateFragments() { - MissionsFragment fragment = new AllMissionsFragment(); + fragment.setDeleteManager(mDeleteDownloadManager); + getFragmentManager().beginTransaction() - .replace(R.id.frame, fragment) + .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); } @@ -80,6 +99,7 @@ public class DownloadActivity extends AppCompatActivity { case R.id.action_settings: { Intent intent = new Intent(this, SettingsActivity.class); startActivity(intent); + deletePending(); return true; } default: @@ -87,4 +107,15 @@ public class DownloadActivity extends AppCompatActivity { } } + @Override + public void onBackPressed() { + super.onBackPressed(); + deletePending(); + } + + private void deletePending() { + Completable.fromAction(mDeleteDownloadManager::deletePending) + .subscribeOn(Schedulers.io()) + .subscribe(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/download/ExtSDDownloadFailedActivity.java b/app/src/main/java/org/schabi/newpipe/download/ExtSDDownloadFailedActivity.java new file mode 100644 index 000000000..c02ef92eb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/download/ExtSDDownloadFailedActivity.java @@ -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(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java index 5707716bf..589d15bd4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java @@ -51,9 +51,6 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC protected Button errorButtonRetry; protected TextView errorTextView; - @State - protected boolean useAsFrontPage = false; - @Override public void onViewCreated(View rootView, Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); @@ -66,9 +63,6 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC wasLoading.set(isLoading.get()); } - public void useAsFrontPage(boolean value) { - useAsFrontPage = value; - } /*////////////////////////////////////////////////////////////////////////// // Init @@ -93,12 +87,7 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC RxView.clicks(errorButtonRetry) .debounce(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Object o) throws Exception { - onRetryButtonClicked(); - } - }); + .subscribe(o -> onRetryButtonClicked()); } protected void onRetryButtonClicked() { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java index e81645202..4ee90f083 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java @@ -14,24 +14,16 @@ public class BlankFragment extends BaseFragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - if(activity != null && activity.getSupportActionBar() != null) { - activity.getSupportActionBar() - .setTitle("NewPipe"); - } + setTitle("NewPipe"); return inflater.inflate(R.layout.fragment_blank, container, false); } @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); - if(isVisibleToUser) { - if(activity != null && activity.getSupportActionBar() != null) { - activity.getSupportActionBar() - .setTitle("NewPipe"); - } - // leave this inline. Will make it harder for copy cats. - // If you are a Copy cat FUCK YOU. - // I WILL FIND YOU, AND I WILL ... - } + setTitle("NewPipe"); + // leave this inline. Will make it harder for copy cats. + // If you are a Copy cat FUCK YOU. + // I WILL FIND YOU, AND I WILL ... } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 31092d3e6..de14997ef 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.fragments; -import android.content.SharedPreferences; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -10,48 +9,37 @@ import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; -import android.support.v7.preference.PreferenceManager; +import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.ServiceList; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.kiosk.KioskList; -import org.schabi.newpipe.fragments.list.channel.ChannelFragment; -import org.schabi.newpipe.local.feed.FeedFragment; -import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; -import org.schabi.newpipe.local.bookmark.BookmarkFragment; -import org.schabi.newpipe.local.subscription.SubscriptionFragment; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.KioskTranslator; +import org.schabi.newpipe.settings.tabs.Tab; +import org.schabi.newpipe.settings.tabs.TabsManager; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; -import org.schabi.newpipe.util.ThemeHelper; + +import java.util.ArrayList; +import java.util.List; public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener { - - public int currentServiceId = -1; private ViewPager viewPager; + private SelectedTabsPagerAdapter pagerAdapter; + private TabLayout tabLayout; - /*////////////////////////////////////////////////////////////////////////// - // Constants - //////////////////////////////////////////////////////////////////////////*/ + private List tabsList = new ArrayList<>(); + private TabsManager tabsManager; - private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getServiceId(); - private static final String FALLBACK_CHANNEL_URL = "https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ"; - private static final String FALLBACK_CHANNEL_NAME = "Music"; - private static final String FALLBACK_KIOSK_ID = "Trending"; - private static final int KIOSK_MENU_OFFSET = 2000; + private boolean hasTabsChanged = false; /*////////////////////////////////////////////////////////////////////////// // Fragment's LifeCycle @@ -61,11 +49,22 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); + + tabsManager = TabsManager.getManager(activity); + tabsManager.setSavedTabsListener(() -> { + if (DEBUG) { + Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed()); + } + if (isResumed()) { + updateTabs(); + } else { + hasTabsChanged = true; + } + }); } @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - currentServiceId = ServiceHelper.getSelectedServiceId(activity); return inflater.inflate(R.layout.fragment_main, container, false); } @@ -73,30 +72,34 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - TabLayout tabLayout = rootView.findViewById(R.id.main_tab_layout); + tabLayout = rootView.findViewById(R.id.main_tab_layout); viewPager = rootView.findViewById(R.id.pager); /* Nested fragment, use child fragment here to maintain backstack in view pager. */ - PagerAdapter adapter = new PagerAdapter(getChildFragmentManager()); - viewPager.setAdapter(adapter); - viewPager.setOffscreenPageLimit(adapter.getCount()); + pagerAdapter = new SelectedTabsPagerAdapter(getChildFragmentManager()); + viewPager.setAdapter(pagerAdapter); tabLayout.setupWithViewPager(viewPager); + tabLayout.addOnTabSelectedListener(this); + updateTabs(); + } - int channelIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_channel); - int whatsHotIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_hot); - int bookmarkIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_bookmark); + @Override + public void onResume() { + super.onResume(); - if (isSubscriptionsPageOnlySelected()) { - tabLayout.getTabAt(0).setIcon(channelIcon); - tabLayout.getTabAt(1).setIcon(bookmarkIcon); - } else { - tabLayout.getTabAt(0).setIcon(whatsHotIcon); - tabLayout.getTabAt(1).setIcon(channelIcon); - tabLayout.getTabAt(2).setIcon(bookmarkIcon); + if (hasTabsChanged) { + hasTabsChanged = false; + updateTabs(); } } + @Override + public void onDestroy() { + super.onDestroy(); + tabsManager.unsetSavedTabsListener(); + } + /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @@ -106,16 +109,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte super.onCreateOptionsMenu(menu, inflater); if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); inflater.inflate(R.menu.main_fragment_menu, menu); - SubMenu kioskMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, 200, getString(R.string.kiosk)); - try { - createKioskMenu(kioskMenu, inflater); - } catch (Exception e) { - ErrorActivity.reportError(activity, e, - activity.getClass(), - null, - ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, - "none", "", R.string.app_ui_crash)); - } ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { @@ -127,7 +120,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_search: - NavigationHelper.openSearchFragment(getFragmentManager(), ServiceHelper.getSelectedServiceId(activity), ""); + try { + NavigationHelper.openSearchFragment( + getFragmentManager(), + ServiceHelper.getSelectedServiceId(activity), + ""); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } return true; } return super.onOptionsItemSelected(item); @@ -137,9 +137,33 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte // Tabs //////////////////////////////////////////////////////////////////////////*/ + public void updateTabs() { + tabsList.clear(); + tabsList.addAll(tabsManager.getTabs()); + pagerAdapter.notifyDataSetChanged(); + + viewPager.setOffscreenPageLimit(pagerAdapter.getCount()); + updateTabsIcon(); + updateCurrentTitle(); + } + + private void updateTabsIcon() { + for (int i = 0; i < tabsList.size(); i++) { + final TabLayout.Tab tabToSet = tabLayout.getTabAt(i); + if (tabToSet != null) { + tabToSet.setIcon(tabsList.get(i).getTabIconRes(activity)); + } + } + } + + private void updateCurrentTitle() { + setTitle(tabsList.get(viewPager.getCurrentItem()).getTabName(requireContext())); + } + @Override - public void onTabSelected(TabLayout.Tab tab) { - viewPager.setCurrentItem(tab.getPosition()); + public void onTabSelected(TabLayout.Tab selectedTab) { + if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]"); + updateCurrentTitle(); } @Override @@ -148,129 +172,58 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte @Override public void onTabReselected(TabLayout.Tab tab) { + if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]"); + updateCurrentTitle(); } - private class PagerAdapter extends FragmentPagerAdapter { - PagerAdapter(FragmentManager fm) { - super(fm); + private class SelectedTabsPagerAdapter extends FragmentPagerAdapter { + private SelectedTabsPagerAdapter(FragmentManager fragmentManager) { + super(fragmentManager); } @Override public Fragment getItem(int position) { - switch (position) { - case 0: - return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment(); - case 1: - if(PreferenceManager.getDefaultSharedPreferences(getActivity()) - .getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key)) - .equals(getString(R.string.subscription_page_key))) { - return new BookmarkFragment(); - } else { - return new SubscriptionFragment(); - } - case 2: - return new BookmarkFragment(); - default: - return new BlankFragment(); + final Tab tab = tabsList.get(position); + + Throwable throwable = null; + Fragment fragment = null; + try { + fragment = tab.getFragment(); + } catch (ExtractionException e) { + throwable = e; } + + if (throwable != null) { + ErrorActivity.reportError(activity, throwable, activity.getClass(), null, + ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); + return new BlankFragment(); + } + + if (fragment instanceof BaseFragment) { + ((BaseFragment) fragment).useAsFrontPage(true); + } + + return fragment; } @Override - public CharSequence getPageTitle(int position) { - //return getString(this.tabTitles[position]); - return ""; + public int getItemPosition(Object object) { + // Causes adapter to reload all Fragments when + // notifyDataSetChanged is called + return POSITION_NONE; } @Override public int getCount() { - return isSubscriptionsPageOnlySelected() ? 2 : 3; + return tabsList.size(); } - } - /*////////////////////////////////////////////////////////////////////////// - // Main page content - //////////////////////////////////////////////////////////////////////////*/ - - private boolean isSubscriptionsPageOnlySelected() { - return PreferenceManager.getDefaultSharedPreferences(activity) - .getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key)) - .equals(getString(R.string.subscription_page_key)); - } - - private Fragment getMainPageFragment() { - if (getActivity() == null) return new BlankFragment(); - - try { - SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(getActivity()); - final String setMainPage = preferences.getString(getString(R.string.main_page_content_key), - getString(R.string.main_page_selectd_kiosk_id)); - if (setMainPage.equals(getString(R.string.blank_page_key))) { - return new BlankFragment(); - } else if (setMainPage.equals(getString(R.string.kiosk_page_key))) { - int serviceId = preferences.getInt(getString(R.string.main_page_selected_service), - FALLBACK_SERVICE_ID); - String kioskId = preferences.getString(getString(R.string.main_page_selectd_kiosk_id), - FALLBACK_KIOSK_ID); - KioskFragment fragment = KioskFragment.getInstance(serviceId, kioskId); - fragment.useAsFrontPage(true); - return fragment; - } else if (setMainPage.equals(getString(R.string.feed_page_key))) { - FeedFragment fragment = new FeedFragment(); - fragment.useAsFrontPage(true); - return fragment; - } else if (setMainPage.equals(getString(R.string.channel_page_key))) { - int serviceId = preferences.getInt(getString(R.string.main_page_selected_service), - FALLBACK_SERVICE_ID); - String url = preferences.getString(getString(R.string.main_page_selected_channel_url), - FALLBACK_CHANNEL_URL); - String name = preferences.getString(getString(R.string.main_page_selected_channel_name), - FALLBACK_CHANNEL_NAME); - ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name); - fragment.useAsFrontPage(true); - return fragment; - } else { - return new BlankFragment(); - } - - } catch (Exception e) { - ErrorActivity.reportError(activity, e, - activity.getClass(), - null, - ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, - "none", "", R.string.app_ui_crash)); - return new BlankFragment(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Select Kiosk - //////////////////////////////////////////////////////////////////////////*/ - - private void createKioskMenu(Menu menu, MenuInflater menuInflater) - throws Exception { - StreamingService service = NewPipe.getService(currentServiceId); - KioskList kl = service.getKioskList(); - int i = 0; - for (final String ks : kl.getAvailableKiosks()) { - menu.add(0, KIOSK_MENU_OFFSET + i, Menu.NONE, - KioskTranslator.getTranslatedKioskName(ks, getContext())) - .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - try { - NavigationHelper.openKioskFragment(getFragmentManager(), currentServiceId, ks); - } catch (Exception e) { - ErrorActivity.reportError(activity, e, - activity.getClass(), - null, - ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, - "none", "", R.string.app_ui_crash)); - } - return true; - } - }); - i++; + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + getChildFragmentManager() + .beginTransaction() + .remove((Fragment) object) + .commitNowAllowingStateLoss(); } } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 68c0a11ff..c726f8cee 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -17,6 +17,7 @@ import android.support.v4.content.ContextCompat; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; @@ -54,14 +55,17 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; @@ -128,7 +132,7 @@ public class VideoDetailFragment private StreamInfo currentInfo; private Disposable currentWorker; - private CompositeDisposable disposables = new CompositeDisposable(); + @NonNull private CompositeDisposable disposables = new CompositeDisposable(); private List sortedVideoStreams; private int selectedVideoStreamIndex = -1; @@ -363,11 +367,15 @@ public class VideoDetailFragment if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { Log.w(TAG, "Can't open channel because we got no channel URL"); } else { - NavigationHelper.openChannelFragment( - getFragmentManager(), - currentInfo.getServiceId(), - currentInfo.getUploaderUrl(), - currentInfo.getUploaderName()); + try { + NavigationHelper.openChannelFragment( + getFragmentManager(), + currentInfo.getServiceId(), + currentInfo.getUploaderUrl(), + currentInfo.getUploaderName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } } break; case R.id.detail_thumbnail_root_layout: @@ -540,7 +548,8 @@ public class VideoDetailFragment final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_popup), - context.getResources().getString(R.string.append_playlist) + context.getResources().getString(R.string.append_playlist), + context.getResources().getString(R.string.share) }; final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { @@ -557,6 +566,9 @@ public class VideoDetailFragment .show(getFragmentManager(), TAG); } break; + case 3: + shareUrl(item.getName(), item.getUrl()); + break; default: break; } @@ -872,10 +884,7 @@ public class VideoDetailFragment if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { openNormalBackgroundPlayer(append); } else { - NavigationHelper.playOnExternalPlayer(activity, - currentInfo.getName(), - currentInfo.getUploaderName(), - audioStream); + startOnExternalPlayer(activity, currentInfo, audioStream); } } @@ -902,10 +911,7 @@ public class VideoDetailFragment if (PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(this.getString(R.string.use_external_video_player_key), false)) { - NavigationHelper.playOnExternalPlayer(activity, - currentInfo.getName(), - currentInfo.getUploaderName(), - selectedVideoStream); + startOnExternalPlayer(activity, currentInfo, selectedVideoStream); } else { openNormalPlayer(selectedVideoStream); } @@ -949,6 +955,20 @@ public class VideoDetailFragment this.autoPlayEnabled = autoplay; } + private void startOnExternalPlayer(@NonNull final Context context, + @NonNull final StreamInfo info, + @NonNull final Stream selectedStream) { + NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(), + currentInfo.getUploaderName(), selectedStream); + + final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); + disposables.add(recordManager.onViewed(info).onErrorComplete() + .subscribe( + ignored -> {/* successful */}, + error -> Log.e(TAG, "Register view failure: ", error) + )); + } + @Nullable private VideoStream getSelectedVideoStream() { return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; @@ -1207,10 +1227,10 @@ public class VideoDetailFragment spinnerToolbar.setVisibility(View.GONE); break; default: + if(info.getAudioStreams().isEmpty()) detailControlsBackground.setVisibility(View.GONE); if (!info.getVideoStreams().isEmpty() || !info.getVideoOnlyStreams().isEmpty()) break; - detailControlsBackground.setVisibility(View.GONE); detailControlsPopup.setVisibility(View.GONE); spinnerToolbar.setVisibility(View.GONE); thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 14ec50775..c70ea2b19 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -6,6 +6,7 @@ import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -24,6 +25,7 @@ import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; @@ -152,18 +154,30 @@ public abstract class BaseListFragment extends BaseStateFragment implem infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() { @Override public void selected(ChannelInfoItem selectedItem) { - onItemSelected(selectedItem); - NavigationHelper.openChannelFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), - selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); + try { + onItemSelected(selectedItem); + NavigationHelper.openChannelFragment(getFM(), + selectedItem.getServiceId(), + selectedItem.getUrl(), + selectedItem.getName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } } }); infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture() { @Override public void selected(PlaylistInfoItem selectedItem) { - onItemSelected(selectedItem); - NavigationHelper.openPlaylistFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), - selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); + try { + onItemSelected(selectedItem); + NavigationHelper.openPlaylistFragment(getFM(), + selectedItem.getServiceId(), + selectedItem.getUrl(), + selectedItem.getName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } } }); @@ -178,7 +192,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem private void onStreamSelected(StreamInfoItem selectedItem) { onItemSelected(selectedItem); - NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), + NavigationHelper.openVideoDetailFragment(getFM(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } @@ -196,7 +210,8 @@ public abstract class BaseListFragment extends BaseStateFragment implem final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_popup), - context.getResources().getString(R.string.append_playlist) + context.getResources().getString(R.string.append_playlist), + context.getResources().getString(R.string.share) }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { @@ -213,6 +228,9 @@ public abstract class BaseListFragment extends BaseStateFragment implem .show(getFragmentManager(), TAG); } break; + case 3: + shareUrl(item.getName(), item.getUrl()); + break; default: break; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index a132213bf..e702c602f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -8,6 +8,9 @@ import android.view.View; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.util.Constants; import java.util.Queue; @@ -166,7 +169,6 @@ public abstract class BaseListInfoFragment public void handleResult(@NonNull I result) { super.handleResult(result); - url = result.getUrl(); name = result.getName(); setTitle(name); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index dc8d764f3..9a52b8d12 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -36,11 +36,11 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; +import org.schabi.newpipe.local.subscription.SubscriptionService; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.local.subscription.SubscriptionService; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; @@ -90,6 +90,8 @@ public class ChannelFragment extends BaseListInfoFragment { private MenuItem menuRssButton; + private boolean mIsVisibleToUser = false; + public static ChannelFragment getInstance(int serviceId, String url, String name) { ChannelFragment instance = new ChannelFragment(); instance.setInitialData(serviceId, url, name); @@ -103,6 +105,7 @@ public class ChannelFragment extends BaseListInfoFragment { @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); + mIsVisibleToUser = isVisibleToUser; if(activity != null && useAsFrontPage && isVisibleToUser) { @@ -161,38 +164,39 @@ public class ChannelFragment extends BaseListInfoFragment { context.getResources().getString(R.string.start_here_on_main), context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_popup), - context.getResources().getString(R.string.append_playlist) + context.getResources().getString(R.string.append_playlist), + context.getResources().getString(R.string.share) }; - final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); - switch (i) { - case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); - break; - case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); - break; - case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); - break; - case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); - break; - case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); - break; - case 5: - if (getFragmentManager() != null) { - PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) - .show(getFragmentManager(), TAG); - } - break; - default: - break; - } + final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { + final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + break; + case 2: + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + break; + case 3: + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + break; + case 4: + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + break; + case 5: + if (getFragmentManager() != null) { + PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) + .show(getFragmentManager(), TAG); + } + break; + case 6: + shareUrl(item.getName(), item.getUrl()); + break; + default: + break; } }; @@ -250,12 +254,12 @@ public class ChannelFragment extends BaseListInfoFragment { private static final int BUTTON_DEBOUNCE_INTERVAL = 100; private void monitorSubscription(final ChannelInfo info) { - final Consumer onError = new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { + final Consumer onError = (Throwable throwable) -> { animateView(headerSubscribeButton, false, 100); - showSnackBarError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.getServiceId()), "Get subscription status", 0); - } + showSnackBarError(throwable, UserAction.SUBSCRIPTION, + NewPipe.getNameOfService(currentInfo.getServiceId()), + "Get subscription status", + 0); }; final Observable> observable = subscriptionService.subscriptionTable() @@ -271,50 +275,38 @@ public class ChannelFragment extends BaseListInfoFragment { // so only update the UI for the latest emission ("sync" the subscribe button's state) .debounce(100, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer>() { - @Override - public void accept(List subscriptionEntities) throws Exception { - updateSubscribeButton(!subscriptionEntities.isEmpty()); - } - }, onError)); + .subscribe((List subscriptionEntities) -> + updateSubscribeButton(!subscriptionEntities.isEmpty()) + , onError)); } private Function mapOnSubscribe(final SubscriptionEntity subscription) { - return new Function() { - @Override - public Object apply(@NonNull Object o) throws Exception { - subscriptionService.subscriptionTable().insert(subscription); - return o; - } + return (@NonNull Object o) -> { + subscriptionService.subscriptionTable().insert(subscription); + return o; }; } private Function mapOnUnsubscribe(final SubscriptionEntity subscription) { - return new Function() { - @Override - public Object apply(@NonNull Object o) throws Exception { - subscriptionService.subscriptionTable().delete(subscription); - return o; - } + return (@NonNull Object o) -> { + subscriptionService.subscriptionTable().delete(subscription); + return o; }; } private void updateSubscription(final ChannelInfo info) { if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]"); - final Action onComplete = new Action() { - @Override - public void run() throws Exception { + final Action onComplete = () -> { if (DEBUG) Log.d(TAG, "Updated subscription: " + info.getUrl()); - } }; - final Consumer onError = new Consumer() { - @Override - public void accept(@NonNull Throwable throwable) throws Exception { - onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(info.getServiceId()), "Updating Subscription for " + info.getUrl(), R.string.subscription_update_failed); - } - }; + final Consumer onError = (@NonNull Throwable throwable) -> + onUnrecoverableError(throwable, + UserAction.SUBSCRIPTION, + NewPipe.getNameOfService(info.getServiceId()), + "Updating Subscription for " + info.getUrl(), + R.string.subscription_update_failed); disposables.add(subscriptionService.updateChannelInfo(info) .subscribeOn(Schedulers.io()) @@ -323,19 +315,16 @@ public class ChannelFragment extends BaseListInfoFragment { } private Disposable monitorSubscribeButton(final Button subscribeButton, final Function action) { - final Consumer onNext = new Consumer() { - @Override - public void accept(@NonNull Object o) throws Exception { + final Consumer onNext = (@NonNull Object o) -> { if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!"); - } }; - final Consumer onError = new Consumer() { - @Override - public void accept(@NonNull Throwable throwable) throws Exception { - onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.getServiceId()), "Subscription Change", R.string.subscription_change_failed); - } - }; + final Consumer onError = (@NonNull Throwable throwable) -> + onUnrecoverableError(throwable, + UserAction.SUBSCRIPTION, + NewPipe.getNameOfService(currentInfo.getServiceId()), + "Subscription Change", + R.string.subscription_change_failed); /* Emit clicks from main thread unto io thread */ return RxView.clicks(subscribeButton) @@ -347,25 +336,25 @@ public class ChannelFragment extends BaseListInfoFragment { } private Consumer> getSubscribeUpdateMonitor(final ChannelInfo info) { - return new Consumer>() { - @Override - public void accept(List subscriptionEntities) throws Exception { - if (DEBUG) - Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]"); - if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); + return (List subscriptionEntities) -> { + if (DEBUG) + Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]"); + if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); - if (subscriptionEntities.isEmpty()) { - if (DEBUG) Log.d(TAG, "No subscription to this channel!"); - SubscriptionEntity channel = new SubscriptionEntity(); - channel.setServiceId(info.getServiceId()); - channel.setUrl(info.getUrl()); - channel.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount()); - subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel)); - } else { - if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); - final SubscriptionEntity subscription = subscriptionEntities.get(0); - subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); - } + if (subscriptionEntities.isEmpty()) { + if (DEBUG) Log.d(TAG, "No subscription to this channel!"); + SubscriptionEntity channel = new SubscriptionEntity(); + channel.setServiceId(info.getServiceId()); + channel.setUrl(info.getUrl()); + channel.setData(info.getName(), + info.getAvatarUrl(), + info.getDescription(), + info.getSubscriberCount()); + subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel)); + } else { + if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); + final SubscriptionEntity subscription = subscriptionEntities.get(0); + subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); } }; } @@ -432,10 +421,12 @@ public class ChannelFragment extends BaseListInfoFragment { imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); - if (result.getSubscriberCount() != -1) { + headerSubscribersTextView.setVisibility(View.VISIBLE); + if (result.getSubscriberCount() >= 0) { headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount())); - headerSubscribersTextView.setVisibility(View.VISIBLE); - } else headerSubscribersTextView.setVisibility(View.GONE); + } else { + headerSubscribersTextView.setText(R.string.subscribers_count_not_available); + } if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); @@ -483,8 +474,11 @@ public class ChannelFragment extends BaseListInfoFragment { super.handleNextItems(result); if (!result.getErrors().isEmpty()) { - showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), - "Get next page of: " + url, R.string.general_error); + showSnackBarError(result.getErrors(), + UserAction.REQUESTED_CHANNEL, + NewPipe.getNameOfService(serviceId), + "Get next page of: " + url, + R.string.general_error); } } @@ -497,7 +491,11 @@ public class ChannelFragment extends BaseListInfoFragment { if (super.onError(exception)) return true; int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, errorId); + onUnrecoverableError(exception, + UserAction.REQUESTED_CHANNEL, + NewPipe.getNameOfService(serviceId), + url, + errorId); return true; } @@ -508,6 +506,6 @@ public class ChannelFragment extends BaseListInfoFragment { @Override public void setTitle(String title) { super.setTitle(title); - headerTitleView.setText(title); + if (!useAsFrontPage) headerTitleView.setText(title); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java index 5dfdcd655..833d0f55e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java @@ -11,22 +11,20 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.UrlIdHandler; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.kiosk.KioskInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.KioskTranslator; -import org.schabi.newpipe.util.NavigationHelper; import icepick.State; import io.reactivex.Single; @@ -59,6 +57,7 @@ public class KioskFragment extends BaseListInfoFragment { protected String kioskId = ""; protected String kioskTranslatedName; + /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ @@ -74,10 +73,10 @@ public class KioskFragment extends BaseListInfoFragment { throws ExtractionException { KioskFragment instance = new KioskFragment(); StreamingService service = NewPipe.getService(serviceId); - UrlIdHandler kioskTypeUrlIdHandler = service.getKioskList() - .getUrlIdHandlerByType(kioskId); + ListLinkHandlerFactory kioskLinkHandlerFactory = service.getKioskList() + .getListLinkHandlerFactoryByType(kioskId); instance.setInitialData(serviceId, - kioskTypeUrlIdHandler.getUrl(kioskId), kioskId); + kioskLinkHandlerFactory.fromId(kioskId).getUrl(), kioskId); instance.kioskId = kioskId; return instance; } @@ -136,7 +135,10 @@ public class KioskFragment extends BaseListInfoFragment { .getDefaultSharedPreferences(activity) .getString(getString(R.string.content_country_key), getString(R.string.default_country_value)); - return ExtractorHelper.getKioskInfo(serviceId, url, contentCountry, forceReload); + return ExtractorHelper.getKioskInfo(serviceId, + url, + contentCountry, + forceReload); } @Override @@ -145,7 +147,10 @@ public class KioskFragment extends BaseListInfoFragment { .getDefaultSharedPreferences(activity) .getString(getString(R.string.content_country_key), getString(R.string.default_country_value)); - return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPageUrl, contentCountry); + return ExtractorHelper.getMoreKioskItems(serviceId, + url, + currentNextPageUrl, + contentCountry); } /*////////////////////////////////////////////////////////////////////////// @@ -163,7 +168,9 @@ public class KioskFragment extends BaseListInfoFragment { super.handleResult(result); name = kioskTranslatedName; - setTitle(kioskTranslatedName); + if(!useAsFrontPage) { + setTitle(kioskTranslatedName); + } if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 0498c95c5..b7a42791c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -6,6 +6,7 @@ import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; @@ -19,6 +20,7 @@ import android.widget.TextView; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipe.App; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; @@ -28,12 +30,14 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; -import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; @@ -44,6 +48,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import io.reactivex.Flowable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -93,7 +98,8 @@ public class PlaylistFragment extends BaseListInfoFragment { super.onCreate(savedInstanceState); disposables = new CompositeDisposable(); isBookmarkButtonReady = new AtomicBoolean(false); - remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext())); + remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance( + requireContext())); } @Override @@ -142,6 +148,7 @@ public class PlaylistFragment extends BaseListInfoFragment { context.getResources().getString(R.string.start_here_on_main), context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_popup), + context.getResources().getString(R.string.share) }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { @@ -162,6 +169,9 @@ public class PlaylistFragment extends BaseListInfoFragment { case 4: NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); break; + case 5: + shareUrl(item.getName(), item.getUrl()); + break; default: break; } @@ -261,11 +271,16 @@ public class PlaylistFragment extends BaseListInfoFragment { if (!TextUtils.isEmpty(result.getUploaderName())) { headerUploaderName.setText(result.getUploaderName()); if (!TextUtils.isEmpty(result.getUploaderUrl())) { - headerUploaderLayout.setOnClickListener(v -> + headerUploaderLayout.setOnClickListener(v -> { + try { NavigationHelper.openChannelFragment(getFragmentManager(), - result.getServiceId(), result.getUploaderUrl(), - result.getUploaderName()) - ); + result.getServiceId(), + result.getUploaderUrl(), + result.getUploaderName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } + }); } } @@ -281,14 +296,11 @@ public class PlaylistFragment extends BaseListInfoFragment { } remotePlaylistManager.getPlaylist(result) + .flatMap(lists -> getUpdateProcessor(lists, result), (lists, id) -> lists) .onBackpressureLatest() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getPlaylistBookmarkSubscriber()); - remotePlaylistManager.onUpdate(result) - .subscribeOn(AndroidSchedulers.mainThread()) - .subscribe(integer -> {/* Do nothing*/}, this::onError); - headerPlayAllButton.setOnClickListener(view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); headerPopupButton.setOnClickListener(view -> @@ -336,7 +348,11 @@ public class PlaylistFragment extends BaseListInfoFragment { if (super.onError(exception)) return true; int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), url, errorId); + onUnrecoverableError(exception, + UserAction.REQUESTED_PLAYLIST, + NewPipe.getNameOfService(serviceId), + url, + errorId); return true; } @@ -344,6 +360,17 @@ public class PlaylistFragment extends BaseListInfoFragment { // Utils //////////////////////////////////////////////////////////////////////////*/ + private Flowable getUpdateProcessor(@NonNull List playlists, + @NonNull PlaylistInfo result) { + final Flowable 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> getPlaylistBookmarkSubscriber() { return new Subscriber>() { @Override @@ -416,4 +443,4 @@ public class PlaylistFragment extends BaseListInfoFragment { playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); playlistBookmarkButton.setTitle(titleRes); } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index d07ff6448..a699e28bc 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -37,26 +37,30 @@ import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.search.SearchEngine; -import org.schabi.newpipe.extractor.search.SearchResult; +import org.schabi.newpipe.extractor.search.SearchExtractor; +import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.LayoutManagerSmoothScroller; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ServiceHelper; import java.io.IOException; import java.io.InterruptedIOException; import java.net.SocketException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Queue; -import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import icepick.State; @@ -65,14 +69,15 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; +import static java.util.Arrays.asList; + import static org.schabi.newpipe.util.AnimationUtils.animateView; public class SearchFragment - extends BaseListFragment + extends BaseListFragment implements BackPressable { /*////////////////////////////////////////////////////////////////////////// @@ -92,19 +97,29 @@ public class SearchFragment @State protected int filterItemCheckedId = -1; - private SearchEngine.Filter filter = SearchEngine.Filter.ANY; @State protected int serviceId = Constants.NO_SERVICE_ID; + + // this three represet the current search query @State - protected String searchQuery; + protected String searchString; @State - protected String lastSearchedQuery; + protected String[] contentFilter; + @State + protected String sortFilter; + + // these represtent the last search + @State + protected String lastSearchedString; + @State protected boolean wasSearchFocused = false; - private int currentPage = 0; - private int currentNextPage = 0; + private Map menuItemToFilterName; + private StreamingService service; + private String currentPageUrl; + private String nextPageUrl; private String contentCountry; private boolean isSuggestionsEnabled = true; private boolean isSearchHistoryEnabled = true; @@ -130,11 +145,11 @@ public class SearchFragment /*////////////////////////////////////////////////////////////////////////*/ - public static SearchFragment getInstance(int serviceId, String query) { + public static SearchFragment getInstance(int serviceId, String searchString) { SearchFragment searchFragment = new SearchFragment(); - searchFragment.setQuery(serviceId, query); + searchFragment.setQuery(serviceId, searchString, new String[0], ""); - if (!TextUtils.isEmpty(query)) { + if (!TextUtils.isEmpty(searchString)) { searchFragment.setSearchOnResume(); } @@ -202,13 +217,22 @@ public class SearchFragment if (DEBUG) Log.d(TAG, "onResume() called"); super.onResume(); - if (!TextUtils.isEmpty(searchQuery)) { + try { + service = NewPipe.getService(serviceId); + } catch (Exception e) { + ErrorActivity.reportError(getActivity(), e, getActivity().getClass(), + getActivity().findViewById(android.R.id.content), + ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, + "", + "", R.string.general_error)); + } + + if (!TextUtils.isEmpty(searchString)) { if (wasLoading.getAndSet(false)) { - if (currentNextPage > currentPage) loadMoreItems(); - else search(searchQuery); + search(searchString, contentFilter, sortFilter); } else if (infoListAdapter.getItemsList().size() == 0) { if (savedState == null) { - search(searchQuery); + search(searchString, contentFilter, sortFilter); } else if (!isLoading.get() && !wasSearchFocused) { infoListAdapter.clearStreamItemList(); showEmptyState(); @@ -218,7 +242,7 @@ public class SearchFragment if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver(); - if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) { + if (TextUtils.isEmpty(searchString) || wasSearchFocused) { showKeyboardSearch(); showSuggestionsPanel(); } else { @@ -247,8 +271,9 @@ public class SearchFragment public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case ReCaptchaActivity.RECAPTCHA_REQUEST: - if (resultCode == Activity.RESULT_OK && !TextUtils.isEmpty(searchQuery)) { - search(searchQuery); + if (resultCode == Activity.RESULT_OK + && !TextUtils.isEmpty(searchString)) { + search(searchString, contentFilter, sortFilter); } else Log.e(TAG, "ReCaptcha failed"); break; @@ -282,20 +307,22 @@ public class SearchFragment @Override public void writeTo(Queue objectsToSave) { super.writeTo(objectsToSave); - objectsToSave.add(currentPage); - objectsToSave.add(currentNextPage); + objectsToSave.add(currentPageUrl); + objectsToSave.add(nextPageUrl); } @Override public void readFrom(@NonNull Queue savedObjects) throws Exception { super.readFrom(savedObjects); - currentPage = (int) savedObjects.poll(); - currentNextPage = (int) savedObjects.poll(); + currentPageUrl = (String) savedObjects.poll(); + nextPageUrl = (String) savedObjects.poll(); } @Override public void onSaveInstanceState(Bundle bundle) { - searchQuery = searchEditText != null ? searchEditText.getText().toString() : searchQuery; + searchString = searchEditText != null + ? searchEditText.getText().toString() + : searchString; super.onSaveInstanceState(bundle); } @@ -305,8 +332,11 @@ public class SearchFragment @Override public void reloadContent() { - if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { - search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString()); + if (!TextUtils.isEmpty(searchString) + || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { + search(!TextUtils.isEmpty(searchString) + ? searchString + : searchEditText.getText().toString(), new String[0], ""); } else { if (searchEditText != null) { searchEditText.setText(""); @@ -330,22 +360,35 @@ public class SearchFragment supportActionBar.setDisplayHomeAsUpEnabled(true); } - inflater.inflate(R.menu.menu_search, menu); + menuItemToFilterName = new HashMap<>(); + + int itemId = 0; + boolean isFirstItem = true; + final Context c = getContext(); + for(String filter : service.getSearchQHFactory().getAvailableContentFilter()) { + menuItemToFilterName.put(itemId, filter); + MenuItem item = menu.add(1, + itemId++, + 0, + ServiceHelper.getTranslatedFilterString(filter, c)); + if(isFirstItem) { + item.setChecked(true); + isFirstItem = false; + } + } + menu.setGroupCheckable(1, true, true); + restoreFilterChecked(menu, filterItemCheckedId); } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_filter_all: - case R.id.menu_filter_video: - case R.id.menu_filter_channel: - case R.id.menu_filter_playlist: - changeFilter(item, getFilterFromMenuId(item.getItemId())); - return true; - default: - return super.onOptionsItemSelected(item); - } + + List contentFilter = new ArrayList<>(1); + contentFilter.add(menuItemToFilterName.get(item.getItemId())); + changeContentFilter(item, contentFilter); + + return true; } private void restoreFilterChecked(Menu menu, int itemId) { @@ -354,21 +397,6 @@ public class SearchFragment if (item == null) return; item.setChecked(true); - filter = getFilterFromMenuId(itemId); - } - } - - private SearchEngine.Filter getFilterFromMenuId(int itemId) { - switch (itemId) { - case R.id.menu_filter_video: - return SearchEngine.Filter.STREAM; - case R.id.menu_filter_channel: - return SearchEngine.Filter.CHANNEL; - case R.id.menu_filter_playlist: - return SearchEngine.Filter.PLAYLIST; - case R.id.menu_filter_all: - default: - return SearchEngine.Filter.ANY; } } @@ -379,14 +407,21 @@ public class SearchFragment private TextWatcher textWatcher; private void showSearchOnStart() { - if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " + searchQuery+", lastSearchedQuery → " + lastSearchedQuery); - searchEditText.setText(searchQuery); + if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " + + searchString + + ", lastSearchedQuery → " + + lastSearchedString); + searchEditText.setText(searchString); - if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) { + if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) { searchToolbarContainer.setTranslationX(100); searchToolbarContainer.setAlpha(0f); searchToolbarContainer.setVisibility(View.VISIBLE); - searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(200).setInterpolator(new DecelerateInterpolator()).start(); + searchToolbarContainer.animate() + .translationX(0) + .alpha(1f) + .setDuration(200) + .setInterpolator(new DecelerateInterpolator()).start(); } else { searchToolbarContainer.setTranslationX(0); searchToolbarContainer.setAlpha(1f); @@ -396,47 +431,38 @@ public class SearchFragment private void initSearchListeners() { if (DEBUG) Log.d(TAG, "initSearchListeners() called"); - searchClear.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); - if (TextUtils.isEmpty(searchEditText.getText())) { - NavigationHelper.gotoMainFragment(getFragmentManager()); - return; - } - - searchEditText.setText(""); - suggestionListAdapter.setItems(new ArrayList()); - showKeyboardSearch(); + searchClear.setOnClickListener(v -> { + if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + if (TextUtils.isEmpty(searchEditText.getText())) { + NavigationHelper.gotoMainFragment(getFragmentManager()); + return; } + + searchEditText.setText(""); + suggestionListAdapter.setItems(new ArrayList<>()); + showKeyboardSearch(); }); TooltipCompat.setTooltipText(searchClear, getString(R.string.clear)); - searchEditText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); - if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { - showSuggestionsPanel(); - } + searchEditText.setOnClickListener(v -> { + if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { + showSuggestionsPanel(); } }); - searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]"); - if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) { - showSuggestionsPanel(); - } + searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { + if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]"); + if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) { + showSuggestionsPanel(); } }); suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() { @Override public void onSuggestionItemSelected(SuggestionItem item) { - search(item.query); + search(item.query, new String[0], ""); searchEditText.setText(item.query); } @@ -469,21 +495,22 @@ public class SearchFragment } }; searchEditText.addTextChangedListener(textWatcher); - searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (DEBUG) { - Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); - } - if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { - search(searchEditText.getText().toString()); - return true; - } - return false; - } - }); + searchEditText.setOnEditorActionListener( + (TextView v, int actionId, KeyEvent event) -> { + if (DEBUG) { + Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); + } + if (event != null + && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER + || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { + search(searchEditText.getText().toString(), new String[0], ""); + return true; + } + return false; + }); - if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver(); + if (suggestionDisposable == null || suggestionDisposable.isDisposed()) + initSuggestionObserver(); } private void unsetSearchListeners() { @@ -513,7 +540,8 @@ public class SearchFragment if (searchEditText == null) return; if (searchEditText.requestFocus()) { - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = (InputMethodManager) activity.getSystemService( + Context.INPUT_METHOD_SERVICE); imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT); } } @@ -522,8 +550,10 @@ public class SearchFragment if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called"); if (searchEditText == null) return; - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + InputMethodManager imm = (InputMethodManager) activity.getSystemService( + Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), + InputMethodManager.HIDE_NOT_ALWAYS); searchEditText.clearFocus(); } @@ -545,8 +575,7 @@ public class SearchFragment .onNext(searchEditText.getText().toString()), throwable -> showSnackBarError(throwable, UserAction.DELETE_FROM_HISTORY, "none", - "Deleting item failed", R.string.general_error) - ); + "Deleting item failed", R.string.general_error)); disposables.add(onDelete); }) .show(); @@ -554,10 +583,12 @@ public class SearchFragment @Override public boolean onBackPressed() { - if (suggestionsPanel.getVisibility() == View.VISIBLE && infoListAdapter.getItemsList().size() > 0 && !isLoading.get()) { + if (suggestionsPanel.getVisibility() == View.VISIBLE + && infoListAdapter.getItemsList().size() > 0 + && !isLoading.get()) { hideSuggestionsPanel(); hideKeyboardSearch(); - searchEditText.setText(lastSearchedQuery); + searchEditText.setText(lastSearchedString); return true; } return false; @@ -573,8 +604,10 @@ public class SearchFragment final Observable observable = suggestionPublisher .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) - .startWith(searchQuery != null ? searchQuery : "") - .filter(query -> isSuggestionsEnabled); + .startWith(searchString != null + ? searchString + : "") + .filter(searchString -> isSuggestionsEnabled); suggestionDisposable = observable .switchMap(query -> { @@ -645,56 +678,44 @@ public class SearchFragment // no-op } - private void search(final String query) { - if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]"); - if (query.isEmpty()) return; + private void search(final String searchString, String[] contentFilter, String sortFilter) { + if (DEBUG) Log.d(TAG, "search() called with: query = [" + searchString + "]"); + if (searchString.isEmpty()) return; try { - final StreamingService service = NewPipe.getServiceByUrl(query); + final StreamingService service = NewPipe.getServiceByUrl(searchString); if (service != null) { showLoading(); disposables.add(Observable - .fromCallable(new Callable() { - @Override - public Intent call() throws Exception { - return NavigationHelper.getIntentByLink(activity, service, query); - } - }) + .fromCallable(() -> + NavigationHelper.getIntentByLink(activity, service, searchString)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Intent intent) throws Exception { - getFragmentManager().popBackStackImmediate(); - activity.startActivity(intent); - } - }, new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - showError(getString(R.string.url_not_supported_toast), false); - } - })); + .subscribe(intent -> { + getFragmentManager().popBackStackImmediate(); + activity.startActivity(intent); + }, throwable -> + showError(getString(R.string.url_not_supported_toast), false))); return; } } catch (Exception e) { // Exception occurred, it's not a url } - lastSearchedQuery = query; - searchQuery = query; - currentPage = 0; + lastSearchedString = this.searchString; + this.searchString = searchString; infoListAdapter.clearStreamItemList(); hideSuggestionsPanel(); hideKeyboardSearch(); - historyRecordManager.onSearched(serviceId, query) + historyRecordManager.onSearched(serviceId, searchString) .observeOn(AndroidSchedulers.mainThread()) .subscribe( ignored -> {}, error -> showSnackBarError(error, UserAction.SEARCHED, - NewPipe.getNameOfService(serviceId), query, 0) + NewPipe.getNameOfService(serviceId), searchString, 0) ); - suggestionPublisher.onNext(query); + suggestionPublisher.onNext(searchString); startLoading(false); } @@ -703,11 +724,16 @@ public class SearchFragment super.startLoading(forceLoad); if (disposables != null) disposables.clear(); if (searchDisposable != null) searchDisposable.dispose(); - searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter) + searchDisposable = ExtractorHelper.searchFor(serviceId, + searchString, + Arrays.asList(contentFilter), + sortFilter, + contentCountry) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnEvent((searchResult, throwable) -> isLoading.set(false)) .subscribe(this::handleResult, this::onError); + } @Override @@ -715,8 +741,13 @@ public class SearchFragment isLoading.set(true); showListFooter(true); if (searchDisposable != null) searchDisposable.dispose(); - currentNextPage = currentPage + 1; - searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter) + searchDisposable = ExtractorHelper.getMoreSearchItems( + serviceId, + searchString, + asList(contentFilter), + sortFilter, + nextPageUrl, + contentCountry) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) @@ -739,19 +770,22 @@ public class SearchFragment // Utils //////////////////////////////////////////////////////////////////////////*/ - private void changeFilter(MenuItem item, SearchEngine.Filter filter) { - this.filter = filter; + private void changeContentFilter(MenuItem item, List contentFilter) { this.filterItemCheckedId = item.getItemId(); item.setChecked(true); - if (!TextUtils.isEmpty(searchQuery)) { - search(searchQuery); + this.contentFilter = new String[] {contentFilter.get(0)}; + + if (!TextUtils.isEmpty(searchString)) { + search(searchString, this.contentFilter, sortFilter); } } - private void setQuery(int serviceId, String searchQuery) { + private void setQuery(int serviceId, String searchString, String[] contentfilter, String sortFilter) { this.serviceId = serviceId; - this.searchQuery = searchQuery; + this.searchString = searchString; + this.contentFilter = contentfilter; + this.sortFilter = sortFilter; } /*////////////////////////////////////////////////////////////////////////// @@ -772,8 +806,11 @@ public class SearchFragment if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]"); if (super.onError(exception)) return; - int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, NewPipe.getNameOfService(serviceId), searchQuery, errorId); + int errorId = exception instanceof ParsingException + ? R.string.parsing_error + : R.string.general_error; + onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, + NewPipe.getNameOfService(serviceId), searchString, errorId); } /*////////////////////////////////////////////////////////////////////////// @@ -798,16 +835,22 @@ public class SearchFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void handleResult(@NonNull SearchResult result) { - if (!result.errors.isEmpty()) { - showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0); + public void handleResult(@NonNull SearchInfo result) { + final List exceptions = result.getErrors(); + if (!exceptions.isEmpty() + && !(exceptions.size() == 1 + && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)){ + showSnackBarError(result.getErrors(), UserAction.SEARCHED, + NewPipe.getNameOfService(serviceId), searchString, 0); } - lastSearchedQuery = searchQuery; + lastSearchedString = searchString; + nextPageUrl = result.getNextPageUrl(); + currentPageUrl = result.getUrl(); if (infoListAdapter.getItemsList().size() == 0) { - if (!result.getResults().isEmpty()) { - infoListAdapter.addInfoItemList(result.getResults()); + if (!result.getRelatedItems().isEmpty()) { + infoListAdapter.addInfoItemList(result.getRelatedItems()); } else { infoListAdapter.clearStreamItemList(); showEmptyState(); @@ -821,12 +864,14 @@ public class SearchFragment @Override public void handleNextItems(ListExtractor.InfoItemsPage result) { showListFooter(false); - currentPage = Integer.parseInt(result.getNextPageUrl()); + currentPageUrl = result.getNextPageUrl(); infoListAdapter.addInfoItemList(result.getItems()); + nextPageUrl = result.getNextPageUrl(); if (!result.getErrors().isEmpty()) { - showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId) - , "\"" + searchQuery + "\" → page " + currentPage, 0); + showSnackBarError(result.getErrors(), UserAction.SEARCHED, + NewPipe.getNameOfService(serviceId) + , "\"" + searchString + "\" → page: " + nextPageUrl, 0); } super.handleNextItems(result); } @@ -835,12 +880,15 @@ public class SearchFragment protected boolean onError(Throwable exception) { if (super.onError(exception)) return true; - if (exception instanceof SearchEngine.NothingFoundException) { + if (exception instanceof SearchExtractor.NothingFoundException) { infoListAdapter.clearStreamItemList(); showEmptyState(); } else { - int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, errorId); + int errorId = exception instanceof ParsingException + ? R.string.parsing_error + : R.string.general_error; + onUnrecoverableError(exception, UserAction.SEARCHED, + NewPipe.getNameOfService(serviceId), searchString, errorId); } return true; diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index f3f390c4d..99bd70f5b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -66,11 +66,10 @@ public final class BookmarkFragment public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - if (activity != null && activity.getSupportActionBar() != null) { - activity.getSupportActionBar().setDisplayShowTitleEnabled(true); - activity.setTitle(R.string.tab_subscriptions); - } + if(!useAsFrontPage) { + setTitle(activity.getString(R.string.tab_bookmarks)); + } return inflater.inflate(R.layout.fragment_bookmarks, container, false); } @@ -99,9 +98,7 @@ public final class BookmarkFragment itemListAdapter.setSelectedListener(new OnClickGesture() { @Override public void selected(LocalItem selectedItem) { - // Requires the parent fragment to find holder for fragment replacement - if (getParentFragment() == null) return; - final FragmentManager fragmentManager = getParentFragment().getFragmentManager(); + final FragmentManager fragmentManager = getFM(); if (selectedItem instanceof PlaylistMetadataEntry) { final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); @@ -110,8 +107,11 @@ public final class BookmarkFragment } else if (selectedItem instanceof PlaylistRemoteEntity) { final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); - NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(), - entry.getUrl(), entry.getName()); + NavigationHelper.openPlaylistFragment( + fragmentManager, + entry.getServiceId(), + entry.getUrl(), + entry.getName()); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java index 4937bb094..634bea62b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java @@ -71,6 +71,10 @@ public class FeedFragment extends BaseListFragment, Voi @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + + if(!useAsFrontPage) { + setTitle(activity.getString(R.string.fragment_whats_new)); + } return inflater.inflate(R.layout.fragment_feed, container, false); } @@ -105,20 +109,19 @@ public class FeedFragment extends BaseListFragment, Voi super.onDestroyView(); } - /*@Override - protected RecyclerView.LayoutManager getListLayoutManager() { - boolean isPortrait = getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels; - return new GridLayoutManager(activity, isPortrait ? 1 : 2); - }*/ + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (activity != null && isVisibleToUser) { + setTitle(activity.getString(R.string.fragment_whats_new)); + } + } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setTitle(R.string.fragment_whats_new); - } if(useAsFrontPage) { supportActionBar.setDisplayShowTitleEnabled(true); @@ -176,19 +179,9 @@ public class FeedFragment extends BaseListFragment, Voi showLoading(); showListFooter(true); subscriptionObserver = subscriptionService.getSubscription() - .onErrorReturnItem(Collections.emptyList()) + .onErrorReturnItem(Collections.emptyList()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer>() { - @Override - public void accept(List subscriptionEntities) throws Exception { - handleResult(subscriptionEntities); - } - }, new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - onError(throwable); - } - }); + .subscribe(this::handleResult, this::onError); } @Override @@ -239,13 +232,12 @@ public class FeedFragment extends BaseListFragment, Voi if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) { subscriptionService.getChannelInfo(subscriptionEntity) .observeOn(AndroidSchedulers.mainThread()) - .onErrorComplete(new Predicate() { - @Override - public boolean test(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception { - return FeedFragment.super.onError(throwable); - } - }) - .subscribe(getChannelInfoObserver(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl())); + .onErrorComplete( + (@io.reactivex.annotations.NonNull Throwable throwable) -> + FeedFragment.super.onError(throwable)) + .subscribe( + getChannelInfoObserver(subscriptionEntity.getServiceId(), + subscriptionEntity.getUrl())); } else { requestFeed(1); } @@ -316,7 +308,10 @@ public class FeedFragment extends BaseListFragment, Voi @Override public void onError(Throwable exception) { - showSnackBarError(exception, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(serviceId), url, 0); + showSnackBarError(exception, + UserAction.SUBSCRIPTION, + NewPipe.getNameOfService(serviceId), + url, 0); requestFeed(1); onDone(); } @@ -361,12 +356,7 @@ public class FeedFragment extends BaseListFragment, Voi delayHandler.removeCallbacksAndMessages(null); // Add a little of a delay when requesting more items because the cache is so fast, // that the view seems stuck to the user when he scroll to the bottom - delayHandler.postDelayed(new Runnable() { - @Override - public void run() { - requestFeed(FEED_LOAD_COUNT); - } - }, 300); + delayHandler.postDelayed(() -> requestFeed(FEED_LOAD_COUNT), 300); } @Override @@ -423,7 +413,9 @@ public class FeedFragment extends BaseListFragment, Voi int heightPixels = getResources().getDisplayMetrics().heightPixels; int itemHeightPixels = activity.getResources().getDimensionPixelSize(R.dimen.video_item_search_height); - int items = itemHeightPixels > 0 ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT : MIN_ITEMS_INITIAL_LOAD; + int items = itemHeightPixels > 0 + ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT + : MIN_ITEMS_INITIAL_LOAD; return Math.max(MIN_ITEMS_INITIAL_LOAD, items); } @@ -441,8 +433,14 @@ public class FeedFragment extends BaseListFragment, Voi protected boolean onError(Throwable exception) { if (super.onError(exception)) return true; - int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Requesting feed", errorId); + int errorId = exception instanceof ExtractionException + ? R.string.parsing_error + : R.string.general_error; + onUnrecoverableError(exception, + UserAction.SOMETHING_ELSE, + "none", + "Requesting feed", + errorId); return true; } } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index eac1873a4..95aeb09d7 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -21,6 +21,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -73,7 +74,7 @@ public class StatisticsPlaylistFragment return results; case MOST_PLAYED: Collections.sort(results, (left, right) -> - ((Long) right.watchCount).compareTo(left.watchCount)); + Long.compare(right.watchCount, left.watchCount)); return results; default: return null; } @@ -96,6 +97,14 @@ public class StatisticsPlaylistFragment return inflater.inflate(R.layout.fragment_playlist, container, false); } + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (activity != null && isVisibleToUser) { + setTitle(activity.getString(R.string.title_activity_history)); + } + } + /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle - Views /////////////////////////////////////////////////////////////////////////// @@ -103,7 +112,9 @@ public class StatisticsPlaylistFragment @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - setTitle(getString(R.string.title_last_played)); + if(!useAsFrontPage) { + setTitle(getString(R.string.title_last_played)); + } } @Override @@ -129,8 +140,10 @@ public class StatisticsPlaylistFragment public void selected(LocalItem selectedItem) { if (selectedItem instanceof StreamStatisticsEntry) { final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem; - NavigationHelper.openVideoDetailFragment(getFragmentManager(), - item.serviceId, item.url, item.title); + NavigationHelper.openVideoDetailFragment(getFM(), + item.serviceId, + item.url, + item.title); } } @@ -298,6 +311,7 @@ public class StatisticsPlaylistFragment context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_popup), context.getResources().getString(R.string.delete), + context.getResources().getString(R.string.share) }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { @@ -321,6 +335,9 @@ public class StatisticsPlaylistFragment case 5: deleteEntry(index); break; + case 6: + shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl()); + break; default: break; } @@ -337,7 +354,7 @@ public class StatisticsPlaylistFragment final Disposable onDelete = recordManager.deleteStreamHistory(entry.streamId) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - howManyDelted -> { + howManyDeleted -> { if(getView() != null) { Snackbar.make(getView(), R.string.one_item_deleted, Snackbar.LENGTH_SHORT).show(); diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index e51fa50a4..35a1530c9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -520,7 +520,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { @@ -549,6 +550,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment onUpdate(final PlaylistInfo playlistInfo) { - return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo))) - .subscribeOn(Schedulers.io()); + public Single onUpdate(final long playlistId, final PlaylistInfo playlistInfo) { + return Single.fromCallable(() -> { + PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); + playlist.setUid(playlistId); + return playlistRemoteTable.update(playlist); + }).subscribeOn(Schedulers.io()); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java index 5f6ea42ee..d8a26f0eb 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java @@ -13,8 +13,10 @@ import android.os.Parcelable; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.FragmentManager; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -38,6 +40,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService; import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; @@ -207,7 +210,8 @@ public class SubscriptionFragment extends BaseStateFragment onImportPreviousSelected()); final int iconColor = ThemeHelper.isLightThemeSelected(getContext()) ? Color.BLACK : Color.WHITE; @@ -239,8 +243,8 @@ public class SubscriptionFragment extends BaseStateFragment() { @Override public void selected(ChannelInfoItem selectedItem) { - // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), - selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); + final FragmentManager fragmentManager = getFM(); + NavigationHelper.openChannelFragment(fragmentManager, + selectedItem.getServiceId(), + selectedItem.getUrl(), + selectedItem.getName()); } }); //noinspection ConstantConditions - whatsNewItemListHeader.setOnClickListener(v -> - NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager())); + whatsNewItemListHeader.setOnClickListener(v -> { + FragmentManager fragmentManager = getFM(); + NavigationHelper.openWhatsNewFragment(fragmentManager); + }); importExportListHeader.setOnClickListener(v -> importExportOptions.switchState()); } @@ -397,10 +405,13 @@ public class SubscriptionFragment extends BaseStateFragment getSubscriptionItems(List subscriptions) { List items = new ArrayList<>(); - for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem()); + for (final SubscriptionEntity subscription : subscriptions) { + items.add(subscription.toChannelInfoItem()); + } Collections.sort(items, - (InfoItem o1, InfoItem o2) -> o1.getName().compareToIgnoreCase(o2.getName())); + (InfoItem o1, InfoItem o2) -> + o1.getName().compareToIgnoreCase(o2.getName())); return items; } @@ -429,7 +440,11 @@ public class SubscriptionFragment extends BaseStateFragment= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha"; private boolean shouldUpdateOnProgress; @@ -192,7 +189,9 @@ public final class BackgroundPlayer extends Service { .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCustomContentView(notRemoteView) .setCustomBigContentView(bigNotRemoteView); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(NotificationCompat.PRIORITY_MAX); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + builder.setPriority(NotificationCompat.PRIORITY_MAX); + } return builder; } @@ -249,15 +248,6 @@ public final class BackgroundPlayer extends Service { notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); } - private void setControlsOpacity(@IntRange(from = 0, to = 255) int opacity) { - if (notRemoteView != null) notRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity); - if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity); - if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity); - if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity); - if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity); - if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity); - } - /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @@ -279,8 +269,16 @@ public final class BackgroundPlayer extends Service { protected class BasePlayerImpl extends BasePlayer { + @NonNull final private AudioPlaybackResolver resolver; + BasePlayerImpl(Context context) { super(context); + this.resolver = new AudioPlaybackResolver(context, dataSource); + } + + @Override + public void initPlayer(boolean playOnReady) { + super.initPlayer(playOnReady); } @Override @@ -293,30 +291,41 @@ public final class BackgroundPlayer extends Service { startForeground(NOTIFICATION_ID, notBuilder.build()); } - @Override - public void initThumbnail(final String url) { - resetNotification(); - if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); - if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); - updateNotification(-1); - super.initThumbnail(url); + /*////////////////////////////////////////////////////////////////////////// + // Thumbnail Loading + //////////////////////////////////////////////////////////////////////////*/ + + private void updateNotificationThumbnail() { + if (basePlayerImpl == null) return; + if (notRemoteView != null) { + notRemoteView.setImageViewBitmap(R.id.notificationCover, + basePlayerImpl.getThumbnail()); + } + if (bigNotRemoteView != null) { + bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, + basePlayerImpl.getThumbnail()); + } } @Override public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { super.onLoadingComplete(imageUri, view, loadedImage); - - if (loadedImage != null) { - // rebuild notification here since remote view does not release bitmaps, causing memory leaks - resetNotification(); - - if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); - if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); - - updateNotification(-1); - } + resetNotification(); + updateNotificationThumbnail(); + updateNotification(-1); } + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + super.onLoadingFailed(imageUri, view, failReason); + resetNotification(); + updateNotificationThumbnail(); + updateNotification(-1); + } + /*////////////////////////////////////////////////////////////////////////// + // States Implementation + //////////////////////////////////////////////////////////////////////////*/ + @Override public void onPrepared(boolean playWhenReady) { super.onPrepared(playWhenReady); @@ -335,6 +344,7 @@ public final class BackgroundPlayer extends Service { if (!shouldUpdateOnProgress) return; resetNotification(); + if(Build.VERSION.SDK_INT >= 26 /*Oreo*/) updateNotificationThumbnail(); if (bigNotRemoteView != null) { bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration)); @@ -390,29 +400,18 @@ public final class BackgroundPlayer extends Service { // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { - if (shouldUpdateOnProgress || hasPlayQueueItemChanged) { - resetNotification(); - updateNotification(-1); - updateMetadata(); - } + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); + resetNotification(); + updateNotificationThumbnail(); + updateNotification(-1); + updateMetadata(); } @Override @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - final MediaSource liveSource = super.sourceOf(item, info); - if (liveSource != null) return liveSource; - - final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); - if (index < 0 || index >= info.getAudioStreams().size()) return null; - - final AudioStream audio = info.getAudioStreams().get(index); - return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), - MediaFormat.getSuffixById(audio.getFormatId())); + return resolver.resolve(info); } @Override @@ -439,8 +438,8 @@ public final class BackgroundPlayer extends Service { } private void updateMetadata() { - if (activityListener != null && currentInfo != null) { - activityListener.onMetadataUpdate(currentInfo); + if (activityListener != null && getCurrentMetadata() != null) { + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); } } @@ -531,44 +530,36 @@ public final class BackgroundPlayer extends Service { updatePlayback(); } - @Override - public void onBlocked() { - super.onBlocked(); - - setControlsOpacity(77); - updateNotification(-1); - } - @Override public void onPlaying() { super.onPlaying(); - - setControlsOpacity(255); + resetNotification(); + updateNotificationThumbnail(); updateNotification(R.drawable.ic_pause_white); - lockManager.acquireWifiAndCpu(); } @Override public void onPaused() { super.onPaused(); - + resetNotification(); + updateNotificationThumbnail(); updateNotification(R.drawable.ic_play_arrow_white); - lockManager.releaseWifiAndCpu(); } @Override public void onCompleted() { super.onCompleted(); - - setControlsOpacity(255); - resetNotification(); - if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); - if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); + if (bigNotRemoteView != null) { + bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); + } + if (notRemoteView != null) { + notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); + } + updateNotificationThumbnail(); updateNotification(R.drawable.ic_replay_white); - lockManager.releaseWifiAndCpu(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 4c3d70421..01a0614fa 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -24,16 +24,14 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.media.AudioManager; -import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Toast; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; @@ -49,15 +47,14 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; +import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.LoadController; @@ -72,6 +69,8 @@ import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.SerializedCache; import java.io.IOException; @@ -82,12 +81,12 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.SerialDisposable; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; -import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; /** * Base for the players, joining the common properties @@ -98,7 +97,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; public abstract class BasePlayer implements Player.EventListener, PlaybackListener, ImageLoadingListener { - public static final boolean DEBUG = true; + public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); @NonNull public static final String TAG = "BasePlayer"; @NonNull final protected Context context; @@ -108,17 +107,26 @@ public abstract class BasePlayer implements @NonNull final protected HistoryRecordManager recordManager; + @NonNull final protected CustomTrackSelector trackSelector; + @NonNull final protected PlayerDataSource dataSource; + + @NonNull final private LoadControl loadControl; + @NonNull final private RenderersFactory renderFactory; + + @NonNull final private SerialDisposable progressUpdateReactor; + @NonNull final private CompositeDisposable databaseUpdateReactor; /*////////////////////////////////////////////////////////////////////////// // Intent //////////////////////////////////////////////////////////////////////////*/ - public static final String REPEAT_MODE = "repeat_mode"; - public static final String PLAYBACK_PITCH = "playback_pitch"; - public static final String PLAYBACK_SPEED = "playback_speed"; - public static final String PLAYBACK_QUALITY = "playback_quality"; - public static final String PLAY_QUEUE_KEY = "play_queue_key"; - public static final String APPEND_ONLY = "append_only"; - public static final String SELECT_ON_APPEND = "select_on_append"; + @NonNull public static final String REPEAT_MODE = "repeat_mode"; + @NonNull public static final String PLAYBACK_PITCH = "playback_pitch"; + @NonNull public static final String PLAYBACK_SPEED = "playback_speed"; + @NonNull public static final String PLAYBACK_SKIP_SILENCE = "playback_skip_silence"; + @NonNull public static final String PLAYBACK_QUALITY = "playback_quality"; + @NonNull public static final String PLAY_QUEUE_KEY = "play_queue_key"; + @NonNull public static final String APPEND_ONLY = "append_only"; + @NonNull public static final String SELECT_ON_APPEND = "select_on_append"; /*////////////////////////////////////////////////////////////////////////// // Playback @@ -129,12 +137,13 @@ public abstract class BasePlayer implements protected PlayQueue playQueue; protected PlayQueueAdapter playQueueAdapter; - protected MediaSourceManager playbackManager; + @Nullable protected MediaSourceManager playbackManager; - protected StreamInfo currentInfo; - protected PlayQueueItem currentItem; + @Nullable private PlayQueueItem currentItem; + @Nullable private MediaSourceTag currentMetadata; + @Nullable private Bitmap currentThumbnail; - protected Toast errorToast; + @Nullable protected Toast errorToast; /*////////////////////////////////////////////////////////////////////////// // Player @@ -145,18 +154,11 @@ public abstract class BasePlayer implements protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500; protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds - protected CustomTrackSelector trackSelector; - protected PlayerDataSource dataSource; - protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; protected MediaSessionManager mediaSessionManager; private boolean isPrepared = false; - private boolean isSynchronizing = false; - - protected Disposable progressUpdateReactor; - protected CompositeDisposable databaseUpdateReactor; //////////////////////////////////////////////////////////////////////////*/ @@ -171,32 +173,34 @@ public abstract class BasePlayer implements }; this.intentFilter = new IntentFilter(); setupBroadcastReceiver(intentFilter); - context.registerReceiver(broadcastReceiver, intentFilter); this.recordManager = new HistoryRecordManager(context); + + this.progressUpdateReactor = new SerialDisposable(); + this.databaseUpdateReactor = new CompositeDisposable(); + + final String userAgent = Downloader.USER_AGENT; + final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); + + final TrackSelection.Factory trackSelectionFactory = + PlayerHelper.getQualitySelector(context, bandwidthMeter); + this.trackSelector = new CustomTrackSelector(trackSelectionFactory); + + this.loadControl = new LoadController(context); + this.renderFactory = new DefaultRenderersFactory(context); } public void setup() { - if (simpleExoPlayer == null) initPlayer(/*playOnInit=*/true); + if (simpleExoPlayer == null) { + initPlayer(/*playOnInit=*/true); + } initListeners(); } public void initPlayer(final boolean playOnReady) { if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); - if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); - databaseUpdateReactor = new CompositeDisposable(); - - final String userAgent = Downloader.USER_AGENT; - final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); - - final TrackSelection.Factory trackSelectionFactory = - PlayerHelper.getQualitySelector(context, bandwidthMeter); - trackSelector = new CustomTrackSelector(trackSelectionFactory); - - final LoadControl loadControl = new LoadController(context); - final RenderersFactory renderFactory = new DefaultRenderersFactory(context); simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); simpleExoPlayer.addListener(this); simpleExoPlayer.setPlayWhenReady(playOnReady); @@ -205,6 +209,8 @@ public abstract class BasePlayer implements audioReactor = new AudioReactor(context, simpleExoPlayer); mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, new BasePlayerMediaSession(this)); + + registerBroadcastReceiver(); } public void initListeners() {} @@ -235,20 +241,24 @@ public abstract class BasePlayer implements final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()); final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); + final boolean playbackSkipSilence = intent.getBooleanExtra(PLAYBACK_SKIP_SILENCE, + getPlaybackSkipSilence()); // Good to go... - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, /*playOnInit=*/true); + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, + /*playOnInit=*/true); } protected void initPlayback(@NonNull final PlayQueue queue, @Player.RepeatMode final int repeatMode, final float playbackSpeed, final float playbackPitch, + final boolean playbackSkipSilence, final boolean playOnReady) { destroyPlayer(); initPlayer(playOnReady); setRepeatMode(repeatMode); - setPlaybackParameters(playbackSpeed, playbackPitch); + setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); playQueue = queue; playQueue.init(); @@ -270,7 +280,6 @@ public abstract class BasePlayer implements if (playQueue != null) playQueue.dispose(); if (audioReactor != null) audioReactor.dispose(); if (playbackManager != null) playbackManager.dispose(); - if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); if (mediaSessionManager != null) mediaSessionManager.dispose(); if (playQueueAdapter != null) { @@ -284,20 +293,22 @@ public abstract class BasePlayer implements destroyPlayer(); unregisterBroadcastReceiver(); - trackSelector = null; + databaseUpdateReactor.clear(); + progressUpdateReactor.set(null); + simpleExoPlayer = null; - mediaSessionManager = null; } /*////////////////////////////////////////////////////////////////////////// // Thumbnail Loading //////////////////////////////////////////////////////////////////////////*/ - public void initThumbnail(final String url) { + private void initThumbnail(final String url) { if (DEBUG) Log.d(TAG, "Thumbnail - initThumbnail() called"); if (url == null || url.isEmpty()) return; ImageLoader.getInstance().resume(); - ImageLoader.getInstance().loadImage(url, this); + ImageLoader.getInstance().loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, + this); } @Override @@ -310,6 +321,7 @@ public abstract class BasePlayer implements public void onLoadingFailed(String imageUri, View view, FailReason failReason) { Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", failReason.getCause()); + currentThumbnail = null; } @Override @@ -317,64 +329,14 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + "imageUri = [" + imageUri + "], view = [" + view + "], " + "loadedImage = [" + loadedImage + "]"); + currentThumbnail = loadedImage; } @Override public void onLoadingCancelled(String imageUri, View view) { if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + "imageUri = [" + imageUri + "], view = [" + view + "]"); - } - /*////////////////////////////////////////////////////////////////////////// - // MediaSource Building - //////////////////////////////////////////////////////////////////////////*/ - - public MediaSource buildLiveMediaSource(@NonNull final String sourceUrl, - @C.ContentType final int type) { - if (DEBUG) { - Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl + - "], content type = [" + type + "]"); - } - if (dataSource == null) return null; - - final Uri uri = Uri.parse(sourceUrl); - switch (type) { - case C.TYPE_SS: - return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); - case C.TYPE_DASH: - return dataSource.getLiveDashMediaSourceFactory().createMediaSource(uri); - case C.TYPE_HLS: - return dataSource.getLiveHlsMediaSourceFactory().createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - - public MediaSource buildMediaSource(@NonNull final String sourceUrl, - @NonNull final String cacheKey, - @NonNull final String overrideExtension) { - if (DEBUG) { - Log.d(TAG, "buildMediaSource() called with: url = [" + sourceUrl + - "], cacheKey = [" + cacheKey + "]" + - "], overrideExtension = [" + overrideExtension + "]"); - } - if (dataSource == null) return null; - - final Uri uri = Uri.parse(sourceUrl); - @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? - Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); - - switch (type) { - case C.TYPE_SS: - return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); - case C.TYPE_DASH: - return dataSource.getDashMediaSourceFactory().createMediaSource(uri); - case C.TYPE_HLS: - return dataSource.getHlsMediaSourceFactory().createMediaSource(uri); - case C.TYPE_OTHER: - return dataSource.getExtractorMediaSourceFactory(cacheKey).createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } + currentThumbnail = null; } /*////////////////////////////////////////////////////////////////////////// @@ -399,11 +361,17 @@ public abstract class BasePlayer implements } } - public void unregisterBroadcastReceiver() { + protected void registerBroadcastReceiver() { + // Try to unregister current first + unregisterBroadcastReceiver(); + context.registerReceiver(broadcastReceiver, intentFilter); + } + + protected void unregisterBroadcastReceiver() { try { context.unregisterReceiver(broadcastReceiver); } catch (final IllegalArgumentException unregisteredException) { - Log.e(TAG, "Broadcast receiver already unregistered.", unregisteredException); + Log.w(TAG, "Broadcast receiver already unregistered (" + unregisteredException.getMessage() + ")"); } } @@ -510,13 +478,11 @@ public abstract class BasePlayer implements public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); protected void startProgressLoop() { - if (progressUpdateReactor != null) progressUpdateReactor.dispose(); - progressUpdateReactor = getProgressReactor(); + progressUpdateReactor.set(getProgressReactor()); } protected void stopProgressLoop() { - if (progressUpdateReactor != null) progressUpdateReactor.dispose(); - progressUpdateReactor = null; + progressUpdateReactor.set(null); } public void triggerProgressUpdate() { @@ -531,7 +497,8 @@ public abstract class BasePlayer implements private Disposable getProgressReactor() { return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> triggerProgressUpdate()); + .subscribe(ignored -> triggerProgressUpdate(), + error -> Log.e(TAG, "Progress update failure: ", error)); } /*////////////////////////////////////////////////////////////////////////// @@ -545,28 +512,16 @@ public abstract class BasePlayer implements (manifest == null ? "no manifest" : "available manifest") + ", " + "timeline size = [" + timeline.getWindowCount() + "], " + "reason = [" + reason + "]"); - if (playQueue == null) return; - switch (reason) { - case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block - case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock - case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes - // Ensures MediaSourceManager#update is complete - final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size(); - // Ensure dynamic/livestream timeline changes does not cause negative position - if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) { - if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " + - "clamping to default position."); - seekToDefault(); - } - break; - } + maybeUpdateCurrentMetadata(); } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { if (DEBUG) Log.d(TAG, "ExoPlayer - onTracksChanged(), " + "track group size = " + trackGroups.length); + + maybeUpdateCurrentMetadata(); } @Override @@ -586,6 +541,8 @@ public abstract class BasePlayer implements } else if (isLoading && !isProgressLoopRunning()) { startProgressLoop(); } + + maybeUpdateCurrentMetadata(); } @Override @@ -609,6 +566,7 @@ public abstract class BasePlayer implements } break; case Player.STATE_READY: //3 + maybeUpdateCurrentMetadata(); maybeCorrectSeekPosition(); if (!isPrepared) { isPrepared = true; @@ -625,38 +583,19 @@ public abstract class BasePlayer implements } private void maybeCorrectSeekPosition() { - if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return; + if (playQueue == null || simpleExoPlayer == null || currentMetadata == null) return; - final int currentSourceIndex = playQueue.getIndex(); final PlayQueueItem currentSourceItem = playQueue.getItem(); if (currentSourceItem == null) return; - final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition(); - final boolean isCurrentWindowCorrect = - simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; + final StreamInfo currentInfo = currentMetadata.getMetadata(); final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; - - if (recoveryPositionMillis != PlayQueueItem.RECOVERY_UNSET && isCurrentWindowCorrect) { - // Is recovering previous playback? - if (DEBUG) Log.d(TAG, "Playback - Rewinding to recovery time=" + - "[" + getTimeString((int)recoveryPositionMillis) + "]"); - seekTo(recoveryPositionMillis); - playQueue.unsetRecovery(currentSourceIndex); - - } else if (isSynchronizing && isLive()) { - if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); - // Is still synchronizing? - seekToDefault(); - - } else if (isSynchronizing && presetStartPositionMillis > 0L) { + if (presetStartPositionMillis > 0L) { + // Has another start position? if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + "position=[" + presetStartPositionMillis + "]"); - // Has another start position? seekTo(presetStartPositionMillis); - currentInfo.setStartPosition(0); } - - isSynchronizing = false; } /** @@ -708,7 +647,7 @@ public abstract class BasePlayer implements setRecovery(); final Throwable cause = error.getCause(); - if (cause instanceof BehindLiveWindowException) { + if (error instanceof BehindLiveWindowException) { reload(); } else if (cause instanceof UnknownHostException) { playQueue.error(/*isNetworkProblem=*/true); @@ -727,22 +666,29 @@ public abstract class BasePlayer implements public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + "reason = [" + reason + "]"); - // Refresh the playback if there is a transition to the next video - final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex(); + if (playQueue == null) return; - /* Discontinuity reasons!! Thank you ExoPlayer lords */ + // Refresh the playback if there is a transition to the next video + final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); switch (reason) { case DISCONTINUITY_REASON_PERIOD_TRANSITION: - if (newPeriodIndex == playQueue.getIndex()) { + // When player is in single repeat mode and a period transition occurs, + // we need to register a view count here since no metadata has changed + if (getRepeatMode() == Player.REPEAT_MODE_ONE && + newWindowIndex == playQueue.getIndex()) { registerView(); - } else { - playQueue.offsetIndex(+1); + break; } case DISCONTINUITY_REASON_SEEK: case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_INTERNAL: + if (playQueue.getIndex() != newWindowIndex) { + playQueue.setIndex(newWindowIndex); + } break; } + + maybeUpdateCurrentMetadata(); } @Override @@ -788,7 +734,7 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called"); currentItem = null; - currentInfo = null; + currentMetadata = null; simpleExoPlayer.stop(); isPrepared = false; @@ -805,42 +751,21 @@ public abstract class BasePlayer implements simpleExoPlayer.prepare(mediaSource); } - @Override - public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info) { + public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + - (info != null ? "available" : "null") + " info, " + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); if (simpleExoPlayer == null || playQueue == null) return; final boolean onPlaybackInitial = currentItem == null; final boolean hasPlayQueueItemChanged = currentItem != item; - final boolean hasStreamInfoChanged = currentInfo != info; final int currentPlayQueueIndex = playQueue.indexOf(item); final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); - // when starting playback on the last item when not repeating, maybe auto queue - if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && - getRepeatMode() == Player.REPEAT_MODE_OFF && - PlayerHelper.isAutoQueueEnabled(context)) { - final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); - if (autoQueue != null) playQueue.append(autoQueue.getStreams()); - } // If nothing to synchronize - if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { - return; - } - + if (!hasPlayQueueItemChanged) return; currentItem = item; - currentInfo = info; - if (hasPlayQueueItemChanged) { - // updates only to the stream info should not trigger another view count - registerView(); - initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); - } - onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); // Check if on wrong window if (currentPlayQueueIndex != playQueue.getIndex()) { @@ -855,39 +780,29 @@ public abstract class BasePlayer implements "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); - // If not playing correct stream, change window position and sets flag - // for synchronizing once window position is corrected - // @see maybeCorrectSeekPosition() } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial || !isPlaying()) { if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + " index=[" + currentPlayQueueIndex + "]," + " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "]."); - isSynchronizing = true; - simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); + + if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition()); + playQueue.unsetRecovery(currentPlayQueueIndex); + } else { + simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); + } } } - abstract protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged); - - @Nullable - @Override - public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { - final StreamType streamType = info.getStreamType(); - if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) { - return null; + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + final StreamInfo info = tag.getMetadata(); + if (DEBUG) { + Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName()); } - if (!info.getHlsUrl().isEmpty()) { - return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); - } else if (!info.getDashMpdUrl().isEmpty()) { - return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH); - } - - return null; + initThumbnail(info.getThumbnailUrl()); + registerView(); } @Override @@ -1020,9 +935,7 @@ public abstract class BasePlayer implements public void seekTo(long positionMillis) { if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); - if (simpleExoPlayer == null || positionMillis < 0 || - positionMillis > simpleExoPlayer.getDuration()) return; - simpleExoPlayer.seekTo(positionMillis); + if (simpleExoPlayer != null) simpleExoPlayer.seekTo(positionMillis); } public void seekBy(long offsetMillis) { @@ -1046,12 +959,14 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ private void registerView() { - if (databaseUpdateReactor == null || currentInfo == null) return; - databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() + if (currentMetadata == null) return; + final StreamInfo currentInfo = currentMetadata.getMetadata(); + final Disposable viewRegister = recordManager.onViewed(currentInfo).onErrorComplete() .subscribe( ignored -> {/* successful */}, error -> Log.e(TAG, "Player onViewed() failure: ", error) - )); + ); + databaseUpdateReactor.add(viewRegister); } protected void reload() { @@ -1065,7 +980,7 @@ public abstract class BasePlayer implements } protected void savePlaybackState(final StreamInfo info, final long progress) { - if (info == null || databaseUpdateReactor == null) return; + if (info == null) return; final Disposable stateSaver = recordManager.saveStreamState(info, progress) .observeOn(AndroidSchedulers.mainThread()) .onErrorComplete() @@ -1077,7 +992,8 @@ public abstract class BasePlayer implements } private void savePlaybackState() { - if (simpleExoPlayer == null || currentInfo == null) return; + if (simpleExoPlayer == null || currentMetadata == null) return; + final StreamInfo currentInfo = currentMetadata.getMetadata(); if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS && simpleExoPlayer.getCurrentPosition() < @@ -1085,6 +1001,36 @@ public abstract class BasePlayer implements savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); } } + + private void maybeUpdateCurrentMetadata() { + if (simpleExoPlayer == null) return; + + final MediaSourceTag metadata; + try { + metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); + } catch (IndexOutOfBoundsException | ClassCastException error) { + if(DEBUG) Log.d(TAG, "Could not update metadata: " + error.getMessage()); + if(DEBUG) error.printStackTrace(); + return; + } + + if (metadata == null) return; + maybeAutoQueueNextStream(metadata); + + if (currentMetadata == metadata) return; + currentMetadata = metadata; + onMetadataChanged(metadata); + } + + private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag currentMetadata) { + if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 || + getRepeatMode() != Player.REPEAT_MODE_OFF || + !PlayerHelper.isAutoQueueEnabled(context)) return; + // auto queue when starting playback on the last item when not repeating + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(currentMetadata.getMetadata(), + playQueue.getStreams()); + if (autoQueue != null) playQueue.append(autoQueue.getStreams()); + } /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ @@ -1101,19 +1047,35 @@ public abstract class BasePlayer implements return currentState; } + @Nullable + public MediaSourceTag getCurrentMetadata() { + return currentMetadata; + } + + @NonNull public String getVideoUrl() { - return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUrl(); + return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getUrl(); } + @NonNull public String getVideoTitle() { - return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getTitle(); + return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getName(); } + @NonNull public String getUploaderName() { - return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader(); + return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getUploaderName(); + } + + @Nullable + public Bitmap getThumbnail() { + return currentThumbnail == null ? + BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail) : + currentThumbnail; } /** Checks if the current playback is a livestream AND is playing at or beyond the live edge */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean isLiveEdge() { if (simpleExoPlayer == null || !isLive()) return false; @@ -1135,6 +1097,9 @@ public abstract class BasePlayer implements return simpleExoPlayer.isCurrentWindowDynamic(); } catch (@NonNull IndexOutOfBoundsException ignored) { // Why would this even happen =( + // But lets log it anyway. Save is save + if(DEBUG) Log.d(TAG, "Could not update metadata: " + ignored.getMessage()); + if(DEBUG) ignored.printStackTrace(); return false; } } @@ -1147,11 +1112,11 @@ public abstract class BasePlayer implements @Player.RepeatMode public int getRepeatMode() { - return simpleExoPlayer.getRepeatMode(); + return simpleExoPlayer == null ? Player.REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode(); } public void setRepeatMode(@Player.RepeatMode final int repeatMode) { - simpleExoPlayer.setRepeatMode(repeatMode); + if (simpleExoPlayer != null) simpleExoPlayer.setRepeatMode(repeatMode); } public float getPlaybackSpeed() { @@ -1162,19 +1127,22 @@ public abstract class BasePlayer implements return getPlaybackParameters().pitch; } + public boolean getPlaybackSkipSilence() { + return getPlaybackParameters().skipSilence; + } + public void setPlaybackSpeed(float speed) { - setPlaybackParameters(speed, getPlaybackPitch()); + setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence()); } public PlaybackParameters getPlaybackParameters() { - final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f); - if (simpleExoPlayer == null) return defaultParameters; + if (simpleExoPlayer == null) return PlaybackParameters.DEFAULT; final PlaybackParameters parameters = simpleExoPlayer.getPlaybackParameters(); - return parameters == null ? defaultParameters : parameters; + return parameters == null ? PlaybackParameters.DEFAULT : parameters; } - public void setPlaybackParameters(float speed, float pitch) { - simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch)); + public void setPlaybackParameters(float speed, float pitch, boolean skipSilence) { + simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch, skipSilence)); } public PlayQueue getPlayQueue() { @@ -1190,7 +1158,7 @@ public abstract class BasePlayer implements } public boolean isProgressLoopRunning() { - return progressUpdateReactor != null && !progressUpdateReactor.isDisposed(); + return progressUpdateReactor.get() != null; } public void setRecovery() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 0dea47e56..4e8398ff2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -36,6 +36,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; +import android.support.v7.content.res.AppCompatResources; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.util.DisplayMetrics; @@ -46,7 +47,9 @@ import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.PopupMenu; +import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.SeekBar; import android.widget.TextView; @@ -58,7 +61,6 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; @@ -67,6 +69,8 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -81,6 +85,7 @@ import java.util.UUID; import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; +import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.animateRotation; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -104,6 +109,7 @@ public final class MainVideoPlayer extends AppCompatActivity @Nullable private PlayerState playerState; private boolean isInMultiWindow; + private boolean isBackPressed; /*////////////////////////////////////////////////////////////////////////// // Activity LifeCycle @@ -125,7 +131,7 @@ public final class MainVideoPlayer extends AppCompatActivity hideSystemUi(); setContentView(R.layout.activity_main_player); - playerImpl = new VideoPlayerImpl(this); + playerImpl = new VideoPlayerImpl(this); playerImpl.setup(findViewById(android.R.id.content)); if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) { @@ -152,7 +158,10 @@ public final class MainVideoPlayer extends AppCompatActivity protected void onNewIntent(Intent intent) { if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); super.onNewIntent(intent); - playerImpl.handleIntent(intent); + if (intent != null) { + playerState = null; + playerImpl.handleIntent(intent); + } } @Override @@ -177,7 +186,7 @@ public final class MainVideoPlayer extends AppCompatActivity playerImpl.setPlaybackQuality(playerState.getPlaybackQuality()); playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(), playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(), - playerState.wasPlaying()); + playerState.isPlaybackSkipSilence(), playerState.wasPlaying()); } } @@ -191,6 +200,12 @@ public final class MainVideoPlayer extends AppCompatActivity } } + @Override + public void onBackPressed() { + super.onBackPressed(); + isBackPressed = true; + } + @Override protected void onSaveInstanceState(Bundle outState) { if (DEBUG) Log.d(TAG, "onSaveInstanceState() called"); @@ -200,7 +215,8 @@ public final class MainVideoPlayer extends AppCompatActivity playerImpl.setRecovery(); playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(), playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(), - playerImpl.getPlaybackQuality(), playerImpl.isPlaying()); + playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(), + playerImpl.isPlaying()); StateSaver.tryToSave(isChangingConfigurations(), null, outState, this); } @@ -208,10 +224,17 @@ public final class MainVideoPlayer extends AppCompatActivity protected void onStop() { if (DEBUG) Log.d(TAG, "onStop() called"); super.onStop(); - playerImpl.destroy(); - PlayerHelper.setScreenBrightness(getApplicationContext(), getWindow().getAttributes().screenBrightness); + + if (playerImpl == null) return; + if (!isBackPressed) { + playerImpl.minimize(); + } + playerImpl.destroy(); + + isInMultiWindow = false; + isBackPressed = false; } /*////////////////////////////////////////////////////////////////////////// @@ -335,18 +358,27 @@ public final class MainVideoPlayer extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// @Override - public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { - if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch); + public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, + boolean playbackSkipSilence) { + if (playerImpl != null) { + playerImpl.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); + } } /////////////////////////////////////////////////////////////////////////// @SuppressWarnings({"unused", "WeakerAccess"}) private class VideoPlayerImpl extends VideoPlayer { + private final float MAX_GESTURE_LENGTH = 0.75f; + private TextView titleTextView; private TextView channelTextView; - private TextView volumeTextView; - private TextView brightnessTextView; + private RelativeLayout volumeRelativeLayout; + private ProgressBar volumeProgressBar; + private ImageView volumeImageView; + private RelativeLayout brightnessRelativeLayout; + private ProgressBar brightnessProgressBar; + private ImageView brightnessImageView; private ImageButton queueButton; private ImageButton repeatButton; private ImageButton shuffleButton; @@ -370,6 +402,8 @@ public final class MainVideoPlayer extends AppCompatActivity private RelativeLayout windowRootLayout; private View secondaryControls; + private int maxGestureLength; + VideoPlayerImpl(final Context context) { super("VideoPlayerImpl" + MainVideoPlayer.TAG, context); } @@ -379,8 +413,12 @@ public final class MainVideoPlayer extends AppCompatActivity super.initViews(rootView); this.titleTextView = rootView.findViewById(R.id.titleTextView); this.channelTextView = rootView.findViewById(R.id.channelTextView); - this.volumeTextView = rootView.findViewById(R.id.volumeTextView); - this.brightnessTextView = rootView.findViewById(R.id.brightnessTextView); + this.volumeRelativeLayout = rootView.findViewById(R.id.volumeRelativeLayout); + this.volumeProgressBar = rootView.findViewById(R.id.volumeProgressBar); + this.volumeImageView = rootView.findViewById(R.id.volumeImageView); + this.brightnessRelativeLayout = rootView.findViewById(R.id.brightnessRelativeLayout); + this.brightnessProgressBar = rootView.findViewById(R.id.brightnessProgressBar); + this.brightnessImageView = rootView.findViewById(R.id.brightnessImageView); this.queueButton = rootView.findViewById(R.id.queueButton); this.repeatButton = rootView.findViewById(R.id.repeatButton); this.shuffleButton = rootView.findViewById(R.id.shuffleButton); @@ -422,7 +460,7 @@ public final class MainVideoPlayer extends AppCompatActivity public void initListeners() { super.initListeners(); - MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); + PlayerGestureListener listener = new PlayerGestureListener(); gestureDetector = new GestureDetector(context, listener); gestureDetector.setIsLongpressEnabled(false); getRootView().setOnTouchListener(listener); @@ -439,6 +477,37 @@ public final class MainVideoPlayer extends AppCompatActivity toggleOrientationButton.setOnClickListener(this); switchBackgroundButton.setOnClickListener(this); switchPopupButton.setOnClickListener(this); + + getRootView().addOnLayoutChangeListener((view, l, t, r, b, ol, ot, or, ob) -> { + if (l != ol || t != ot || r != or || b != ob) { + // Use smaller value to be consistent between screen orientations + // (and to make usage easier) + int width = r - l, height = b - t; + maxGestureLength = (int) (Math.min(width, height) * MAX_GESTURE_LENGTH); + + if (DEBUG) Log.d(TAG, "maxGestureLength = " + maxGestureLength); + + volumeProgressBar.setMax(maxGestureLength); + brightnessProgressBar.setMax(maxGestureLength); + + setInitialGestureValues(); + } + }); + } + + public void minimize() { + switch (PlayerHelper.getMinimizeOnExitAction(context)) { + case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND: + onPlayBackgroundButtonClicked(); + break; + case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP: + onFullScreenButtonClicked(); + break; + case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE: + default: + // No action + break; + } } /*////////////////////////////////////////////////////////////////////////// @@ -461,14 +530,11 @@ public final class MainVideoPlayer extends AppCompatActivity // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { - super.onMetadataChanged(item, info, newPlayQueueIndex, false); + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); - titleTextView.setText(getVideoTitle()); - channelTextView.setText(getUploaderName()); + titleTextView.setText(tag.getMetadata().getName()); + channelTextView.setText(tag.getMetadata().getUploaderName()); } @Override @@ -501,6 +567,7 @@ public final class MainVideoPlayer extends AppCompatActivity this.getRepeatMode(), this.getPlaybackSpeed(), this.getPlaybackPitch(), + this.getPlaybackSkipSilence(), this.getPlaybackQuality() ); context.startService(intent); @@ -522,6 +589,7 @@ public final class MainVideoPlayer extends AppCompatActivity this.getRepeatMode(), this.getPlaybackSpeed(), this.getPlaybackPitch(), + this.getPlaybackSkipSilence(), this.getPlaybackQuality() ); context.startService(intent); @@ -617,7 +685,8 @@ public final class MainVideoPlayer extends AppCompatActivity @Override public void onPlaybackSpeedClicked() { - PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch()) + PlaybackParameterDialog + .newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence()) .show(getSupportFragmentManager(), TAG); } @@ -647,14 +716,19 @@ public final class MainVideoPlayer extends AppCompatActivity } @Override - protected int getDefaultResolutionIndex(final List sortedVideos) { - return ListHelper.getDefaultResolutionIndex(context, sortedVideos); - } + protected VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(List sortedVideos) { + return ListHelper.getDefaultResolutionIndex(context, sortedVideos); + } - @Override - protected int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); + @Override + public int getOverrideResolutionIndex(List sortedVideos, + String playbackQuality) { + return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); + } + }; } /*////////////////////////////////////////////////////////////////////////// @@ -678,7 +752,6 @@ public final class MainVideoPlayer extends AppCompatActivity @Override public void onBuffering() { super.onBuffering(); - animatePlayButtons(false, 100); getRootView().setKeepScreenOn(true); } @@ -728,6 +801,13 @@ public final class MainVideoPlayer extends AppCompatActivity // Utils //////////////////////////////////////////////////////////////////////////*/ + private void setInitialGestureValues() { + if (getAudioReactor() != null) { + final float currentVolumeNormalized = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume(); + volumeProgressBar.setProgress((int) (volumeProgressBar.getMax() * currentVolumeNormalized)); + } + } + @Override public void showControlsThenHide() { if (queueVisible) return; @@ -831,12 +911,28 @@ public final class MainVideoPlayer extends AppCompatActivity return channelTextView; } - public TextView getVolumeTextView() { - return volumeTextView; + public RelativeLayout getVolumeRelativeLayout() { + return volumeRelativeLayout; } - public TextView getBrightnessTextView() { - return brightnessTextView; + public ProgressBar getVolumeProgressBar() { + return volumeProgressBar; + } + + public ImageView getVolumeImageView() { + return volumeImageView; + } + + public RelativeLayout getBrightnessRelativeLayout() { + return brightnessRelativeLayout; + } + + public ProgressBar getBrightnessProgressBar() { + return brightnessProgressBar; + } + + public ImageView getBrightnessImageView() { + return brightnessImageView; } public ImageButton getRepeatButton() { @@ -846,15 +942,18 @@ public final class MainVideoPlayer extends AppCompatActivity public ImageButton getPlayPauseButton() { return playPauseButton; } + + public int getMaxGestureLength() { + return maxGestureLength; + } } - private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { + private class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { private boolean isMoving; @Override public boolean onDoubleTap(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); - if (!playerImpl.isPlaying()) return false; if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) { playerImpl.onFastForward(); @@ -888,91 +987,91 @@ public final class MainVideoPlayer extends AppCompatActivity return super.onDown(e); } + private static final int MOVEMENT_THRESHOLD = 40; + private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext()); + private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume(); - private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; - private float currentBrightness = getWindow().getAttributes().screenBrightness > 0 - ? getWindow().getAttributes().screenBrightness - : 0.5f; - - private int currentVolume, maxVolume = playerImpl.getAudioReactor().getMaxVolume(); - private final float stepsVolume = 15, stepVolume = (float) Math.ceil(maxVolume / stepsVolume), minVolume = 0; - - private final String brightnessUnicode = new String(Character.toChars(0x2600)); - private final String volumeUnicode = new String(Character.toChars(0x1F508)); - - private final int MOVEMENT_THRESHOLD = 40; - private final int eventsThreshold = 8; - private boolean triggered = false; - private int eventsNum; - - // TODO: Improve video gesture controls @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) { if (!isPlayerGestureEnabled) return false; //noinspection PointlessBooleanExpression if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " + - ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + - ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + + ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + + ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", distanceXy = [" + distanceX + ", " + distanceY + "]"); - float abs = Math.abs(e2.getY() - e1.getY()); - if (!triggered) { - triggered = abs > MOVEMENT_THRESHOLD; + + final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; + if (!isMoving && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) + || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { return false; } - if (eventsNum++ % eventsThreshold != 0 || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) return false; isMoving = true; -// boolean up = !((e2.getY() - e1.getY()) > 0) && distanceY > 0; // Android's origin point is on top - boolean up = distanceY > 0; - - if (e1.getX() > playerImpl.getRootView().getWidth() / 2) { - double floor = Math.floor(up ? stepVolume : -stepVolume); - currentVolume = (int) (playerImpl.getAudioReactor().getVolume() + floor); - if (currentVolume >= maxVolume) currentVolume = maxVolume; - if (currentVolume <= minVolume) currentVolume = (int) minVolume; + if (initialEvent.getX() > playerImpl.getRootView().getWidth() / 2) { + playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); + float currentProgressPercent = + (float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); + int currentVolume = (int) (maxVolume * currentProgressPercent); playerImpl.getAudioReactor().setVolume(currentVolume); - currentVolume = playerImpl.getAudioReactor().getVolume(); if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); - final String volumeText = volumeUnicode + " " + Math.round((((float) currentVolume) / maxVolume) * 100) + "%"; - playerImpl.getVolumeTextView().setText(volumeText); - if (playerImpl.getVolumeTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getVolumeTextView(), true, 200); - if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE); + final int resId = + currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp + : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp + : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp + : R.drawable.ic_volume_up_white_72dp; + + playerImpl.getVolumeImageView().setImageDrawable( + AppCompatResources.getDrawable(getApplicationContext(), resId) + ); + + if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { + animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); + } + if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); + } } else { - WindowManager.LayoutParams lp = getWindow().getAttributes(); - currentBrightness += up ? stepBrightness : -stepBrightness; - if (currentBrightness >= 1f) currentBrightness = 1f; - if (currentBrightness <= minBrightness) currentBrightness = minBrightness; + playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY); + float currentProgressPercent = + (float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength(); + WindowManager.LayoutParams layoutParams = getWindow().getAttributes(); + layoutParams.screenBrightness = currentProgressPercent; + getWindow().setAttributes(layoutParams); - lp.screenBrightness = currentBrightness; - getWindow().setAttributes(lp); - if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentBrightness); - int brightnessNormalized = Math.round(currentBrightness * 100); + if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentProgressPercent); - final String brightnessText = brightnessUnicode + " " + (brightnessNormalized == 1 ? 0 : brightnessNormalized) + "%"; - playerImpl.getBrightnessTextView().setText(brightnessText); + final int resId = + currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp + : currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp + : R.drawable.ic_brightness_high_white_72dp; - if (playerImpl.getBrightnessTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), true, 200); - if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); + playerImpl.getBrightnessImageView().setImageDrawable( + AppCompatResources.getDrawable(getApplicationContext(), resId) + ); + + if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { + animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); + } + if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); + } } return true; } private void onScrollEnd() { if (DEBUG) Log.d(TAG, "onScrollEnd() called"); - triggered = false; - eventsNum = 0; - /* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); - if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/ - if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getVolumeTextView(), false, 200, 200); + + if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); } - if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getBrightnessTextView(), false, 200, 200); + if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); } if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java index 8ffcb6b29..359159809 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java @@ -14,21 +14,26 @@ public class PlayerState implements Serializable { private final float playbackSpeed; private final float playbackPitch; @Nullable private final String playbackQuality; + private final boolean playbackSkipSilence; private final boolean wasPlaying; PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, - final float playbackSpeed, final float playbackPitch, final boolean wasPlaying) { - this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, wasPlaying); + final float playbackSpeed, final float playbackPitch, + final boolean playbackSkipSilence, final boolean wasPlaying) { + this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, + playbackSkipSilence, wasPlaying); } PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, final float playbackSpeed, final float playbackPitch, - @Nullable final String playbackQuality, final boolean wasPlaying) { + @Nullable final String playbackQuality, final boolean playbackSkipSilence, + final boolean wasPlaying) { this.playQueue = playQueue; this.repeatMode = repeatMode; this.playbackSpeed = playbackSpeed; this.playbackPitch = playbackPitch; this.playbackQuality = playbackQuality; + this.playbackSkipSilence = playbackSkipSilence; this.wasPlaying = wasPlaying; } @@ -62,6 +67,10 @@ public class PlayerState implements Serializable { return playbackQuality; } + public boolean isPlaybackSkipSilence() { + return playbackSkipSilence; + } + public boolean wasPlaying() { return wasPlaying; } diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index f9892ecb5..0e7328020 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -19,6 +19,8 @@ package org.schabi.newpipe.player; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.annotation.SuppressLint; import android.app.NotificationManager; import android.app.PendingIntent; @@ -34,7 +36,7 @@ import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import android.support.design.widget.FloatingActionButton; import android.support.v4.app.NotificationCompat; import android.util.DisplayMetrics; import android.util.Log; @@ -42,7 +44,9 @@ import android.view.GestureDetector; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; +import android.view.animation.AnticipateInterpolator; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.PopupMenu; @@ -56,17 +60,17 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; +import com.nostra13.universalimageloader.core.assist.FailReason; import org.schabi.newpipe.BuildConfig; -import org.schabi.newpipe.CheckForNewAppVersionTask; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.old.PlayVideoActivity; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -99,11 +103,19 @@ public final class PopupVideoPlayer extends Service { private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; - private WindowManager windowManager; - private WindowManager.LayoutParams windowLayoutParams; - private GestureDetector gestureDetector; + private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS | + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + + private WindowManager windowManager; + private WindowManager.LayoutParams popupLayoutParams; + private GestureDetector popupGestureDetector; + + private View closeOverlayView; + private FloatingActionButton closeOverlayButton; + private WindowManager.LayoutParams closeOverlayLayoutParams; - private int shutdownFlingVelocity; private int tossFlingVelocity; private float screenWidth, screenHeight; @@ -118,6 +130,7 @@ public final class PopupVideoPlayer extends Service { private VideoPlayerImpl playerImpl; private LockManager lockManager; + private boolean isPopupClosing = false; /*////////////////////////////////////////////////////////////////////////// // Service-Activity Binder @@ -146,7 +159,10 @@ public final class PopupVideoPlayer extends Service { public int onStartCommand(final Intent intent, int flags, int startId) { if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); - if (playerImpl.getPlayer() == null) initPopup(); + if (playerImpl.getPlayer() == null) { + initPopup(); + initPopupCloseOverlay(); + } if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true); playerImpl.handleIntent(intent); @@ -156,15 +172,16 @@ public final class PopupVideoPlayer extends Service { @Override public void onConfigurationChanged(Configuration newConfig) { + if (DEBUG) Log.d(TAG, "onConfigurationChanged() called with: newConfig = [" + newConfig + "]"); updateScreenSize(); - updatePopupSize(windowLayoutParams.width, -1); - checkPositionBounds(); + updatePopupSize(popupLayoutParams.width, -1); + checkPopupPositionBounds(); } @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy() called"); - onClose(); + closePopup(); } @Override @@ -182,7 +199,6 @@ public final class PopupVideoPlayer extends Service { View rootView = View.inflate(this, R.layout.player_popup, null); playerImpl.setup(rootView); - shutdownFlingVelocity = PlayerHelper.getShutdownFlingVelocity(this); tossFlingVelocity = PlayerHelper.getTossFlingVelocity(this); updateScreenSize(); @@ -192,28 +208,56 @@ public final class PopupVideoPlayer extends Service { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize; - final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_PHONE : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? + WindowManager.LayoutParams.TYPE_PHONE : + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - windowLayoutParams = new WindowManager.LayoutParams( + popupLayoutParams = new WindowManager.LayoutParams( (int) popupWidth, (int) getMinimumVideoHeight(popupWidth), layoutParamType, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + IDLE_WINDOW_FLAGS, PixelFormat.TRANSLUCENT); - windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; + popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; + popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; int centerX = (int) (screenWidth / 2f - popupWidth / 2f); int centerY = (int) (screenHeight / 2f - popupHeight / 2f); - windowLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; - windowLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; + popupLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; + popupLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; - checkPositionBounds(); + checkPopupPositionBounds(); - MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); - gestureDetector = new GestureDetector(this, listener); + PopupWindowGestureListener listener = new PopupWindowGestureListener(); + popupGestureDetector = new GestureDetector(this, listener); rootView.setOnTouchListener(listener); - playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width); - playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height); - windowManager.addView(rootView, windowLayoutParams); + + playerImpl.getLoadingPanel().setMinimumWidth(popupLayoutParams.width); + playerImpl.getLoadingPanel().setMinimumHeight(popupLayoutParams.height); + windowManager.addView(rootView, popupLayoutParams); + } + + @SuppressLint("RtlHardcoded") + private void initPopupCloseOverlay() { + if (DEBUG) Log.d(TAG, "initPopupCloseOverlay() called"); + closeOverlayView = View.inflate(this, R.layout.player_popup_close_overlay, null); + closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton); + + final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? + WindowManager.LayoutParams.TYPE_PHONE : + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + + closeOverlayLayoutParams = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, + layoutParamType, + flags, + PixelFormat.TRANSLUCENT); + closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; + closeOverlayLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + + closeOverlayButton.setVisibility(View.GONE); + windowManager.addView(closeOverlayView, closeOverlayLayoutParams); } /*////////////////////////////////////////////////////////////////////////// @@ -229,6 +273,7 @@ public final class PopupVideoPlayer extends Service { notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); + notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail()); notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); @@ -244,11 +289,15 @@ public final class PopupVideoPlayer extends Service { setRepeatModeRemote(notRemoteView, playerImpl.getRepeatMode()); - return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) .setOngoing(true) .setSmallIcon(R.drawable.ic_newpipe_triangle_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContent(notRemoteView); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + builder.setPriority(NotificationCompat.PRIORITY_MAX); + } + return builder; } /** @@ -268,44 +317,105 @@ public final class PopupVideoPlayer extends Service { // Misc //////////////////////////////////////////////////////////////////////////*/ - public void onClose() { - if (DEBUG) Log.d(TAG, "onClose() called"); + public void closePopup() { + if (DEBUG) Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); + if (isPopupClosing) return; + isPopupClosing = true; if (playerImpl != null) { if (playerImpl.getRootView() != null) { windowManager.removeView(playerImpl.getRootView()); - playerImpl.setRootView(null); } + playerImpl.setRootView(null); playerImpl.stopActivityBinding(); playerImpl.destroy(); + playerImpl = null; } + + mBinder = null; if (lockManager != null) lockManager.releaseWifiAndCpu(); if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); - mBinder = null; - playerImpl = null; - stopForeground(true); - stopSelf(); + animateOverlayAndFinishService(); + } + + private void animateOverlayAndFinishService() { + final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - closeOverlayButton.getY()); + + closeOverlayButton.animate().setListener(null).cancel(); + closeOverlayButton.animate() + .setInterpolator(new AnticipateInterpolator()) + .translationY(targetTranslationY) + .setDuration(400) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + end(); + } + + @Override + public void onAnimationEnd(Animator animation) { + end(); + } + + private void end() { + windowManager.removeView(closeOverlayView); + + stopForeground(true); + stopSelf(); + } + }).start(); } /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ - private void checkPositionBounds() { - if (windowLayoutParams.x > screenWidth - windowLayoutParams.width) - windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width); - if (windowLayoutParams.x < 0) windowLayoutParams.x = 0; - if (windowLayoutParams.y > screenHeight - windowLayoutParams.height) - windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height); - if (windowLayoutParams.y < 0) windowLayoutParams.y = 0; + /** + * @see #checkPopupPositionBounds(float, float) + */ + @SuppressWarnings("UnusedReturnValue") + private boolean checkPopupPositionBounds() { + return checkPopupPositionBounds(screenWidth, screenHeight); + } + + /** + * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary that goes from (0,0) to (boundaryWidth,boundaryHeight). + *

+ * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed and {@code true} is returned + * to represent this change. + * + * @return if the popup was out of bounds and have been moved back to it + */ + private boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) { + if (DEBUG) { + Log.d(TAG, "checkPopupPositionBounds() called with: boundaryWidth = [" + boundaryWidth + "], boundaryHeight = [" + boundaryHeight + "]"); + } + + if (popupLayoutParams.x < 0) { + popupLayoutParams.x = 0; + return true; + } else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) { + popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width); + return true; + } + + if (popupLayoutParams.y < 0) { + popupLayoutParams.y = 0; + return true; + } else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) { + popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height); + return true; + } + + return false; } private void savePositionAndSize() { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this); - sharedPreferences.edit().putInt(POPUP_SAVED_X, windowLayoutParams.x).apply(); - sharedPreferences.edit().putInt(POPUP_SAVED_Y, windowLayoutParams.y).apply(); - sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, windowLayoutParams.width).apply(); + sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply(); + sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply(); + sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply(); } private float getMinimumVideoHeight(float width) { @@ -340,13 +450,13 @@ public final class PopupVideoPlayer extends Service { if (height == -1) height = (int) getMinimumVideoHeight(width); else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height); - windowLayoutParams.width = width; - windowLayoutParams.height = height; + popupLayoutParams.width = width; + popupLayoutParams.height = height; popupWidth = width; popupHeight = height; if (DEBUG) Log.d(TAG, "updatePopupSize() updated values: width = [" + width + "], height = [" + height + "]"); - windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams); + windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); } protected void setRepeatModeRemote(final RemoteViews remoteViews, final int repeatMode) { @@ -367,6 +477,12 @@ public final class PopupVideoPlayer extends Service { } } + private void updateWindowFlags(final int flags) { + if (popupLayoutParams == null || windowManager == null || playerImpl == null) return; + + popupLayoutParams.flags = flags; + windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); + } /////////////////////////////////////////////////////////////////////////// protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener { @@ -375,6 +491,7 @@ public final class PopupVideoPlayer extends Service { private ImageView videoPlayPause; private View extraOptionsView; + private View closingOverlayView; @Override public void handleIntent(Intent intent) { @@ -395,12 +512,18 @@ public final class PopupVideoPlayer extends Service { fullScreenButton = rootView.findViewById(R.id.fullScreenButton); fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked()); videoPlayPause = rootView.findViewById(R.id.videoPlayPause); - videoPlayPause.setOnClickListener(this::onPlayPauseButtonPressed); extraOptionsView = rootView.findViewById(R.id.extraOptionsView); + closingOverlayView = rootView.findViewById(R.id.closingOverlay); rootView.addOnLayoutChangeListener(this); } + @Override + public void initListeners() { + super.initListeners(); + videoPlayPause.setOnClickListener(v -> onPlayPause()); + } + @Override protected void setupSubtitleView(@NonNull SubtitleView view, final float captionScale, @@ -411,10 +534,6 @@ public final class PopupVideoPlayer extends Service { view.setStyle(captionStyle); } - private void onPlayPauseButtonPressed(View ib) { - onPlayPause(); - } - @Override public void onLayoutChange(final View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { @@ -429,21 +548,6 @@ public final class PopupVideoPlayer extends Service { super.destroy(); } - @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - super.onLoadingComplete(imageUri, view, loadedImage); - if (loadedImage != null) { - // rebuild notification here since remote view does not release bitmaps, causing memory leaks - notBuilder = createNotification(); - - if (notRemoteView != null) { - notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); - } - - updateNotification(-1); - } - } - @Override public void onFullScreenButtonClicked() { super.onFullScreenButtonClicked(); @@ -460,6 +564,7 @@ public final class PopupVideoPlayer extends Service { this.getRepeatMode(), this.getPlaybackSpeed(), this.getPlaybackPitch(), + this.getPlaybackSkipSilence(), this.getPlaybackQuality() ); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -472,7 +577,7 @@ public final class PopupVideoPlayer extends Service { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent); - onClose(); + closePopup(); } @Override @@ -511,14 +616,47 @@ public final class PopupVideoPlayer extends Service { } @Override - protected int getDefaultResolutionIndex(final List sortedVideos) { - return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); + protected VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(List sortedVideos) { + return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); + } + + @Override + public int getOverrideResolutionIndex(List sortedVideos, + String playbackQuality) { + return ListHelper.getPopupResolutionIndex(context, sortedVideos, + playbackQuality); + } + }; + } + + /*////////////////////////////////////////////////////////////////////////// + // Thumbnail Loading + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + super.onLoadingComplete(imageUri, view, loadedImage); + // rebuild notification here since remote view does not release bitmaps, + // causing memory leaks + resetNotification(); + updateNotification(-1); } @Override - protected int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality); + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + super.onLoadingFailed(imageUri, view, failReason); + resetNotification(); + updateNotification(-1); + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + super.onLoadingCancelled(imageUri, view); + resetNotification(); + updateNotification(-1); } /*////////////////////////////////////////////////////////////////////////// @@ -539,8 +677,8 @@ public final class PopupVideoPlayer extends Service { } private void updateMetadata() { - if (activityListener != null && currentInfo != null) { - activityListener.onMetadataUpdate(currentInfo); + if (activityListener != null && getCurrentMetadata() != null) { + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); } } @@ -572,8 +710,9 @@ public final class PopupVideoPlayer extends Service { public void onRepeatModeChanged(int i) { super.onRepeatModeChanged(i); setRepeatModeRemote(notRemoteView, i); - updateNotification(-1); updatePlayback(); + resetNotification(); + updateNotification(-1); } @Override @@ -586,18 +725,17 @@ public final class PopupVideoPlayer extends Service { // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { - super.onMetadataChanged(item, info, newPlayQueueIndex, false); + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); + resetNotification(); + updateNotification(-1); updateMetadata(); } @Override public void onPlaybackShutdown() { super.onPlaybackShutdown(); - onClose(); + closePopup(); } /*////////////////////////////////////////////////////////////////////////// @@ -623,7 +761,7 @@ public final class PopupVideoPlayer extends Service { if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); switch (intent.getAction()) { case ACTION_CLOSE: - onClose(); + closePopup(); break; case ACTION_PLAY_PAUSE: onPlayPause(); @@ -653,49 +791,70 @@ public final class PopupVideoPlayer extends Service { @Override public void onBlocked() { super.onBlocked(); + resetNotification(); updateNotification(R.drawable.ic_play_arrow_white); } @Override public void onPlaying() { super.onPlaying(); - updateNotification(R.drawable.ic_pause_white); - videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); - lockManager.acquireWifiAndCpu(); + updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); + + resetNotification(); + updateNotification(R.drawable.ic_pause_white); + + videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - // Check for new version - //new CheckForNewAppVersionTask().execute(); + startForeground(NOTIFICATION_ID, notBuilder.build()); + lockManager.acquireWifiAndCpu(); } @Override public void onBuffering() { super.onBuffering(); + resetNotification(); updateNotification(R.drawable.ic_play_arrow_white); } @Override public void onPaused() { super.onPaused(); + + updateWindowFlags(IDLE_WINDOW_FLAGS); + + resetNotification(); updateNotification(R.drawable.ic_play_arrow_white); + videoPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_white); lockManager.releaseWifiAndCpu(); + + stopForeground(false); } @Override public void onPausedSeek() { super.onPausedSeek(); - videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); + resetNotification(); updateNotification(R.drawable.ic_play_arrow_white); + + videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); } @Override public void onCompleted() { super.onCompleted(); + + updateWindowFlags(IDLE_WINDOW_FLAGS); + + resetNotification(); updateNotification(R.drawable.ic_replay_white); + videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white); lockManager.releaseWifiAndCpu(); + + stopForeground(false); } @Override @@ -713,16 +872,15 @@ public final class PopupVideoPlayer extends Service { super.hideControlsAndButton(duration, delay, videoPlayPause); } - - /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ /*package-private*/ void enableVideoRenderer(final boolean enable) { final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); - if (trackSelector != null && videoRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setRendererDisabled(videoRendererIndex, !enable); + if (videoRendererIndex != RENDERER_UNAVAILABLE) { + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(videoRendererIndex, !enable)); } } @@ -734,12 +892,15 @@ public final class PopupVideoPlayer extends Service { public TextView getResizingIndicator() { return resizingIndicator; } + + public View getClosingOverlayView() { + return closingOverlayView; + } } - private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { + private class PopupWindowGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { private int initialPopupX, initialPopupY; private boolean isMoving; - private boolean isResizing; @Override @@ -775,10 +936,15 @@ public final class PopupVideoPlayer extends Service { @Override public boolean onDown(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); - initialPopupX = windowLayoutParams.x; - initialPopupY = windowLayoutParams.y; - popupWidth = windowLayoutParams.width; - popupHeight = windowLayoutParams.height; + + // Fix popup position when the user touch it, it may have the wrong one + // because the soft input is visible (the draggable area is currently resized). + checkPopupPositionBounds(closeOverlayView.getWidth(), closeOverlayView.getHeight()); + + initialPopupX = popupLayoutParams.x; + initialPopupY = popupLayoutParams.y; + popupWidth = popupLayoutParams.width; + popupHeight = popupLayoutParams.height; return super.onDown(e); } @@ -786,20 +952,22 @@ public final class PopupVideoPlayer extends Service { public void onLongPress(MotionEvent e) { if (DEBUG) Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); updateScreenSize(); - checkPositionBounds(); + checkPopupPositionBounds(); updatePopupSize((int) screenWidth, -1); } @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - if (isResizing || playerImpl == null) return super.onScroll(e1, e2, distanceX, distanceY); + public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) { + if (isResizing || playerImpl == null) return super.onScroll(initialEvent, movingEvent, distanceX, distanceY); + + if (!isMoving) { + animateView(closeOverlayButton, true, 200); + } - if (playerImpl.getCurrentState() != BasePlayer.STATE_BUFFERING - && (!isMoving || playerImpl.getControlsRoot().getAlpha() != 1f)) playerImpl.showControls(0); isMoving = true; - float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX); - float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY); + float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()), posX = (int) (initialPopupX + diffX); + float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()), posY = (int) (initialPopupY + diffY); if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth); else if (posX < 0) posX = 0; @@ -807,26 +975,49 @@ public final class PopupVideoPlayer extends Service { if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight); else if (posY < 0) posY = 0; - windowLayoutParams.x = (int) posX; - windowLayoutParams.y = (int) posY; + popupLayoutParams.x = (int) posX; + popupLayoutParams.y = (int) posY; + + final View closingOverlayView = playerImpl.getClosingOverlayView(); + if (isInsideClosingRadius(movingEvent)) { + if (closingOverlayView.getVisibility() == View.GONE) { + animateView(closingOverlayView, true, 250); + } + } else { + if (closingOverlayView.getVisibility() == View.VISIBLE) { + animateView(closingOverlayView, false, 0); + } + } //noinspection PointlessBooleanExpression - if (DEBUG && false) Log.d(TAG, "PopupVideoPlayer.onScroll = " + - ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + - ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + - ", distanceXy = [" + distanceX + ", " + distanceY + "]" + - ", posXy = [" + posX + ", " + posY + "]" + - ", popupWh = [" + popupWidth + " x " + popupHeight + "]"); - windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams); + if (DEBUG && false) { + Log.d(TAG, "PopupVideoPlayer.onScroll = " + + ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + ", e1.getX,Y = [" + initialEvent.getX() + ", " + initialEvent.getY() + "]" + + ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "]" + + ", distanceX,Y = [" + distanceX + ", " + distanceY + "]" + + ", posX,Y = [" + posX + ", " + posY + "]" + + ", popupW,H = [" + popupWidth + " x " + popupHeight + "]"); + } + windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); return true; } - private void onScrollEnd() { + private void onScrollEnd(MotionEvent event) { if (DEBUG) Log.d(TAG, "onScrollEnd() called"); if (playerImpl == null) return; if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } + + if (isInsideClosingRadius(event)) { + closePopup(); + } else { + animateView(playerImpl.getClosingOverlayView(), false, 0); + + if (!isPopupClosing) { + animateView(closeOverlayButton, false, 200); + } + } } @Override @@ -836,14 +1027,11 @@ public final class PopupVideoPlayer extends Service { final float absVelocityX = Math.abs(velocityX); final float absVelocityY = Math.abs(velocityY); - if (absVelocityX > shutdownFlingVelocity) { - onClose(); - return true; - } else if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { - if (absVelocityX > tossFlingVelocity) windowLayoutParams.x = (int) velocityX; - if (absVelocityY > tossFlingVelocity) windowLayoutParams.y = (int) velocityY; - checkPositionBounds(); - windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams); + if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { + if (absVelocityX > tossFlingVelocity) popupLayoutParams.x = (int) velocityX; + if (absVelocityY > tossFlingVelocity) popupLayoutParams.y = (int) velocityY; + checkPopupPositionBounds(); + windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); return true; } return false; @@ -851,7 +1039,7 @@ public final class PopupVideoPlayer extends Service { @Override public boolean onTouch(View v, MotionEvent event) { - gestureDetector.onTouchEvent(event); + popupGestureDetector.onTouchEvent(event); if (playerImpl == null) return false; if (event.getPointerCount() == 2 && !isResizing) { if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); @@ -874,7 +1062,7 @@ public final class PopupVideoPlayer extends Service { Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); if (isMoving) { isMoving = false; - onScrollEnd(); + onScrollEnd(event); } if (isResizing) { @@ -882,7 +1070,10 @@ public final class PopupVideoPlayer extends Service { animateView(playerImpl.getResizingIndicator(), false, 100, 0); playerImpl.changeState(playerImpl.getCurrentState()); } - savePositionAndSize(); + + if (!isPopupClosing) { + savePositionAndSize(); + } } v.performClick(); @@ -898,13 +1089,13 @@ public final class PopupVideoPlayer extends Service { final float diff = Math.abs(firstPointerX - secondPointerX); if (firstPointerX > secondPointerX) { // second pointer is the anchor (the leftmost pointer) - windowLayoutParams.x = (int) (event.getRawX() - diff); + popupLayoutParams.x = (int) (event.getRawX() - diff); } else { // first pointer is the anchor - windowLayoutParams.x = (int) event.getRawX(); + popupLayoutParams.x = (int) event.getRawX(); } - checkPositionBounds(); + checkPopupPositionBounds(); updateScreenSize(); final int width = (int) Math.min(screenWidth, diff); @@ -912,5 +1103,29 @@ public final class PopupVideoPlayer extends Service { return true; } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private int distanceFromCloseButton(MotionEvent popupMotionEvent) { + final int closeOverlayButtonX = closeOverlayButton.getLeft() + closeOverlayButton.getWidth() / 2; + final int closeOverlayButtonY = closeOverlayButton.getTop() + closeOverlayButton.getHeight() / 2; + + float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); + float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); + + return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + Math.pow(closeOverlayButtonY - fingerY, 2)); + } + + private float getClosingRadius() { + final int buttonRadius = closeOverlayButton.getWidth() / 2; + // 20% wider than the button itself + return buttonRadius * 1.2f; + } + + private boolean isInsideClosingRadius(MotionEvent popupMotionEvent) { + return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); + } } } \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 8b96b651e..94305e6c4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -16,6 +16,7 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.PopupMenu; @@ -187,6 +188,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity this.player.getRepeatMode(), this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(), + this.player.getPlaybackSkipSilence(), null ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } @@ -340,6 +342,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return true; }); + final MenuItem share = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/3, + Menu.NONE, R.string.share); + share.setOnMenuItemClickListener(menuItem -> { + shareUrl(item.getTitle(), item.getUrl()); + return true; + }); + menu.show(); } @@ -459,13 +468,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void openPlaybackParameterDialog() { if (player == null) return; - PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), - player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), + player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag()); } @Override - public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { - if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch); + public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, + boolean playbackSkipSilence) { + if (player != null) { + player.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); + } } //////////////////////////////////////////////////////////////////////////// @@ -509,6 +521,18 @@ public abstract class ServicePlayerActivity extends AppCompatActivity .show(getSupportFragmentManager(), getTag()); } + //////////////////////////////////////////////////////////////////////////// + // Share + //////////////////////////////////////////////////////////////////////////// + + private void shareUrl(String subject, String url) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_SUBJECT, subject); + intent.putExtra(Intent.EXTRA_TEXT, url); + startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); + } + //////////////////////////////////////////////////////////////////////////// // Binding Service Listener //////////////////////////////////////////////////////////////////////////// @@ -539,6 +563,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (player != null) { progressLiveSync.setClickable(!player.isLiveEdge()); } + + // this will make shure progressCurrentTime has the same width as progressEndTime + final ViewGroup.LayoutParams endTimeParams = progressEndTime.getLayoutParams(); + final ViewGroup.LayoutParams currentTimeParams = progressCurrentTime.getLayoutParams(); + currentTimeParams.width = progressEndTime.getWidth(); + progressCurrentTime.setLayoutParams(currentTimeParams); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 5ea1c74a0..679fc6645 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -29,7 +29,6 @@ import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; -import android.net.Uri; import android.os.Build; import android.os.Handler; import android.support.annotation.NonNull; @@ -47,11 +46,9 @@ import android.widget.SeekBar; import android.widget.TextView; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.CaptionStyleCompat; @@ -62,21 +59,17 @@ import com.google.android.exoplayer2.video.VideoListener; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.Subtitles; -import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.util.ListHelper; import java.util.ArrayList; import java.util.List; -import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; -import static com.google.android.exoplayer2.C.TIME_UNSET; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -105,13 +98,12 @@ public abstract class VideoPlayer extends BasePlayer public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds - private ArrayList availableStreams; + private List availableStreams; private int selectedStreamIndex; - protected String playbackQuality; - protected boolean wasPlaying = false; + @NonNull final private VideoPlaybackResolver resolver; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ @@ -162,6 +154,7 @@ public abstract class VideoPlayer extends BasePlayer public VideoPlayer(String debugTag, Context context) { super(context); this.TAG = debugTag; + this.resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); } public void setup(View rootView) { @@ -241,7 +234,8 @@ public abstract class VideoPlayer extends BasePlayer // Setup audio session with onboard equalizer if (Build.VERSION.SDK_INT >= 21) { - trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)); + trackSelector.setParameters(trackSelector.buildUponParameters() + .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); } } @@ -297,8 +291,9 @@ public abstract class VideoPlayer extends BasePlayer 0, Menu.NONE, R.string.caption_none); captionOffItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setRendererDisabled(textRendererIndex, true); + if (textRendererIndex != RENDERER_UNAVAILABLE) { + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(textRendererIndex, true)); } return true; }); @@ -310,68 +305,61 @@ public abstract class VideoPlayer extends BasePlayer i + 1, Menu.NONE, captionLanguage); captionItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { + if (textRendererIndex != RENDERER_UNAVAILABLE) { trackSelector.setPreferredTextLanguage(captionLanguage); - trackSelector.setRendererDisabled(textRendererIndex, false); + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(textRendererIndex, false)); } return true; }); } captionPopupMenu.setOnDismissListener(this); } - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - protected abstract int getDefaultResolutionIndex(final List sortedVideos); - protected abstract int getOverrideResolutionIndex(final List sortedVideos, final String playbackQuality); + private void updateStreamRelatedViews() { + if (getCurrentMetadata() == null) return; + + final MediaSourceTag tag = getCurrentMetadata(); + final StreamInfo metadata = tag.getMetadata(); - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); playbackEndTime.setVisibility(View.GONE); playbackLiveSync.setVisibility(View.GONE); - final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); - - switch (streamType) { + switch (metadata.getStreamType()) { case AUDIO_STREAM: surfaceView.setVisibility(View.GONE); + endScreen.setVisibility(View.VISIBLE); playbackEndTime.setVisibility(View.VISIBLE); break; case AUDIO_LIVE_STREAM: surfaceView.setVisibility(View.GONE); + endScreen.setVisibility(View.VISIBLE); playbackLiveSync.setVisibility(View.VISIBLE); break; case LIVE_STREAM: surfaceView.setVisibility(View.VISIBLE); + endScreen.setVisibility(View.GONE); playbackLiveSync.setVisibility(View.VISIBLE); break; case VIDEO_STREAM: - if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) break; - - final List videos = ListHelper.getSortedStreamVideosList(context, - info.getVideoStreams(), info.getVideoOnlyStreams(), false); - availableStreams = new ArrayList<>(videos); - if (playbackQuality == null) { - selectedStreamIndex = getDefaultResolutionIndex(videos); - } else { - selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality()); - } + if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size() == 0) + break; + availableStreams = tag.getSortedAvailableVideoStreams(); + selectedStreamIndex = tag.getSelectedVideoStreamIndex(); buildQualityMenu(); - qualityTextView.setVisibility(View.VISIBLE); + qualityTextView.setVisibility(View.VISIBLE); surfaceView.setVisibility(View.VISIBLE); default: + endScreen.setVisibility(View.GONE); playbackEndTime.setVisibility(View.VISIBLE); break; } @@ -379,69 +367,21 @@ public abstract class VideoPlayer extends BasePlayer buildPlaybackSpeedMenu(); playbackSpeedTextView.setVisibility(View.VISIBLE); } + /*////////////////////////////////////////////////////////////////////////// + // Playback Listener + //////////////////////////////////////////////////////////////////////////*/ + + protected abstract VideoPlaybackResolver.QualityResolver getQualityResolver(); + + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); + updateStreamRelatedViews(); + } @Override @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - final MediaSource liveSource = super.sourceOf(item, info); - if (liveSource != null) return liveSource; - - List mediaSources = new ArrayList<>(); - - // Create video stream source - final List 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 audioStreams = info.getAudioStreams(); - final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( - ListHelper.getDefaultAudioFormat(context, audioStreams)); - // Use the audio stream if there is no video stream, or - // Merge with audio stream in case if video does not contain audio - if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { - final MediaSource audioSource = buildMediaSource(audio.getUrl(), - PlayerHelper.cacheKeyOf(info, audio), - MediaFormat.getSuffixById(audio.getFormatId())); - mediaSources.add(audioSource); - } - - // If there is no audio or video sources, then this media source cannot be played back - if (mediaSources.isEmpty()) return null; - // Below are auxiliary media sources - - // Create subtitle sources - for (final Subtitles subtitle : info.getSubtitles()) { - final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); - if (mimeType == null) continue; - - final Format textFormat = Format.createTextSampleFormat(null, mimeType, - SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); - final MediaSource textSource = dataSource.getSampleMediaSourceFactory() - .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); - mediaSources.add(textSource); - } - - if (mediaSources.size() == 1) { - return mediaSources.get(0); - } else { - return new MergingMediaSource(mediaSources.toArray( - new MediaSource[mediaSources.size()])); - } + return resolver.resolve(info); } /*////////////////////////////////////////////////////////////////////////// @@ -460,7 +400,6 @@ public abstract class VideoPlayer extends BasePlayer if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); - animateView(endScreen, false, 0); loadingPanel.setBackgroundColor(Color.BLACK); animateView(loadingPanel, true, 0); animateView(surfaceForeground, true, 100); @@ -470,6 +409,8 @@ public abstract class VideoPlayer extends BasePlayer public void onPlaying() { super.onPlaying(); + updateStreamRelatedViews(); + showAndAnimateControl(-1, true); playbackSeekBar.setEnabled(true); @@ -480,14 +421,12 @@ public abstract class VideoPlayer extends BasePlayer loadingPanel.setVisibility(View.GONE); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); - animateView(endScreen, false, 0); } @Override public void onBuffering() { if (DEBUG) Log.d(TAG, "onBuffering() called"); loadingPanel.setBackgroundColor(Color.TRANSPARENT); - animateView(loadingPanel, true, 500); } @Override @@ -552,8 +491,7 @@ public abstract class VideoPlayer extends BasePlayer final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT); if (captionTextView == null) return; - if (trackSelector == null || trackSelector.getCurrentMappedTrackInfo() == null || - textRenderer == RENDERER_UNAVAILABLE) { + if (trackSelector.getCurrentMappedTrackInfo() == null || textRenderer == RENDERER_UNAVAILABLE) { captionTextView.setVisibility(View.GONE); return; } @@ -575,8 +513,8 @@ public abstract class VideoPlayer extends BasePlayer // Build UI buildCaptionMenu(availableLanguages); - if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null || - !availableLanguages.contains(preferredLanguage)) { + if (trackSelector.getParameters().getRendererDisabled(textRenderer) || + preferredLanguage == null || !availableLanguages.contains(preferredLanguage)) { captionTextView.setText(R.string.caption_none); } else { captionTextView.setText(preferredLanguage); @@ -905,11 +843,12 @@ public abstract class VideoPlayer extends BasePlayer //////////////////////////////////////////////////////////////////////////*/ public void setPlaybackQuality(final String quality) { - this.playbackQuality = quality; + this.resolver.setPlaybackQuality(quality); } + @Nullable public String getPlaybackQuality() { - return playbackQuality; + return resolver.getPlaybackQuality(); } public AspectRatioFrameLayout getAspectRatioFrameLayout() { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java index b174ed3ed..63c0bf333 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -39,10 +39,13 @@ public class MediaSessionManager { return MediaButtonReceiver.handleIntent(mediaSession, intent); } + /** + * Should be called on player destruction to prevent leakage. + * */ public void dispose() { this.sessionConnector.setPlayer(null, null); this.sessionConnector.setQueueNavigator(null); this.mediaSession.setActive(false); this.mediaSession.release(); - } + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 7c7d87791..d6453f579 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -21,25 +21,34 @@ import static org.schabi.newpipe.player.BasePlayer.DEBUG; public class PlaybackParameterDialog extends DialogFragment { @NonNull private static final String TAG = "PlaybackParameterDialog"; - public static final double MINIMUM_PLAYBACK_VALUE = 0.25f; + // Minimum allowable range in ExoPlayer + public static final double MINIMUM_PLAYBACK_VALUE = 0.10f; public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f; public static final char STEP_UP_SIGN = '+'; public static final char STEP_DOWN_SIGN = '-'; - public static final double PLAYBACK_STEP_VALUE = 0.05f; - public static final double NIGHTCORE_TEMPO = 1.20f; - public static final double NIGHTCORE_PITCH_LOWER = 1.15f; - public static final double NIGHTCORE_PITCH_UPPER = 1.25f; + public static final double STEP_ONE_PERCENT_VALUE = 0.01f; + public static final double STEP_FIVE_PERCENT_VALUE = 0.05f; + public static final double STEP_TEN_PERCENT_VALUE = 0.10f; + public static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f; + public static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f; public static final double DEFAULT_TEMPO = 1.00f; public static final double DEFAULT_PITCH = 1.00f; + public static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE; + public static final boolean DEFAULT_SKIP_SILENCE = false; @NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; @NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; + @NonNull private static final String TEMPO_KEY = "tempo_key"; + @NonNull private static final String PITCH_KEY = "pitch_key"; + @NonNull private static final String STEP_SIZE_KEY = "step_size_key"; + public interface Callback { - void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch); + void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, + final boolean playbackSkipSilence); } @Nullable private Callback callback; @@ -50,6 +59,11 @@ public class PlaybackParameterDialog extends DialogFragment { private double initialTempo = DEFAULT_TEMPO; private double initialPitch = DEFAULT_PITCH; + private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE; + + private double tempo = DEFAULT_TEMPO; + private double pitch = DEFAULT_PITCH; + private double stepSize = DEFAULT_STEP; @Nullable private SeekBar tempoSlider; @Nullable private TextView tempoMinimumText; @@ -65,16 +79,26 @@ public class PlaybackParameterDialog extends DialogFragment { @Nullable private TextView pitchStepDownText; @Nullable private TextView pitchStepUpText; - @Nullable private CheckBox unhookingCheckbox; + @Nullable private TextView stepSizeOnePercentText; + @Nullable private TextView stepSizeFivePercentText; + @Nullable private TextView stepSizeTenPercentText; + @Nullable private TextView stepSizeTwentyFivePercentText; + @Nullable private TextView stepSizeOneHundredPercentText; - @Nullable private TextView nightCorePresetText; - @Nullable private TextView resetPresetText; + @Nullable private CheckBox unhookingCheckbox; + @Nullable private CheckBox skipSilenceCheckbox; public static PlaybackParameterDialog newInstance(final double playbackTempo, - final double playbackPitch) { + final double playbackPitch, + final boolean playbackSkipSilence) { PlaybackParameterDialog dialog = new PlaybackParameterDialog(); dialog.initialTempo = playbackTempo; dialog.initialPitch = playbackPitch; + + dialog.tempo = playbackTempo; + dialog.pitch = playbackPitch; + + dialog.initialSkipSilence = playbackSkipSilence; return dialog; } @@ -98,6 +122,10 @@ public class PlaybackParameterDialog extends DialogFragment { if (savedInstanceState != null) { initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH); + + tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO); + pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH); + stepSize = savedInstanceState.getDouble(STEP_SIZE_KEY, DEFAULT_STEP); } } @@ -106,6 +134,10 @@ public class PlaybackParameterDialog extends DialogFragment { super.onSaveInstanceState(outState); outState.putDouble(INITIAL_TEMPO_KEY, initialTempo); outState.putDouble(INITIAL_PITCH_KEY, initialPitch); + + outState.putDouble(TEMPO_KEY, getCurrentTempo()); + outState.putDouble(PITCH_KEY, getCurrentPitch()); + outState.putDouble(STEP_SIZE_KEY, getCurrentStepSize()); } /*////////////////////////////////////////////////////////////////////////// @@ -123,7 +155,9 @@ public class PlaybackParameterDialog extends DialogFragment { .setView(view) .setCancelable(true) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> - setPlaybackParameters(initialTempo, initialPitch)) + setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence)) + .setNeutralButton(R.string.playback_reset, (dialogInterface, i) -> + setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE)) .setPositiveButton(R.string.finish, (dialogInterface, i) -> setCurrentPlaybackParameters()); @@ -136,9 +170,13 @@ public class PlaybackParameterDialog extends DialogFragment { private void setupControlViews(@NonNull View rootView) { setupHookingControl(rootView); + setupSkipSilenceControl(rootView); + setupTempoControl(rootView); setupPitchControl(rootView); - setupPresetControl(rootView); + + changeStepSize(stepSize); + setupStepSizeSelector(rootView); } private void setupTempoControl(@NonNull View rootView) { @@ -150,31 +188,15 @@ public class PlaybackParameterDialog extends DialogFragment { tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); if (tempoCurrentText != null) - tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); + tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); if (tempoMaximumText != null) tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); if (tempoMinimumText != null) tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); - if (tempoStepUpText != null) { - tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); - tempoStepUpText.setOnClickListener(view -> { - onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE); - setCurrentPlaybackParameters(); - }); - } - - if (tempoStepDownText != null) { - tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); - tempoStepDownText.setOnClickListener(view -> { - onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE); - setCurrentPlaybackParameters(); - }); - } - if (tempoSlider != null) { tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); - tempoSlider.setProgress(strategy.progressOf(initialTempo)); + tempoSlider.setProgress(strategy.progressOf(tempo)); tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); } } @@ -188,31 +210,15 @@ public class PlaybackParameterDialog extends DialogFragment { pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); if (pitchCurrentText != null) - pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); + pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); if (pitchMaximumText != null) pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); if (pitchMinimumText != null) pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); - if (pitchStepUpText != null) { - pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); - pitchStepUpText.setOnClickListener(view -> { - onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE); - setCurrentPlaybackParameters(); - }); - } - - if (pitchStepDownText != null) { - pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); - pitchStepDownText.setOnClickListener(view -> { - onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE); - setCurrentPlaybackParameters(); - }); - } - if (pitchSlider != null) { pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); - pitchSlider.setProgress(strategy.progressOf(initialPitch)); + pitchSlider.setProgress(strategy.progressOf(pitch)); pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); } } @@ -220,7 +226,7 @@ public class PlaybackParameterDialog extends DialogFragment { private void setupHookingControl(@NonNull View rootView) { unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); if (unhookingCheckbox != null) { - unhookingCheckbox.setChecked(initialPitch != initialTempo); + unhookingCheckbox.setChecked(pitch != tempo); unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { if (isChecked) return; // When unchecked, slide back to the minimum of current tempo or pitch @@ -231,24 +237,84 @@ public class PlaybackParameterDialog extends DialogFragment { } } - private void setupPresetControl(@NonNull View rootView) { - nightCorePresetText = rootView.findViewById(R.id.presetNightcore); - if (nightCorePresetText != null) { - nightCorePresetText.setOnClickListener(view -> { - final double randomPitch = NIGHTCORE_PITCH_LOWER + - Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); + private void setupSkipSilenceControl(@NonNull View rootView) { + skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox); + if (skipSilenceCheckbox != null) { + skipSilenceCheckbox.setChecked(initialSkipSilence); + skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> + setCurrentPlaybackParameters()); + } + } - setTempoSlider(NIGHTCORE_TEMPO); - setPitchSlider(randomPitch); + private void setupStepSizeSelector(@NonNull final View rootView) { + stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent); + stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent); + stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent); + stepSizeTwentyFivePercentText = rootView.findViewById(R.id.stepSizeTwentyFivePercent); + stepSizeOneHundredPercentText = rootView.findViewById(R.id.stepSizeOneHundredPercent); + + if (stepSizeOnePercentText != null) { + stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE)); + stepSizeOnePercentText.setOnClickListener(view -> + changeStepSize(STEP_ONE_PERCENT_VALUE)); + } + + if (stepSizeFivePercentText != null) { + stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE)); + stepSizeFivePercentText.setOnClickListener(view -> + changeStepSize(STEP_FIVE_PERCENT_VALUE)); + } + + if (stepSizeTenPercentText != null) { + stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE)); + stepSizeTenPercentText.setOnClickListener(view -> + changeStepSize(STEP_TEN_PERCENT_VALUE)); + } + + if (stepSizeTwentyFivePercentText != null) { + stepSizeTwentyFivePercentText.setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE)); + stepSizeTwentyFivePercentText.setOnClickListener(view -> + changeStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE)); + } + + if (stepSizeOneHundredPercentText != null) { + stepSizeOneHundredPercentText.setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE)); + stepSizeOneHundredPercentText.setOnClickListener(view -> + changeStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE)); + } + } + + private void changeStepSize(final double stepSize) { + this.stepSize = stepSize; + + if (tempoStepUpText != null) { + tempoStepUpText.setText(getStepUpPercentString(stepSize)); + tempoStepUpText.setOnClickListener(view -> { + onTempoSliderUpdated(getCurrentTempo() + stepSize); setCurrentPlaybackParameters(); }); } - resetPresetText = rootView.findViewById(R.id.presetReset); - if (resetPresetText != null) { - resetPresetText.setOnClickListener(view -> { - setTempoSlider(DEFAULT_TEMPO); - setPitchSlider(DEFAULT_PITCH); + if (tempoStepDownText != null) { + tempoStepDownText.setText(getStepDownPercentString(stepSize)); + tempoStepDownText.setOnClickListener(view -> { + onTempoSliderUpdated(getCurrentTempo() - stepSize); + setCurrentPlaybackParameters(); + }); + } + + if (pitchStepUpText != null) { + pitchStepUpText.setText(getStepUpPercentString(stepSize)); + pitchStepUpText.setOnClickListener(view -> { + onPitchSliderUpdated(getCurrentPitch() + stepSize); + setCurrentPlaybackParameters(); + }); + } + + if (pitchStepDownText != null) { + pitchStepDownText.setText(getStepDownPercentString(stepSize)); + pitchStepDownText.setOnClickListener(view -> { + onPitchSliderUpdated(getCurrentPitch() - stepSize); setCurrentPlaybackParameters(); }); } @@ -342,10 +408,11 @@ public class PlaybackParameterDialog extends DialogFragment { //////////////////////////////////////////////////////////////////////////*/ private void setCurrentPlaybackParameters() { - setPlaybackParameters(getCurrentTempo(), getCurrentPitch()); + setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence()); } - private void setPlaybackParameters(final double tempo, final double pitch) { + private void setPlaybackParameters(final double tempo, final double pitch, + final boolean skipSilence) { if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { if (DEBUG) Log.d(TAG, "Setting playback parameters to " + "tempo=[" + tempo + "], " + @@ -353,27 +420,40 @@ public class PlaybackParameterDialog extends DialogFragment { tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); - callback.onPlaybackParameterChanged((float) tempo, (float) pitch); + callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence); } } private double getCurrentTempo() { - return tempoSlider == null ? initialTempo : strategy.valueOf( + return tempoSlider == null ? tempo : strategy.valueOf( tempoSlider.getProgress()); } private double getCurrentPitch() { - return pitchSlider == null ? initialPitch : strategy.valueOf( + return pitchSlider == null ? pitch : strategy.valueOf( pitchSlider.getProgress()); } + private double getCurrentStepSize() { + return stepSize; + } + + private boolean getCurrentSkipSilence() { + return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked(); + } + @NonNull private static String getStepUpPercentString(final double percent) { - return STEP_UP_SIGN + PlayerHelper.formatPitch(percent); + return STEP_UP_SIGN + getPercentString(percent); } @NonNull private static String getStepDownPercentString(final double percent) { - return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent); + return STEP_DOWN_SIGN + getPercentString(percent); + } + + @NonNull + private static String getPercentString(final double percent) { + return PlayerHelper.formatPitch(percent); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index dbe0e9f46..ae187a834 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; +import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.accessibility.CaptioningManager; @@ -28,6 +29,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import java.lang.annotation.Retention; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; @@ -42,6 +44,8 @@ import java.util.concurrent.TimeUnit; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; +import static java.lang.annotation.RetentionPolicy.SOURCE; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*; public class PlayerHelper { private PlayerHelper() {} @@ -51,6 +55,14 @@ public class PlayerHelper { private static final NumberFormat speedFormatter = new DecimalFormat("0.##x"); private static final NumberFormat pitchFormatter = new DecimalFormat("##%"); + @Retention(SOURCE) + @IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND, + MINIMIZE_ON_EXIT_MODE_POPUP}) + public @interface MinimizeMode { + int MINIMIZE_ON_EXIT_MODE_NONE = 0; + int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1; + int MINIMIZE_ON_EXIT_MODE_POPUP = 2; + } //////////////////////////////////////////////////////////////////////////// // Exposed helpers //////////////////////////////////////////////////////////////////////////// @@ -173,6 +185,22 @@ public class PlayerHelper { return isAutoQueueEnabled(context, false); } + @MinimizeMode + public static int getMinimizeOnExitAction(@NonNull final Context context) { + final String defaultAction = context.getString(R.string.minimize_on_exit_none_key); + final String popupAction = context.getString(R.string.minimize_on_exit_popup_key); + final String backgroundAction = context.getString(R.string.minimize_on_exit_background_key); + + final String action = getMinimizeOnExitAction(context, defaultAction); + if (action.equals(popupAction)) { + return MINIMIZE_ON_EXIT_MODE_POPUP; + } else if (action.equals(backgroundAction)) { + return MINIMIZE_ON_EXIT_MODE_BACKGROUND; + } else { + return MINIMIZE_ON_EXIT_MODE_NONE; + } + } + @NonNull public static SeekParameters getSeekParameters(@NonNull final Context context) { return isUsingInexactSeek(context, false) ? @@ -213,7 +241,6 @@ public class PlayerHelper { public static TrackSelection.Factory getQualitySelector(@NonNull final Context context, @NonNull final BandwidthMeter meter) { return new AdaptiveTrackSelection.Factory(meter, - AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE, /*bufferDurationRequiredForQualityIncrease=*/1000, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -224,10 +251,6 @@ public class PlayerHelper { return true; } - public static int getShutdownFlingVelocity(@NonNull final Context context) { - return 10000; - } - public static int getTossFlingVelocity(@NonNull final Context context) { return 2500; } @@ -249,7 +272,6 @@ public class PlayerHelper { * System font scaling: * Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f * */ - @NonNull public static float getCaptionScale(@NonNull final Context context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1f; @@ -322,4 +344,10 @@ public class PlayerHelper { return sp.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness); } } + + private static String getMinimizeOnExitAction(@NonNull final Context context, + final String key) { + return getPreferences(context).getString(context.getString(R.string.minimize_on_exit_key), + key); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index 8d498a9bf..2f233c464 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -4,6 +4,7 @@ import android.support.annotation.NonNull; import android.util.Log; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.upstream.Allocator; @@ -11,7 +12,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import java.io.IOException; -public class FailedMediaSource implements ManagedMediaSource { +public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource { private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); public static class FailedMediaSourceException extends Exception { @@ -72,11 +73,6 @@ public class FailedMediaSource implements ManagedMediaSource { return System.currentTimeMillis() >= retryTimestamp; } - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - Log.e(TAG, "Loading failed source: ", error); - } - @Override public void maybeThrowSourceInfoRefreshError() throws IOException { throw new IOException(error); @@ -90,8 +86,14 @@ public class FailedMediaSource implements ManagedMediaSource { @Override public void releasePeriod(MediaPeriod mediaPeriod) {} + @Override - public void releaseSource() {} + protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + Log.e(TAG, "Loading failed source: ", error); + } + + @Override + protected void releaseSourceInternal() {} @Override public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index 1a9cfeb4d..c39b0a03d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.player.mediasource; +import android.os.Handler; import android.support.annotation.NonNull; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.upstream.Allocator; import org.schabi.newpipe.player.playqueue.PlayQueueItem; @@ -34,7 +36,8 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, + SourceInfoRefreshListener listener) { source.prepareSource(player, isTopLevelSource, listener); } @@ -54,8 +57,18 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public void releaseSource() { - source.releaseSource(); + public void releaseSource(SourceInfoRefreshListener listener) { + source.releaseSource(listener); + } + + @Override + public void addEventListener(Handler handler, MediaSourceEventListener eventListener) { + source.addEventListener(handler, eventListener); + } + + @Override + public void removeEventListener(MediaSourceEventListener eventListener) { + source.removeEventListener(eventListener); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java index 310f1062b..5fe107657 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java @@ -3,14 +3,14 @@ package org.schabi.newpipe.player.mediasource; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ShuffleOrder; public class ManagedMediaSourcePlaylist { - @NonNull private final DynamicConcatenatingMediaSource internalSource; + @NonNull private final ConcatenatingMediaSource internalSource; public ManagedMediaSourcePlaylist() { - internalSource = new DynamicConcatenatingMediaSource(/*isPlaylistAtomic=*/false, + internalSource = new ConcatenatingMediaSource(/*isPlaylistAtomic=*/false, new ShuffleOrder.UnshuffledShuffleOrder(0)); } @@ -32,12 +32,8 @@ public class ManagedMediaSourcePlaylist { null : (ManagedMediaSource) internalSource.getMediaSource(index); } - public void dispose() { - internalSource.releaseSource(); - } - @NonNull - public DynamicConcatenatingMediaSource getParentMediaSource() { + public ConcatenatingMediaSource getParentMediaSource() { return internalSource; } @@ -46,7 +42,7 @@ public class ManagedMediaSourcePlaylist { //////////////////////////////////////////////////////////////////////////*/ /** - * Expands the {@link DynamicConcatenatingMediaSource} by appending it with a + * Expands the {@link ConcatenatingMediaSource} by appending it with a * {@link PlaceholderMediaSource}. * * @see #append(ManagedMediaSource) @@ -56,17 +52,17 @@ public class ManagedMediaSourcePlaylist { } /** - * Appends a {@link ManagedMediaSource} to the end of {@link DynamicConcatenatingMediaSource}. - * @see DynamicConcatenatingMediaSource#addMediaSource + * Appends a {@link ManagedMediaSource} to the end of {@link ConcatenatingMediaSource}. + * @see ConcatenatingMediaSource#addMediaSource * */ public synchronized void append(@NonNull final ManagedMediaSource source) { internalSource.addMediaSource(source); } /** - * Removes a {@link ManagedMediaSource} from {@link DynamicConcatenatingMediaSource} + * Removes a {@link ManagedMediaSource} from {@link ConcatenatingMediaSource} * at the given index. If this index is out of bound, then the removal is ignored. - * @see DynamicConcatenatingMediaSource#removeMediaSource(int) + * @see ConcatenatingMediaSource#removeMediaSource(int) * */ public synchronized void remove(final int index) { if (index < 0 || index > internalSource.getSize()) return; @@ -75,10 +71,10 @@ public class ManagedMediaSourcePlaylist { } /** - * Moves a {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource} + * Moves a {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} * from the given source index to the target index. If either index is out of bound, * then the call is ignored. - * @see DynamicConcatenatingMediaSource#moveMediaSource(int, int) + * @see ConcatenatingMediaSource#moveMediaSource(int, int) * */ public synchronized void move(final int source, final int target) { if (source < 0 || target < 0) return; @@ -99,7 +95,7 @@ public class ManagedMediaSourcePlaylist { } /** - * Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource} + * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} * at the given index with a given {@link ManagedMediaSource}. * @see #update(int, ManagedMediaSource, Runnable) * */ @@ -108,11 +104,11 @@ public class ManagedMediaSourcePlaylist { } /** - * Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource} + * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} * at the given index with a given {@link ManagedMediaSource}. If the index is out of bound, * then the replacement is ignored. - * @see DynamicConcatenatingMediaSource#addMediaSource - * @see DynamicConcatenatingMediaSource#removeMediaSource(int, Runnable) + * @see ConcatenatingMediaSource#addMediaSource + * @see ConcatenatingMediaSource#removeMediaSource(int, Runnable) * */ public synchronized void update(final int index, @NonNull final ManagedMediaSource source, @Nullable final Runnable finalizingAction) { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index 318f9a316..bfd734393 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -3,20 +3,19 @@ package org.schabi.newpipe.player.mediasource; import android.support.annotation.NonNull; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.upstream.Allocator; import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import java.io.IOException; - -public class PlaceholderMediaSource implements ManagedMediaSource { +public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource { // Do nothing, so this will stall the playback - @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {} - @Override public void maybeThrowSourceInfoRefreshError() throws IOException {} + @Override public void maybeThrowSourceInfoRefreshError() {} @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; } @Override public void releasePeriod(MediaPeriod mediaPeriod) {} - @Override public void releaseSource() {} + @Override protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {} + @Override protected void releaseSourceInternal() {} @Override public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 8ab3cba98..b27dc3dd6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -5,12 +5,10 @@ import android.support.annotation.Nullable; import android.support.v4.util.ArraySet; import android.util.Log; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.mediasource.FailedMediaSource; import org.schabi.newpipe.player.mediasource.LoadedMediaSource; import org.schabi.newpipe.player.mediasource.ManagedMediaSource; @@ -24,10 +22,8 @@ import org.schabi.newpipe.player.playqueue.events.RemoveEvent; import org.schabi.newpipe.player.playqueue.events.ReorderEvent; import org.schabi.newpipe.util.ServiceHelper; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -37,8 +33,6 @@ import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.disposables.SerialDisposable; -import io.reactivex.functions.Consumer; import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; @@ -104,7 +98,6 @@ public class MediaSourceManager { private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; @NonNull private final CompositeDisposable loaderReactor; @NonNull private final Set loadingItems; - @NonNull private final SerialDisposable syncReactor; @NonNull private final AtomicBoolean isBlocked; @@ -144,7 +137,6 @@ public class MediaSourceManager { this.playQueueReactor = EmptySubscription.INSTANCE; this.loaderReactor = new CompositeDisposable(); - this.syncReactor = new SerialDisposable(); this.isBlocked = new AtomicBoolean(false); @@ -171,8 +163,6 @@ public class MediaSourceManager { playQueueReactor.cancel(); loaderReactor.dispose(); - syncReactor.dispose(); - playlist.dispose(); } /*////////////////////////////////////////////////////////////////////////// @@ -311,21 +301,7 @@ public class MediaSourceManager { final PlayQueueItem currentItem = playQueue.getItem(); if (isBlocked.get() || currentItem == null) return; - final Consumer onSuccess = info -> syncInternal(currentItem, info); - final Consumer onError = throwable -> syncInternal(currentItem, null); - - final Disposable sync = currentItem.getStream() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onSuccess, onError); - syncReactor.set(sync); - } - - private void syncInternal(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info) { - // Ensure the current item is up to date with the play queue - if (playQueue.getItem() == item) { - playbackListener.onPlaybackSynchronize(item, info); - } + playbackListener.onPlaybackSynchronize(currentItem); } private synchronized void maybeSynchronizePlayer() { @@ -424,7 +400,8 @@ public class MediaSourceManager { } /** - * Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource} + * Checks if the corresponding MediaSource in + * {@link com.google.android.exoplayer2.source.ConcatenatingMediaSource} * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback * readiness or playlist desynchronization. *

@@ -481,8 +458,6 @@ public class MediaSourceManager { private void resetSources() { if (DEBUG) Log.d(TAG, "resetSources() called."); - - playlist.dispose(); playlist = new ManagedMediaSourcePlaylist(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index 4dcb30aa3..238bdfcd0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -45,7 +45,7 @@ public interface PlaybackListener { * * May be called anytime at any amount once unblock is called. * */ - void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); + void onPlaybackSynchronize(@NonNull final PlayQueueItem item); /** * Requests the listener to resolve a stream info into a media source diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index a21560abd..c9e07c96a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -6,6 +6,7 @@ import android.util.Log; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.player.playqueue.events.AppendEvent; import org.schabi.newpipe.player.playqueue.events.ErrorEvent; import org.schabi.newpipe.player.playqueue.events.InitEvent; @@ -41,7 +42,7 @@ import io.reactivex.subjects.BehaviorSubject; public abstract class PlayQueue implements Serializable { private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode()); - public static final boolean DEBUG = true; + public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); private ArrayList backup; private ArrayList streams; diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java new file mode 100644 index 000000000..6bb556850 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java @@ -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); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java new file mode 100644 index 000000000..bbe5d33ca --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java @@ -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 sortedAvailableVideoStreams; + private final int selectedVideoStreamIndex; + + public MediaSourceTag(@NonNull final StreamInfo metadata, + @NonNull final List 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 getSortedAvailableVideoStreams() { + return sortedAvailableVideoStreams; + } + + public int getSelectedVideoStreamIndex() { + return selectedVideoStreamIndex; + } + + @Nullable + public VideoStream getSelectedVideoStream() { + return selectedVideoStreamIndex < 0 || + selectedVideoStreamIndex >= sortedAvailableVideoStreams.size() ? null : + sortedAvailableVideoStreams.get(selectedVideoStreamIndex); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java new file mode 100644 index 000000000..1da3ec211 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java @@ -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 { + + @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); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java new file mode 100644 index 000000000..4bd795574 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java @@ -0,0 +1,8 @@ +package org.schabi.newpipe.player.resolver; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public interface Resolver { + @Nullable Product resolve(@NonNull Source source); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java new file mode 100644 index 000000000..8f91f4886 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -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 sortedVideos); + int getOverrideResolutionIndex(final List 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 mediaSources = new ArrayList<>(); + + // Create video stream source + final List 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 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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/report/UserAction.java b/app/src/main/java/org/schabi/newpipe/report/UserAction.java index 93a3ce16c..00a25ed8d 100644 --- a/app/src/main/java/org/schabi/newpipe/report/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/report/UserAction.java @@ -15,7 +15,8 @@ public enum UserAction { REQUESTED_CHANNEL("requested channel"), REQUESTED_PLAYLIST("requested playlist"), REQUESTED_KIOSK("requested kiosk"), - DELETE_FROM_HISTORY("delete from history"); + DELETE_FROM_HISTORY("delete from history"), + PLAY_STREAM("Play stream"); private final String message; diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index a02a9df34..0ca78b34a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -9,6 +9,7 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; import android.support.v7.preference.ListPreference; import android.support.v7.preference.Preference; import android.util.Log; @@ -20,15 +21,12 @@ import com.nostra13.universalimageloader.core.ImageLoader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.ZipHelper; -import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; @@ -42,11 +40,8 @@ import java.util.Date; import java.util.Locale; import java.util.Map; import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; -import static android.content.Context.MODE_PRIVATE; - public class ContentSettingsFragment extends BasePreferenceFragment { private static final int REQUEST_IMPORT_PATH = 8945; @@ -56,6 +51,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { private File databasesDir; private File newpipe_db; private File newpipe_db_journal; + private File newpipe_db_shm; + private File newpipe_db_wal; private File newpipe_settings; private String thumbnailLoadToggleKey; @@ -88,73 +85,14 @@ public class ContentSettingsFragment extends BasePreferenceFragment { databasesDir = new File(homeDir + "/databases"); newpipe_db = new File(homeDir + "/databases/newpipe.db"); newpipe_db_journal = new File(homeDir + "/databases/newpipe.db-journal"); + newpipe_db_shm = new File(homeDir + "/databases/newpipe.db-shm"); + newpipe_db_wal = new File(homeDir + "/databases/newpipe.db-wal"); + newpipe_settings = new File(homeDir + "/databases/newpipe.settings"); newpipe_settings.delete(); addPreferencesFromResource(R.xml.content_settings); - final ListPreference mainPageContentPref = (ListPreference) findPreference(getString(R.string.main_page_content_key)); - mainPageContentPref.setOnPreferenceChangeListener((Preference preference, Object newValueO) -> { - final String newValue = newValueO.toString(); - - final String mainPrefOldValue = - defaultPreferences.getString(getString(R.string.main_page_content_key), "blank_page"); - final String mainPrefOldSummary = getMainPagePrefSummery(mainPrefOldValue, mainPageContentPref); - - if(newValue.equals(getString(R.string.kiosk_page_key))) { - SelectKioskFragment selectKioskFragment = new SelectKioskFragment(); - selectKioskFragment.setOnSelectedLisener((String kioskId, int service_id) -> { - defaultPreferences.edit() - .putInt(getString(R.string.main_page_selected_service), service_id).apply(); - defaultPreferences.edit() - .putString(getString(R.string.main_page_selectd_kiosk_id), kioskId).apply(); - String serviceName = ""; - try { - serviceName = NewPipe.getService(service_id).getServiceInfo().getName(); - } catch (ExtractionException e) { - onError(e); - } - String kioskName = KioskTranslator.getTranslatedKioskName(kioskId, - getContext()); - - String summary = - String.format(getString(R.string.service_kiosk_string), - serviceName, - kioskName); - - mainPageContentPref.setSummary(summary); - }); - selectKioskFragment.setOnCancelListener(() -> { - mainPageContentPref.setSummary(mainPrefOldSummary); - mainPageContentPref.setValue(mainPrefOldValue); - }); - selectKioskFragment.show(getFragmentManager(), "select_kiosk"); - } else if(newValue.equals(getString(R.string.channel_page_key))) { - SelectChannelFragment selectChannelFragment = new SelectChannelFragment(); - selectChannelFragment.setOnSelectedLisener((String url, String name, int service) -> { - defaultPreferences.edit() - .putInt(getString(R.string.main_page_selected_service), service).apply(); - defaultPreferences.edit() - .putString(getString(R.string.main_page_selected_channel_url), url).apply(); - defaultPreferences.edit() - .putString(getString(R.string.main_page_selected_channel_name), name).apply(); - - mainPageContentPref.setSummary(name); - }); - selectChannelFragment.setOnCancelListener(() -> { - mainPageContentPref.setSummary(mainPrefOldSummary); - mainPageContentPref.setValue(mainPrefOldValue); - }); - selectChannelFragment.show(getFragmentManager(), "select_channel"); - } else { - mainPageContentPref.setSummary(getMainPageSummeryByKey(newValue)); - } - - defaultPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); - - return true; - }); - Preference importDataPreference = findPreference(getString(R.string.import_data)); importDataPreference.setOnPreferenceClickListener((Preference p) -> { Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) @@ -207,7 +145,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { new BufferedOutputStream( new FileOutputStream(path))); ZipHelper.addFileToZip(outZip, newpipe_db.getPath(), "newpipe.db"); - ZipHelper.addFileToZip(outZip, newpipe_db_journal.getPath(), "newpipe.db-journal"); + saveSharedPreferencesToFile(newpipe_settings); ZipHelper.addFileToZip(outZip, newpipe_settings.getPath(), "newpipe.settings"); @@ -263,8 +201,16 @@ public class ContentSettingsFragment extends BasePreferenceFragment { throw new Exception("Could not create databases dir"); } - if(!(ZipHelper.extractFileFromZip(filePath, newpipe_db.getPath(), "newpipe.db") - && ZipHelper.extractFileFromZip(filePath, newpipe_db_journal.getPath(), "newpipe.db-journal"))) { + final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, + newpipe_db.getPath(), "newpipe.db"); + + if (isDbFileExtracted) { + newpipe_db_journal.delete(); + newpipe_db_wal.delete(); + newpipe_db_shm.delete(); + + } else { + Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG) .show(); } @@ -336,66 +282,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } } - @Override - public void onResume() { - super.onResume(); - - final String mainPageContentKey = getString(R.string.main_page_content_key); - final Preference mainPagePref = findPreference(getString(R.string.main_page_content_key)); - final String bpk = getString(R.string.blank_page_key); - if(defaultPreferences.getString(mainPageContentKey, bpk) - .equals(getString(R.string.channel_page_key))) { - mainPagePref.setSummary(defaultPreferences.getString(getString(R.string.main_page_selected_channel_name), "error")); - } else if(defaultPreferences.getString(mainPageContentKey, bpk) - .equals(getString(R.string.kiosk_page_key))) { - try { - StreamingService service = NewPipe.getService( - defaultPreferences.getInt( - getString(R.string.main_page_selected_service), 0)); - - String kioskName = KioskTranslator.getTranslatedKioskName( - defaultPreferences.getString( - getString(R.string.main_page_selectd_kiosk_id), "Trending"), - getContext()); - - String summary = - String.format(getString(R.string.service_kiosk_string), - service.getServiceInfo().getName(), - kioskName); - - mainPagePref.setSummary(summary); - } catch (Exception e) { - onError(e); - } - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - private String getMainPagePrefSummery(final String mainPrefOldValue, final ListPreference mainPageContentPref) { - if(mainPrefOldValue.equals(getString(R.string.channel_page_key))) { - return defaultPreferences.getString(getString(R.string.main_page_selected_channel_name), "error"); - } else { - return mainPageContentPref.getSummary().toString(); - } - } - - private int getMainPageSummeryByKey(final String key) { - if(key.equals(getString(R.string.blank_page_key))) { - return R.string.blank_page_summary; - } else if(key.equals(getString(R.string.kiosk_page_key))) { - return R.string.kiosk_page_summary; - } else if(key.equals(getString(R.string.feed_page_key))) { - return R.string.feed_page_summary; - } else if(key.equals(getString(R.string.subscription_page_key))) { - return R.string.subscription_page_summary; - } else if(key.equals(getString(R.string.channel_page_key))) { - return R.string.channel_page_summary; - } - return R.string.blank_page_summary; - } - /*////////////////////////////////////////////////////////////////////////// // Error //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 92f98a9a2..2a0e2645b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -71,7 +71,7 @@ public class NewPipeSettings { } public static File getVideoDownloadFolder(Context context) { - return getFolder(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES); + return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES); } public static String getVideoDownloadPath(Context context) { @@ -81,7 +81,7 @@ public class NewPipeSettings { } public static File getAudioDownloadFolder(Context context) { - return getFolder(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); + return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); } public static String getAudioDownloadPath(Context context) { @@ -90,21 +90,37 @@ public class NewPipeSettings { return prefs.getString(key, Environment.DIRECTORY_MUSIC); } - private static File getFolder(Context context, int keyID, String defaultDirectoryName) { + private static File getDir(Context context, int keyID, String defaultDirectoryName) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final String key = context.getString(keyID); String downloadPath = prefs.getString(key, null); if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim()); - final File folder = getFolder(defaultDirectoryName); + final File dir = getDir(defaultDirectoryName); SharedPreferences.Editor spEditor = prefs.edit(); - spEditor.putString(key, new File(folder, "NewPipe").getAbsolutePath()); + spEditor.putString(key, getNewPipeChildFolderPathForDir(dir)); spEditor.apply(); - return folder; + return dir; } @NonNull - private static File getFolder(String defaultDirectoryName) { + private static File getDir(String defaultDirectoryName) { return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName); } + + public static void resetDownloadFolders(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC); + resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES); + } + + private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) { + SharedPreferences.Editor spEditor = prefs.edit(); + spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); + spEditor.apply(); + } + + private static String getNewPipeChildFolderPathForDir(File dir) { + return new File(dir, "NewPipe").getAbsolutePath(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java index e961de969..0ebdbefe0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java @@ -66,7 +66,7 @@ public class SelectChannelFragment extends DialogFragment { //////////////////////////////////////////////////////////////////////////*/ public interface OnSelectedLisener { - void onChannelSelected(String url, String name, int service); + void onChannelSelected(int serviceId, String url, String name); } OnSelectedLisener onSelectedLisener = null; public void setOnSelectedLisener(OnSelectedLisener listener) { @@ -126,7 +126,7 @@ public class SelectChannelFragment extends DialogFragment { private void clickedItem(int position) { if(onSelectedLisener != null) { SubscriptionEntity entry = subscriptions.get(position); - onSelectedLisener.onChannelSelected(entry.getUrl(), entry.getName(), entry.getServiceId()); + onSelectedLisener.onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName()); } dismiss(); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java index 00b618889..44cb16682 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java @@ -56,7 +56,7 @@ public class SelectKioskFragment extends DialogFragment { //////////////////////////////////////////////////////////////////////////*/ public interface OnSelectedLisener { - void onKioskSelected(String kioskId, int service_id); + void onKioskSelected(int serviceId, String kioskId, String kioskName); } OnSelectedLisener onSelectedLisener = null; @@ -101,7 +101,7 @@ public class SelectKioskFragment extends DialogFragment { private void clickedItem(SelectKioskAdapter.Entry entry) { if(onSelectedLisener != null) { - onSelectedLisener.onKioskSelected(entry.kioskId, entry.serviceId); + onSelectedLisener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName); } dismiss(); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 7d6f8d633..a8482e0eb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -77,7 +77,8 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc finish(); } else getSupportFragmentManager().popBackStack(); } - return true; + + return super.onOptionsItemSelected(item); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java new file mode 100644 index 000000000..695f81ff5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java @@ -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; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java new file mode 100644 index 000000000..b86f13d14 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -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 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 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 { + 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); + } + } + }; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java new file mode 100644 index 000000000..d7c249a3e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -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, ""); + } + + 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, ""); + } + + 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, "", ""); + } + + 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, ""); + channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, ""); + } + + public int getChannelServiceId() { + return channelServiceId; + } + + public String getChannelUrl() { + return channelUrl; + } + + public String getChannelName() { + return channelName; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java new file mode 100644 index 000000000..332e244c8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java @@ -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 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. + *

+ * 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. + *

+ * 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 getTabsFromJson(@Nullable String tabsJson) throws InvalidJsonException { + if (tabsJson == null || tabsJson.isEmpty()) { + return FALLBACK_INITIAL_TABS_LIST; + } + + final List 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 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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java new file mode 100644 index 000000000..a7d8dffa4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java @@ -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 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 tabList) { + final String jsonToSave = TabsJsonHelper.getJsonToSave(tabList); + sharedPreferences.edit().putString(savedTabsKey, jsonToSave).apply(); + } + + public void resetTabs() { + sharedPreferences.edit().remove(savedTabsKey).apply(); + } + + public List 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(); + } + }; + } + +} + + + + + + + diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java index a6aec96e2..b01b6df6a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Constants.java +++ b/app/src/main/java/org/schabi/newpipe/util/Constants.java @@ -6,7 +6,7 @@ public class Constants { public static final String KEY_TITLE = "key_title"; public static final String KEY_LINK_TYPE = "key_link_type"; public static final String KEY_OPEN_SEARCH = "key_open_search"; - public static final String KEY_QUERY = "key_query"; + public static final String KEY_SEARCH_STRING = "key_search_string"; public static final String KEY_THEME_CHANGE = "key_theme_change"; public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 1897589c6..e445233c3 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -37,9 +37,8 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; -import org.schabi.newpipe.extractor.search.SearchEngine; -import org.schabi.newpipe.extractor.search.SearchResult; -import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.search.SearchInfo; +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -50,7 +49,6 @@ import java.util.List; import io.reactivex.Maybe; import io.reactivex.Single; -import io.reactivex.annotations.NonNull; public final class ExtractorHelper { private static final String TAG = ExtractorHelper.class.getSimpleName(); @@ -66,29 +64,35 @@ public final class ExtractorHelper { } } - public static Single searchFor(final int serviceId, - final String query, - final int pageNumber, - final String contentCountry, - final SearchEngine.Filter filter) { + public static Single searchFor(final int serviceId, + final String searchString, + final List contentFilter, + final String sortFilter, + final String contentCountry) { checkServiceId(serviceId); return Single.fromCallable(() -> - SearchResult.getSearchResult(NewPipe.getService(serviceId).getSearchEngine(), - query, pageNumber, contentCountry, filter) - ); + SearchInfo.getInfo(NewPipe.getService(serviceId), + NewPipe.getService(serviceId) + .getSearchQHFactory() + .fromQuery(searchString, contentFilter, sortFilter), + contentCountry)); } public static Single getMoreSearchItems(final int serviceId, - final String query, - final int nextPageNumber, - final String searchLanguage, - final SearchEngine.Filter filter) { + final String searchString, + final List contentFilter, + final String sortFilter, + final String pageUrl, + final String contentCountry) { checkServiceId(serviceId); - return searchFor(serviceId, query, nextPageNumber, searchLanguage, filter) - .map((@NonNull SearchResult searchResult) -> - new InfoItemsPage(searchResult.resultList, - nextPageNumber + "", - searchResult.errors)); + return Single.fromCallable(() -> + SearchInfo.getMoreItems(NewPipe.getService(serviceId), + NewPipe.getService(serviceId) + .getSearchQHFactory() + .fromQuery(searchString, contentFilter, sortFilter), + contentCountry, + pageUrl)); + } public static Single> suggestionsFor(final int serviceId, @@ -233,7 +237,6 @@ public final class ExtractorHelper { serviceId == -1 ? "none" : NewPipe.getNameOfService(serviceId), url + (optionalErrorMessage == null ? "" : optionalErrorMessage), errorId)); } }); - } /** diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java index 4740b82e0..392892cef 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java +++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java @@ -24,7 +24,7 @@ import org.schabi.newpipe.R; public class KioskTranslator { public static String getTranslatedKioskName(String kioskId, Context c) { - switch(kioskId) { + switch (kioskId) { case "Trending": return c.getString(R.string.trending); case "Top 50": @@ -35,4 +35,17 @@ public class KioskTranslator { return kioskId; } } + + public static int getKioskIcons(String kioskId, Context c) { + switch(kioskId) { + case "Trending": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); + case "Top 50": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); + case "New & hot": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); + default: + return 0; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index 4f607b581..1a5bf14f7 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -443,11 +443,11 @@ public final class ListHelper { /** * Are we connected to wifi? * @param context App context - * @return True if connected to wifi + * @return {@code true} if connected to wifi */ private static boolean isWifiActive(Context context) { ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - return manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI; + return manager != null && manager.getActiveNetworkInfo() != null && manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI; } } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index ebbeb06f8..13767125d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -26,6 +26,7 @@ import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; @@ -33,12 +34,14 @@ import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; +import org.schabi.newpipe.local.bookmark.BookmarkFragment; import org.schabi.newpipe.local.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; +import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; @@ -100,11 +103,13 @@ public class NavigationHelper { final int repeatMode, final float playbackSpeed, final float playbackPitch, + final boolean playbackSkipSilence, @Nullable final String playbackQuality) { return getPlayerIntent(context, targetClazz, playQueue, playbackQuality) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) - .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch); + .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch) + .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence); } public static void playOnMainPlayer(final Context context, final PlayQueue queue) { @@ -281,9 +286,11 @@ public class NavigationHelper { return fragmentManager.popBackStackImmediate(SEARCH_FRAGMENT_TAG, 0); } - public static void openSearchFragment(FragmentManager fragmentManager, int serviceId, String query) { + public static void openSearchFragment(FragmentManager fragmentManager, + int serviceId, + String searchString) { defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, query)) + .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, searchString)) .addToBackStack(SEARCH_FRAGMENT_TAG) .commit(); } @@ -312,7 +319,11 @@ public class NavigationHelper { .commit(); } - public static void openChannelFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { + public static void openChannelFragment( + FragmentManager fragmentManager, + int serviceId, + String url, + String name) { if (name == null) name = ""; defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name)) @@ -320,7 +331,10 @@ public class NavigationHelper { .commit(); } - public static void openPlaylistFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { + public static void openPlaylistFragment(FragmentManager fragmentManager, + int serviceId, + String url, + String name) { if (name == null) name = ""; defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name)) @@ -335,6 +349,20 @@ public class NavigationHelper { .commit(); } + public static void openBookmarksFragment(FragmentManager fragmentManager) { + defaultTransaction(fragmentManager) + .replace(R.id.fragment_holder, new BookmarkFragment()) + .addToBackStack(null) + .commit(); + } + + public static void openSubscriptionFragment(FragmentManager fragmentManager) { + defaultTransaction(fragmentManager) + .replace(R.id.fragment_holder, new SubscriptionFragment()) + .addToBackStack(null) + .commit(); + } + public static void openKioskFragment(FragmentManager fragmentManager, int serviceId, String kioskId) throws ExtractionException { defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, KioskFragment.getInstance(serviceId, kioskId)) @@ -368,10 +396,10 @@ public class NavigationHelper { // Through Intents //////////////////////////////////////////////////////////////////////////*/ - public static void openSearch(Context context, int serviceId, String query) { + public static void openSearch(Context context, int serviceId, String searchString) { Intent mIntent = new Intent(context, MainActivity.class); mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); - mIntent.putExtra(Constants.KEY_QUERY, query); + mIntent.putExtra(Constants.KEY_SEARCH_STRING, searchString); mIntent.putExtra(Constants.KEY_OPEN_SEARCH, true); context.startActivity(mIntent); } @@ -465,7 +493,8 @@ public class NavigationHelper { switch (linkType) { case STREAM: - rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context) + rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, + PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(R.string.autoplay_through_intent_key), false)); break; } diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index d86f27f2f..7c781eb14 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -5,7 +5,6 @@ import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; import android.support.annotation.StringRes; -import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.ServiceList; @@ -31,6 +30,18 @@ public class ServiceHelper { } } + public static String getTranslatedFilterString(String filter, Context c) { + switch(filter) { + case "all": return c.getString(R.string.all); + case "videos": return c.getString(R.string.videos); + case "channels": return c.getString(R.string.channels); + case "playlists": return c.getString(R.string.playlists); + case "tracks": return c.getString(R.string.tracks); + case "users": return c.getString(R.string.users); + default: return filter; + } + } + /** * Get a resource string with instructions for importing subscriptions for each service. * diff --git a/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java index ecd3ce562..3294f5164 100755 --- a/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java @@ -1,10 +1,17 @@ package us.shandian.giga.get; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; +import org.schabi.newpipe.download.ExtSDDownloadFailedActivity; + import java.io.File; import java.io.FilenameFilter; +import java.io.IOException; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; @@ -23,7 +30,9 @@ public class DownloadManagerImpl implements DownloadManager { private static final String TAG = DownloadManagerImpl.class.getSimpleName(); private final DownloadDataSource mDownloadDataSource; - private final ArrayList mMissions = new ArrayList(); + private final ArrayList mMissions = new ArrayList<>(); + @NonNull + private final Context context; /** * Create a new instance @@ -33,6 +42,13 @@ public class DownloadManagerImpl implements DownloadManager { */ public DownloadManagerImpl(Collection searchLocations, DownloadDataSource downloadDataSource) { mDownloadDataSource = downloadDataSource; + this.context = null; + loadMissions(searchLocations); + } + + public DownloadManagerImpl(Collection searchLocations, DownloadDataSource downloadDataSource, Context context) { + mDownloadDataSource = downloadDataSource; + this.context = context; loadMissions(searchLocations); } @@ -277,10 +293,12 @@ public class DownloadManagerImpl implements DownloadManager { } private class Initializer extends Thread { - private DownloadMission mission; + private final DownloadMission mission; + private final Handler handler; public Initializer(DownloadMission mission) { this.mission = mission; + this.handler = new Handler(); } @Override @@ -335,6 +353,13 @@ public class DownloadManagerImpl implements DownloadManager { af.close(); mission.start(); + } catch (IOException ie) { + if(context == null) throw new RuntimeException(ie); + + if(ie.getMessage().contains("Permission denied")) { + handler.post(() -> + context.startActivity(new Intent(context, ExtSDDownloadFailedActivity.class))); + } else throw new RuntimeException(ie); } catch (Exception e) { // TODO Notify throw new RuntimeException(e); diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index 50975728f..59f5e2225 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -81,7 +81,7 @@ public class DownloadManagerService extends Service { ArrayList paths = new ArrayList<>(2); paths.add(NewPipeSettings.getVideoDownloadPath(this)); paths.add(NewPipeSettings.getAudioDownloadPath(this)); - mManager = new DownloadManagerImpl(paths, mDataSource); + mManager = new DownloadManagerImpl(paths, mDataSource, this); if (DEBUG) { Log.d(TAG, "mManager == null"); Log.d(TAG, "Download directory: " + paths); diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 12c81c127..8127c3467 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -25,10 +25,13 @@ import android.widget.TextView; import android.widget.Toast; import org.schabi.newpipe.R; +import org.schabi.newpipe.download.DeleteDownloadManager; import java.io.File; import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -52,18 +55,34 @@ public class MissionAdapter extends RecyclerView.Adapter mItemList; private DownloadManagerService.DMBinder mBinder; private int mLayout; - public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager manager, boolean isLinear) { + public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager downloadManager, DeleteDownloadManager deleteDownloadManager, boolean isLinear) { mContext = context; - mManager = manager; + mDownloadManager = downloadManager; + mDeleteDownloadManager = deleteDownloadManager; mBinder = binder; mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item; + + mItemList = new ArrayList<>(); + updateItemList(); + } + + public void updateItemList() { + mItemList.clear(); + + for (int i = 0; i < mDownloadManager.getCount(); i++) { + DownloadMission mission = mDownloadManager.getMission(i); + if (!mDeleteDownloadManager.contains(mission)) { + mItemList.add(mDownloadManager.getMission(i)); + } + } } @Override @@ -102,7 +121,7 @@ public class MissionAdapter extends RecyclerView.Adapter= Build.VERSION_CODES.LOLLIPOP) { - intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); - } - //mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - Log.v(TAG, "Starting intent: " + intent); - mContext.startActivity(intent); - } - private void viewFileWithFileProvider(File file, String mimetype) { String ourPackage = mContext.getApplicationContext().getPackageName(); Uri uri = FileProvider.getUriForFile(mContext, ourPackage + ".provider", file); diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 2ff83086f..14439f6c8 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -10,6 +10,8 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -19,13 +21,15 @@ import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.R; +import org.schabi.newpipe.download.DeleteDownloadManager; +import io.reactivex.disposables.Disposable; import us.shandian.giga.get.DownloadManager; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.ui.adapter.MissionAdapter; public abstract class MissionsFragment extends Fragment { - private DownloadManager mManager; + private DownloadManager mDownloadManager; private DownloadManagerService.DMBinder mBinder; private SharedPreferences mPrefs; @@ -37,14 +41,19 @@ public abstract class MissionsFragment extends Fragment { private GridLayoutManager mGridManager; private LinearLayoutManager mLinearManager; private Context mActivity; + private DeleteDownloadManager mDeleteDownloadManager; + private Disposable mDeleteDisposable; private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder binder) { mBinder = (DownloadManagerService.DMBinder) binder; - mManager = setupDownloadManager(mBinder); - updateList(); + mDownloadManager = setupDownloadManager(mBinder); + if (mDeleteDownloadManager != null) { + mDeleteDownloadManager.setDownloadManager(mDownloadManager); + updateList(); + } } @Override @@ -55,6 +64,14 @@ public abstract class MissionsFragment extends Fragment { }; + public void setDeleteManager(@NonNull DeleteDownloadManager deleteDownloadManager) { + mDeleteDownloadManager = deleteDownloadManager; + if (mDownloadManager != null) { + mDeleteDownloadManager.setDownloadManager(mDownloadManager); + updateList(); + } + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.missions, container, false); @@ -104,10 +121,26 @@ public abstract class MissionsFragment extends Fragment { mActivity = activity; } + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (mDeleteDownloadManager != null) { + mDeleteDisposable = mDeleteDownloadManager.getUndoObservable().subscribe(mission -> { + if (mAdapter != null) { + mAdapter.updateItemList(); + mAdapter.notifyDataSetChanged(); + } + }); + } + } + @Override public void onDestroyView() { super.onDestroyView(); getActivity().unbindService(mConnection); + if (mDeleteDisposable != null) { + mDeleteDisposable.dispose(); + } } @Override @@ -129,7 +162,7 @@ public abstract class MissionsFragment extends Fragment { } private void updateList() { - mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mManager, mLinear); + mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mDownloadManager, mDeleteDownloadManager, mLinear); if (mLinear) { mList.setLayoutManager(mLinearManager); @@ -143,7 +176,7 @@ public abstract class MissionsFragment extends Fragment { mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list); } - mPrefs.edit().putBoolean("linear", mLinear).commit(); + mPrefs.edit().putBoolean("linear", mLinear).apply(); } protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder); diff --git a/app/src/main/res/drawable-hdpi/ic_add.png b/app/src/main/res/drawable-hdpi/ic_add.png new file mode 100644 index 000000000..1ae5b2dc4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_add.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-hdpi/ic_arrow_down_white.png new file mode 100644 index 000000000..33939600d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_down_white.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-hdpi/ic_arrow_up_white.png new file mode 100644 index 000000000..0972a9bca Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_up_white.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_remove.png b/app/src/main/res/drawable-hdpi/ic_remove.png new file mode 100644 index 000000000..75e65bc9c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_remove.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_add.png b/app/src/main/res/drawable-mdpi/ic_add.png new file mode 100644 index 000000000..d51f0ddad Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_add.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-mdpi/ic_arrow_down_white.png new file mode 100644 index 000000000..40a0f499e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_down_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-mdpi/ic_arrow_up_white.png new file mode 100644 index 000000000..fe67b4673 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_up_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_remove.png b/app/src/main/res/drawable-mdpi/ic_remove.png new file mode 100644 index 000000000..a1816d4c6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_remove.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_add.png b/app/src/main/res/drawable-xhdpi/ic_add.png new file mode 100644 index 000000000..9ea0eeb7e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_add.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-xhdpi/ic_arrow_down_white.png new file mode 100644 index 000000000..86bc5db3b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_down_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-xhdpi/ic_arrow_up_white.png new file mode 100644 index 000000000..dda36882e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_up_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_remove.png b/app/src/main/res/drawable-xhdpi/ic_remove.png new file mode 100644 index 000000000..ffbdaa6ed Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_remove.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add.png b/app/src/main/res/drawable-xxhdpi/ic_add.png new file mode 100644 index 000000000..75f192aab Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_down_white.png new file mode 100644 index 000000000..7e901e098 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_down_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_up_white.png new file mode 100644 index 000000000..bc71e23de Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_up_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_remove.png b/app/src/main/res/drawable-xxhdpi/ic_remove.png new file mode 100644 index 000000000..d35469d3c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_remove.png differ diff --git a/app/src/main/res/drawable/background_oval_black_transparent.xml b/app/src/main/res/drawable/background_oval_black_transparent.xml new file mode 100644 index 000000000..5db5969c6 --- /dev/null +++ b/app/src/main/res/drawable/background_oval_black_transparent.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_black_24dp.xml b/app/src/main/res/drawable/ic_add_black_24dp.xml new file mode 100644 index 000000000..0258249cc --- /dev/null +++ b/app/src/main/res/drawable/ic_add_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_add_white_24dp.xml b/app/src/main/res/drawable/ic_add_white_24dp.xml new file mode 100644 index 000000000..e3979cd7f --- /dev/null +++ b/app/src/main/res/drawable/ic_add_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_blank_page_black_24dp.xml b/app/src/main/res/drawable/ic_blank_page_black_24dp.xml new file mode 100644 index 000000000..e8c60a1a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_blank_page_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_blank_page_white_24dp.xml b/app/src/main/res/drawable/ic_blank_page_white_24dp.xml new file mode 100644 index 000000000..86a68484f --- /dev/null +++ b/app/src/main/res/drawable/ic_blank_page_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_brightness_high_white_72dp.xml b/app/src/main/res/drawable/ic_brightness_high_white_72dp.xml new file mode 100644 index 000000000..12d0084a8 --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness_high_white_72dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_brightness_low_white_72dp.xml b/app/src/main/res/drawable/ic_brightness_low_white_72dp.xml new file mode 100644 index 000000000..9c4f2f71e --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness_low_white_72dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_brightness_medium_white_72dp.xml b/app/src/main/res/drawable/ic_brightness_medium_white_72dp.xml new file mode 100644 index 000000000..fc100086f --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness_medium_white_72dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_backup_restore_black_24dp.xml b/app/src/main/res/drawable/ic_settings_backup_restore_black_24dp.xml new file mode 100644 index 000000000..aa424c0d4 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_backup_restore_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml b/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml new file mode 100644 index 000000000..03a26f550 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_down_white_72dp.xml b/app/src/main/res/drawable/ic_volume_down_white_72dp.xml new file mode 100644 index 000000000..a7fafb3a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_down_white_72dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_mute_white_72dp.xml b/app/src/main/res/drawable/ic_volume_mute_white_72dp.xml new file mode 100644 index 000000000..1a8ab7e86 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_mute_white_72dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_off_white_72dp.xml b/app/src/main/res/drawable/ic_volume_off_white_72dp.xml new file mode 100644 index 000000000..07f24d7aa --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_off_white_72dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_up_white_72dp.xml b/app/src/main/res/drawable/ic_volume_up_white_72dp.xml new file mode 100644 index 000000000..b2fb235a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_up_white_72dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/progress_circular_white.xml b/app/src/main/res/drawable/progress_circular_white.xml new file mode 100644 index 000000000..daa6649bc --- /dev/null +++ b/app/src/main/res/drawable/progress_circular_white.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-v21/drawer_header.xml b/app/src/main/res/layout-v21/drawer_header.xml new file mode 100644 index 000000000..4cdc2b30e --- /dev/null +++ b/app/src/main/res/layout-v21/drawer_header.xml @@ -0,0 +1,71 @@ + + + +