From c45514b9896d83d858daefd5f6a58e2ae61c5bfc Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Mon, 30 Dec 2019 00:15:01 +0300 Subject: [PATCH] All players in one place - main, background, popup players now connected via one service, one view, one fragment, one activity and one gesture listener - playback position is synchronized between players. Easy to switch from one to another - expandable player at the bottom of the screen with cool animation and additional features like long click to open channel of a video, play/pause/close buttons and swipe down to dismiss - in-player integrated buttons for opening in browser, playing with Kodi, sharing a video - better background playback that can be activated in settings. Allows to automatically switch to audio-only mode when going to background and then switching to video-mode when returning to the app. --- app/src/main/AndroidManifest.xml | 8 + .../java/org/schabi/newpipe/MainActivity.java | 65 +- .../org/schabi/newpipe/RouterActivity.java | 15 +- .../newpipe/fragments/detail/StackItem.java | 8 +- .../fragments/detail/VideoDetailFragment.java | 906 ++++++++-- .../player/BackgroundPlayerActivity.java | 30 +- .../org/schabi/newpipe/player/BasePlayer.java | 28 +- .../org/schabi/newpipe/player/MainPlayer.java | 353 ++++ .../newpipe/player/MainVideoPlayer.java | 13 +- .../newpipe/player/PopupVideoPlayer.java | 11 +- .../player/PopupVideoPlayerActivity.java | 8 +- .../newpipe/player/ServicePlayerActivity.java | 41 +- .../schabi/newpipe/player/VideoPlayer.java | 13 +- .../newpipe/player/VideoPlayerImpl.java | 1552 +++++++++++++++++ .../event/CustomBottomSheetBehavior.java | 47 + .../player/event/PlayerGestureListener.java | 462 +++++ .../event/PlayerServiceEventListener.java | 15 + .../helper/PlaybackParameterDialog.java | 8 +- .../settings/SettingsContentObserver.java | 29 + .../schabi/newpipe/util/NavigationHelper.java | 66 +- .../org/schabi/newpipe/util/ShareUtils.java | 2 +- .../activity_main_player.xml | 280 +-- .../fragment_video_detail.xml | 161 +- app/src/main/res/layout/activity_main.xml | 18 + .../main/res/layout/activity_main_player.xml | 278 +-- .../main/res/layout/fragment_video_detail.xml | 164 +- app/src/main/res/menu/menu_play_queue_bg.xml | 5 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/xml/history_settings.xml | 1 - 29 files changed, 4057 insertions(+), 531 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/MainPlayer.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/SettingsContentObserver.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9052dabab..0f8339f81 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,6 +43,14 @@ + + + + + + " + getTitle(); 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 14e989625..bae6e57aa 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 @@ -1,24 +1,32 @@ package org.schabi.newpipe.fragments.detail; +import android.animation.ValueAnimator; import android.app.Activity; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; +import android.content.*; +import android.content.pm.ActivityInfo; +import android.graphics.Bitmap; +import android.media.AudioManager; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; import android.preference.PreferenceManager; +import android.provider.Settings; +import android.view.*; +import android.view.animation.DecelerateInterpolator; +import android.widget.*; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.tabs.TabLayout; import androidx.fragment.app.Fragment; import androidx.core.content.ContextCompat; import androidx.viewpager.widget.ViewPager; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import android.text.Html; import android.text.Spanned; @@ -27,21 +35,6 @@ import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; -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.AdapterView; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; @@ -69,23 +62,15 @@ import org.schabi.newpipe.fragments.list.comments.CommentsFragment; import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.MainVideoPlayer; -import org.schabi.newpipe.player.PopupVideoPlayer; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.player.*; +import org.schabi.newpipe.player.event.PlayerEventListener; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.playqueue.*; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.ImageDisplayConstants; -import org.schabi.newpipe.util.InfoCache; -import org.schabi.newpipe.util.ListHelper; -import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.ShareUtils; -import org.schabi.newpipe.util.StreamItemAdapter; -import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; +import org.schabi.newpipe.settings.SettingsContentObserver; +import org.schabi.newpipe.util.*; import org.schabi.newpipe.views.AnimatedProgressBar; import java.io.Serializable; @@ -102,6 +87,7 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; +import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class VideoDetailFragment @@ -109,14 +95,22 @@ public class VideoDetailFragment implements BackPressable, SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener, - View.OnLongClickListener { + View.OnLongClickListener, + PlayerEventListener, + PlayerServiceEventListener, + SettingsContentObserver.OnChangeListener { public static final String AUTO_PLAY = "auto_play"; + private boolean isFragmentStopped; + private int updateFlags = 0; private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1; private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2; private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4; private static final int COMMENTS_UPDATE_FLAG = 0x8; + private static final float MAX_OVERLAY_ALPHA = 0.9f; + + public static final String ACTION_SHOW_MAIN_PLAYER = "org.schabi.newpipe.fragments.VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER"; private boolean autoPlayEnabled; private boolean showRelatedStreams; @@ -129,6 +123,10 @@ public class VideoDetailFragment protected String name; @State protected String url; + @State + protected PlayQueue playQueue; + @State + int bottomSheetState = BottomSheetBehavior.STATE_HIDDEN; private StreamInfo currentInfo; private Disposable currentWorker; @@ -139,6 +137,8 @@ public class VideoDetailFragment private List sortedVideoStreams; private int selectedVideoStreamIndex = -1; + private BottomSheetBehavior bottomSheetBehavior; + private BroadcastReceiver broadcastReceiver; /*////////////////////////////////////////////////////////////////////////// // Views @@ -182,6 +182,15 @@ public class VideoDetailFragment private ImageView thumbsDownImageView; private TextView thumbsDisabledTextView; + private RelativeLayout overlay; + private LinearLayout overlayMetadata; + private ImageView overlayThumbnailImageView; + private TextView overlayTitleTextView; + private TextView overlayChannelTextView; + private LinearLayout overlayButtons; + private ImageButton overlayPlayPauseButton; + private ImageButton overlayCloseButton; + private static final String COMMENTS_TAB_TAG = "COMMENTS"; private static final String RELATED_TAB_TAG = "NEXT VIDEO"; private static final String EMPTY_TAB_TAG = "EMPTY TAB"; @@ -192,12 +201,103 @@ public class VideoDetailFragment private TabLayout tabLayout; private FrameLayout relatedStreamsLayout; + private SettingsContentObserver settingsContentObserver; + private ServiceConnection serviceConnection; + private boolean bounded; + private MainPlayer playerService; + private VideoPlayerImpl player; + + + /*////////////////////////////////////////////////////////////////////////// + // Service management + //////////////////////////////////////////////////////////////////////////*/ + + private ServiceConnection getServiceConnection(boolean playAfterConnect) { + return new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) { + if (DEBUG) Log.d(TAG, "Player service is disconnected"); + + unbind(); + } + + @Override + public void onServiceConnected(ComponentName compName, IBinder service) { + if (DEBUG) Log.d(TAG, "Player service is connected"); + MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service; + + playerService = localBinder.getService(); + player = localBinder.getPlayer(); + + startPlayerListener(); + + // It will do nothing if the player is not in fullscreen mode + hideSystemUIIfNeeded(); + + if (!player.videoPlayerSelected()) return; + + if (currentInfo == null && !wasCleared()) selectAndLoadVideo(serviceId, url, name, playQueue); + + if (player.getPlayQueue() != null) addVideoPlayerView(); + + // If the video is playing but orientation changed let's make the video in fullscreen again + + if (isLandscape()) checkLandscape(); + else if (player.isInFullscreen()) player.toggleFullscreen(); + + if (currentInfo != null && isAutoplayEnabled() && player.getParentActivity() == null || playAfterConnect) + openVideoPlayer(); + } + }; + } + + private void bind() { + if (DEBUG) Log.d(TAG, "bind() called"); + + Intent serviceIntent = new Intent(getContext(), MainPlayer.class); + final boolean success = getContext().bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); + + if (!success) getContext().unbindService(serviceConnection); + bounded = success; + } + + private void unbind() { + if (DEBUG) Log.d(TAG, "unbind() called"); + + if (bounded) { + getContext().unbindService(serviceConnection); + bounded = false; + stopPlayerListener(); + playerService = null; + player = null; + } + } + + private void startPlayerListener() { + if (player != null) player.setFragmentListener(this); + } + + private void stopPlayerListener() { + if (player != null) player.removeFragmentListener(this); + } + + private void startService(boolean playAfterConnect) { + getContext().startService(new Intent(getContext(), MainPlayer.class)); + serviceConnection = getServiceConnection(playAfterConnect); + bind(); + } + + private void stopService() { + getContext().stopService(new Intent(getContext(), MainPlayer.class)); + unbind(); + } + /*////////////////////////////////////////////////////////////////////////*/ - public static VideoDetailFragment getInstance(int serviceId, String videoUrl, String name) { + public static VideoDetailFragment getInstance(int serviceId, String videoUrl, String name, PlayQueue playQueue) { VideoDetailFragment instance = new VideoDetailFragment(); - instance.setInitialData(serviceId, videoUrl, name); + instance.setInitialData(serviceId, videoUrl, name, playQueue); return instance; } @@ -211,6 +311,12 @@ public class VideoDetailFragment super.onCreate(savedInstanceState); setHasOptionsMenu(true); + ThemeHelper.setTheme(getContext()); + // Let's play all streams automatically + setAutoplay(true); + + activity.setVolumeControlStream(AudioManager.STREAM_MUSIC); + showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(getString(R.string.show_next_video_key), true); @@ -222,6 +328,10 @@ public class VideoDetailFragment PreferenceManager.getDefaultSharedPreferences(activity) .registerOnSharedPreferenceChangeListener(this); + + startService(false); + setupBroadcastReceiver(); + settingsContentObserver = new SettingsContentObserver(new Handler(), this); } @Override @@ -233,6 +343,9 @@ public class VideoDetailFragment public void onPause() { super.onPause(); if (currentWorker != null) currentWorker.dispose(); + + setupBrightness(true); + getContext().getContentResolver().unregisterContentObserver(settingsContentObserver); PreferenceManager.getDefaultSharedPreferences(getContext()) .edit() .putString(getString(R.string.stream_info_selected_tab_key), pageAdapter.getItemTitle(viewPager.getCurrentItem())) @@ -243,6 +356,13 @@ public class VideoDetailFragment public void onResume() { super.onResume(); + isFragmentStopped = false; + getContext().getContentResolver().registerContentObserver( + android.provider.Settings.System.CONTENT_URI, true, + settingsContentObserver); + + setupBrightness(false); + if (updateFlags != 0) { if (!isLoading.get() && currentInfo != null) { if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) startLoading(false); @@ -259,18 +379,31 @@ public class VideoDetailFragment } // Check if it was loading when the fragment was stopped/paused, - if (wasLoading.getAndSet(false)) { - selectAndLoadVideo(serviceId, url, name); + if (wasLoading.getAndSet(false) && !wasCleared()) { + selectAndLoadVideo(serviceId, url, name, playQueue); } else if (currentInfo != null) { updateProgressInfo(currentInfo); } + + if (player != null && player.videoPlayerSelected()) addVideoPlayerView(); + } + + @Override + public void onStop() { + super.onStop(); + + isFragmentStopped = true; } @Override public void onDestroy() { super.onDestroy(); + + unbind(); + PreferenceManager.getDefaultSharedPreferences(activity) .unregisterOnSharedPreferenceChangeListener(this); + getActivity().unregisterReceiver(broadcastReceiver); if (positionSubscriber != null) positionSubscriber.dispose(); if (currentWorker != null) currentWorker.dispose(); @@ -340,6 +473,7 @@ public class VideoDetailFragment outState.putSerializable(INFO_KEY, currentInfo); } + if (playQueue != null) outState.putSerializable(VideoPlayer.PLAY_QUEUE_KEY, playQueue); outState.putSerializable(STACK_KEY, stack); } @@ -359,7 +493,7 @@ public class VideoDetailFragment //noinspection unchecked stack.addAll((Collection) serializable); } - + playQueue = (PlayQueue) savedState.getSerializable(VideoPlayer.PLAY_QUEUE_KEY); } /*////////////////////////////////////////////////////////////////////////// @@ -390,19 +524,7 @@ public class VideoDetailFragment } break; case R.id.detail_uploader_root_layout: - if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { - Log.w(TAG, "Can't open channel because we got no channel URL"); - } else { - try { - NavigationHelper.openChannelFragment( - getFragmentManager(), - currentInfo.getServiceId(), - currentInfo.getUploaderUrl(), - currentInfo.getUploaderName()); - } catch (Exception e) { - ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); - } - } + openChannel(); break; case R.id.detail_thumbnail_root_layout: if (currentInfo.getVideoStreams().isEmpty() @@ -415,6 +537,24 @@ public class VideoDetailFragment case R.id.detail_title_root_layout: toggleTitleAndDescription(); break; + case R.id.overlay_thumbnail: + case R.id.overlay_metadata_layout: + case R.id.overlay_buttons_layout: + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + break; + case R.id.overlay_play_pause_button: + if (player != null) { + player.onPlayPause(); + player.hideControls(0,0); + } + else openVideoPlayer(); + + setOverlayPlayPauseImage(); + break; + case R.id.overlay_close_button: + cleanUp(); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + break; } } @@ -432,6 +572,10 @@ public class VideoDetailFragment case R.id.detail_controls_download: NavigationHelper.openDownloads(getActivity()); break; + case R.id.overlay_thumbnail: + case R.id.overlay_metadata_layout: + openChannel(); + break; } return true; @@ -494,6 +638,15 @@ public class VideoDetailFragment uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view); uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view); + overlay = rootView.findViewById(R.id.overlay_layout); + overlayMetadata = rootView.findViewById(R.id.overlay_metadata_layout); + overlayThumbnailImageView = rootView.findViewById(R.id.overlay_thumbnail); + overlayTitleTextView = rootView.findViewById(R.id.overlay_title_text_view); + overlayChannelTextView = rootView.findViewById(R.id.overlay_channel_text_view); + overlayButtons = rootView.findViewById(R.id.overlay_buttons_layout); + overlayPlayPauseButton = rootView.findViewById(R.id.overlay_play_pause_button); + overlayCloseButton = rootView.findViewById(R.id.overlay_close_button); + appBarLayout = rootView.findViewById(R.id.appbarlayout); viewPager = rootView.findViewById(R.id.viewpager); pageAdapter = new TabAdaptor(getChildFragmentManager()); @@ -504,8 +657,6 @@ public class VideoDetailFragment relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout); setHeightThumbnail(); - - } @Override @@ -525,8 +676,19 @@ public class VideoDetailFragment detailControlsPopup.setLongClickable(true); detailControlsBackground.setOnLongClickListener(this); detailControlsPopup.setOnLongClickListener(this); + + overlayThumbnailImageView.setOnClickListener(this); + overlayThumbnailImageView.setOnLongClickListener(this); + overlayMetadata.setOnClickListener(this); + overlayMetadata.setOnLongClickListener(this); + overlayButtons.setOnClickListener(this); + overlayCloseButton.setOnClickListener(this); + overlayPlayPauseButton.setOnClickListener(this); + detailControlsBackground.setOnTouchListener(getOnControlsTouchListener()); detailControlsPopup.setOnTouchListener(getOnControlsTouchListener()); + + setupBottomPlayer(); } private View.OnTouchListener getOnControlsTouchListener() { @@ -546,18 +708,25 @@ public class VideoDetailFragment private void initThumbnailViews(@NonNull StreamInfo info) { thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); + overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); + if (!TextUtils.isEmpty(info.getThumbnailUrl())) { final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); - final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { + final ImageLoadingListener loadingListener = new SimpleImageLoadingListener() { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE, infoServiceName, imageUri, R.string.could_not_load_thumbnails); } + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + overlayThumbnailImageView.setImageBitmap(loadedImage); + } }; imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView, - ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener); + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, loadingListener); } if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { @@ -578,7 +747,7 @@ public class VideoDetailFragment // CAUTION set item properties programmatically otherwise it would not be accepted by // appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu); - inflater.inflate(R.menu.video_detail_menu, menu); + /*inflater.inflate(R.menu.video_detail_menu, menu); updateMenuItemVisibility(); @@ -586,15 +755,14 @@ public class VideoDetailFragment if (supportActionBar != null) { supportActionBar.setDisplayHomeAsUpEnabled(true); supportActionBar.setDisplayShowTitleEnabled(false); - } + }*/ } private void updateMenuItemVisibility() { - - // show kodi if set in settings + /*// show kodi if set in settings menu.findItem(R.id.action_play_with_kodi).setVisible( PreferenceManager.getDefaultSharedPreferences(activity).getBoolean( - activity.getString(R.string.show_play_with_kodi_key), false)); + activity.getString(R.string.show_play_with_kodi_key), false));*/ } @Override @@ -619,20 +787,20 @@ public class VideoDetailFragment return true; } case R.id.action_play_with_kodi: - try { + /*try { NavigationHelper.playWithKore(activity, Uri.parse( url.replace("https", "http"))); } catch (Exception e) { if (DEBUG) Log.i(TAG, "Failed to start kore", e); showInstallKoreDialog(activity); - } + }*/ return true; default: return super.onOptionsItemSelected(item); } } - private static void showInstallKoreDialog(final Context context) { + /*private static void showInstallKoreDialog(final Context context) { final AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setMessage(R.string.kore_not_found) .setPositiveButton(R.string.install, (DialogInterface dialog, int which) -> @@ -645,7 +813,7 @@ public class VideoDetailFragment private void setupActionBarOnError(final String url) { if (DEBUG) Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + url + "]"); Log.e("-----", "missing code"); - } + }*/ private void setupActionBar(final StreamInfo info) { if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]"); @@ -659,9 +827,10 @@ public class VideoDetailFragment false); selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams); - final StreamItemAdapter streamsAdapter = + /*final StreamItemAdapter streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled); + spinnerToolbar.setAdapter(streamsAdapter); spinnerToolbar.setSelection(selectedVideoStreamIndex); spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @@ -673,7 +842,7 @@ public class VideoDetailFragment @Override public void onNothingSelected(AdapterView parent) { } - }); + });*/ } /*////////////////////////////////////////////////////////////////////////// @@ -686,15 +855,16 @@ public class VideoDetailFragment */ protected final LinkedList stack = new LinkedList<>(); - public void pushToStack(int serviceId, String videoUrl, String name) { + public void pushToStack(int serviceId, String videoUrl, String name, PlayQueue playQueue) { if (DEBUG) { Log.d(TAG, "pushToStack() called with: serviceId = [" - + serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]"); + + serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "], playQueue = [" + playQueue + "]"); } if (stack.size() > 0 && stack.peek().getServiceId() == serviceId - && stack.peek().getUrl().equals(videoUrl)) { + && stack.peek().getUrl().equals(videoUrl) + && stack.peek().getPlayQueue().getClass().equals(playQueue.getClass())) { Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]"); return; @@ -702,7 +872,7 @@ public class VideoDetailFragment Log.d(TAG, "pushToStack() wasn't equal"); } - stack.push(new StackItem(serviceId, videoUrl, name)); + stack.push(new StackItem(serviceId, videoUrl, name, playQueue)); } public void setTitleToUrl(int serviceId, String videoUrl, String name) { @@ -719,19 +889,34 @@ public class VideoDetailFragment @Override public boolean onBackPressed() { if (DEBUG) Log.d(TAG, "onBackPressed() called"); + + if (player != null && player.isInFullscreen()) { + player.onPause(); + restoreDefaultOrientation(); + return true; + } + // That means that we are on the start of the stack, // return false to let the MainActivity handle the onBack - if (stack.size() <= 1) return false; + if (stack.size() <= 1) { + restoreDefaultOrientation(); + + return false; + } // Remove top stack.pop(); // Get stack item from the new top StackItem peek = stack.peek(); - selectAndLoadVideo(peek.getServiceId(), + hideMainPlayer(); + + setAutoplay(false); + selectAndLoadVideo( + peek.getServiceId(), peek.getUrl(), - !TextUtils.isEmpty(peek.getTitle()) - ? peek.getTitle() - : ""); + !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : "", + peek.getPlayQueue()); + return true; } @@ -741,21 +926,30 @@ public class VideoDetailFragment @Override protected void doInitialLoadLogic() { + if (wasCleared()) return; + if (currentInfo == null) prepareAndLoadInfo(); else prepareAndHandleInfo(currentInfo, false); } - public void selectAndLoadVideo(int serviceId, String videoUrl, String name) { - setInitialData(serviceId, videoUrl, name); - prepareAndLoadInfo(); + public void selectAndLoadVideo(int serviceId, String videoUrl, String name, PlayQueue playQueue) { + boolean streamIsTheSame = videoUrl.equals(url) && currentInfo != null; + setInitialData(serviceId, videoUrl, name, playQueue); + + // Situation when user switches from players to main player. All needed data is here, we can start watching + if (streamIsTheSame) { + handleResult(currentInfo); + openVideoPlayer(); + return; + } + startLoading(false); } public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) { if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]"); - setInitialData(info.getServiceId(), info.getUrl(), info.getName()); - pushToStack(serviceId, url, name); + setInitialData(info.getServiceId(), info.getUrl(), info.getName(), new SinglePlayQueue(info)); showLoading(); initTabs(); @@ -767,7 +961,6 @@ public class VideoDetailFragment protected void prepareAndLoadInfo() { appBarLayout.setExpanded(true, true); - pushToStack(serviceId, url, name); startLoading(false); } @@ -784,9 +977,10 @@ public class VideoDetailFragment .observeOn(AndroidSchedulers.mainThread()) .subscribe((@NonNull StreamInfo result) -> { isLoading.set(false); - currentInfo = result; + hideMainPlayer(); handleResult(result); showContent(); + if (isAutoplayEnabled()) openVideoPlayer(); }, (@NonNull Throwable throwable) -> { isLoading.set(false); onError(throwable); @@ -859,23 +1053,21 @@ public class VideoDetailFragment return; } - final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); + // See UI changes while remote playQueue changes + if (!bounded) startService(false); + + PlayQueue queue = setupPlayQueueForIntent(append); if (append) { - NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue, false); + NavigationHelper.enqueueOnPopupPlayer(activity, queue, false); } else { - Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - final Intent intent = NavigationHelper.getPlayerIntent( - activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true - ); - activity.startService(intent); + NavigationHelper.playOnPopupPlayer(activity, queue, true); } } private void openVideoPlayer() { - VideoStream selectedVideoStream = getSelectedVideoStream(); - if (PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(this.getString(R.string.use_external_video_player_key), false)) { + VideoStream selectedVideoStream = getSelectedVideoStream(); startOnExternalPlayer(activity, currentInfo, selectedVideoStream); } else { openNormalPlayer(); @@ -883,22 +1075,70 @@ public class VideoDetailFragment } private void openNormalBackgroundPlayer(final boolean append) { - final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); + // See UI changes while remote playQueue changes + if (!bounded) startService(false); + + PlayQueue queue = setupPlayQueueForIntent(append); if (append) { - NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue, false); + NavigationHelper.enqueueOnBackgroundPlayer(activity, queue, false); } else { - NavigationHelper.playOnBackgroundPlayer(activity, itemQueue, true); + NavigationHelper.playOnBackgroundPlayer(activity, queue, true); } } private void openNormalPlayer() { - Intent mIntent; - final PlayQueue playQueue = new SinglePlayQueue(currentInfo); - mIntent = NavigationHelper.getPlayerIntent(activity, - MainVideoPlayer.class, - playQueue, - getSelectedVideoStream().getResolution(), true); - startActivity(mIntent); + if (playerService == null) { + startService(true); + return; + } + if (currentInfo == null || playQueue == null) + return; + + PlayQueue queue = setupPlayQueueForIntent(false); + + addVideoPlayerView(); + playerService.getView().setVisibility(View.GONE); + + Intent playerIntent = NavigationHelper.getPlayerIntent( + getContext(), MainPlayer.class, queue, null, true); + activity.startService(playerIntent); + } + + private void hideMainPlayer() { + if (playerService == null || playerService.getView() == null || !player.videoPlayerSelected()) + return; + + removeVideoPlayerView(); + playerService.stop(); + playerService.getView().setVisibility(View.GONE); + } + + private PlayQueue setupPlayQueueForIntent(boolean append) { + if (append) return new SinglePlayQueue(currentInfo); + + PlayQueue queue = playQueue; + // Size can be 0 because queue removes bad stream automatically when error occurs + if (playQueue == null || playQueue.size() == 0) + queue = new SinglePlayQueue(currentInfo); + this.playQueue = queue; + + return queue; + } + + private void openChannel() { + if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { + Log.w(TAG, "Can't open channel because we got no channel URL"); + } else { + try { + NavigationHelper.openChannelFragment( + getFragmentManager(), + currentInfo.getServiceId(), + currentInfo.getUploaderUrl(), + currentInfo.getUploaderName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } + } } /*////////////////////////////////////////////////////////////////////////// @@ -923,6 +1163,53 @@ public class VideoDetailFragment )); } + private boolean isExternalPlayerEnabled() { + return PreferenceManager.getDefaultSharedPreferences(getContext()) + .getBoolean(getString(R.string.use_external_video_player_key), false); + } + + // This method overrides default behaviour when setAutoplay() is called. + // Don't auto play if the user selected an external player + private boolean isAutoplayEnabled() { + return playQueue != null && playQueue.getStreams().size() != 0 && !isExternalPlayerEnabled() && autoPlayEnabled; + } + + private void addVideoPlayerView() { + if (player == null) return; + + FrameLayout viewHolder = getView().findViewById(R.id.player_placeholder); + + // Check if viewHolder already contains a child + if (player.getRootView() != viewHolder) removeVideoPlayerView(); + + final int newHeight; + if (player.isInFullscreen()) + newHeight = activity.getWindow().getDecorView().getHeight(); + else + newHeight = FrameLayout.LayoutParams.MATCH_PARENT; + + if (viewHolder.getLayoutParams().height != newHeight) { + viewHolder.getLayoutParams().height = newHeight; + viewHolder.requestLayout(); + } + + // Prevent from re-adding a view multiple times + if (player.getRootView().getParent() == null) viewHolder.addView(player.getRootView()); + } + + private void removeVideoPlayerView() { + makeDefaultHeightForVideoPlaceholder(); + + playerService.removeViewFromParent(); + } + + private void makeDefaultHeightForVideoPlaceholder() { + FrameLayout viewHolder = getView().findViewById(R.id.player_placeholder); + viewHolder.getLayoutParams().height = FrameLayout.LayoutParams.MATCH_PARENT; + viewHolder.requestLayout(); + } + + @Nullable private VideoStream getSelectedVideoStream() { return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; @@ -967,10 +1254,11 @@ public class VideoDetailFragment contentRootLayoutHiding.setVisibility(View.VISIBLE); } - protected void setInitialData(int serviceId, String url, String name) { + protected void setInitialData(int serviceId, String url, String name, PlayQueue playQueue) { this.serviceId = serviceId; this.url = url; this.name = !TextUtils.isEmpty(name) ? name : ""; + this.playQueue = playQueue; } private void setErrorImage(final int imageResource) { @@ -991,6 +1279,67 @@ public class VideoDetailFragment setErrorImage(imageError); } + private void setupBroadcastReceiver() { + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if(intent.getAction().equals(ACTION_SHOW_MAIN_PLAYER)) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } + } + }; + IntentFilter intentFilter = new IntentFilter(ACTION_SHOW_MAIN_PLAYER); + getActivity().registerReceiver(broadcastReceiver, intentFilter); + } + + + /*////////////////////////////////////////////////////////////////////////// + // Orientation listener + //////////////////////////////////////////////////////////////////////////*/ + + private boolean globalScreenOrientationLocked() { + // 1: Screen orientation changes using accelerometer + // 0: Screen orientation is locked + return !(android.provider.Settings.System.getInt( + getContext().getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1); + } + + private void restoreDefaultOrientation() { + if (player == null || !player.videoPlayerSelected()) return; + + if (player != null && player.isInFullscreen()) player.toggleFullscreen(); + // This will show systemUI and pause the player. + // User can tap on Play button and video will be in fullscreen mode again + if (globalScreenOrientationLocked()) removeVideoPlayerView(); + getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + + private void setupOrientation() { + if (player == null || !player.videoPlayerSelected()) return; + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + int newOrientation; + if (globalScreenOrientationLocked()) { + boolean lastOrientationWasLandscape + = sharedPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false); + newOrientation = lastOrientationWasLandscape + ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + } else + newOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + + if (newOrientation != getActivity().getRequestedOrientation()) + getActivity().setRequestedOrientation(newOrientation); + } + + @Override + public void onSettingsChanged() { + if (player == null) return; + + if(!globalScreenOrientationLocked()) + getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + /*////////////////////////////////////////////////////////////////////////// // Contract //////////////////////////////////////////////////////////////////////////*/ @@ -1005,7 +1354,7 @@ public class VideoDetailFragment contentRootLayoutHiding.setVisibility(View.INVISIBLE); } - animateView(spinnerToolbar, false, 200); + //animateView(spinnerToolbar, false, 200); animateView(thumbnailPlayButton, false, 50); animateView(detailDurationView, false, 100); animateView(detailPositionView, false, 100); @@ -1038,29 +1387,33 @@ public class VideoDetailFragment public void handleResult(@NonNull StreamInfo info) { super.handleResult(info); - setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); + currentInfo = info; + setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName(), + playQueue == null ? new SinglePlayQueue(info) : playQueue); + + pushToStack(serviceId, url, name, playQueue); if(showRelatedStreams){ if(null == relatedStreamsLayout){ //phone - pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(currentInfo)); + pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(info)); pageAdapter.notifyDataSetUpdate(); }else{ //tablet getChildFragmentManager().beginTransaction() - .replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(currentInfo)) + .replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(info)) .commitNow(); relatedStreamsLayout.setVisibility(View.VISIBLE); } } - //pushToStack(serviceId, url, name); - animateView(thumbnailPlayButton, true, 200); videoTitleTextView.setText(name); + overlayTitleTextView.setText(name); if (!TextUtils.isEmpty(info.getUploaderName())) { uploaderTextView.setText(info.getUploaderName()); uploaderTextView.setVisibility(View.VISIBLE); uploaderTextView.setSelected(true); + overlayChannelTextView.setText(info.getUploaderName()); } else { uploaderTextView.setVisibility(View.GONE); } @@ -1138,7 +1491,7 @@ public class VideoDetailFragment prepareDescription(info.getDescription()); updateProgressInfo(info); - animateView(spinnerToolbar, true, 500); + //animateView(spinnerToolbar, true, 500); setupActionBar(info); initThumbnailViews(info); @@ -1169,12 +1522,6 @@ public class VideoDetailFragment thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp); break; } - - if (autoPlayEnabled) { - openVideoPlayer(); - // Only auto play in the first open - autoPlayEnabled = false; - } } @@ -1237,9 +1584,20 @@ public class VideoDetailFragment final boolean playbackResumeEnabled = prefs.getBoolean(activity.getString(R.string.enable_watch_history_key), true) && prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true); + final boolean showPlaybackPosition = prefs.getBoolean( + activity.getString(R.string.enable_playback_state_lists_key), true); if (!playbackResumeEnabled || info.getDuration() <= 0) { - positionView.setVisibility(View.INVISIBLE); - detailPositionView.setVisibility(View.GONE); + if (playQueue == null || playQueue.getStreams().isEmpty() + || playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET || !showPlaybackPosition) { + positionView.setVisibility(View.INVISIBLE); + detailPositionView.setVisibility(View.GONE); + } else { + // Show saved position from backStack if user allows it + showPlaybackProgress(playQueue.getItem().getRecoveryPosition(), + playQueue.getItem().getDuration() * 1000); + animateView(positionView, true, 500); + animateView(detailPositionView, true, 500); + } return; } final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); @@ -1248,17 +1606,309 @@ public class VideoDetailFragment .onErrorComplete() .observeOn(AndroidSchedulers.mainThread()) .subscribe(state -> { - final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()); - positionView.setMax((int) info.getDuration()); - positionView.setProgressAnimated(seconds); - detailPositionView.setText(Localization.getDurationString(seconds)); + showPlaybackProgress(state.getProgressTime(), info.getDuration() * 1000); animateView(positionView, true, 500); animateView(detailPositionView, true, 500); }, e -> { if (DEBUG) e.printStackTrace(); }, () -> { - animateView(positionView, false, 500); - animateView(detailPositionView, false, 500); + // OnComplete, do nothing }); } + + private void showPlaybackProgress(long progress, long duration) { + final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress); + final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration); + positionView.setMax(durationSeconds); + positionView.setProgress(progressSeconds); + detailPositionView.setText(Localization.getDurationString(progressSeconds)); + } + + /*////////////////////////////////////////////////////////////////////////// + // Player event listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters) { + setOverlayPlayPauseImage(); + + switch (state) { + case BasePlayer.STATE_COMPLETED: + restoreDefaultOrientation(); + break; + case BasePlayer.STATE_PLAYING: + if (positionView.getAlpha() != 1f) animateView(positionView, true, 100); + if (detailPositionView.getAlpha() != 1f) animateView(detailPositionView, true, 100); + setupOrientation(); + break; + } + } + + @Override + public void onProgressUpdate(int currentProgress, int duration, int bufferPercent) { + // Progress updates every second even if media is paused. It's useless until playing + if (!player.getPlayer().isPlaying() || playQueue == null) return; + + // Update current progress in cached playQueue because playQueue in popup and background players + // are different instances + playQueue.setRecovery(playQueue.getIndex(), currentProgress); + + showPlaybackProgress(currentProgress, duration); + + // We don't want to interrupt playback and don't want to see notification if player is stopped + // since next lines of code will enable background playback if needed + if (!player.videoPlayerSelected()) return; + + // This will be called when user goes to another app + if (isFragmentStopped) { + // Video enabled. Let's think what to do with source in background + if (player.backgroundPlaybackEnabled()) + player.useVideoSource(false); + else if (player.minimizeOnPopupEnabled()) + NavigationHelper.playOnPopupPlayer(activity, playQueue, true); + else + player.getPlayer().setPlayWhenReady(false); + } + else player.useVideoSource(true); + } + + @Override + public void onMetadataUpdate(StreamInfo info) { + if (currentInfo == info) return; + + currentInfo = info; + setAutoplay(false); + prepareAndHandleInfo(info, true); + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + if (error.type == ExoPlaybackException.TYPE_SOURCE || error.type == ExoPlaybackException.TYPE_UNEXPECTED) { + hideMainPlayer(); + if (playerService != null && player.isInFullscreen()) + player.toggleFullscreen(); + } + } + + @Override + public void onServiceStopped() { + unbind(); + setOverlayPlayPauseImage(); + } + + @Override + public void onFullscreenStateChanged(boolean fullscreen) { + if (playerService.getView() == null || player.getParentActivity() == null) + return; + + View view = playerService.getView(); + ViewGroup parent = (ViewGroup) view.getParent(); + if (parent == null) return; + + if (fullscreen) { + hideSystemUIIfNeeded(); + } else { + showSystemUi(); + } + + if (relatedStreamsLayout != null) relatedStreamsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE); + + addVideoPlayerView(); + } + + /* + * Will scroll down to description view after long click on moreOptionsButton + * */ + @Override + public void onMoreOptionsLongClicked() { + CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams(); + AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior(); + ValueAnimator valueAnimator = ValueAnimator.ofInt(0, -getView().findViewById(R.id.player_placeholder).getHeight()); + valueAnimator.setInterpolator(new DecelerateInterpolator()); + valueAnimator.addUpdateListener(animation -> { + behavior.setTopAndBottomOffset((int) animation.getAnimatedValue()); + appBarLayout.requestLayout(); + }); + valueAnimator.setInterpolator(new DecelerateInterpolator()); + valueAnimator.setDuration(500); + valueAnimator.start(); + } + + @Override + public boolean isFragmentStopped() { + return isFragmentStopped; + } + + /*////////////////////////////////////////////////////////////////////////// + // Player related utils + //////////////////////////////////////////////////////////////////////////*/ + + private void showSystemUi() { + if (DEBUG) Log.d(TAG, "showSystemUi() called"); + + getActivity().getWindow().getDecorView().setSystemUiVisibility(0); + getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + private void hideSystemUi() { + if (DEBUG) Log.d(TAG, "hideSystemUi() called"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + } + activity.getWindow().getDecorView().setSystemUiVisibility(visibility); + } + activity.getWindow().setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + + // Listener implementation + public void hideSystemUIIfNeeded() { + if (player != null && player.isInFullscreen() && bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) + hideSystemUi(); + } + + private void setupBrightness(boolean save) { + WindowManager.LayoutParams lp = getActivity().getWindow().getAttributes(); + float brightnessLevel; + + if (save) { + // Save current brightness level + PlayerHelper.setScreenBrightness(activity, lp.screenBrightness); + + // Restore the old brightness when fragment.onPause() called. + // It means when user leaves this fragment brightness will be set to system brightness + lp.screenBrightness = -1; + } else { + // Restore already saved brightness level + brightnessLevel = PlayerHelper.getScreenBrightness(activity); + if (brightnessLevel <= 0.0f && brightnessLevel > 1.0f) + return; + + lp.screenBrightness = brightnessLevel; + } + getActivity().getWindow().setAttributes(lp); + } + + private void checkLandscape() { + if ((!player.isPlaying() && player.getPlayQueue() != playQueue) || player.getPlayQueue() == null) + setAutoplay(true); + + // Let's give a user time to look at video information page if video is not playing + if (player.isPlaying()) + player.checkLandscape(); + } + + private boolean isLandscape() { + return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels; + } + + /* + * Means that the player fragment was swiped away via BottomSheetLayout and is empty but ready for any new actions. See cleanUp() + * */ + private boolean wasCleared() { + return url == null; + } + + /* + * Remove unneeded information while waiting for a next task + * */ + private void cleanUp() { + // New beginning + stack.clear(); + stopService(); + setInitialData(0,null,"", null); + currentInfo = null; + } + + /*////////////////////////////////////////////////////////////////////////// + // Bottom mini player + //////////////////////////////////////////////////////////////////////////*/ + + private void setupBottomPlayer() { + CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams(); + AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior(); + + FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder); + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout); + bottomSheetBehavior.setState(bottomSheetState); + final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height); + if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) { + bottomSheetBehavior.setPeekHeight(peekHeight); + if (bottomSheetState == BottomSheetBehavior.STATE_COLLAPSED) + setOverlayLook(appBarLayout, behavior, 1 - MAX_OVERLAY_ALPHA); + else if (bottomSheetState == BottomSheetBehavior.STATE_EXPANDED) + setOverlayElementsClickable(false); + } + + bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { + bottomSheetState = newState; + + switch (newState) { + case BottomSheetBehavior.STATE_HIDDEN: + bottomSheetBehavior.setPeekHeight(0); + cleanUp(); + break; + case BottomSheetBehavior.STATE_EXPANDED: + bottomSheetBehavior.setPeekHeight(peekHeight); + // Disable click because overlay buttons located on top of buttons from the player + setOverlayElementsClickable(false); + hideSystemUIIfNeeded(); + break; + case BottomSheetBehavior.STATE_COLLAPSED: + // Re-enable clicks + setOverlayElementsClickable(true); + if (player != null && player.isInFullscreen()) player.toggleFullscreen(); + break; + case BottomSheetBehavior.STATE_DRAGGING: + break; + case BottomSheetBehavior.STATE_SETTLING: + break; + } + Log.d(TAG, "onStateChanged: " + newState); + } + @Override public void onSlide(@NonNull View bottomSheet, float slideOffset) { + setOverlayLook(appBarLayout, behavior, slideOffset); + } + }); + + // User opened a new page and the player will hide itself + activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> { + if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + }); + } + + private void setOverlayPlayPauseImage() { + boolean playing = player != null && player.getPlayer().getPlayWhenReady(); + int attr = playing ? R.attr.pause : R.attr.play; + overlayPlayPauseButton.setImageResource(ThemeHelper.resolveResourceIdFromAttr(activity, attr)); + } + + private void setOverlayLook(AppBarLayout appBarLayout, AppBarLayout.Behavior behavior, float slideOffset) { + if (behavior != null) { + overlay.setAlpha(Math.min(MAX_OVERLAY_ALPHA, 1 - slideOffset)); + + behavior.setTopAndBottomOffset((int)(-appBarLayout.getTotalScrollRange() * (1 - slideOffset) / 3)); + appBarLayout.requestLayout(); + } + } + + private void setOverlayElementsClickable(boolean enable) { + overlayThumbnailImageView.setClickable(enable); + overlayThumbnailImageView.setLongClickable(enable); + overlayMetadata.setClickable(enable); + overlayMetadata.setLongClickable(enable); + overlayButtons.setClickable(enable); + overlayPlayPauseButton.setClickable(enable); + overlayCloseButton.setClickable(enable); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java index 761b50d85..bf3e202d2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.player; import android.content.Intent; +import android.view.Menu; import android.view.MenuItem; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import static org.schabi.newpipe.player.BackgroundPlayer.ACTION_CLOSE; @@ -24,20 +26,20 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { @Override public Intent getBindIntent() { - return new Intent(this, BackgroundPlayer.class); + return new Intent(this, MainPlayer.class); } @Override public void startPlayerListener() { - if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) { - ((BackgroundPlayer.BasePlayerImpl) player).setActivityListener(this); + if (player instanceof VideoPlayerImpl) { + ((VideoPlayerImpl) player).setActivityListener(this); } } @Override public void stopPlayerListener() { - if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) { - ((BackgroundPlayer.BasePlayerImpl) player).removeActivityListener(this); + if (player instanceof VideoPlayerImpl) { + ((VideoPlayerImpl) player).removeActivityListener(this); } } @@ -56,14 +58,28 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { } this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startService(getSwitchIntent(PopupVideoPlayer.class)); + NavigationHelper.playOnPopupPlayer(getApplicationContext(), player.playQueue, true); return true; } + + if (item.getItemId() == R.id.action_switch_background) { + this.player.setRecovery(); + NavigationHelper.playOnBackgroundPlayer(getApplicationContext(), player.playQueue, true); + return true; + } + return false; } @Override + public void setupMenu(Menu menu) { + if(player == null) return; + + menu.findItem(R.id.action_switch_popup).setVisible(!((VideoPlayerImpl)player).popupPlayerSelected()); + menu.findItem(R.id.action_switch_background).setVisible(!((VideoPlayerImpl)player).audioPlayerSelected()); + } + + //@Override public Intent getPlayerShutdownIntent() { return new Intent(ACTION_CLOSE); } 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 6452a9850..2ae822d7f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -151,6 +151,8 @@ public abstract class BasePlayer implements public static final String RESUME_PLAYBACK = "resume_playback"; @NonNull public static final String SELECT_ON_APPEND = "select_on_append"; + @NonNull + public static final String PLAYER_TYPE = "player_type"; /*////////////////////////////////////////////////////////////////////////// // Playback @@ -182,6 +184,10 @@ 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 + public final static int PLAYER_TYPE_VIDEO = 0; + public final static int PLAYER_TYPE_AUDIO = 1; + public final static int PLAYER_TYPE_POPUP = 2; + protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; protected MediaSessionManager mediaSessionManager; @@ -290,12 +296,24 @@ public abstract class BasePlayer implements if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { stateLoader = recordManager.loadStreamState(item) .observeOn(AndroidSchedulers.mainThread()) - .doFinally(() -> initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, - /*playOnInit=*/true)) + // Do not place initPlayback() in doFinally() because it restarts playback after destroy() + //.doFinally() .subscribe( - state -> queue.setRecovery(queue.getIndex(), state.getProgressTime()), + state -> { + queue.setRecovery(queue.getIndex(), state.getProgressTime()); + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, + /*playOnInit=*/true); + }, error -> { if (DEBUG) error.printStackTrace(); + // In case any error we can start playback without history + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, + /*playOnInit=*/true); + }, + () -> { + // Completed but not found in history + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, + /*playOnInit=*/true); } ); databaseUpdateReactor.add(stateLoader); @@ -354,7 +372,7 @@ public abstract class BasePlayer implements databaseUpdateReactor.clear(); progressUpdateReactor.set(null); - + ImageLoader.getInstance().stop(); } /*////////////////////////////////////////////////////////////////////////// @@ -931,6 +949,7 @@ public abstract class BasePlayer implements } simpleExoPlayer.setPlayWhenReady(true); + savePlaybackState(); } public void onPause() { @@ -939,6 +958,7 @@ public abstract class BasePlayer implements audioReactor.abandonAudioFocus(); simpleExoPlayer.setPlayWhenReady(false); + savePlaybackState(); } public void onPlayPause() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java new file mode 100644 index 000000000..a0d4eb7d9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -0,0 +1,353 @@ +/* + * Copyright 2017 Mauricio Colli + * BackgroundPlayer.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.player; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.view.ViewGroup; +import android.view.WindowManager; +import androidx.core.app.NotificationCompat; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; + +import com.google.android.exoplayer2.Player; + +import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.player.helper.LockManager; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ThemeHelper; + + +/** + * One service for all players + * + * @author mauriciocolli + */ +public final class MainPlayer extends Service { + private static final String TAG = "MainPlayer"; + private static final boolean DEBUG = BasePlayer.DEBUG; + + private VideoPlayerImpl playerImpl; + private WindowManager windowManager; + private LockManager lockManager; + + private final IBinder mBinder = new MainPlayer.LocalBinder(); + + public enum PlayerType { + VIDEO, + AUDIO, + POPUP + } + + /*////////////////////////////////////////////////////////////////////////// + // Notification + //////////////////////////////////////////////////////////////////////////*/ + + static final int NOTIFICATION_ID = 123789; + private NotificationManager notificationManager; + private NotificationCompat.Builder notBuilder; + private RemoteViews notRemoteView; + private RemoteViews bigNotRemoteView; + + static final String ACTION_CLOSE = "org.schabi.newpipe.player.MainPlayer.CLOSE"; + static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE"; + static final String ACTION_OPEN_CONTROLS = "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS"; + static final String ACTION_REPEAT = "org.schabi.newpipe.player.MainPlayer.REPEAT"; + static final String ACTION_PLAY_NEXT = "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT"; + static final String ACTION_PLAY_PREVIOUS = "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS"; + static final String ACTION_FAST_REWIND = "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND"; + static final String ACTION_FAST_FORWARD = "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD"; + + private static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource"; + + /*////////////////////////////////////////////////////////////////////////// + // Service's LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate() { + if (DEBUG) Log.d(TAG, "onCreate() called"); + notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); + lockManager = new LockManager(this); + windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + + ThemeHelper.setTheme(this); + createView(); + } + + private void createView() { + View layout = View.inflate(this, R.layout.activity_main_player, null); + + playerImpl = new VideoPlayerImpl(this); + playerImpl.setup(layout); + playerImpl.shouldUpdateOnProgress = true; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + + "], flags = [" + flags + "], startId = [" + startId + "]"); + playerImpl.handleIntent(intent); + if (playerImpl.mediaSessionManager != null) { + playerImpl.mediaSessionManager.handleMediaButtonIntent(intent); + } + return START_NOT_STICKY; + } + + public void stop() { + if (DEBUG) + Log.d(TAG, "stop() called"); + + if (playerImpl.getPlayer() != null) { + playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); + playerImpl.getPlayer().stop(false); + playerImpl.setRecovery(); + } + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + onDestroy(); + // Unload from memory completely + Runtime.getRuntime().halt(0); + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "destroy() called"); + onClose(); + } + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + /*////////////////////////////////////////////////////////////////////////// + // Actions + //////////////////////////////////////////////////////////////////////////*/ + private void onClose() { + if (DEBUG) Log.d(TAG, "onClose() called"); + + if (lockManager != null) { + lockManager.releaseWifiAndCpu(); + } + if (playerImpl != null) { + removeViewFromParent(); + + playerImpl.savePlaybackState(); + playerImpl.stopActivityBinding(); + playerImpl.destroy(); + } + if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); + playerImpl = null; + lockManager = null; + + stopForeground(true); + stopSelf(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + boolean isLandscape() { + return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels; + } + + public View getView() { + if (playerImpl == null) + return null; + + return playerImpl.getRootView(); + } + + public void removeViewFromParent() { + if (getView().getParent() != null) { + if (playerImpl.getParentActivity() != null) { + // This means view was added to fragment + ViewGroup parent = (ViewGroup) getView().getParent(); + parent.removeView(getView()); + } else + // This means view was added by windowManager for popup player + windowManager.removeViewImmediate(getView()); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Notification + //////////////////////////////////////////////////////////////////////////*/ + + void resetNotification() { + notBuilder = createNotification(); + } + + private NotificationCompat.Builder createNotification() { + notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); + bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); + + setupNotification(notRemoteView); + setupNotification(bigNotRemoteView); + + 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) + .setCustomContentView(notRemoteView) + .setCustomBigContentView(bigNotRemoteView); + builder.setPriority(NotificationCompat.PRIORITY_MAX); + return builder; + } + + private void setupNotification(RemoteViews remoteViews) { + // Don't show anything until player is playing + if (playerImpl == null) return; + + remoteViews.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); + remoteViews.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); + remoteViews.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail()); + + remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationStop, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); + // Starts VideoDetailFragment or opens BackgroundPlayerActivity. + remoteViews.setOnClickPendingIntent(R.id.notificationContent, + PendingIntent.getActivity(this, NOTIFICATION_ID, getIntentForNotification(), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationRepeat, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); + + + if (playerImpl.playQueue != null && playerImpl.playQueue.size() > 1) { + remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_previous); + remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_next); + remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationFForward, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT)); + } else { + remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_rewind); + remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_fastforward); + remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationFForward, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT)); + } + + setRepeatModeIcon(remoteViews, playerImpl.getRepeatMode()); + } + + /** + * Updates the notification, and the play/pause button in it. + * Used for changes on the remoteView + * + * @param drawableId if != -1, sets the drawable with that id on the play/pause button + */ + synchronized void updateNotification(int drawableId) { + //if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); + if (notBuilder == null) return; + if (drawableId != -1) { + if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + } + notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) { + if (remoteViews == null) return; + + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_off); + break; + case Player.REPEAT_MODE_ONE: + remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_one); + break; + case Player.REPEAT_MODE_ALL: + remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_all); + break; + } + } + + private Intent getIntentForNotification() { + Intent intent; + if (playerImpl.audioPlayerSelected() || playerImpl.popupPlayerSelected()) { + // Means we play in popup or audio only. Let's show BackgroundPlayerActivity + intent = NavigationHelper.getBackgroundPlayerActivityIntent(getApplicationContext()); + } else { + // We are playing in fragment. Don't open another activity just show fragment. That's it + intent = NavigationHelper.getPlayerIntent(this, MainActivity.class, null, true); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + } + return intent; + } + + /*////////////////////////////////////////////////////////////////////////// + // Getters + //////////////////////////////////////////////////////////////////////////*/ + + LockManager getLockManager() { + return lockManager; + } + + NotificationCompat.Builder getNotBuilder() { + return notBuilder; + } + + RemoteViews getBigNotRemoteView() { + return bigNotRemoteView; + } + + RemoteViews getNotRemoteView() { + return notRemoteView; + } + + + public class LocalBinder extends Binder { + + public MainPlayer getService() { + return MainPlayer.this; + } + + public VideoPlayerImpl getPlayer() { + return MainPlayer.this.playerImpl; + } + } +} 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 7a3e60c66..a958c8b31 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -546,7 +546,7 @@ public final class MainVideoPlayer extends AppCompatActivity onPlayBackgroundButtonClicked(); break; case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP: - onFullScreenButtonClicked(); + toggleFullscreen(); break; case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE: default: @@ -593,8 +593,8 @@ public final class MainVideoPlayer extends AppCompatActivity //////////////////////////////////////////////////////////////////////////*/ @Override - public void onFullScreenButtonClicked() { - super.onFullScreenButtonClicked(); + public void toggleFullscreen() { + super.toggleFullscreen(); if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); if (simpleExoPlayer == null) return; @@ -678,7 +678,7 @@ public final class MainVideoPlayer extends AppCompatActivity onScreenRotationClicked(); } else if (v.getId() == switchPopupButton.getId()) { - onFullScreenButtonClicked(); + toggleFullscreen(); } else if (v.getId() == switchBackgroundButton.getId()) { onPlayBackgroundButtonClicked(); @@ -746,7 +746,7 @@ public final class MainVideoPlayer extends AppCompatActivity @Override public void onPlaybackSpeedClicked() { PlaybackParameterDialog - .newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence()) + .newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence(), MainVideoPlayer.this) .show(getSupportFragmentManager(), TAG); } @@ -909,6 +909,9 @@ public final class MainVideoPlayer extends AppCompatActivity ); } + @Override + public void hideSystemUIIfNeeded() { } + private void updatePlaybackButtons() { if (repeatButton == null || shuffleButton == null || simpleExoPlayer == null || playQueue == null) return; 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 969c47990..0a15e7169 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -285,7 +285,7 @@ public final class PopupVideoPlayer extends Service { PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); // Starts popup player activity -- attempts to unlock lockscreen - final Intent intent = NavigationHelper.getPopupPlayerActivityIntent(this); + final Intent intent = NavigationHelper.getBackgroundPlayerActivityIntent(this); notRemoteView.setOnClickPendingIntent(R.id.notificationContent, PendingIntent.getActivity(this, NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)); @@ -513,7 +513,7 @@ public final class PopupVideoPlayer extends Service { super.initViews(rootView); resizingIndicator = rootView.findViewById(R.id.resizing_indicator); fullScreenButton = rootView.findViewById(R.id.fullScreenButton); - fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked()); + fullScreenButton.setOnClickListener(v -> toggleFullscreen()); videoPlayPause = rootView.findViewById(R.id.videoPlayPause); extraOptionsView = rootView.findViewById(R.id.extraOptionsView); @@ -552,8 +552,8 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onFullScreenButtonClicked() { - super.onFullScreenButtonClicked(); + public void toggleFullscreen() { + super.toggleFullscreen(); if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); @@ -867,6 +867,9 @@ public final class PopupVideoPlayer extends Service { super.hideControlsAndButton(duration, delay, videoPlayPause); } + @Override + public void hideSystemUIIfNeeded() { } + /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java index 44fcdb8dd..f29c5a14c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player; import android.content.Intent; +import android.view.Menu; import android.view.MenuItem; import org.schabi.newpipe.R; @@ -50,13 +51,18 @@ public final class PopupVideoPlayerActivity extends ServicePlayerActivity { if (item.getItemId() == R.id.action_switch_background) { this.player.setRecovery(); getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startService(getSwitchIntent(BackgroundPlayer.class)); + getApplicationContext().startService(getSwitchIntent(MainPlayer.class, MainPlayer.PlayerType.AUDIO)); return true; } return false; } @Override + public void setupMenu(Menu menu) { + + } + + //@Override public Intent getPlayerShutdownIntent() { return new Intent(ACTION_CLOSE); } 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 2207808ac..c8d564557 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -26,17 +26,17 @@ import android.widget.TextView; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; -import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; -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.playqueue.*; +import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -109,7 +109,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity public abstract boolean onPlayerOptionSelected(MenuItem item); - public abstract Intent getPlayerShutdownIntent(); + public abstract void setupMenu(Menu menu); //////////////////////////////////////////////////////////////////////////// // Activity Lifecycle //////////////////////////////////////////////////////////////////////////// @@ -148,6 +148,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return true; } + // Allow to setup visibility of menuItems + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + setupMenu(menu); + return super.onPrepareOptionsMenu(menu); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -166,8 +173,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return true; case R.id.action_switch_main: this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startActivity(getSwitchIntent(MainVideoPlayer.class)); + getApplicationContext().startActivity(getSwitchIntent(MainActivity.class, MainPlayer.PlayerType.VIDEO)); return true; } return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item); @@ -179,8 +185,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity unbind(); } - protected Intent getSwitchIntent(final Class clazz) { - return NavigationHelper.getPlayerIntent( + Intent getSwitchIntent(final Class clazz, final MainPlayer.PlayerType playerType) { + Intent intent = NavigationHelper.getPlayerIntent( getApplicationContext(), clazz, this.player.getPlayQueue(), @@ -189,8 +195,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity this.player.getPlaybackPitch(), this.player.getPlaybackSkipSilence(), null, - false - ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + true); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); + intent.putExtra(Constants.KEY_URL, this.player.getVideoUrl()); + intent.putExtra(Constants.KEY_TITLE, this.player.getVideoTitle()); + intent.putExtra(VideoDetailFragment.AUTO_PLAY, true); + intent.putExtra(Constants.KEY_SERVICE_ID, this.player.getCurrentMetadata().getMetadata().getServiceId()); + intent.putExtra(VideoPlayer.PLAYER_TYPE, playerType); + return intent; } //////////////////////////////////////////////////////////////////////////// @@ -236,6 +249,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (service instanceof PlayerServiceBinder) { player = ((PlayerServiceBinder) service).getPlayerInstance(); + } else if (service instanceof MainPlayer.LocalBinder) { + player = ((MainPlayer.LocalBinder) service).getPlayer(); } if (player == null || player.getPlayQueue() == null || @@ -474,7 +489,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void openPlaybackParameterDialog() { if (player == null) return; PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), - player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag()); + player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag()); } @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 0734139e1..3a906d80c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -478,7 +478,6 @@ public abstract class VideoPlayer extends BasePlayer super.onCompleted(); showControls(500); - animateView(endScreen, true, 800); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); loadingPanel.setVisibility(View.GONE); @@ -575,11 +574,6 @@ public abstract class VideoPlayer extends BasePlayer playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed())); super.onPrepared(playWhenReady); - - if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) { - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION); - } } @Override @@ -615,7 +609,7 @@ public abstract class VideoPlayer extends BasePlayer if (loadedImage != null) endScreen.setImageBitmap(loadedImage); } - protected void onFullScreenButtonClicked() { + protected void toggleFullscreen() { changeState(STATE_BLOCKED); } @@ -724,7 +718,7 @@ public abstract class VideoPlayer extends BasePlayer showControls(DEFAULT_CONTROLS_DURATION); } - private void onResizeClicked() { + void onResizeClicked() { if (getAspectRatioFrameLayout() != null) { final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); final int newResizeMode = nextResizeMode(currentResizeMode); @@ -888,6 +882,9 @@ public abstract class VideoPlayer extends BasePlayer animateView(controlsRoot, false,duration); }; } + + public abstract void hideSystemUIIfNeeded(); + /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java new file mode 100644 index 000000000..8cef5b453 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -0,0 +1,1552 @@ +/* + * Copyright 2017 Mauricio Colli + * PopupVideoPlayer.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.player; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.SuppressLint; +import android.content.*; +import android.graphics.Bitmap; +import android.graphics.PixelFormat; +import android.net.Uri; +import android.os.Build; +import android.preference.PreferenceManager; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.*; +import android.view.animation.AnticipateInterpolator; +import android.widget.*; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.text.CaptionStyleCompat; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.nostra13.universalimageloader.core.assist.FailReason; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; +import org.schabi.newpipe.player.event.PlayerEventListener; +import org.schabi.newpipe.player.event.PlayerGestureListener; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.helper.PlaybackParameterDialog; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.playqueue.*; +import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; +import org.schabi.newpipe.util.*; + +import java.util.List; + +import static android.content.Context.WINDOW_SERVICE; +import static org.schabi.newpipe.player.MainPlayer.*; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; +import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; +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; +import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; +import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; + +/** + * Unified UI for all players + * + * @author mauriciocolli + */ + +public class VideoPlayerImpl extends VideoPlayer + implements View.OnLayoutChangeListener, + PlaybackParameterDialog.Callback, + View.OnLongClickListener{ + private static final String TAG = ".VideoPlayerImpl"; + + private final float MAX_GESTURE_LENGTH = 0.75f; + + private TextView titleTextView; + private TextView channelTextView; + private RelativeLayout volumeRelativeLayout; + private ProgressBar volumeProgressBar; + private ImageView volumeImageView; + private RelativeLayout brightnessRelativeLayout; + private ProgressBar brightnessProgressBar; + private ImageView brightnessImageView; + private TextView resizingIndicator; + private ImageButton queueButton; + private ImageButton repeatButton; + private ImageButton shuffleButton; + private ImageButton playWithKodi; + private ImageButton openInBrowser; + private ImageButton fullscreenButton; + + private ImageButton playPauseButton; + private ImageButton playPreviousButton; + private ImageButton playNextButton; + + private RelativeLayout queueLayout; + private ImageButton itemsListCloseButton; + private RecyclerView itemsList; + private ItemTouchHelper itemTouchHelper; + + private boolean queueVisible; + private MainPlayer.PlayerType playerType = MainPlayer.PlayerType.VIDEO; + + private ImageButton moreOptionsButton; + private ImageButton shareButton; + + private View primaryControls; + private LinearLayout topControls; + private View secondaryControls; + + private int maxGestureLength; + + private boolean audioOnly = false; + private boolean isFullscreen = false; + boolean shouldUpdateOnProgress; + + private MainPlayer service; + private PlayerServiceEventListener fragmentListener; + private PlayerEventListener activityListener; + private GestureDetector gestureDetector; + private SharedPreferences defaultPreferences; + @NonNull + final private AudioPlaybackResolver resolver; + + private int cachedDuration; + private String cachedDurationString; + + // Popup + private WindowManager.LayoutParams popupLayoutParams; + public WindowManager windowManager; + + private View closingOverlayView; + private View closeOverlayView; + private FloatingActionButton closeOverlayButton; + + public boolean isPopupClosing = false; + + static final String POPUP_SAVED_WIDTH = "popup_saved_width"; + static final String POPUP_SAVED_X = "popup_saved_x"; + static final String POPUP_SAVED_Y = "popup_saved_y"; + private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; + 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 float screenWidth, screenHeight; + private float popupWidth, popupHeight; + private float minimumWidth, minimumHeight; + private float maximumWidth, maximumHeight; + // Popup end + + + @Override + public void handleIntent(Intent intent) { + if (intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) == null) return; + + MainPlayer.PlayerType oldPlayerType = playerType; + choosePlayerTypeFromIntent(intent); + audioOnly = audioPlayerSelected(); + + // We need to setup audioOnly before super(), see "sourceOf" + super.handleIntent(intent); + + if (oldPlayerType != playerType && playQueue != null) { + // If playerType changes from one to another we should reload the player + // (to disable/enable video stream or to set quality) + setRecovery(); + reload(); + } + + service.resetNotification(); + if (service.getBigNotRemoteView() != null) + service.getBigNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false); + if (service.getNotRemoteView() != null) + service.getNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false); + service.startForeground(NOTIFICATION_ID, service.getNotBuilder().build()); + setupElementsVisibility(); + + if (audioPlayerSelected()) { + service.removeViewFromParent(); + } else if (popupPlayerSelected()) { + getRootView().setVisibility(View.VISIBLE); + initPopup(); + initPopupCloseOverlay(); + } else { + getRootView().setVisibility(View.VISIBLE); + initVideoPlayer(); + } + + onPlay(); + } + + VideoPlayerImpl(final MainPlayer service) { + super("MainPlayer" + TAG, service); + this.service = service; + this.shouldUpdateOnProgress = true; + this.windowManager = (WindowManager) service.getSystemService(WINDOW_SERVICE); + this.defaultPreferences = PreferenceManager.getDefaultSharedPreferences(service); + this.resolver = new AudioPlaybackResolver(context, dataSource); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public void initViews(View rootView) { + super.initViews(rootView); + this.titleTextView = rootView.findViewById(R.id.titleTextView); + this.channelTextView = rootView.findViewById(R.id.channelTextView); + 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.resizingIndicator = rootView.findViewById(R.id.resizing_indicator); + this.queueButton = rootView.findViewById(R.id.queueButton); + this.repeatButton = rootView.findViewById(R.id.repeatButton); + this.shuffleButton = rootView.findViewById(R.id.shuffleButton); + this.playWithKodi = rootView.findViewById(R.id.playWithKodi); + this.openInBrowser = rootView.findViewById(R.id.openInBrowser); + this.fullscreenButton = rootView.findViewById(R.id.fullScreenButton); + + this.playPauseButton = rootView.findViewById(R.id.playPauseButton); + this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton); + this.playNextButton = rootView.findViewById(R.id.playNextButton); + + this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton); + this.primaryControls = rootView.findViewById(R.id.primaryControls); + this.topControls = rootView.findViewById(R.id.topControls); + this.secondaryControls = rootView.findViewById(R.id.secondaryControls); + this.shareButton = rootView.findViewById(R.id.share); + + this.queueLayout = rootView.findViewById(R.id.playQueuePanel); + this.itemsListCloseButton = rootView.findViewById(R.id.playQueueClose); + this.itemsList = rootView.findViewById(R.id.playQueue); + + closingOverlayView = rootView.findViewById(R.id.closingOverlay); + + titleTextView.setSelected(true); + channelTextView.setSelected(true); + } + + @Override + protected void setupSubtitleView(@NonNull SubtitleView view, + final float captionScale, + @NonNull final CaptionStyleCompat captionStyle) { + if (popupPlayerSelected()) { + float captionRatio = (captionScale - 1f) / 5f + 1f; + view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); + view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); + view.setStyle(captionStyle); + } else { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + final float captionRatioInverse = 20f + 4f * (1f - captionScale); + view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, + (float) minimumLength / captionRatioInverse); + view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); + view.setStyle(captionStyle); + } + } + + private void setupElementsVisibility() { + if (popupPlayerSelected()) { + fullscreenButton.setVisibility(View.VISIBLE); + getRootView().findViewById(R.id.metadataView).setVisibility(View.GONE); + queueButton.setVisibility(View.GONE); + moreOptionsButton.setVisibility(View.GONE); + topControls.setOrientation(LinearLayout.HORIZONTAL); + primaryControls.getLayoutParams().width = LinearLayout.LayoutParams.WRAP_CONTENT; + secondaryControls.setAlpha(1f); + secondaryControls.setVisibility(View.VISIBLE); + secondaryControls.setTranslationY(0); + shareButton.setVisibility(View.GONE); + playWithKodi.setVisibility(View.GONE); + openInBrowser.setVisibility(View.GONE); + } else { + fullscreenButton.setVisibility(View.GONE); + getRootView().findViewById(R.id.metadataView).setVisibility(View.VISIBLE); + moreOptionsButton.setVisibility(View.VISIBLE); + topControls.setOrientation(LinearLayout.VERTICAL); + primaryControls.getLayoutParams().width = LinearLayout.LayoutParams.MATCH_PARENT; + secondaryControls.setVisibility(View.GONE); + moreOptionsButton.setImageDrawable(service.getResources().getDrawable( + R.drawable.ic_expand_more_white_24dp)); + shareButton.setVisibility(View.VISIBLE); + playWithKodi.setVisibility( + defaultPreferences.getBoolean(service.getString(R.string.show_play_with_kodi_key), false) ? View.VISIBLE : View.GONE); + openInBrowser.setVisibility(View.VISIBLE); + } + if (!isInFullscreen()) { + titleTextView.setVisibility(View.GONE); + channelTextView.setVisibility(View.GONE); + } else { + titleTextView.setVisibility(View.VISIBLE); + channelTextView.setVisibility(View.VISIBLE); + } + + animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0); + } + + @Override + public void initListeners() { + super.initListeners(); + + PlayerGestureListener listener = new PlayerGestureListener(this, service); + gestureDetector = new GestureDetector(context, listener); + getRootView().setOnTouchListener(listener); + + queueButton.setOnClickListener(this); + repeatButton.setOnClickListener(this); + shuffleButton.setOnClickListener(this); + + playPauseButton.setOnClickListener(this); + playPreviousButton.setOnClickListener(this); + playNextButton.setOnClickListener(this); + + moreOptionsButton.setOnClickListener(this); + moreOptionsButton.setOnLongClickListener(this); + shareButton.setOnClickListener(this); + fullscreenButton.setOnClickListener(this); + playWithKodi.setOnClickListener(this); + openInBrowser.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 AppCompatActivity getParentActivity() { + // ! instanceof ViewGroup means that view was added via windowManager for Popup + if (getRootView() == null || getRootView().getParent() == null || !(getRootView().getParent() instanceof ViewGroup)) + return null; + + ViewGroup parent = (ViewGroup) getRootView().getParent(); + return (AppCompatActivity) parent.getContext(); + } + + /*////////////////////////////////////////////////////////////////////////// + // View + //////////////////////////////////////////////////////////////////////////*/ + + private void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + imageButton.setImageResource(R.drawable.exo_controls_repeat_off); + break; + case Player.REPEAT_MODE_ONE: + imageButton.setImageResource(R.drawable.exo_controls_repeat_one); + break; + case Player.REPEAT_MODE_ALL: + imageButton.setImageResource(R.drawable.exo_controls_repeat_all); + break; + } + } + + private void setShuffleButton(final ImageButton shuffleButton, final boolean shuffled) { + final int shuffleAlpha = shuffled ? 255 : 77; + shuffleButton.setImageAlpha(shuffleAlpha); + } + + //////////////////////////////////////////////////////////////////////////// + // Playback Parameters Listener + //////////////////////////////////////////////////////////////////////////// + + @Override + public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, boolean playbackSkipSilence) { + setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); + } + + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Video Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onRepeatModeChanged(int i) { + super.onRepeatModeChanged(i); + updatePlaybackButtons(); + updatePlayback(); + service.resetNotification(); + service.updateNotification(-1); + } + + @Override + public void onShuffleClicked() { + super.onShuffleClicked(); + updatePlaybackButtons(); + updatePlayback(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Playback Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onPlayerError(ExoPlaybackException error) { + super.onPlayerError(error); + + if (fragmentListener != null) + fragmentListener.onPlayerError(error); + } + + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); + + titleTextView.setText(tag.getMetadata().getName()); + channelTextView.setText(tag.getMetadata().getUploaderName()); + + service.resetNotification(); + service.updateNotification(-1); + updateMetadata(); + } + + @Override + public void onPlaybackShutdown() { + if (DEBUG) Log.d(TAG, "onPlaybackShutdown() called"); + // Override it because we don't want playerImpl destroyed + } + + @Override + public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { + super.onUpdateProgress(currentProgress, duration, bufferPercent); + + updateProgress(currentProgress, duration, bufferPercent); + + if (!shouldUpdateOnProgress || getCurrentState() == BasePlayer.STATE_COMPLETED + || getCurrentState() == BasePlayer.STATE_PAUSED || getPlayQueue() == null) + return; + + if (service.getBigNotRemoteView() != null) { + if (cachedDuration != duration) { + cachedDuration = duration; + cachedDurationString = getTimeString(duration); + } + service.getBigNotRemoteView() + .setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); + service.getBigNotRemoteView() + .setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + cachedDurationString); + } + if (service.getNotRemoteView() != null) { + service.getNotRemoteView() + .setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); + } + service.updateNotification(-1); + } + + @Override + @Nullable + public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { + // For LiveStream or video/popup players we can use super() method + // but not for audio player + if (!audioOnly) + return super.sourceOf(item, info); + else { + return resolver.resolve(info); + } + } + + @Override + public void onPlayPrevious() { + super.onPlayPrevious(); + triggerProgressUpdate(); + } + + @Override + public void onPlayNext() { + super.onPlayNext(); + triggerProgressUpdate(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Player Overrides + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void toggleFullscreen() { + if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); + if (simpleExoPlayer == null || getCurrentMetadata() == null) return; + + if (popupPlayerSelected()) { + setRecovery(); + service.removeViewFromParent(); + Intent intent = NavigationHelper.getPlayerIntent( + service, + MainActivity.class, + this.getPlayQueue(), + this.getRepeatMode(), + this.getPlaybackSpeed(), + this.getPlaybackPitch(), + this.getPlaybackSkipSilence(), + null, + true + ); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Constants.KEY_SERVICE_ID, getCurrentMetadata().getMetadata().getServiceId()); + intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); + intent.putExtra(Constants.KEY_URL, getVideoUrl()); + intent.putExtra(Constants.KEY_TITLE, getVideoTitle()); + intent.putExtra(VideoDetailFragment.AUTO_PLAY, true); + context.startActivity(intent); + } else { + if (fragmentListener == null) return; + + isFullscreen = !isFullscreen; + fragmentListener.onFullscreenStateChanged(isInFullscreen()); + // When user presses back button in landscape mode and in fullscreen and uses ZOOM mode + // a video can be larger than screen. Prevent it like this + if (getAspectRatioFrameLayout().getResizeMode() == AspectRatioFrameLayout.RESIZE_MODE_ZOOM + && !isInFullscreen() + && service.isLandscape()) + onResizeClicked(); + } + + if (!isInFullscreen()) { + titleTextView.setVisibility(View.GONE); + channelTextView.setVisibility(View.GONE); + } else { + titleTextView.setVisibility(View.VISIBLE); + channelTextView.setVisibility(View.VISIBLE); + } + } + + @Override + public void onClick(View v) { + super.onClick(v); + if (v.getId() == playPauseButton.getId()) { + onPlayPause(); + + } else if (v.getId() == playPreviousButton.getId()) { + onPlayPrevious(); + + } else if (v.getId() == playNextButton.getId()) { + onPlayNext(); + + } else if (v.getId() == queueButton.getId()) { + onQueueClicked(); + return; + } else if (v.getId() == repeatButton.getId()) { + onRepeatClicked(); + return; + } else if (v.getId() == shuffleButton.getId()) { + onShuffleClicked(); + return; + } else if (v.getId() == moreOptionsButton.getId()) { + onMoreOptionsClicked(); + + } else if (v.getId() == shareButton.getId()) { + onShareClicked(); + + } else if (v.getId() == playWithKodi.getId()) { + onPlayWithKodiClicked(); + + } else if (v.getId() == openInBrowser.getId()) { + onOpenInBrowserClicked(); + + } else if (v.getId() == fullscreenButton.getId()) { + toggleFullscreen(); + + } + + if (getCurrentState() != STATE_COMPLETED) { + getControlsVisibilityHandler().removeCallbacksAndMessages(null); + animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { + if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { + hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + }); + } + } + + @Override + public boolean onLongClick(View v) { + if (v.getId() == moreOptionsButton.getId() && isInFullscreen()) { + fragmentListener.onMoreOptionsLongClicked(); + hideControls(0, 0); + hideSystemUIIfNeeded(); + } + return true; + } + + private void onQueueClicked() { + queueVisible = true; + + buildQueue(); + updatePlaybackButtons(); + + getControlsRoot().setVisibility(View.INVISIBLE); + animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true, + DEFAULT_CONTROLS_DURATION); + + itemsList.scrollToPosition(playQueue.getIndex()); + + if (playQueue.getStreams().size() > 4 && !isInFullscreen()) toggleFullscreen(); + } + + private void onQueueClosed() { + animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false, + DEFAULT_CONTROLS_DURATION); + queueVisible = false; + } + + private void onMoreOptionsClicked() { + if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); + + final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE; + + animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, + isMoreControlsVisible ? 0 : 180); + animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, + DEFAULT_CONTROLS_DURATION); + showControls(DEFAULT_CONTROLS_DURATION); + } + + private void onShareClicked() { + // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) + ShareUtils.shareUrl(service, + getVideoTitle(), + getVideoUrl() + "&t=" + getPlaybackSeekBar().getProgress() / 1000); + } + + private void onPlayWithKodiClicked() { + if (getCurrentMetadata() == null) return; + + try { + NavigationHelper.playWithKore(getParentActivity(), Uri.parse( + getCurrentMetadata().getMetadata().getUrl().replace("https", "http"))); + } catch (Exception e) { + if (DEBUG) Log.i(TAG, "Failed to start kore", e); + showInstallKoreDialog(getParentActivity()); + } + } + + private void onOpenInBrowserClicked() { + if (getCurrentMetadata() == null) return; + + ShareUtils.openUrlInBrowser(getParentActivity(), getCurrentMetadata().getMetadata().getOriginalUrl()); + } + + private static void showInstallKoreDialog(final Context context) { + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setMessage(R.string.kore_not_found) + .setPositiveButton(R.string.install, (DialogInterface dialog, int which) -> + NavigationHelper.installKore(context)) + .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> { + }); + builder.create().show(); + } + + @Override + public void onPlaybackSpeedClicked() { + if (videoPlayerSelected()) { + // It hides status bar in fullscreen mode + hideSystemUIIfNeeded(); + + PlaybackParameterDialog + .newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence(), this) + .show(getParentActivity().getSupportFragmentManager(), null); + } else { + super.onPlaybackSpeedClicked(); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + super.onStopTrackingTouch(seekBar); + if (wasPlaying()) showControlsThenHide(); + } + + @Override + public void onDismiss(PopupMenu menu) { + super.onDismiss(menu); + if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); + } + + @Override + public void onLayoutChange(final View view, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (popupPlayerSelected()) { + float widthDp = Math.abs(right - left) / service.getResources().getDisplayMetrics().density; + final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP ? View.VISIBLE : View.GONE; + secondaryControls.setVisibility(visibility); + } else if (videoPlayerSelected() + && !isInFullscreen() + && getAspectRatioFrameLayout().getMeasuredHeight() > service.getResources().getDisplayMetrics().heightPixels * 0.8) { + // Resize mode is ZOOM probably. In this mode video will grow down and it will be weird. + // So let's open it in fullscreen + toggleFullscreen(); + } + } + + @Override + protected int nextResizeMode(int currentResizeMode) { + final int newResizeMode; + switch (currentResizeMode) { + case AspectRatioFrameLayout.RESIZE_MODE_FIT: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; + break; + case AspectRatioFrameLayout.RESIZE_MODE_FILL: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + break; + default: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + break; + } + + storeResizeMode(newResizeMode); + return newResizeMode; + } + + private void storeResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) { + defaultPreferences.edit() + .putInt(service.getString(R.string.last_resize_mode), resizeMode) + .apply(); + } + + @Override + protected VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(List sortedVideos) { + return videoPlayerSelected() ? ListHelper.getDefaultResolutionIndex(context, sortedVideos) + : ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); + } + + @Override + public int getOverrideResolutionIndex(List sortedVideos, + String playbackQuality) { + return videoPlayerSelected() ? getResolutionIndex(context, sortedVideos, playbackQuality) + : getPopupResolutionIndex(context, sortedVideos, playbackQuality); + } + }; + } + + /*////////////////////////////////////////////////////////////////////////// + // States + //////////////////////////////////////////////////////////////////////////*/ + + private void animatePlayButtons(final boolean show, final int duration) { + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + if (playQueue.getIndex() > 0) + animateView(playPreviousButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + if (playQueue.getIndex() + 1 < playQueue.getStreams().size()) + animateView(playNextButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + + } + + @Override + public void changeState(int state) { + super.changeState(state); + updatePlayback(); + } + + @Override + public void onBlocked() { + super.onBlocked(); + playPauseButton.setImageResource(R.drawable.ic_pause_white); + animatePlayButtons(false, 100); + getRootView().setKeepScreenOn(false); + + service.resetNotification(); + service.updateNotification(R.drawable.ic_play_arrow_white); + } + + @Override + public void onBuffering() { + super.onBuffering(); + getRootView().setKeepScreenOn(true); + + service.resetNotification(); + service.updateNotification(R.drawable.ic_play_arrow_white); + } + + @Override + public void onPlaying() { + super.onPlaying(); + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { + playPauseButton.setImageResource(R.drawable.ic_pause_white); + animatePlayButtons(true, 200); + }); + + updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); + checkLandscape(); + getRootView().setKeepScreenOn(true); + + service.getLockManager().acquireWifiAndCpu(); + service.resetNotification(); + service.updateNotification(R.drawable.ic_pause_white); + } + + @Override + public void onPaused() { + super.onPaused(); + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { + playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); + animatePlayButtons(true, 200); + }); + + updateWindowFlags(IDLE_WINDOW_FLAGS); + + service.resetNotification(); + service.updateNotification(R.drawable.ic_play_arrow_white); + + getRootView().setKeepScreenOn(false); + + service.getLockManager().releaseWifiAndCpu(); + } + + @Override + public void onPausedSeek() { + super.onPausedSeek(); + animatePlayButtons(false, 100); + getRootView().setKeepScreenOn(true); + + service.resetNotification(); + service.updateNotification(R.drawable.ic_play_arrow_white); + } + + + @Override + public void onCompleted() { + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { + playPauseButton.setImageResource(R.drawable.ic_replay_white); + animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); + }); + getRootView().setKeepScreenOn(false); + + updateWindowFlags(IDLE_WINDOW_FLAGS); + + service.resetNotification(); + service.updateNotification(R.drawable.ic_replay_white); + + service.getLockManager().releaseWifiAndCpu(); + + super.onCompleted(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Broadcast Receiver + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void setupBroadcastReceiver(IntentFilter intentFilter) { + super.setupBroadcastReceiver(intentFilter); + if (DEBUG) Log.d(TAG, "setupBroadcastReceiver() called with: intentFilter = [" + intentFilter + "]"); + + intentFilter.addAction(ACTION_CLOSE); + intentFilter.addAction(ACTION_PLAY_PAUSE); + intentFilter.addAction(ACTION_OPEN_CONTROLS); + intentFilter.addAction(ACTION_REPEAT); + intentFilter.addAction(ACTION_PLAY_PREVIOUS); + intentFilter.addAction(ACTION_PLAY_NEXT); + intentFilter.addAction(ACTION_FAST_REWIND); + intentFilter.addAction(ACTION_FAST_FORWARD); + + intentFilter.addAction(Intent.ACTION_SCREEN_ON); + intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + + intentFilter.addAction(Intent.ACTION_HEADSET_PLUG); + } + + @Override + public void onBroadcastReceived(Intent intent) { + super.onBroadcastReceived(intent); + if (intent == null || intent.getAction() == null) + return; + + if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + + switch (intent.getAction()) { + case ACTION_CLOSE: + service.onDestroy(); + break; + case ACTION_PLAY_NEXT: + onPlayNext(); + break; + case ACTION_PLAY_PREVIOUS: + onPlayPrevious(); + break; + case ACTION_FAST_FORWARD: + onFastForward(); + break; + case ACTION_FAST_REWIND: + onFastRewind(); + break; + case ACTION_PLAY_PAUSE: + onPlayPause(); + break; + case ACTION_REPEAT: + onRepeatClicked(); + break; + case Intent.ACTION_SCREEN_ON: + shouldUpdateOnProgress = true; + // Interrupt playback only when screen turns on and user is watching video in fragment + if (backgroundPlaybackEnabled() && getPlayer() != null && (isPlaying() || getPlayer().isLoading())) + useVideoSource(true); + break; + case Intent.ACTION_SCREEN_OFF: + shouldUpdateOnProgress = false; + // Interrupt playback only when screen turns off with video working + if (backgroundPlaybackEnabled() && getPlayer() != null && (isPlaying() || getPlayer().isLoading())) + useVideoSource(false); + break; + } + service.resetNotification(); + } + + /*////////////////////////////////////////////////////////////////////////// + // 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 + service.resetNotification(); + service.updateNotification(-1); + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + super.onLoadingFailed(imageUri, view, failReason); + service.resetNotification(); + service.updateNotification(-1); + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + super.onLoadingCancelled(imageUri, view); + service.resetNotification(); + service.updateNotification(-1); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void setInitialGestureValues() { + if (getAudioReactor() != null) { + final float currentVolumeNormalized = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume(); + volumeProgressBar.setProgress((int) (volumeProgressBar.getMax() * currentVolumeNormalized)); + } + } + + private void choosePlayerTypeFromIntent(Intent intent) { + // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra + if (intent.getIntExtra(PLAYER_TYPE, PLAYER_TYPE_VIDEO) == PLAYER_TYPE_AUDIO) { + playerType = MainPlayer.PlayerType.AUDIO; + } else if (intent.getIntExtra(PLAYER_TYPE, PLAYER_TYPE_VIDEO) == PLAYER_TYPE_POPUP) { + playerType = MainPlayer.PlayerType.POPUP; + } else { + playerType = MainPlayer.PlayerType.VIDEO; + } + } + + public boolean backgroundPlaybackEnabled() { + return PlayerHelper.getMinimizeOnExitAction(service) == MINIMIZE_ON_EXIT_MODE_BACKGROUND; + } + + public boolean minimizeOnPopupEnabled() { + return PlayerHelper.getMinimizeOnExitAction(service) == PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; + } + + public boolean audioPlayerSelected() { + return playerType == MainPlayer.PlayerType.AUDIO; + } + + public boolean videoPlayerSelected() { + return playerType == MainPlayer.PlayerType.VIDEO; + } + + public boolean popupPlayerSelected() { + return playerType == MainPlayer.PlayerType.POPUP; + } + + 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; + } + + public boolean isInsideClosingRadius(MotionEvent popupMotionEvent) { + return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); + } + + public boolean isInFullscreen() { + return isFullscreen; + } + + @Override + public void showControlsThenHide() { + if (queueVisible) return; + + showOrHideButtons(); + super.showControlsThenHide(); + } + + @Override + public void showControls(long duration) { + if (queueVisible) return; + + showOrHideButtons(); + super.showControls(duration); + } + + @Override + public void hideControls(final long duration, long delay) { + if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); + + showOrHideButtons(); + + getControlsVisibilityHandler().removeCallbacksAndMessages(null); + getControlsVisibilityHandler().postDelayed(() -> + animateView(getControlsRoot(), false, duration, 0, + this::hideSystemUIIfNeeded), + /*delayMillis=*/delay + ); + } + + private void showOrHideButtons() { + if (playQueue == null) + return; + + if (playQueue.getIndex() == 0) + playPreviousButton.setVisibility(View.INVISIBLE); + else + playPreviousButton.setVisibility(View.VISIBLE); + + if (playQueue.getIndex() + 1 == playQueue.getStreams().size()) + playNextButton.setVisibility(View.INVISIBLE); + else + playNextButton.setVisibility(View.VISIBLE); + + if (playQueue.getStreams().size() <= 1 || popupPlayerSelected()) + queueButton.setVisibility(View.GONE); + else + queueButton.setVisibility(View.VISIBLE); + } + + @Override + public void hideSystemUIIfNeeded() { + if (fragmentListener != null) + fragmentListener.hideSystemUIIfNeeded(); + } + + private void updatePlaybackButtons() { + if (repeatButton == null || shuffleButton == null || + simpleExoPlayer == null || playQueue == null) return; + + setRepeatModeButton(repeatButton, getRepeatMode()); + setShuffleButton(shuffleButton, playQueue.isShuffled()); + } + + public void checkLandscape() { + AppCompatActivity parent = getParentActivity(); + if (parent != null && service.isLandscape() != isInFullscreen() + && getCurrentState() != STATE_COMPLETED && videoPlayerSelected()) + toggleFullscreen(); + } + + private void buildQueue() { + itemsList.setAdapter(playQueueAdapter); + itemsList.setClickable(true); + itemsList.setLongClickable(true); + + itemsList.clearOnScrollListeners(); + itemsList.addOnScrollListener(getQueueScrollListener()); + + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(itemsList); + + playQueueAdapter.setSelectedListener(getOnSelectedListener()); + + itemsListCloseButton.setOnClickListener(view -> onQueueClosed()); + } + + public void useVideoSource(boolean video) { + // Return when: old value of audioOnly equals to the new value, audio player is selected, + // video player is selected AND fragment is not shown + if (playQueue == null + || audioOnly == !video + || audioPlayerSelected() + || (video && videoPlayerSelected() && fragmentListener.isFragmentStopped())) + return; + + audioOnly = !video; + setRecovery(); + reload(); + } + + private OnScrollBelowItemsListener getQueueScrollListener() { + return new OnScrollBelowItemsListener() { + @Override + public void onScrolledDown(RecyclerView recyclerView) { + if (playQueue != null && !playQueue.isComplete()) { + playQueue.fetch(); + } else if (itemsList != null) { + itemsList.clearOnScrollListeners(); + } + } + }; + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new PlayQueueItemTouchCallback() { + @Override + public void onMove(int sourceIndex, int targetIndex) { + if (playQueue != null) playQueue.move(sourceIndex, targetIndex); + } + + @Override + public void onSwiped(int index) { + if (index != -1) playQueue.remove(index); + } + }; + } + + private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { + return new PlayQueueItemBuilder.OnSelectedListener() { + @Override + public void selected(PlayQueueItem item, View view) { + onSelected(item); + } + + @Override + public void held(PlayQueueItem item, View view) { + final int index = playQueue.indexOf(item); + if (index != -1) playQueue.remove(index); + } + + @Override + public void onStartDrag(PlayQueueItemHolder viewHolder) { + if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder); + } + }; + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + @SuppressLint("RtlHardcoded") + private void initPopup() { + if (DEBUG) Log.d(TAG, "initPopup() called"); + + updateScreenSize(); + + final boolean popupRememberSizeAndPos = PlayerHelper.isRememberingPopupDimensions(service); + final float defaultSize = service.getResources().getDimension(R.dimen.popup_default_width); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(service); + 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; + + popupLayoutParams = new WindowManager.LayoutParams( + (int) popupWidth, (int) getMinimumVideoHeight(popupWidth), + layoutParamType, + IDLE_WINDOW_FLAGS, + PixelFormat.TRANSLUCENT); + 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); + popupLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; + popupLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; + + checkPopupPositionBounds(); + + getLoadingPanel().setMinimumWidth(popupLayoutParams.width); + getLoadingPanel().setMinimumHeight(popupLayoutParams.height); + + service.removeViewFromParent(); + windowManager.addView(service.getView(), popupLayoutParams); + + if (getAspectRatioFrameLayout().getResizeMode() == AspectRatioFrameLayout.RESIZE_MODE_ZOOM) + onResizeClicked(); + } + + @SuppressLint("RtlHardcoded") + private void initPopupCloseOverlay() { + if (DEBUG) Log.d(TAG, "initPopupCloseOverlay() called"); + closeOverlayView = View.inflate(service, 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; + + WindowManager.LayoutParams 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); + } + + private void initVideoPlayer() { + service.getView().setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + } + + /*////////////////////////////////////////////////////////////////////////// + // Popup utils + //////////////////////////////////////////////////////////////////////////*/ + + /** + * @see #checkPopupPositionBounds(float, float) + */ + @SuppressWarnings("UnusedReturnValue") + public 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 + */ + public 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; + } + + public void savePositionAndSize() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(service); + 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) { + //if (DEBUG) Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], returned: " + height); + return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have + } + + public void updateScreenSize() { + DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(metrics); + + screenWidth = metrics.widthPixels; + screenHeight = metrics.heightPixels; + if (DEBUG) + Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", screenHeight = " + screenHeight); + + popupWidth = service.getResources().getDimension(R.dimen.popup_default_width); + popupHeight = getMinimumVideoHeight(popupWidth); + + minimumWidth = service.getResources().getDimension(R.dimen.popup_minimum_width); + minimumHeight = getMinimumVideoHeight(minimumWidth); + + maximumWidth = screenWidth; + maximumHeight = screenHeight; + } + + public void updatePopupSize(int width, int height) { + if (DEBUG) Log.d(TAG, "updatePopupSize() called with: width = [" + width + "], height = [" + height + "]"); + + if (popupLayoutParams == null || windowManager == null || !popupPlayerSelected() || getRootView().getParent() == null) + return; + + width = (int) (width > maximumWidth ? maximumWidth : width < minimumWidth ? minimumWidth : width); + + if (height == -1) height = (int) getMinimumVideoHeight(width); + else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : 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(getRootView(), popupLayoutParams); + } + + private void updateWindowFlags(final int flags) { + if (popupLayoutParams == null || windowManager == null || !popupPlayerSelected() || getRootView().getParent() == null) + return; + + popupLayoutParams.flags = flags; + windowManager.updateViewLayout(getRootView(), popupLayoutParams); + } + + /*////////////////////////////////////////////////////////////////////////// + // Misc + //////////////////////////////////////////////////////////////////////////*/ + + public void closePopup() { + if (DEBUG) Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); + if (isPopupClosing) return; + isPopupClosing = true; + + savePlaybackState(); + windowManager.removeView(getRootView()); + + 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); + + service.onDestroy(); + } + }).start(); + } + + /////////////////////////////////////////////////////////////////////////// + // Manipulations with listener + /////////////////////////////////////////////////////////////////////////// + + public void setFragmentListener(PlayerServiceEventListener listener) { + fragmentListener = listener; + updateMetadata(); + updatePlayback(); + triggerProgressUpdate(); + } + + public void removeFragmentListener(PlayerServiceEventListener listener) { + if (fragmentListener == listener) { + fragmentListener = null; + } + } + + /*package-private*/ void setActivityListener(PlayerEventListener listener) { + activityListener = listener; + updateMetadata(); + updatePlayback(); + triggerProgressUpdate(); + } + + /*package-private*/ void removeActivityListener(PlayerEventListener listener) { + if (activityListener == listener) { + activityListener = null; + } + } + + private void updateMetadata() { + if (fragmentListener != null && getCurrentMetadata() != null) { + fragmentListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); + } + if (activityListener != null && getCurrentMetadata() != null) { + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); + } + } + + private void updatePlayback() { + if (fragmentListener != null && simpleExoPlayer != null && playQueue != null) { + fragmentListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); + } + if (activityListener != null && simpleExoPlayer != null && playQueue != null) { + activityListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), getPlaybackParameters()); + } + } + + private void updateProgress(int currentProgress, int duration, int bufferPercent) { + if (fragmentListener != null) { + fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + if (activityListener != null) { + activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + } + + void stopActivityBinding() { + if (fragmentListener != null) { + fragmentListener.onServiceStopped(); + fragmentListener = null; + } + if (activityListener != null) { + activityListener.onServiceStopped(); + activityListener = null; + } + } + + /////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////// + + public RelativeLayout getVolumeRelativeLayout() { + return volumeRelativeLayout; + } + + 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 getPlayPauseButton() { + return playPauseButton; + } + + public int getMaxGestureLength() { + return maxGestureLength; + } + + public TextView getResizingIndicator() { + return resizingIndicator; + } + + public GestureDetector getGestureDetector() { + return gestureDetector; + } + + public WindowManager.LayoutParams getPopupLayoutParams() { + return popupLayoutParams; + } + + public float getScreenWidth() { + return screenWidth; + } + + public float getScreenHeight() { + return screenHeight; + } + + public float getPopupWidth() { + return popupWidth; + } + + public float getPopupHeight() { + return popupHeight; + } + + public void setPopupWidth(float width) { + popupWidth = width; + } + + public void setPopupHeight(float height) { + popupHeight = height; + } + + public View getCloseOverlayButton() { + return closeOverlayButton; + } + + public View getCloseOverlayView() { + return closeOverlayView; + } + + public View getClosingOverlayView() { + return closingOverlayView; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java new file mode 100644 index 000000000..85db3b201 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.player.event; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import org.schabi.newpipe.R; + +public class CustomBottomSheetBehavior extends BottomSheetBehavior { + + public CustomBottomSheetBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent event) { + // Without overriding scrolling will not work in detail_content_root_layout + ViewGroup controls = child.findViewById(R.id.detail_content_root_layout); + if (controls != null) { + Rect rect = new Rect(); + controls.getGlobalVisibleRect(rect); + if (rect.contains((int) event.getX(), (int) event.getY())) return false; + } + + // Without overriding scrolling will not work on relatedStreamsLayout + ViewGroup relatedStreamsLayout = child.findViewById(R.id.relatedStreamsLayout); + if (relatedStreamsLayout != null) { + Rect rect = new Rect(); + relatedStreamsLayout.getGlobalVisibleRect(rect); + if (rect.contains((int) event.getX(), (int) event.getY())) return false; + } + + ViewGroup playQueue = child.findViewById(R.id.playQueue); + if (playQueue != null) { + Rect rect = new Rect(); + playQueue.getGlobalVisibleRect(rect); + if (rect.contains((int) event.getX(), (int) event.getY())) return false; + } + + return super.onInterceptTouchEvent(parent, child, event); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java new file mode 100644 index 000000000..72462beff --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -0,0 +1,462 @@ +package org.schabi.newpipe.player.event; + +import android.app.Activity; +import android.util.Log; +import android.view.*; +import androidx.appcompat.content.res.AppCompatResources; +import org.schabi.newpipe.R; +import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.VideoPlayerImpl; +import org.schabi.newpipe.player.helper.PlayerHelper; + +import static org.schabi.newpipe.player.BasePlayer.*; +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.animateView; + +public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { + private static final String TAG = ".PlayerGestureListener"; + private static final boolean DEBUG = BasePlayer.DEBUG; + + private VideoPlayerImpl playerImpl; + private MainPlayer service; + + private int initialPopupX, initialPopupY; + + private boolean isMovingInMain, isMovingInPopup; + + private boolean isResizing; + + private int tossFlingVelocity; + + private final boolean isVolumeGestureEnabled; + private final boolean isBrightnessGestureEnabled; + private final int maxVolume; + private static final int MOVEMENT_THRESHOLD = 40; + + + public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) { + this.playerImpl = playerImpl; + this.service = service; + this.tossFlingVelocity = PlayerHelper.getTossFlingVelocity(service); + + isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service); + isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(service); + maxVolume = playerImpl.getAudioReactor().getMaxVolume(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Helpers + //////////////////////////////////////////////////////////////////////////*/ + + /* + * Main and popup players' gesture listeners is too different. + * So it will be better to have different implementations of them + * */ + @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.popupPlayerSelected()) return onDoubleTapInPopup(e); + else return onDoubleTapInMain(e); + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + + if (playerImpl.popupPlayerSelected()) return onSingleTapConfirmedInPopup(e); + else return onSingleTapConfirmedInMain(e); + } + + @Override + public boolean onDown(MotionEvent e) { + if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); + + if (playerImpl.popupPlayerSelected()) return onDownInPopup(e); + else return true; + } + @Override + public void onLongPress(MotionEvent e) { + if (DEBUG) Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); + + if (playerImpl.popupPlayerSelected()) onLongPressInPopup(e); + } + + @Override + public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) { + if (playerImpl.popupPlayerSelected()) return onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY); + else return onScrollInMain(initialEvent, movingEvent, distanceX, distanceY); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (DEBUG) Log.d(TAG, "onFling() called with velocity: dX=[" + velocityX + "], dY=[" + velocityY + "]"); + + if (playerImpl.popupPlayerSelected()) return onFlingInPopup(e1, e2, velocityX, velocityY); + else return true; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + //noinspection PointlessBooleanExpression,ConstantConditions + if (DEBUG && false) Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]"); + + if (playerImpl.popupPlayerSelected()) return onTouchInPopup(v, event); + else return onTouchInMain(v, event); + } + + + /*////////////////////////////////////////////////////////////////////////// + // Main player listener + //////////////////////////////////////////////////////////////////////////*/ + + private boolean onDoubleTapInMain(MotionEvent e) { + if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) { + playerImpl.onFastForward(); + } else if (e.getX() < playerImpl.getRootView().getWidth() / 3) { + playerImpl.onFastRewind(); + } else { + playerImpl.getPlayPauseButton().performClick(); + } + + return true; + } + + + private boolean onSingleTapConfirmedInMain(MotionEvent e) { + if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + + if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true; + + if (playerImpl.isControlsVisible()) { + playerImpl.hideControls(150, 0); + } else { + if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { + playerImpl.showControls(0); + } else { + playerImpl.showControlsThenHide(); + } + if (playerImpl.isInFullscreen()) { + int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_FULLSCREEN; + playerImpl.getParentActivity().getWindow().getDecorView().setSystemUiVisibility(visibility); + playerImpl.getParentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + playerImpl.getParentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + } + return true; + } + + private boolean onScrollInMain(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) { + if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) return false; + + //noinspection PointlessBooleanExpression + if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " + + ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + + ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + + ", distanceXy = [" + distanceX + ", " + distanceY + "]"); + + final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; + if (!isMovingInMain && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) + || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { + return false; + } + + isMovingInMain = true; + + boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled; + boolean acceptVolumeArea = acceptAnyArea || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2; + boolean acceptBrightnessArea = acceptAnyArea || !acceptVolumeArea; + + if (isVolumeGestureEnabled && acceptVolumeArea) { + playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); + float currentProgressPercent = + (float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); + int currentVolume = (int) (maxVolume * currentProgressPercent); + playerImpl.getAudioReactor().setVolume(currentVolume); + + if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); + + 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(service, 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 if (isBrightnessGestureEnabled && acceptBrightnessArea) { + Activity parent = playerImpl.getParentActivity(); + if (parent == null) return true; + + Window window = parent.getWindow(); + + playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY); + float currentProgressPercent = + (float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength(); + WindowManager.LayoutParams layoutParams = window.getAttributes(); + layoutParams.screenBrightness = currentProgressPercent; + window.setAttributes(layoutParams); + + if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentProgressPercent); + + 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; + + playerImpl.getBrightnessImageView().setImageDrawable( + AppCompatResources.getDrawable(service, 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 onScrollEndInMain() { + if (DEBUG) Log.d(TAG, "onScrollEnd() called"); + + if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, 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) { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + } + + private boolean onTouchInMain(View v, MotionEvent event) { + playerImpl.getGestureDetector().onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP && isMovingInMain) { + isMovingInMain = false; + onScrollEndInMain(); + } + // This hack allows to stop receiving touch events on appbar while touching video player view + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + v.getParent().requestDisallowInterceptTouchEvent(playerImpl.isInFullscreen()); + return true; + case MotionEvent.ACTION_UP: + v.getParent().requestDisallowInterceptTouchEvent(false); + return false; + default: + return true; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Popup player listener + //////////////////////////////////////////////////////////////////////////*/ + + private boolean onDoubleTapInPopup(MotionEvent e) { + if (playerImpl == null || !playerImpl.isPlaying()) return false; + + playerImpl.hideControls(0, 0); + + if (e.getX() > playerImpl.getPopupWidth() / 2) { + playerImpl.onFastForward(); + } else { + playerImpl.onFastRewind(); + } + + return true; + } + + private boolean onSingleTapConfirmedInPopup(MotionEvent e) { + if (playerImpl == null || playerImpl.getPlayer() == null) return false; + if (playerImpl.isControlsVisible()) { + playerImpl.hideControls(100, 100); + } else { + playerImpl.showControlsThenHide(); + + } + playerImpl.onPlayPause(); + return true; + } + + private boolean onDownInPopup(MotionEvent e) { + // 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). + playerImpl.checkPopupPositionBounds(playerImpl.getCloseOverlayView().getWidth(), playerImpl.getCloseOverlayView().getHeight()); + + initialPopupX = playerImpl.getPopupLayoutParams().x; + initialPopupY = playerImpl.getPopupLayoutParams().y; + playerImpl.setPopupWidth(playerImpl.getPopupLayoutParams().width); + playerImpl.setPopupHeight(playerImpl.getPopupLayoutParams().height); + return super.onDown(e); + } + + private void onLongPressInPopup(MotionEvent e) { + playerImpl.updateScreenSize(); + playerImpl.checkPopupPositionBounds(); + playerImpl.updatePopupSize((int) playerImpl.getScreenWidth(), -1); + } + + private boolean onScrollInPopup(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) { + if (isResizing || playerImpl == null) return super.onScroll(initialEvent, movingEvent, distanceX, distanceY); + + if (!isMovingInPopup) { + animateView(playerImpl.getCloseOverlayButton(), true, 200); + } + + isMovingInPopup = true; + + float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()), posX = (int) (initialPopupX + diffX); + float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()), posY = (int) (initialPopupY + diffY); + + if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth()); + else if (posX < 0) posX = 0; + + if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight()); + else if (posY < 0) posY = 0; + + playerImpl.getPopupLayoutParams().x = (int) posX; + playerImpl.getPopupLayoutParams().y = (int) posY; + + final View closingOverlayView = playerImpl.getClosingOverlayView(); + if (playerImpl.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 = [" + 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 = [" + playerImpl.getPopupWidth() + " x " + playerImpl.getPopupHeight() + "]"); + } + playerImpl.windowManager.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams()); + return true; + } + + private void onScrollEndInPopup(MotionEvent event) { + if (playerImpl == null) return; + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + + if (playerImpl.isInsideClosingRadius(event)) { + playerImpl.closePopup(); + } else { + animateView(playerImpl.getClosingOverlayView(), false, 0); + + if (!playerImpl.isPopupClosing) { + animateView(playerImpl.getCloseOverlayButton(), false, 200); + } + } + } + + private boolean onFlingInPopup(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (playerImpl == null) return false; + + final float absVelocityX = Math.abs(velocityX); + final float absVelocityY = Math.abs(velocityY); + if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { + if (absVelocityX > tossFlingVelocity) playerImpl.getPopupLayoutParams().x = (int) velocityX; + if (absVelocityY > tossFlingVelocity) playerImpl.getPopupLayoutParams().y = (int) velocityY; + playerImpl.checkPopupPositionBounds(); + playerImpl.windowManager.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams()); + return true; + } + return false; + } + + private boolean onTouchInPopup(View v, MotionEvent event) { + playerImpl.getGestureDetector().onTouchEvent(event); + if (playerImpl == null) return false; + if (event.getPointerCount() == 2 && !isMovingInPopup && !isResizing) { + if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); + playerImpl.showAndAnimateControl(-1, true); + playerImpl.getLoadingPanel().setVisibility(View.GONE); + + playerImpl.hideControls(0, 0); + animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); + animateView(playerImpl.getResizingIndicator(), true, 200, 0); + isResizing = true; + } + + if (event.getAction() == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) { + if (DEBUG) Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + return handleMultiDrag(event); + } + + if (event.getAction() == MotionEvent.ACTION_UP) { + if (DEBUG) + Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + if (isMovingInPopup) { + isMovingInPopup = false; + onScrollEndInPopup(event); + } + + if (isResizing) { + isResizing = false; + animateView(playerImpl.getResizingIndicator(), false, 100, 0); + playerImpl.changeState(playerImpl.getCurrentState()); + } + + if (!playerImpl.isPopupClosing) { + playerImpl.savePositionAndSize(); + } + } + + v.performClick(); + return true; + } + + private boolean handleMultiDrag(final MotionEvent event) { + if (event.getPointerCount() != 2) return false; + + final float firstPointerX = event.getX(0); + final float secondPointerX = event.getX(1); + + final float diff = Math.abs(firstPointerX - secondPointerX); + if (firstPointerX > secondPointerX) { + // second pointer is the anchor (the leftmost pointer) + playerImpl.getPopupLayoutParams().x = (int) (event.getRawX() - diff); + } else { + // first pointer is the anchor + playerImpl.getPopupLayoutParams().x = (int) event.getRawX(); + } + + playerImpl.checkPopupPositionBounds(); + playerImpl.updateScreenSize(); + + final int width = (int) Math.min(playerImpl.getScreenWidth(), diff); + playerImpl.updatePopupSize(width, -1); + + return true; + } + +} + + diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java new file mode 100644 index 000000000..7422f9442 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java @@ -0,0 +1,15 @@ +package org.schabi.newpipe.player.event; + +import com.google.android.exoplayer2.ExoPlaybackException; + +public interface PlayerServiceEventListener extends PlayerEventListener { + void onFullscreenStateChanged(boolean fullscreen); + + void onMoreOptionsLongClicked(); + + void onPlayerError(ExoPlaybackException error); + + boolean isFragmentStopped(); + + void hideSystemUIIfNeeded(); +} \ No newline at end of file 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 4feed74fe..457b72120 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 @@ -80,8 +80,10 @@ public class PlaybackParameterDialog extends DialogFragment { public static PlaybackParameterDialog newInstance(final double playbackTempo, final double playbackPitch, - final boolean playbackSkipSilence) { + final boolean playbackSkipSilence, + Callback callback) { PlaybackParameterDialog dialog = new PlaybackParameterDialog(); + dialog.callback = callback; dialog.initialTempo = playbackTempo; dialog.initialPitch = playbackPitch; @@ -99,9 +101,9 @@ public class PlaybackParameterDialog extends DialogFragment { @Override public void onAttach(Context context) { super.onAttach(context); - if (context != null && context instanceof Callback) { + if (context instanceof Callback) { callback = (Callback) context; - } else { + } else if (callback == null) { dismiss(); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsContentObserver.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsContentObserver.java new file mode 100644 index 000000000..534fb26c3 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsContentObserver.java @@ -0,0 +1,29 @@ +package org.schabi.newpipe.settings; + +import android.database.ContentObserver; +import android.os.Handler; + +public class SettingsContentObserver extends ContentObserver { + private OnChangeListener listener; + + public interface OnChangeListener { + void onSettingsChanged(); + } + + public SettingsContentObserver(Handler handler, OnChangeListener listener) { + super(handler); + this.listener = listener; + } + + @Override + public boolean deliverSelfNotifications() { + return super.deliverSelfNotifications(); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + if (listener != null) + listener.onSettingsChanged(); + } +} \ No newline at end of file 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 e2b03c8e8..07ef0b6ac 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -10,6 +10,7 @@ import android.os.Build; import android.preference.PreferenceManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; @@ -44,14 +45,9 @@ 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; -import org.schabi.newpipe.player.BasePlayer; -import org.schabi.newpipe.player.MainVideoPlayer; -import org.schabi.newpipe.player.PopupVideoPlayer; -import org.schabi.newpipe.player.PopupVideoPlayerActivity; -import org.schabi.newpipe.player.VideoPlayer; +import org.schabi.newpipe.player.*; import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.settings.SettingsActivity; import java.util.ArrayList; @@ -78,6 +74,9 @@ public class NavigationHelper { if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback); + int playerType = intent.getIntExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO); + intent.putExtra(VideoPlayer.PLAYER_TYPE, playerType); + return intent; } @@ -117,10 +116,13 @@ public class NavigationHelper { .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence); } - public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { - final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback); - playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(playerIntent); + public static void playOnMainPlayer(final AppCompatActivity activity, final PlayQueue queue, final boolean resumePlayback) { + playOnMainPlayer(activity.getSupportFragmentManager(), queue, resumePlayback); + } + + public static void playOnMainPlayer(final FragmentManager fragmentManager, final PlayQueue queue, boolean autoPlay) { + PlayQueueItem currentStream = queue.getItem(); + NavigationHelper.openVideoDetailFragment(fragmentManager, currentStream.getServiceId(), currentStream.getUrl(), currentStream.getTitle(), autoPlay, queue); } public static void playOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { @@ -130,12 +132,16 @@ public class NavigationHelper { } Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback)); + Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP); + startService(context, intent); } public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); - startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback)); + Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); + startService(context, intent); } public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { @@ -149,8 +155,9 @@ public class NavigationHelper { } Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); - startService(context, - getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend, resumePlayback)); + Intent intent = getPlayerEnqueueIntent(context, MainPlayer.class, queue, selectOnAppend, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP); + startService(context, intent); } public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { @@ -159,8 +166,9 @@ public class NavigationHelper { public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); - startService(context, - getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend, resumePlayback)); + Intent intent = getPlayerEnqueueIntent(context, MainPlayer.class, queue, selectOnAppend, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); + startService(context, intent); } public static void startService(@NonNull final Context context, @NonNull final Intent intent) { @@ -281,29 +289,35 @@ public class NavigationHelper { } public static void openVideoDetailFragment(FragmentManager fragmentManager, int serviceId, String url, String title) { - openVideoDetailFragment(fragmentManager, serviceId, url, title, false); + openVideoDetailFragment(fragmentManager, serviceId, url, title, true, null); } - public static void openVideoDetailFragment(FragmentManager fragmentManager, int serviceId, String url, String title, boolean autoPlay) { - Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_holder); + public static void openVideoDetailFragment(FragmentManager fragmentManager, int serviceId, String url, String title, boolean autoPlay, PlayQueue playQueue) { + Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_player_holder); if (title == null) title = ""; if (fragment instanceof VideoDetailFragment && fragment.isVisible()) { + expandMainPlayer(fragment.getActivity()); VideoDetailFragment detailFragment = (VideoDetailFragment) fragment; detailFragment.setAutoplay(autoPlay); - detailFragment.selectAndLoadVideo(serviceId, url, title); + detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue); return; } - VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, title); + VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, title, playQueue); instance.setAutoplay(autoPlay); defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, instance) - .addToBackStack(null) + .replace(R.id.fragment_player_holder, instance) + .runOnCommit(() -> expandMainPlayer(instance.getActivity())) .commit(); } + public static void expandMainPlayer(Context context) { + final Intent intent = new Intent(VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER); + context.sendBroadcast(intent); + } + public static void openChannelFragment( FragmentManager fragmentManager, int serviceId, @@ -458,10 +472,6 @@ public class NavigationHelper { return getServicePlayerActivityIntent(context, BackgroundPlayerActivity.class); } - public static Intent getPopupPlayerActivityIntent(final Context context) { - return getServicePlayerActivityIntent(context, PopupVideoPlayerActivity.class); - } - private static Intent getServicePlayerActivityIntent(final Context context, final Class activityClass) { Intent intent = new Intent(context, activityClass); diff --git a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java index c5c78a726..17768cd08 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java @@ -17,6 +17,6 @@ public class ShareUtils { intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, subject); intent.putExtra(Intent.EXTRA_TEXT, url); - context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_dialog_title))); + context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_dialog_title)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } } diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml index b535db2b8..cbcc6eeb0 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -2,9 +2,10 @@ @@ -120,7 +121,7 @@ android:layout_height="match_parent" android:layout_below="@id/playQueueControl" android:scrollbars="vertical" - app:layoutManager="LinearLayoutManager" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/play_queue_item"/> @@ -133,37 +134,46 @@ android:visibility="gone" tools:visibility="visible"> + - + + + tools:ignore="RtlHardcoded" + android:layout_weight="1"> + tools:text="1x"/> + tools:ignore="ContentDescription,RtlHardcoded" + android:visibility="gone"/> - - + + @@ -274,9 +284,7 @@ android:id="@+id/resizeTextView" android:layout_width="wrap_content" android:layout_height="35dp" - android:layout_marginLeft="8dp" android:layout_marginRight="8dp" - android:layout_alignParentLeft="true" android:gravity="center" android:minWidth="50dp" android:textColor="@android:color/white" @@ -291,18 +299,50 @@ android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" - android:layout_toLeftOf="@id/switchBackground" - android:layout_toRightOf="@id/resizeTextView" android:gravity="center|left" android:minHeight="35dp" - android:minWidth="40dp" - android:paddingLeft="2dp" - android:paddingRight="2dp" + android:minWidth="50dp" android:textColor="@android:color/white" android:textStyle="bold" android:background="?attr/selectableItemBackground" tools:ignore="RelativeOverlap,RtlHardcoded" - tools:text="English" /> + tools:text="English"/> + + + + + + - + + + - - - + android:scaleType="fitCenter" + android:src="@drawable/ic_fullscreen_white" + tools:ignore="ContentDescription,RtlHardcoded" + android:visibility="gone" + tools:visibility="visible"/> @@ -388,7 +396,7 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center" - android:minHeight="40dp" + android:minHeight="30dp" android:text="-:--:--" android:textColor="@android:color/white" tools:ignore="HardcodedText" @@ -433,65 +441,52 @@ + + + + + + - - -