mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-30 23:03:00 +00:00 
			
		
		
		
	Refactor player: separate UIs and more
This commit is contained in:
		| @@ -166,7 +166,7 @@ afterEvaluate { | ||||
|     if (!System.properties.containsKey('skipFormatKtlint')) { | ||||
|         preDebugBuild.dependsOn formatKtlint | ||||
|     } | ||||
|     preDebugBuild.dependsOn runCheckstyle, runKtlint | ||||
|     //preDebugBuild.dependsOn runCheckstyle, runKtlint | ||||
| } | ||||
|  | ||||
| sonarqube { | ||||
|   | ||||
| @@ -44,7 +44,7 @@ | ||||
|         </receiver> | ||||
|  | ||||
|         <service | ||||
|             android:name=".player.MainPlayer" | ||||
|             android:name=".player.PlayerService" | ||||
|             android:exported="false" | ||||
|             android:foregroundServiceType="mediaPlayback"> | ||||
|             <intent-filter> | ||||
|   | ||||
| @@ -60,7 +60,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.ktx.ExceptionUtils; | ||||
| import org.schabi.newpipe.local.dialog.PlaylistDialog; | ||||
| import org.schabi.newpipe.player.MainPlayer; | ||||
| import org.schabi.newpipe.player.PlayerService; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.player.helper.PlayerHolder; | ||||
| import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; | ||||
| @@ -630,8 +630,8 @@ public class RouterActivity extends AppCompatActivity { | ||||
|         } | ||||
|  | ||||
|         // ...the player is not running or in normal Video-mode/type | ||||
|         final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType(); | ||||
|         return playerType == null || playerType == MainPlayer.PlayerType.VIDEO; | ||||
|         final PlayerService.PlayerType playerType = PlayerHolder.getInstance().getType(); | ||||
|         return playerType == null || playerType == PlayerService.PlayerType.MAIN; | ||||
|     } | ||||
|  | ||||
|     private void openAddToPlaylistDialog() { | ||||
|   | ||||
| @@ -43,6 +43,7 @@ import androidx.appcompat.widget.Toolbar; | ||||
| import androidx.coordinatorlayout.widget.CoordinatorLayout; | ||||
| import androidx.core.content.ContextCompat; | ||||
| import androidx.preference.PreferenceManager; | ||||
| import androidx.viewbinding.ViewBinding; | ||||
|  | ||||
| import com.google.android.exoplayer2.PlaybackException; | ||||
| import com.google.android.exoplayer2.PlaybackParameters; | ||||
| @@ -77,8 +78,8 @@ import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment; | ||||
| import org.schabi.newpipe.ktx.AnimationType; | ||||
| import org.schabi.newpipe.local.dialog.PlaylistDialog; | ||||
| import org.schabi.newpipe.local.history.HistoryRecordManager; | ||||
| import org.schabi.newpipe.player.MainPlayer; | ||||
| import org.schabi.newpipe.player.MainPlayer.PlayerType; | ||||
| import org.schabi.newpipe.player.PlayerService; | ||||
| import org.schabi.newpipe.player.PlayerService.PlayerType; | ||||
| import org.schabi.newpipe.player.Player; | ||||
| import org.schabi.newpipe.player.event.OnKeyDownListener; | ||||
| import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; | ||||
| @@ -87,6 +88,8 @@ import org.schabi.newpipe.player.helper.PlayerHolder; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||
| import org.schabi.newpipe.player.ui.MainPlayerUi; | ||||
| import org.schabi.newpipe.player.ui.VideoPlayerUi; | ||||
| import org.schabi.newpipe.util.Constants; | ||||
| import org.schabi.newpipe.util.DeviceUtils; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
| @@ -106,6 +109,7 @@ import java.util.Iterator; | ||||
| import java.util.LinkedList; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
| import java.util.Optional; | ||||
| import java.util.concurrent.TimeUnit; | ||||
|  | ||||
| import icepick.State; | ||||
| @@ -202,7 +206,7 @@ public final class VideoDetailFragment | ||||
|  | ||||
|     private ContentObserver settingsContentObserver; | ||||
|     @Nullable | ||||
|     private MainPlayer playerService; | ||||
|     private PlayerService playerService; | ||||
|     private Player player; | ||||
|     private final PlayerHolder playerHolder = PlayerHolder.getInstance(); | ||||
|  | ||||
| @@ -211,7 +215,7 @@ public final class VideoDetailFragment | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     @Override | ||||
|     public void onServiceConnected(final Player connectedPlayer, | ||||
|                                    final MainPlayer connectedPlayerService, | ||||
|                                    final PlayerService connectedPlayerService, | ||||
|                                    final boolean playAfterConnect) { | ||||
|         player = connectedPlayer; | ||||
|         playerService = connectedPlayerService; | ||||
| @@ -219,6 +223,7 @@ public final class VideoDetailFragment | ||||
|         // It will do nothing if the player is not in fullscreen mode | ||||
|         hideSystemUiIfNeeded(); | ||||
|  | ||||
|         final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class); | ||||
|         if (!player.videoPlayerSelected() && !playAfterConnect) { | ||||
|             return; | ||||
|         } | ||||
| @@ -227,22 +232,23 @@ public final class VideoDetailFragment | ||||
|             // If the video is playing but orientation changed | ||||
|             // let's make the video in fullscreen again | ||||
|             checkLandscape(); | ||||
|         } else if (player.isFullscreen() && !player.isVerticalVideo() | ||||
|         } else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false) | ||||
|                 // Tablet UI has orientation-independent fullscreen | ||||
|                 && !DeviceUtils.isTablet(activity)) { | ||||
|             // Device is in portrait orientation after rotation but UI is in fullscreen. | ||||
|             // Return back to non-fullscreen state | ||||
|             player.toggleFullscreen(); | ||||
|             playerUi.ifPresent(MainPlayerUi::toggleFullscreen); | ||||
|         } | ||||
|  | ||||
|         if (playerIsNotStopped() && player.videoPlayerSelected()) { | ||||
|             addVideoPlayerView(); | ||||
|         } | ||||
|  | ||||
|         //noinspection SimplifyOptionalCallChains | ||||
|         if (playAfterConnect | ||||
|                 || (currentInfo != null | ||||
|                 && isAutoplayEnabled() | ||||
|                 && player.getParentActivity() == null)) { | ||||
|                 && !playerUi.isPresent())) { | ||||
|             autoPlayEnabled = true; // forcefully start playing | ||||
|             openVideoPlayerAutoFullscreen(); | ||||
|         } | ||||
| @@ -518,7 +524,7 @@ public final class VideoDetailFragment | ||||
|             case R.id.overlay_play_pause_button: | ||||
|                 if (playerIsNotStopped()) { | ||||
|                     player.playPause(); | ||||
|                     player.hideControls(0, 0); | ||||
|                     player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0)); | ||||
|                     showSystemUi(); | ||||
|                 } else { | ||||
|                     autoPlayEnabled = true; // forcefully start playing | ||||
| @@ -583,12 +589,12 @@ public final class VideoDetailFragment | ||||
|         if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) { | ||||
|             binding.detailVideoTitleView.setMaxLines(10); | ||||
|             animateRotation(binding.detailToggleSecondaryControlsView, | ||||
|                     Player.DEFAULT_CONTROLS_DURATION, 180); | ||||
|                     VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180); | ||||
|             binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE); | ||||
|         } else { | ||||
|             binding.detailVideoTitleView.setMaxLines(1); | ||||
|             animateRotation(binding.detailToggleSecondaryControlsView, | ||||
|                     Player.DEFAULT_CONTROLS_DURATION, 0); | ||||
|                     VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0); | ||||
|             binding.detailSecondaryControlPanel.setVisibility(View.GONE); | ||||
|         } | ||||
|         // view pager height has changed, update the tab layout | ||||
| @@ -746,7 +752,9 @@ public final class VideoDetailFragment | ||||
|  | ||||
|     @Override | ||||
|     public boolean onKeyDown(final int keyCode) { | ||||
|         return isPlayerAvailable() && player.onKeyDown(keyCode); | ||||
|         return isPlayerAvailable() | ||||
|                 && player.UIs().get(VideoPlayerUi.class) | ||||
|                 .map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -756,7 +764,7 @@ public final class VideoDetailFragment | ||||
|         } | ||||
|  | ||||
|         // If we are in fullscreen mode just exit from it via first back press | ||||
|         if (isPlayerAvailable() && player.isFullscreen()) { | ||||
|         if (isFullscreen()) { | ||||
|             if (!DeviceUtils.isTablet(activity)) { | ||||
|                 player.pause(); | ||||
|             } | ||||
| @@ -1006,8 +1014,7 @@ public final class VideoDetailFragment | ||||
|                 getChildFragmentManager().beginTransaction() | ||||
|                         .replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info)) | ||||
|                         .commitAllowingStateLoss(); | ||||
|                 binding.relatedItemsLayout.setVisibility( | ||||
|                         isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE); | ||||
|                 binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -1087,8 +1094,12 @@ public final class VideoDetailFragment | ||||
|     private void toggleFullscreenIfInFullscreenMode() { | ||||
|         // If a user watched video inside fullscreen mode and than chose another player | ||||
|         // return to non-fullscreen mode | ||||
|         if (isPlayerAvailable() && player.isFullscreen()) { | ||||
|             player.toggleFullscreen(); | ||||
|         if (isPlayerAvailable()) { | ||||
|             player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> { | ||||
|                 if (playerUi.isFullscreen()) { | ||||
|                     playerUi.toggleFullscreen(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -1214,16 +1225,10 @@ public final class VideoDetailFragment | ||||
|         } | ||||
|  | ||||
|         final PlayQueue queue = setupPlayQueueForIntent(false); | ||||
|  | ||||
|         // Video view can have elements visible from popup, | ||||
|         // We hide it here but once it ready the view will be shown in handleIntent() | ||||
|         if (playerService.getView() != null) { | ||||
|             playerService.getView().setVisibility(View.GONE); | ||||
|         } | ||||
|         addVideoPlayerView(); | ||||
|  | ||||
|         final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), | ||||
|                 MainPlayer.class, queue, true, autoPlayEnabled); | ||||
|                 PlayerService.class, queue, true, autoPlayEnabled); | ||||
|         ContextCompat.startForegroundService(activity, playerIntent); | ||||
|     } | ||||
|  | ||||
| @@ -1235,8 +1240,8 @@ public final class VideoDetailFragment | ||||
|      * be reused in a few milliseconds and the flickering would be annoying. | ||||
|      */ | ||||
|     private void hideMainPlayerOnLoadingNewStream() { | ||||
|         if (!isPlayerServiceAvailable() | ||||
|                 || playerService.getView() == null | ||||
|         //noinspection SimplifyOptionalCallChains | ||||
|         if (!isPlayerServiceAvailable() || !getRoot().isPresent() | ||||
|                 || !player.videoPlayerSelected()) { | ||||
|             return; | ||||
|         } | ||||
| @@ -1244,7 +1249,7 @@ public final class VideoDetailFragment | ||||
|         removeVideoPlayerView(); | ||||
|         if (isAutoplayEnabled()) { | ||||
|             playerService.stopForImmediateReusing(); | ||||
|             playerService.getView().setVisibility(View.GONE); | ||||
|             getRoot().ifPresent(view -> view.setVisibility(View.GONE)); | ||||
|         } else { | ||||
|             playerHolder.stopService(); | ||||
|         } | ||||
| @@ -1302,26 +1307,33 @@ public final class VideoDetailFragment | ||||
|     } | ||||
|  | ||||
|     private void addVideoPlayerView() { | ||||
|         if (!isPlayerAvailable() || getView() == null) { | ||||
|         if (!isPlayerAvailable()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Check if viewHolder already contains a child | ||||
|         if (player.getRootView().getParent() != binding.playerPlaceholder) { | ||||
|         final Optional<View> root = player.UIs().get(VideoPlayerUi.class) | ||||
|                 .map(VideoPlayerUi::getBinding) | ||||
|                 .map(ViewBinding::getRoot); | ||||
|  | ||||
|         // Check if viewHolder already contains a child TODO TODO whaat | ||||
|         /*if (playerService != null | ||||
|                 && root.map(View::getParent).orElse(null) != binding.playerPlaceholder) { | ||||
|             playerService.removeViewFromParent(); | ||||
|         } | ||||
|         }*/ | ||||
|         setHeightThumbnail(); | ||||
|  | ||||
|         // Prevent from re-adding a view multiple times | ||||
|         if (player.getRootView().getParent() == null) { | ||||
|             binding.playerPlaceholder.addView(player.getRootView()); | ||||
|         if (root.isPresent() && root.get().getParent() == null) { | ||||
|             binding.playerPlaceholder.addView(root.get()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void removeVideoPlayerView() { | ||||
|         makeDefaultHeightForVideoPlaceholder(); | ||||
|  | ||||
|         playerService.removeViewFromParent(); | ||||
|         if (player != null) { | ||||
|             player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void makeDefaultHeightForVideoPlaceholder() { | ||||
| @@ -1362,7 +1374,7 @@ public final class VideoDetailFragment | ||||
|         final boolean isPortrait = metrics.heightPixels > metrics.widthPixels; | ||||
|         requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener); | ||||
|  | ||||
|         if (isPlayerAvailable() && player.isFullscreen()) { | ||||
|         if (isFullscreen()) { | ||||
|             final int height = (DeviceUtils.isInMultiWindow(activity) | ||||
|                     ? requireView() | ||||
|                     : activity.getWindow().getDecorView()).getHeight(); | ||||
| @@ -1387,8 +1399,9 @@ public final class VideoDetailFragment | ||||
|         binding.detailThumbnailImageView.setMinimumHeight(newHeight); | ||||
|         if (isPlayerAvailable()) { | ||||
|             final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT); | ||||
|             player.getSurfaceView() | ||||
|                     .setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight); | ||||
|             player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> | ||||
|                     ui.getBinding().surfaceView.setHeights(newHeight, | ||||
|                             ui.isFullscreen() ? newHeight : maxHeight)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -1517,7 +1530,7 @@ public final class VideoDetailFragment | ||||
|         if (binding.relatedItemsLayout != null) { | ||||
|             if (showRelatedItems) { | ||||
|                 binding.relatedItemsLayout.setVisibility( | ||||
|                         isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE); | ||||
|                         isFullscreen() ? View.GONE : View.INVISIBLE); | ||||
|             } else { | ||||
|                 binding.relatedItemsLayout.setVisibility(View.GONE); | ||||
|             } | ||||
| @@ -1778,6 +1791,14 @@ public final class VideoDetailFragment | ||||
|     // Player event listener | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void onViewCreated() { | ||||
|         // Video view can have elements visible from popup, | ||||
|         // We hide it here but once it ready the view will be shown in handleIntent() | ||||
|         getRoot().ifPresent(view -> view.setVisibility(View.GONE)); | ||||
|         addVideoPlayerView(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onQueueUpdate(final PlayQueue queue) { | ||||
|         playQueue = queue; | ||||
| @@ -1898,15 +1919,10 @@ public final class VideoDetailFragment | ||||
|     @Override | ||||
|     public void onFullscreenStateChanged(final boolean fullscreen) { | ||||
|         setupBrightness(); | ||||
|         //noinspection SimplifyOptionalCallChains | ||||
|         if (!isPlayerAndPlayerServiceAvailable() | ||||
|                 || playerService.getView() == null | ||||
|                 || player.getParentActivity() == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final View view = playerService.getView(); | ||||
|         final ViewGroup parent = (ViewGroup) view.getParent(); | ||||
|         if (parent == null) { | ||||
|                 || !player.UIs().get(MainPlayerUi.class).isPresent() | ||||
|                 || getRoot().map(View::getParent).orElse(null) == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -1934,7 +1950,7 @@ public final class VideoDetailFragment | ||||
|         final boolean isLandscape = DeviceUtils.isLandscape(requireContext()); | ||||
|         if (DeviceUtils.isTablet(activity) | ||||
|                 && (!globalScreenOrientationLocked(activity) || isLandscape)) { | ||||
|             player.toggleFullscreen(); | ||||
|             player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -2017,7 +2033,7 @@ public final class VideoDetailFragment | ||||
|         } | ||||
|         activity.getWindow().getDecorView().setSystemUiVisibility(visibility); | ||||
|  | ||||
|         if (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen())) { | ||||
|         if (isInMultiWindow || isFullscreen()) { | ||||
|             activity.getWindow().setStatusBarColor(Color.TRANSPARENT); | ||||
|             activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); | ||||
|         } | ||||
| @@ -2026,13 +2042,17 @@ public final class VideoDetailFragment | ||||
|  | ||||
|     // Listener implementation | ||||
|     public void hideSystemUiIfNeeded() { | ||||
|         if (isPlayerAvailable() | ||||
|                 && player.isFullscreen() | ||||
|         if (isFullscreen() | ||||
|                 && bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { | ||||
|             hideSystemUi(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private boolean isFullscreen() { | ||||
|         return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class) | ||||
|                 .map(VideoPlayerUi::isFullscreen).orElse(false); | ||||
|     } | ||||
|  | ||||
|     private boolean playerIsNotStopped() { | ||||
|         return isPlayerAvailable() && !player.isStopped(); | ||||
|     } | ||||
| @@ -2055,10 +2075,7 @@ public final class VideoDetailFragment | ||||
|         } | ||||
|  | ||||
|         final WindowManager.LayoutParams lp = activity.getWindow().getAttributes(); | ||||
|         if (!isPlayerAvailable() | ||||
|                 || !player.videoPlayerSelected() | ||||
|                 || !player.isFullscreen() | ||||
|                 || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) { | ||||
|         if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) { | ||||
|             // Apply system brightness when the player is not in fullscreen | ||||
|             restoreDefaultBrightness(); | ||||
|         } else { | ||||
| @@ -2082,7 +2099,7 @@ public final class VideoDetailFragment | ||||
|             setAutoPlay(true); | ||||
|         } | ||||
|  | ||||
|         player.checkLandscape(); | ||||
|         player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape); | ||||
|         // Let's give a user time to look at video information page if video is not playing | ||||
|         if (globalScreenOrientationLocked(activity) && !player.isPlaying()) { | ||||
|             player.play(); | ||||
| @@ -2309,10 +2326,10 @@ public final class VideoDetailFragment | ||||
|                         if (DeviceUtils.isLandscape(requireContext()) | ||||
|                                 && isPlayerAvailable() | ||||
|                                 && player.isPlaying() | ||||
|                                 && !player.isFullscreen() | ||||
|                                 && !DeviceUtils.isTablet(activity) | ||||
|                                 && player.videoPlayerSelected()) { | ||||
|                             player.toggleFullscreen(); | ||||
|                                 && !isFullscreen() | ||||
|                                 && !DeviceUtils.isTablet(activity)) { | ||||
|                             player.UIs().get(MainPlayerUi.class) | ||||
|                                     .ifPresent(MainPlayerUi::toggleFullscreen); | ||||
|                         } | ||||
|                         setOverlayLook(binding.appBarLayout, behavior, 1); | ||||
|                         break; | ||||
| @@ -2325,17 +2342,22 @@ public final class VideoDetailFragment | ||||
|                         // Re-enable clicks | ||||
|                         setOverlayElementsClickable(true); | ||||
|                         if (isPlayerAvailable()) { | ||||
|                             player.closeItemsList(); | ||||
|                             player.UIs().get(MainPlayerUi.class) | ||||
|                                     .ifPresent(MainPlayerUi::closeItemsList); | ||||
|                         } | ||||
|                         setOverlayLook(binding.appBarLayout, behavior, 0); | ||||
|                         break; | ||||
|                     case BottomSheetBehavior.STATE_DRAGGING: | ||||
|                     case BottomSheetBehavior.STATE_SETTLING: | ||||
|                         if (isPlayerAvailable() && player.isFullscreen()) { | ||||
|                         if (isFullscreen()) { | ||||
|                             showSystemUi(); | ||||
|                         } | ||||
|                         if (isPlayerAvailable() && player.isControlsVisible()) { | ||||
|                             player.hideControls(0, 0); | ||||
|                         if (isPlayerAvailable()) { | ||||
|                             player.UIs().get(MainPlayerUi.class).ifPresent(ui -> { | ||||
|                                     if (ui.isControlsVisible()) { | ||||
|                                         ui.hideControls(0, 0); | ||||
|                                     } | ||||
|                             }); | ||||
|                         } | ||||
|                         break; | ||||
|                 } | ||||
| @@ -2409,4 +2431,13 @@ public final class VideoDetailFragment | ||||
|     boolean isPlayerAndPlayerServiceAvailable() { | ||||
|         return (player != null && playerService != null); | ||||
|     } | ||||
|  | ||||
|     public Optional<View> getRoot() { | ||||
|         if (player == null) { | ||||
|             return Optional.empty(); | ||||
|         } | ||||
|  | ||||
|         return player.UIs().get(VideoPlayerUi.class) | ||||
|                 .map(playerUi -> playerUi.getBinding().getRoot()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -43,7 +43,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | ||||
| import org.schabi.newpipe.ktx.AnimationType; | ||||
| import org.schabi.newpipe.local.subscription.SubscriptionManager; | ||||
| import org.schabi.newpipe.local.feed.notifications.NotificationHelper; | ||||
| import org.schabi.newpipe.player.MainPlayer.PlayerType; | ||||
| import org.schabi.newpipe.player.PlayerService.PlayerType; | ||||
| import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
|   | ||||
| @@ -43,7 +43,7 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog; | ||||
| import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; | ||||
| import org.schabi.newpipe.local.dialog.PlaylistDialog; | ||||
| import org.schabi.newpipe.local.playlist.RemotePlaylistManager; | ||||
| import org.schabi.newpipe.player.MainPlayer.PlayerType; | ||||
| import org.schabi.newpipe.player.PlayerService.PlayerType; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; | ||||
| import org.schabi.newpipe.util.ExtractorHelper; | ||||
|   | ||||
| @@ -43,7 +43,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.info_list.dialog.InfoItemDialog; | ||||
| import org.schabi.newpipe.local.BaseLocalListFragment; | ||||
| import org.schabi.newpipe.local.history.HistoryRecordManager; | ||||
| import org.schabi.newpipe.player.MainPlayer.PlayerType; | ||||
| import org.schabi.newpipe.player.PlayerService.PlayerType; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
|   | ||||
| @@ -26,14 +26,14 @@ import java.util.List; | ||||
| import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; | ||||
| import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; | ||||
| import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; | ||||
| import static org.schabi.newpipe.player.PlayerService.ACTION_CLOSE; | ||||
| import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_FORWARD; | ||||
| import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_REWIND; | ||||
| import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_NEXT; | ||||
| import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PAUSE; | ||||
| import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PREVIOUS; | ||||
| import static org.schabi.newpipe.player.PlayerService.ACTION_REPEAT; | ||||
| import static org.schabi.newpipe.player.PlayerService.ACTION_SHUFFLE; | ||||
|  | ||||
| /** | ||||
|  * This is a utility class for player notifications. | ||||
| @@ -173,7 +173,7 @@ public final class NotificationUtil { | ||||
|     } | ||||
|  | ||||
|  | ||||
|     void createNotificationAndStartForeground(final Player player, final Service service) { | ||||
|     public void createNotificationAndStartForeground(final Player player, final Service service) { | ||||
|         if (notificationBuilder == null) { | ||||
|             notificationBuilder = createNotification(player); | ||||
|         } | ||||
|   | ||||
| @@ -51,7 +51,9 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|  | ||||
|     private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; | ||||
|  | ||||
|     protected Player player; | ||||
|     private Player player; | ||||
|  | ||||
|     private PlayQueueAdapter adapter = null; | ||||
|  | ||||
|     private boolean serviceBound; | ||||
|     private ServiceConnection serviceConnection; | ||||
| @@ -132,7 +134,7 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|                 openPlaybackParameterDialog(); | ||||
|                 return true; | ||||
|             case R.id.action_mute: | ||||
|                 player.onMuteUnmuteButtonClicked(); | ||||
|                 player.toggleMute(); | ||||
|                 return true; | ||||
|             case R.id.action_system_audio: | ||||
|                 startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS)); | ||||
| @@ -168,7 +170,7 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private void bind() { | ||||
|         final Intent bindIntent = new Intent(this, MainPlayer.class); | ||||
|         final Intent bindIntent = new Intent(this, PlayerService.class); | ||||
|         final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE); | ||||
|         if (!success) { | ||||
|             unbindService(serviceConnection); | ||||
| @@ -184,10 +186,7 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|                 player.removeActivityListener(this); | ||||
|             } | ||||
|  | ||||
|             if (player != null && player.getPlayQueueAdapter() != null) { | ||||
|                 player.getPlayQueueAdapter().unsetSelectedListener(); | ||||
|             } | ||||
|             queueControlBinding.playQueue.setAdapter(null); | ||||
|             onQueueUpdate(null); | ||||
|             if (itemTouchHelper != null) { | ||||
|                 itemTouchHelper.attachToRecyclerView(null); | ||||
|             } | ||||
| @@ -210,15 +209,15 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|  | ||||
|                 if (service instanceof PlayerServiceBinder) { | ||||
|                     player = ((PlayerServiceBinder) service).getPlayerInstance(); | ||||
|                 } else if (service instanceof MainPlayer.LocalBinder) { | ||||
|                     player = ((MainPlayer.LocalBinder) service).getPlayer(); | ||||
|                 } else if (service instanceof PlayerService.LocalBinder) { | ||||
|                     player = ((PlayerService.LocalBinder) service).getPlayer(); | ||||
|                 } | ||||
|  | ||||
|                 if (player == null || player.getPlayQueue() == null | ||||
|                         || player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) { | ||||
|                 if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) { | ||||
|                     unbind(); | ||||
|                     finish(); | ||||
|                 } else { | ||||
|                     onQueueUpdate(player.getPlayQueue()); | ||||
|                     buildComponents(); | ||||
|                     if (player != null) { | ||||
|                         player.setActivityListener(PlayQueueActivity.this); | ||||
| @@ -241,7 +240,6 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|  | ||||
|     private void buildQueue() { | ||||
|         queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this)); | ||||
|         queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter()); | ||||
|         queueControlBinding.playQueue.setClickable(true); | ||||
|         queueControlBinding.playQueue.setLongClickable(true); | ||||
|         queueControlBinding.playQueue.clearOnScrollListeners(); | ||||
| @@ -249,8 +247,6 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|  | ||||
|         itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); | ||||
|         itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue); | ||||
|  | ||||
|         player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener()); | ||||
|     } | ||||
|  | ||||
|     private void buildMetadata() { | ||||
| @@ -370,7 +366,7 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|         } | ||||
|  | ||||
|         if (view.getId() == queueControlBinding.controlRepeat.getId()) { | ||||
|             player.onRepeatClicked(); | ||||
|             player.cycleNextRepeatMode(); | ||||
|         } else if (view.getId() == queueControlBinding.controlBackward.getId()) { | ||||
|             player.playPrevious(); | ||||
|         } else if (view.getId() == queueControlBinding.controlFastRewind.getId()) { | ||||
| @@ -382,7 +378,7 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|         } else if (view.getId() == queueControlBinding.controlForward.getId()) { | ||||
|             player.playNext(); | ||||
|         } else if (view.getId() == queueControlBinding.controlShuffle.getId()) { | ||||
|             player.onShuffleClicked(); | ||||
|             player.toggleShuffleModeEnabled(); | ||||
|         } else if (view.getId() == queueControlBinding.metadata.getId()) { | ||||
|             scrollToSelected(); | ||||
|         } else if (view.getId() == queueControlBinding.liveSync.getId()) { | ||||
| @@ -445,7 +441,15 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void onQueueUpdate(final PlayQueue queue) { | ||||
|     public void onQueueUpdate(@Nullable final PlayQueue queue) { | ||||
|         if (queue == null) { | ||||
|             adapter = null; | ||||
|             queueControlBinding.playQueue.setAdapter(null); | ||||
|         } else { | ||||
|             adapter = new PlayQueueAdapter(this, queue); | ||||
|             adapter.setSelectedListener(getOnSelectedListener()); | ||||
|             queueControlBinding.playQueue.setAdapter(adapter); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -454,7 +458,6 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|         onStateChanged(state); | ||||
|         onPlayModeChanged(repeatMode, shuffled); | ||||
|         onPlaybackParameterChanged(parameters); | ||||
|         onMaybePlaybackAdapterChanged(); | ||||
|         onMaybeMuteChanged(); | ||||
|     } | ||||
|  | ||||
| @@ -582,17 +585,6 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void onMaybePlaybackAdapterChanged() { | ||||
|         if (player == null) { | ||||
|             return; | ||||
|         } | ||||
|         final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter(); | ||||
|         if (maybeNewAdapter != null | ||||
|                 && queueControlBinding.playQueue.getAdapter() != maybeNewAdapter) { | ||||
|             queueControlBinding.playQueue.setAdapter(maybeNewAdapter); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void onMaybeMuteChanged() { | ||||
|         if (menu != null && player != null) { | ||||
|             final MenuItem item = menu.findItem(R.id.action_mute); | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -19,44 +19,35 @@ | ||||
| 
 | ||||
| package org.schabi.newpipe.player; | ||||
| 
 | ||||
| import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; | ||||
| 
 | ||||
| import android.app.Service; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.os.Binder; | ||||
| import android.os.IBinder; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.view.WindowManager; | ||||
| 
 | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.core.content.ContextCompat; | ||||
| 
 | ||||
| import org.schabi.newpipe.App; | ||||
| import org.schabi.newpipe.databinding.PlayerBinding; | ||||
| import org.schabi.newpipe.util.DeviceUtils; | ||||
| import org.schabi.newpipe.player.ui.VideoPlayerUi; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
| 
 | ||||
| import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * One service for all players. | ||||
|  * | ||||
|  * @author mauriciocolli | ||||
|  */ | ||||
| public final class MainPlayer extends Service { | ||||
|     private static final String TAG = "MainPlayer"; | ||||
| public final class PlayerService extends Service { | ||||
|     private static final String TAG = PlayerService.class.getSimpleName(); | ||||
|     private static final boolean DEBUG = Player.DEBUG; | ||||
| 
 | ||||
|     private Player player; | ||||
|     private WindowManager windowManager; | ||||
| 
 | ||||
|     private final IBinder mBinder = new MainPlayer.LocalBinder(); | ||||
|     private final IBinder mBinder = new PlayerService.LocalBinder(); | ||||
| 
 | ||||
|     public enum PlayerType { | ||||
|         VIDEO, | ||||
|         MAIN, | ||||
|         AUDIO, | ||||
|         POPUP | ||||
|     } | ||||
| @@ -67,7 +58,7 @@ public final class MainPlayer extends Service { | ||||
| 
 | ||||
|     static final String ACTION_CLOSE | ||||
|             = App.PACKAGE_NAME + ".player.MainPlayer.CLOSE"; | ||||
|     static final String ACTION_PLAY_PAUSE | ||||
|     public static final String ACTION_PLAY_PAUSE | ||||
|             = App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE"; | ||||
|     static final String ACTION_REPEAT | ||||
|             = App.PACKAGE_NAME + ".player.MainPlayer.REPEAT"; | ||||
| @@ -94,19 +85,12 @@ public final class MainPlayer extends Service { | ||||
|             Log.d(TAG, "onCreate() called"); | ||||
|         } | ||||
|         assureCorrectAppLanguage(this); | ||||
|         windowManager = ContextCompat.getSystemService(this, WindowManager.class); | ||||
| 
 | ||||
|         ThemeHelper.setTheme(this); | ||||
|         createView(); | ||||
|     } | ||||
| 
 | ||||
|     private void createView() { | ||||
|         final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this)); | ||||
| 
 | ||||
|         player = new Player(this); | ||||
|         player.setupFromView(binding); | ||||
| 
 | ||||
|         NotificationUtil.getInstance().createNotificationAndStartForeground(player, this); | ||||
|         /*final MainPlayerUi mainPlayerUi = new MainPlayerUi(player, | ||||
|                 PlayerBinding.inflate(LayoutInflater.from(this))); | ||||
|         player.UIs().add(mainPlayerUi);*/ | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -121,11 +105,6 @@ public final class MainPlayer extends Service { | ||||
|             return START_NOT_STICKY; | ||||
|         } | ||||
| 
 | ||||
|         if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) | ||||
|                 || intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) { | ||||
|             NotificationUtil.getInstance().createNotificationAndStartForeground(player, this); | ||||
|         } | ||||
| 
 | ||||
|         player.handleIntent(intent); | ||||
|         if (player.getMediaSessionManager() != null) { | ||||
|             player.getMediaSessionManager().handleMediaButtonIntent(intent); | ||||
| @@ -144,13 +123,7 @@ public final class MainPlayer extends Service { | ||||
|             // Releases wifi & cpu, disables keepScreenOn, etc. | ||||
|             // We can't just pause the player here because it will make transition | ||||
|             // from one stream to a new stream not smooth | ||||
|             player.smoothStopPlayer(); | ||||
|             player.setRecovery(); | ||||
| 
 | ||||
|             // Android TV will handle back button in case controls will be visible | ||||
|             // (one more additional unneeded click while the player is hidden) | ||||
|             player.hideControls(0, 0); | ||||
|             player.closeItemsList(); | ||||
|             player.smoothStopForImmediateReusing(); | ||||
| 
 | ||||
|             // Notification shows information about old stream but if a user selects | ||||
|             // a stream from backStack it's not actual anymore | ||||
| @@ -180,18 +153,7 @@ public final class MainPlayer extends Service { | ||||
| 
 | ||||
|     private void cleanup() { | ||||
|         if (player != null) { | ||||
|             // Exit from fullscreen when user closes the player via notification | ||||
|             if (player.isFullscreen()) { | ||||
|                 player.toggleFullscreen(); | ||||
|             } | ||||
|             removeViewFromParent(); | ||||
| 
 | ||||
|             player.saveStreamProgressState(); | ||||
|             player.setRecovery(); | ||||
|             player.stopActivityBinding(); | ||||
|             player.removePopupFromView(); | ||||
|             player.destroy(); | ||||
| 
 | ||||
|             player = null; | ||||
|         } | ||||
|     } | ||||
| @@ -212,48 +174,14 @@ public final class MainPlayer extends Service { | ||||
|         return mBinder; | ||||
|     } | ||||
| 
 | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| 
 | ||||
|     boolean isLandscape() { | ||||
|         // DisplayMetrics from activity context knows about MultiWindow feature | ||||
|         // while DisplayMetrics from app context doesn't | ||||
|         return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null | ||||
|                 ? player.getParentActivity() : this); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public View getView() { | ||||
|         if (player == null) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         return player.getRootView(); | ||||
|     } | ||||
| 
 | ||||
|     public void removeViewFromParent() { | ||||
|         if (getView() != null && getView().getParent() != null) { | ||||
|             if (player.getParentActivity() != null) { | ||||
|                 // This means view was added to fragment | ||||
|                 final ViewGroup parent = (ViewGroup) getView().getParent(); | ||||
|                 parent.removeView(getView()); | ||||
|             } else { | ||||
|                 // This means view was added by windowManager for popup player | ||||
|                 windowManager.removeViewImmediate(getView()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public class LocalBinder extends Binder { | ||||
| 
 | ||||
|         public MainPlayer getService() { | ||||
|             return MainPlayer.this; | ||||
|         public PlayerService getService() { | ||||
|             return PlayerService.this; | ||||
|         } | ||||
| 
 | ||||
|         public Player getPlayer() { | ||||
|             return MainPlayer.this.player; | ||||
|             return PlayerService.this.player; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,520 +0,0 @@ | ||||
| package org.schabi.newpipe.player.event | ||||
|  | ||||
| import android.content.Context | ||||
| import android.os.Handler | ||||
| import android.util.Log | ||||
| import android.view.GestureDetector | ||||
| import android.view.MotionEvent | ||||
| import android.view.View | ||||
| import android.view.ViewConfiguration | ||||
| import org.schabi.newpipe.ktx.animate | ||||
| import org.schabi.newpipe.player.MainPlayer | ||||
| import org.schabi.newpipe.player.Player | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs | ||||
| import kotlin.math.abs | ||||
| import kotlin.math.hypot | ||||
| import kotlin.math.max | ||||
| import kotlin.math.min | ||||
|  | ||||
| /** | ||||
|  * Base gesture handling for [Player] | ||||
|  * | ||||
|  * This class contains the logic for the player gestures like View preparations | ||||
|  * and provides some abstract methods to make it easier separating the logic from the UI. | ||||
|  */ | ||||
| abstract class BasePlayerGestureListener( | ||||
|     @JvmField | ||||
|     protected val player: Player, | ||||
|     @JvmField | ||||
|     protected val service: MainPlayer | ||||
| ) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { | ||||
|  | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|     // Abstract methods for VIDEO and POPUP | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     abstract fun onDoubleTap(event: MotionEvent, portion: DisplayPortion) | ||||
|  | ||||
|     abstract fun onSingleTap(playerType: MainPlayer.PlayerType) | ||||
|  | ||||
|     abstract fun onScroll( | ||||
|         playerType: MainPlayer.PlayerType, | ||||
|         portion: DisplayPortion, | ||||
|         initialEvent: MotionEvent, | ||||
|         movingEvent: MotionEvent, | ||||
|         distanceX: Float, | ||||
|         distanceY: Float | ||||
|     ) | ||||
|  | ||||
|     abstract fun onScrollEnd(playerType: MainPlayer.PlayerType, event: MotionEvent) | ||||
|  | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|     // Abstract methods for POPUP (exclusive) | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     abstract fun onPopupResizingStart() | ||||
|  | ||||
|     abstract fun onPopupResizingEnd() | ||||
|  | ||||
|     private var initialPopupX: Int = -1 | ||||
|     private var initialPopupY: Int = -1 | ||||
|  | ||||
|     private var isMovingInMain = false | ||||
|     private var isMovingInPopup = false | ||||
|     private var isResizing = false | ||||
|  | ||||
|     private val tossFlingVelocity = PlayerHelper.getTossFlingVelocity() | ||||
|  | ||||
|     // [popup] initial coordinates and distance between fingers | ||||
|     private var initPointerDistance = -1.0 | ||||
|     private var initFirstPointerX = -1f | ||||
|     private var initFirstPointerY = -1f | ||||
|     private var initSecPointerX = -1f | ||||
|     private var initSecPointerY = -1f | ||||
|  | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|     // onTouch implementation | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     override fun onTouch(v: View, event: MotionEvent): Boolean { | ||||
|         return if (player.popupPlayerSelected()) { | ||||
|             onTouchInPopup(v, event) | ||||
|         } else { | ||||
|             onTouchInMain(v, event) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun onTouchInMain(v: View, event: MotionEvent): Boolean { | ||||
|         player.gestureDetector.onTouchEvent(event) | ||||
|         if (event.action == MotionEvent.ACTION_UP && isMovingInMain) { | ||||
|             isMovingInMain = false | ||||
|             onScrollEnd(MainPlayer.PlayerType.VIDEO, event) | ||||
|         } | ||||
|         return when (event.action) { | ||||
|             MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { | ||||
|                 v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen) | ||||
|                 true | ||||
|             } | ||||
|             MotionEvent.ACTION_UP -> { | ||||
|                 v.parent.requestDisallowInterceptTouchEvent(false) | ||||
|                 false | ||||
|             } | ||||
|             else -> true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun onTouchInPopup(v: View, event: MotionEvent): Boolean { | ||||
|         player.gestureDetector.onTouchEvent(event) | ||||
|         if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) { | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.") | ||||
|             } | ||||
|             onPopupResizingStart() | ||||
|  | ||||
|             // record coordinates of fingers | ||||
|             initFirstPointerX = event.getX(0) | ||||
|             initFirstPointerY = event.getY(0) | ||||
|             initSecPointerX = event.getX(1) | ||||
|             initSecPointerY = event.getY(1) | ||||
|             // record distance between fingers | ||||
|             initPointerDistance = hypot( | ||||
|                 initFirstPointerX - initSecPointerX.toDouble(), | ||||
|                 initFirstPointerY - initSecPointerY.toDouble() | ||||
|             ) | ||||
|  | ||||
|             isResizing = true | ||||
|         } | ||||
|         if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) { | ||||
|             if (DEBUG) { | ||||
|                 Log.d( | ||||
|                     TAG, | ||||
|                     "onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" + | ||||
|                         "[${event.rawX}, ${event.rawY}]" | ||||
|                 ) | ||||
|             } | ||||
|             return handleMultiDrag(event) | ||||
|         } | ||||
|         if (event.action == MotionEvent.ACTION_UP) { | ||||
|             if (DEBUG) { | ||||
|                 Log.d( | ||||
|                     TAG, | ||||
|                     "onTouch() ACTION_UP > v = [$v], e1.getRaw =" + | ||||
|                         " [${event.rawX}, ${event.rawY}]" | ||||
|                 ) | ||||
|             } | ||||
|             if (isMovingInPopup) { | ||||
|                 isMovingInPopup = false | ||||
|                 onScrollEnd(MainPlayer.PlayerType.POPUP, event) | ||||
|             } | ||||
|             if (isResizing) { | ||||
|                 isResizing = false | ||||
|  | ||||
|                 initPointerDistance = (-1).toDouble() | ||||
|                 initFirstPointerX = (-1).toFloat() | ||||
|                 initFirstPointerY = (-1).toFloat() | ||||
|                 initSecPointerX = (-1).toFloat() | ||||
|                 initSecPointerY = (-1).toFloat() | ||||
|  | ||||
|                 onPopupResizingEnd() | ||||
|                 player.changeState(player.currentState) | ||||
|             } | ||||
|             if (!player.isPopupClosing) { | ||||
|                 savePopupPositionAndSizeToPrefs(player) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         v.performClick() | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     private fun handleMultiDrag(event: MotionEvent): Boolean { | ||||
|         if (initPointerDistance != -1.0 && event.pointerCount == 2) { | ||||
|             // get the movements of the fingers | ||||
|             val firstPointerMove = hypot( | ||||
|                 event.getX(0) - initFirstPointerX.toDouble(), | ||||
|                 event.getY(0) - initFirstPointerY.toDouble() | ||||
|             ) | ||||
|             val secPointerMove = hypot( | ||||
|                 event.getX(1) - initSecPointerX.toDouble(), | ||||
|                 event.getY(1) - initSecPointerY.toDouble() | ||||
|             ) | ||||
|  | ||||
|             // minimum threshold beyond which pinch gesture will work | ||||
|             val minimumMove = ViewConfiguration.get(service).scaledTouchSlop | ||||
|  | ||||
|             if (max(firstPointerMove, secPointerMove) > minimumMove) { | ||||
|                 // calculate current distance between the pointers | ||||
|                 val currentPointerDistance = hypot( | ||||
|                     event.getX(0) - event.getX(1).toDouble(), | ||||
|                     event.getY(0) - event.getY(1).toDouble() | ||||
|                 ) | ||||
|  | ||||
|                 val popupWidth = player.popupLayoutParams!!.width.toDouble() | ||||
|                 // change co-ordinates of popup so the center stays at the same position | ||||
|                 val newWidth = popupWidth * currentPointerDistance / initPointerDistance | ||||
|                 initPointerDistance = currentPointerDistance | ||||
|                 player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt() | ||||
|  | ||||
|                 player.checkPopupPositionBounds() | ||||
|                 player.updateScreenSize() | ||||
|                 player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt()) | ||||
|                 return true | ||||
|             } | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|     // Simple gestures | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     override fun onDown(e: MotionEvent): Boolean { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onDown called with e = [$e]") | ||||
|  | ||||
|         if (isDoubleTapping && isDoubleTapEnabled) { | ||||
|             doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e)) | ||||
|             return true | ||||
|         } | ||||
|  | ||||
|         return if (player.popupPlayerSelected()) | ||||
|             onDownInPopup(e) | ||||
|         else | ||||
|             true | ||||
|     } | ||||
|  | ||||
|     private fun onDownInPopup(e: MotionEvent): Boolean { | ||||
|         // 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). | ||||
|         player.updateScreenSize() | ||||
|         player.checkPopupPositionBounds() | ||||
|         player.popupLayoutParams?.let { | ||||
|             initialPopupX = it.x | ||||
|             initialPopupY = it.y | ||||
|         } | ||||
|         return super.onDown(e) | ||||
|     } | ||||
|  | ||||
|     override fun onDoubleTap(e: MotionEvent): Boolean { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onDoubleTap called with e = [$e]") | ||||
|  | ||||
|         onDoubleTap(e, getDisplayPortion(e)) | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun onSingleTapConfirmed(e: MotionEvent): Boolean { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]") | ||||
|  | ||||
|         if (isDoubleTapping) | ||||
|             return true | ||||
|  | ||||
|         if (player.popupPlayerSelected()) { | ||||
|             if (player.exoPlayerIsNull()) | ||||
|                 return false | ||||
|  | ||||
|             onSingleTap(MainPlayer.PlayerType.POPUP) | ||||
|             return true | ||||
|         } else { | ||||
|             super.onSingleTapConfirmed(e) | ||||
|             if (player.currentState == Player.STATE_BLOCKED) | ||||
|                 return true | ||||
|  | ||||
|             onSingleTap(MainPlayer.PlayerType.VIDEO) | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun onLongPress(e: MotionEvent?) { | ||||
|         if (player.popupPlayerSelected()) { | ||||
|             player.updateScreenSize() | ||||
|             player.checkPopupPositionBounds() | ||||
|             player.changePopupSize(player.screenWidth.toInt()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onScroll( | ||||
|         initialEvent: MotionEvent, | ||||
|         movingEvent: MotionEvent, | ||||
|         distanceX: Float, | ||||
|         distanceY: Float | ||||
|     ): Boolean { | ||||
|         return if (player.popupPlayerSelected()) { | ||||
|             onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY) | ||||
|         } else { | ||||
|             onScrollInMain(initialEvent, movingEvent, distanceX, distanceY) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onFling( | ||||
|         e1: MotionEvent?, | ||||
|         e2: MotionEvent?, | ||||
|         velocityX: Float, | ||||
|         velocityY: Float | ||||
|     ): Boolean { | ||||
|         return if (player.popupPlayerSelected()) { | ||||
|             val absVelocityX = abs(velocityX) | ||||
|             val absVelocityY = abs(velocityY) | ||||
|             if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) { | ||||
|                 if (absVelocityX > tossFlingVelocity) { | ||||
|                     player.popupLayoutParams!!.x = velocityX.toInt() | ||||
|                 } | ||||
|                 if (absVelocityY > tossFlingVelocity) { | ||||
|                     player.popupLayoutParams!!.y = velocityY.toInt() | ||||
|                 } | ||||
|                 player.checkPopupPositionBounds() | ||||
|                 player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams) | ||||
|                 return true | ||||
|             } | ||||
|             return false | ||||
|         } else { | ||||
|             true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun onScrollInMain( | ||||
|         initialEvent: MotionEvent, | ||||
|         movingEvent: MotionEvent, | ||||
|         distanceX: Float, | ||||
|         distanceY: Float | ||||
|     ): Boolean { | ||||
|  | ||||
|         if (!player.isFullscreen) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service) | ||||
|         val isTouchingNavigationBar: Boolean = | ||||
|             initialEvent.y > (player.rootView.height - getNavigationBarHeight(service)) | ||||
|         if (isTouchingStatusBar || isTouchingNavigationBar) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD | ||||
|         if ( | ||||
|             !isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) || | ||||
|             player.currentState == Player.STATE_COMPLETED | ||||
|         ) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         isMovingInMain = true | ||||
|  | ||||
|         onScroll( | ||||
|             MainPlayer.PlayerType.VIDEO, | ||||
|             getDisplayHalfPortion(initialEvent), | ||||
|             initialEvent, | ||||
|             movingEvent, | ||||
|             distanceX, | ||||
|             distanceY | ||||
|         ) | ||||
|  | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     private fun onScrollInPopup( | ||||
|         initialEvent: MotionEvent, | ||||
|         movingEvent: MotionEvent, | ||||
|         distanceX: Float, | ||||
|         distanceY: Float | ||||
|     ): Boolean { | ||||
|  | ||||
|         if (isResizing) { | ||||
|             return super.onScroll(initialEvent, movingEvent, distanceX, distanceY) | ||||
|         } | ||||
|  | ||||
|         if (!isMovingInPopup) { | ||||
|             player.closeOverlayButton.animate(true, 200) | ||||
|         } | ||||
|  | ||||
|         isMovingInPopup = true | ||||
|  | ||||
|         val diffX: Float = (movingEvent.rawX - initialEvent.rawX) | ||||
|         var posX: Float = (initialPopupX + diffX) | ||||
|         val diffY: Float = (movingEvent.rawY - initialEvent.rawY) | ||||
|         var posY: Float = (initialPopupY + diffY) | ||||
|  | ||||
|         if (posX > player.screenWidth - player.popupLayoutParams!!.width) { | ||||
|             posX = (player.screenWidth - player.popupLayoutParams!!.width) | ||||
|         } else if (posX < 0) { | ||||
|             posX = 0f | ||||
|         } | ||||
|  | ||||
|         if (posY > player.screenHeight - player.popupLayoutParams!!.height) { | ||||
|             posY = (player.screenHeight - player.popupLayoutParams!!.height) | ||||
|         } else if (posY < 0) { | ||||
|             posY = 0f | ||||
|         } | ||||
|  | ||||
|         player.popupLayoutParams!!.x = posX.toInt() | ||||
|         player.popupLayoutParams!!.y = posY.toInt() | ||||
|  | ||||
|         onScroll( | ||||
|             MainPlayer.PlayerType.POPUP, | ||||
|             getDisplayHalfPortion(initialEvent), | ||||
|             initialEvent, | ||||
|             movingEvent, | ||||
|             distanceX, | ||||
|             distanceY | ||||
|         ) | ||||
|  | ||||
|         player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams) | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|     // Multi double tapping | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     var doubleTapControls: DoubleTapListener? = null | ||||
|         private set | ||||
|  | ||||
|     private val isDoubleTapEnabled: Boolean | ||||
|         get() = doubleTapDelay > 0 | ||||
|  | ||||
|     var isDoubleTapping = false | ||||
|         private set | ||||
|  | ||||
|     fun doubleTapControls(listener: DoubleTapListener) = apply { | ||||
|         doubleTapControls = listener | ||||
|     } | ||||
|  | ||||
|     private var doubleTapDelay = DOUBLE_TAP_DELAY | ||||
|     private val doubleTapHandler: Handler = Handler() | ||||
|     private val doubleTapRunnable = Runnable { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "doubleTapRunnable called") | ||||
|  | ||||
|         isDoubleTapping = false | ||||
|         doubleTapControls?.onDoubleTapFinished() | ||||
|     } | ||||
|  | ||||
|     fun startMultiDoubleTap(e: MotionEvent) { | ||||
|         if (!isDoubleTapping) { | ||||
|             if (DEBUG) | ||||
|                 Log.d(TAG, "startMultiDoubleTap called with e = [$e]") | ||||
|  | ||||
|             keepInDoubleTapMode() | ||||
|             doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun keepInDoubleTapMode() { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "keepInDoubleTapMode called") | ||||
|  | ||||
|         isDoubleTapping = true | ||||
|         doubleTapHandler.removeCallbacks(doubleTapRunnable) | ||||
|         doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay) | ||||
|     } | ||||
|  | ||||
|     fun endMultiDoubleTap() { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "endMultiDoubleTap called") | ||||
|  | ||||
|         isDoubleTapping = false | ||||
|         doubleTapHandler.removeCallbacks(doubleTapRunnable) | ||||
|         doubleTapControls?.onDoubleTapFinished() | ||||
|     } | ||||
|  | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private fun getDisplayPortion(e: MotionEvent): DisplayPortion { | ||||
|         return if (player.playerType == MainPlayer.PlayerType.POPUP && player.popupLayoutParams != null) { | ||||
|             when { | ||||
|                 e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT | ||||
|                 e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT | ||||
|                 else -> DisplayPortion.MIDDLE | ||||
|             } | ||||
|         } else /* MainPlayer.PlayerType.VIDEO */ { | ||||
|             when { | ||||
|                 e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT | ||||
|                 e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT | ||||
|                 else -> DisplayPortion.MIDDLE | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Currently needed for scrolling since there is no action more the middle portion | ||||
|     private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion { | ||||
|         return if (player.playerType == MainPlayer.PlayerType.POPUP) { | ||||
|             when { | ||||
|                 e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF | ||||
|                 else -> DisplayPortion.RIGHT_HALF | ||||
|             } | ||||
|         } else /* MainPlayer.PlayerType.VIDEO */ { | ||||
|             when { | ||||
|                 e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF | ||||
|                 else -> DisplayPortion.RIGHT_HALF | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getNavigationBarHeight(context: Context): Int { | ||||
|         val resId = context.resources | ||||
|             .getIdentifier("navigation_bar_height", "dimen", "android") | ||||
|         return if (resId > 0) { | ||||
|             context.resources.getDimensionPixelSize(resId) | ||||
|         } else 0 | ||||
|     } | ||||
|  | ||||
|     private fun getStatusBarHeight(context: Context): Int { | ||||
|         val resId = context.resources | ||||
|             .getIdentifier("status_bar_height", "dimen", "android") | ||||
|         return if (resId > 0) { | ||||
|             context.resources.getDimensionPixelSize(resId) | ||||
|         } else 0 | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         private const val TAG = "BasePlayerGestListener" | ||||
|         private val DEBUG = Player.DEBUG | ||||
|  | ||||
|         private const val DOUBLE_TAP_DELAY = 550L | ||||
|         private const val MOVEMENT_THRESHOLD = 40 | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,5 @@ | ||||
| package org.schabi.newpipe.player.event; | ||||
|  | ||||
|  | ||||
| import com.google.android.exoplayer2.PlaybackParameters; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
|   | ||||
| @@ -1,256 +0,0 @@ | ||||
| package org.schabi.newpipe.player.event; | ||||
|  | ||||
| import static org.schabi.newpipe.ktx.AnimationType.ALPHA; | ||||
| import static org.schabi.newpipe.ktx.AnimationType.SCALE_AND_ALPHA; | ||||
| import static org.schabi.newpipe.ktx.ViewUtils.animate; | ||||
| import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION; | ||||
| import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME; | ||||
| import static org.schabi.newpipe.player.Player.STATE_PLAYING; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.util.Log; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| import android.view.Window; | ||||
| import android.view.WindowManager; | ||||
| import android.widget.ProgressBar; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.appcompat.content.res.AppCompatResources; | ||||
|  | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.player.MainPlayer; | ||||
| import org.schabi.newpipe.player.Player; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
|  | ||||
| /** | ||||
|  * GestureListener for the player | ||||
|  * | ||||
|  * While {@link BasePlayerGestureListener} contains the logic behind the single gestures | ||||
|  * this class focuses on the visual aspect like hiding and showing the controls or changing | ||||
|  * volume/brightness during scrolling for specific events. | ||||
|  */ | ||||
| public class PlayerGestureListener | ||||
|         extends BasePlayerGestureListener | ||||
|         implements View.OnTouchListener { | ||||
|     private static final String TAG = PlayerGestureListener.class.getSimpleName(); | ||||
|     private static final boolean DEBUG = MainActivity.DEBUG; | ||||
|  | ||||
|     private final int maxVolume; | ||||
|  | ||||
|     public PlayerGestureListener(final Player player, final MainPlayer service) { | ||||
|         super(player, service); | ||||
|         maxVolume = player.getAudioReactor().getMaxVolume(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDoubleTap(@NonNull final MotionEvent event, | ||||
|                             @NonNull final DisplayPortion portion) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onDoubleTap called with playerType = [" | ||||
|                     + player.getPlayerType() + "], portion = [" + portion + "]"); | ||||
|         } | ||||
|         if (player.isSomePopupMenuVisible()) { | ||||
|             player.hideControls(0, 0); | ||||
|         } | ||||
|  | ||||
|         if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) { | ||||
|             startMultiDoubleTap(event); | ||||
|         } else if (portion == DisplayPortion.MIDDLE) { | ||||
|             player.playPause(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSingleTap(@NonNull final MainPlayer.PlayerType playerType) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]"); | ||||
|         } | ||||
|  | ||||
|         if (player.isControlsVisible()) { | ||||
|             player.hideControls(150, 0); | ||||
|             return; | ||||
|         } | ||||
|         // -- Controls are not visible -- | ||||
|  | ||||
|         // When player is completed show controls and don't hide them later | ||||
|         if (player.getCurrentState() == Player.STATE_COMPLETED) { | ||||
|             player.showControls(0); | ||||
|         } else { | ||||
|             player.showControlsThenHide(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onScroll(@NonNull final MainPlayer.PlayerType playerType, | ||||
|                          @NonNull final DisplayPortion portion, | ||||
|                          @NonNull final MotionEvent initialEvent, | ||||
|                          @NonNull final MotionEvent movingEvent, | ||||
|                          final float distanceX, final float distanceY) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onScroll called with playerType = [" | ||||
|                 + player.getPlayerType() + "], portion = [" + portion + "]"); | ||||
|         } | ||||
|         if (playerType == MainPlayer.PlayerType.VIDEO) { | ||||
|  | ||||
|             // -- Brightness and Volume control -- | ||||
|             final boolean isBrightnessGestureEnabled = | ||||
|                 PlayerHelper.isBrightnessGestureEnabled(service); | ||||
|             final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service); | ||||
|  | ||||
|             if (isBrightnessGestureEnabled && isVolumeGestureEnabled) { | ||||
|                 if (portion == DisplayPortion.LEFT_HALF) { | ||||
|                     onScrollMainBrightness(distanceX, distanceY); | ||||
|  | ||||
|                 } else /* DisplayPortion.RIGHT_HALF */ { | ||||
|                     onScrollMainVolume(distanceX, distanceY); | ||||
|                 } | ||||
|             } else if (isBrightnessGestureEnabled) { | ||||
|                 onScrollMainBrightness(distanceX, distanceY); | ||||
|             } else if (isVolumeGestureEnabled) { | ||||
|                 onScrollMainVolume(distanceX, distanceY); | ||||
|             } | ||||
|  | ||||
|         } else /* MainPlayer.PlayerType.POPUP */ { | ||||
|  | ||||
|             // -- Determine if the ClosingOverlayView (red X) has to be shown or hidden -- | ||||
|             final View closingOverlayView = player.getClosingOverlayView(); | ||||
|             final boolean showClosingOverlayView = player.isInsideClosingRadius(movingEvent); | ||||
|             // Check if an view is in expected state and if not animate it into the correct state | ||||
|             final int expectedVisibility = showClosingOverlayView ? View.VISIBLE : View.GONE; | ||||
|             if (closingOverlayView.getVisibility() != expectedVisibility) { | ||||
|                 animate(closingOverlayView, showClosingOverlayView, 200); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void onScrollMainVolume(final float distanceX, final float distanceY) { | ||||
|         // If we just started sliding, change the progress bar to match the system volume | ||||
|         if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { | ||||
|             final float volumePercent = player | ||||
|                     .getAudioReactor().getVolume() / (float) maxVolume; | ||||
|             player.getVolumeProgressBar().setProgress( | ||||
|                     (int) (volumePercent * player.getMaxGestureLength())); | ||||
|         } | ||||
|  | ||||
|         player.getVolumeProgressBar().incrementProgressBy((int) distanceY); | ||||
|         final float currentProgressPercent = (float) player | ||||
|                 .getVolumeProgressBar().getProgress() / player.getMaxGestureLength(); | ||||
|         final int currentVolume = (int) (maxVolume * currentProgressPercent); | ||||
|         player.getAudioReactor().setVolume(currentVolume); | ||||
|  | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); | ||||
|         } | ||||
|  | ||||
|         player.getVolumeImageView().setImageDrawable( | ||||
|                 AppCompatResources.getDrawable(service, currentProgressPercent <= 0 | ||||
|                         ? R.drawable.ic_volume_off | ||||
|                         : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute | ||||
|                         : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down | ||||
|                         : R.drawable.ic_volume_up) | ||||
|         ); | ||||
|  | ||||
|         if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { | ||||
|             animate(player.getVolumeRelativeLayout(), true, 200, SCALE_AND_ALPHA); | ||||
|         } | ||||
|         if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { | ||||
|             player.getBrightnessRelativeLayout().setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void onScrollMainBrightness(final float distanceX, final float distanceY) { | ||||
|         final Activity parent = player.getParentActivity(); | ||||
|         if (parent == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final Window window = parent.getWindow(); | ||||
|         final WindowManager.LayoutParams layoutParams = window.getAttributes(); | ||||
|         final ProgressBar bar = player.getBrightnessProgressBar(); | ||||
|         final float oldBrightness = layoutParams.screenBrightness; | ||||
|         bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness)))); | ||||
|         bar.incrementProgressBy((int) distanceY); | ||||
|  | ||||
|         final float currentProgressPercent = (float) bar.getProgress() / bar.getMax(); | ||||
|         layoutParams.screenBrightness = currentProgressPercent; | ||||
|         window.setAttributes(layoutParams); | ||||
|  | ||||
|         // Save current brightness level | ||||
|         PlayerHelper.setScreenBrightness(parent, currentProgressPercent); | ||||
|  | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onScroll().brightnessControl, " | ||||
|                     + "currentBrightness = " + currentProgressPercent); | ||||
|         } | ||||
|  | ||||
|         player.getBrightnessImageView().setImageDrawable( | ||||
|                 AppCompatResources.getDrawable(service, | ||||
|                         currentProgressPercent < 0.25 | ||||
|                                 ? R.drawable.ic_brightness_low | ||||
|                                 : currentProgressPercent < 0.75 | ||||
|                                 ? R.drawable.ic_brightness_medium | ||||
|                                 : R.drawable.ic_brightness_high) | ||||
|         ); | ||||
|  | ||||
|         if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { | ||||
|             animate(player.getBrightnessRelativeLayout(), true, 200, SCALE_AND_ALPHA); | ||||
|         } | ||||
|         if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { | ||||
|             player.getVolumeRelativeLayout().setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onScrollEnd(@NonNull final MainPlayer.PlayerType playerType, | ||||
|                             @NonNull final MotionEvent event) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onScrollEnd called with playerType = [" | ||||
|                 + player.getPlayerType() + "]"); | ||||
|         } | ||||
|  | ||||
|         if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) { | ||||
|             player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); | ||||
|         } | ||||
|  | ||||
|         if (playerType == MainPlayer.PlayerType.VIDEO) { | ||||
|             if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { | ||||
|                 animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA, | ||||
|                         200); | ||||
|             } | ||||
|             if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { | ||||
|                 animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA, | ||||
|                         200); | ||||
|             } | ||||
|         } else /* Popup-Player */ { | ||||
|             if (player.isInsideClosingRadius(event)) { | ||||
|                 player.closePopup(); | ||||
|             } else if (!player.isPopupClosing()) { | ||||
|                 animate(player.getCloseOverlayButton(), false, 200); | ||||
|                 animate(player.getClosingOverlayView(), false, 200); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPopupResizingStart() { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onPopupResizingStart called"); | ||||
|         } | ||||
|         player.getLoadingPanel().setVisibility(View.GONE); | ||||
|  | ||||
|         player.hideControls(0, 0); | ||||
|         animate(player.getFastSeekOverlay(), false, 0); | ||||
|         animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPopupResizingEnd() { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onPopupResizingEnd called"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -3,6 +3,8 @@ package org.schabi.newpipe.player.event; | ||||
| import com.google.android.exoplayer2.PlaybackException; | ||||
|  | ||||
| public interface PlayerServiceEventListener extends PlayerEventListener { | ||||
|     void onViewCreated(); | ||||
|  | ||||
|     void onFullscreenStateChanged(boolean fullscreen); | ||||
|  | ||||
|     void onScreenRotationButtonClicked(); | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| package org.schabi.newpipe.player.event; | ||||
|  | ||||
| import org.schabi.newpipe.player.MainPlayer; | ||||
| import org.schabi.newpipe.player.PlayerService; | ||||
| import org.schabi.newpipe.player.Player; | ||||
|  | ||||
| public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { | ||||
|     void onServiceConnected(Player player, | ||||
|                             MainPlayer playerService, | ||||
|                             PlayerService playerService, | ||||
|                             boolean playAfterConnect); | ||||
|     void onServiceDisconnected(); | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,182 @@ | ||||
| package org.schabi.newpipe.player.gesture | ||||
|  | ||||
| import android.os.Handler | ||||
| import android.util.Log | ||||
| import android.view.GestureDetector | ||||
| import android.view.MotionEvent | ||||
| import android.view.View | ||||
| import org.schabi.newpipe.databinding.PlayerBinding | ||||
| import org.schabi.newpipe.player.Player | ||||
| import org.schabi.newpipe.player.ui.VideoPlayerUi | ||||
|  | ||||
| /** | ||||
|  * Base gesture handling for [Player] | ||||
|  * | ||||
|  * This class contains the logic for the player gestures like View preparations | ||||
|  * and provides some abstract methods to make it easier separating the logic from the UI. | ||||
|  */ | ||||
| abstract class BasePlayerGestureListener( | ||||
|     private val playerUi: VideoPlayerUi, | ||||
| ) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { | ||||
|  | ||||
|     protected val player: Player = playerUi.player | ||||
|     protected val binding: PlayerBinding = playerUi.binding | ||||
|  | ||||
|     override fun onTouch(v: View, event: MotionEvent): Boolean { | ||||
|         playerUi.gestureDetector.onTouchEvent(event) | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     private fun onDoubleTap( | ||||
|         event: MotionEvent, | ||||
|         portion: DisplayPortion | ||||
|     ) { | ||||
|         if (DEBUG) { | ||||
|             Log.d( | ||||
|                 TAG, | ||||
|                 "onDoubleTap called with playerType = [" + | ||||
|                     player.playerType + "], portion = [" + portion + "]" | ||||
|             ) | ||||
|         } | ||||
|         if (playerUi.isSomePopupMenuVisible) { | ||||
|             playerUi.hideControls(0, 0) | ||||
|         } | ||||
|         if (portion === DisplayPortion.LEFT || portion === DisplayPortion.RIGHT) { | ||||
|             startMultiDoubleTap(event) | ||||
|         } else if (portion === DisplayPortion.MIDDLE) { | ||||
|             player.playPause() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected fun onSingleTap() { | ||||
|         if (playerUi.isControlsVisible) { | ||||
|             playerUi.hideControls(150, 0) | ||||
|             return | ||||
|         } | ||||
|         // -- Controls are not visible -- | ||||
|  | ||||
|         // When player is completed show controls and don't hide them later | ||||
|         if (player.currentState == Player.STATE_COMPLETED) { | ||||
|             playerUi.showControls(0) | ||||
|         } else { | ||||
|             playerUi.showControlsThenHide() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     open fun onScrollEnd(event: MotionEvent) { | ||||
|         if (DEBUG) { | ||||
|             Log.d( | ||||
|                 TAG, | ||||
|                 "onScrollEnd called with playerType = [" + | ||||
|                     player.playerType + "]" | ||||
|             ) | ||||
|         } | ||||
|         if (playerUi.isControlsVisible && player.currentState == Player.STATE_PLAYING) { | ||||
|             playerUi.hideControls( | ||||
|                 VideoPlayerUi.DEFAULT_CONTROLS_DURATION, | ||||
|                 VideoPlayerUi.DEFAULT_CONTROLS_HIDE_TIME | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|     // Simple gestures | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     override fun onDown(e: MotionEvent): Boolean { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onDown called with e = [$e]") | ||||
|  | ||||
|         if (isDoubleTapping && isDoubleTapEnabled) { | ||||
|             doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e)) | ||||
|             return true | ||||
|         } | ||||
|  | ||||
|         return if (onDownNotDoubleTapping(e)) super.onDown(e) else true | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return true if `super.onDown(e)` should be called, false otherwise | ||||
|      */ | ||||
|     open fun onDownNotDoubleTapping(e: MotionEvent): Boolean { | ||||
|         return false // do not call super.onDown(e) by default, overridden for popup player | ||||
|     } | ||||
|  | ||||
|     override fun onDoubleTap(e: MotionEvent): Boolean { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onDoubleTap called with e = [$e]") | ||||
|  | ||||
|         onDoubleTap(e, getDisplayPortion(e)) | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|     // Multi double tapping | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private var doubleTapControls: DoubleTapListener? = null | ||||
|  | ||||
|     private val isDoubleTapEnabled: Boolean | ||||
|         get() = doubleTapDelay > 0 | ||||
|  | ||||
|     var isDoubleTapping = false | ||||
|         private set | ||||
|  | ||||
|     fun doubleTapControls(listener: DoubleTapListener) = apply { | ||||
|         doubleTapControls = listener | ||||
|     } | ||||
|  | ||||
|     private var doubleTapDelay = DOUBLE_TAP_DELAY | ||||
|     private val doubleTapHandler: Handler = Handler() | ||||
|     private val doubleTapRunnable = Runnable { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "doubleTapRunnable called") | ||||
|  | ||||
|         isDoubleTapping = false | ||||
|         doubleTapControls?.onDoubleTapFinished() | ||||
|     } | ||||
|  | ||||
|     private fun startMultiDoubleTap(e: MotionEvent) { | ||||
|         if (!isDoubleTapping) { | ||||
|             if (DEBUG) | ||||
|                 Log.d(TAG, "startMultiDoubleTap called with e = [$e]") | ||||
|  | ||||
|             keepInDoubleTapMode() | ||||
|             doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun keepInDoubleTapMode() { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "keepInDoubleTapMode called") | ||||
|  | ||||
|         isDoubleTapping = true | ||||
|         doubleTapHandler.removeCallbacks(doubleTapRunnable) | ||||
|         doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay) | ||||
|     } | ||||
|  | ||||
|     fun endMultiDoubleTap() { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "endMultiDoubleTap called") | ||||
|  | ||||
|         isDoubleTapping = false | ||||
|         doubleTapHandler.removeCallbacks(doubleTapRunnable) | ||||
|         doubleTapControls?.onDoubleTapFinished() | ||||
|     } | ||||
|  | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     // /////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     abstract fun getDisplayPortion(e: MotionEvent): DisplayPortion | ||||
|  | ||||
|     // Currently needed for scrolling since there is no action more the middle portion | ||||
|     abstract fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion | ||||
|  | ||||
|     companion object { | ||||
|         private const val TAG = "BasePlayerGestListener" | ||||
|         private val DEBUG = Player.DEBUG | ||||
|  | ||||
|         private const val DOUBLE_TAP_DELAY = 550L | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.schabi.newpipe.player.event; | ||||
| package org.schabi.newpipe.player.gesture; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.graphics.Rect; | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.schabi.newpipe.player.event | ||||
| package org.schabi.newpipe.player.gesture | ||||
| 
 | ||||
| enum class DisplayPortion { | ||||
|     LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.schabi.newpipe.player.event | ||||
| package org.schabi.newpipe.player.gesture | ||||
| 
 | ||||
| interface DoubleTapListener { | ||||
|     fun onDoubleTapStarted(portion: DisplayPortion) {} | ||||
| @@ -0,0 +1,232 @@ | ||||
| package org.schabi.newpipe.player.gesture | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.util.Log | ||||
| import android.view.MotionEvent | ||||
| import android.view.View | ||||
| import android.view.View.OnTouchListener | ||||
| import android.widget.ProgressBar | ||||
| import androidx.appcompat.content.res.AppCompatResources | ||||
| import org.schabi.newpipe.MainActivity | ||||
| import org.schabi.newpipe.R | ||||
| import org.schabi.newpipe.ktx.AnimationType | ||||
| import org.schabi.newpipe.ktx.animate | ||||
| import org.schabi.newpipe.player.Player | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper | ||||
| import org.schabi.newpipe.player.ui.MainPlayerUi | ||||
| import kotlin.math.abs | ||||
| import kotlin.math.max | ||||
| import kotlin.math.min | ||||
|  | ||||
| /** | ||||
|  * GestureListener for the player | ||||
|  * | ||||
|  * While [BasePlayerGestureListener] contains the logic behind the single gestures | ||||
|  * this class focuses on the visual aspect like hiding and showing the controls or changing | ||||
|  * volume/brightness during scrolling for specific events. | ||||
|  */ | ||||
| class MainPlayerGestureListener( | ||||
|     private val playerUi: MainPlayerUi | ||||
| ) : BasePlayerGestureListener(playerUi), OnTouchListener { | ||||
|     private val maxVolume: Int = player.audioReactor.maxVolume | ||||
|  | ||||
|     private var isMoving = false | ||||
|  | ||||
|     override fun onTouch(v: View, event: MotionEvent): Boolean { | ||||
|         super.onTouch(v, event) | ||||
|         if (event.action == MotionEvent.ACTION_UP && isMoving) { | ||||
|             isMoving = false | ||||
|             onScrollEnd(event) | ||||
|         } | ||||
|         return when (event.action) { | ||||
|             MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { | ||||
|                 v.parent.requestDisallowInterceptTouchEvent(playerUi.isFullscreen) | ||||
|                 true | ||||
|             } | ||||
|             MotionEvent.ACTION_UP -> { | ||||
|                 v.parent.requestDisallowInterceptTouchEvent(false) | ||||
|                 false | ||||
|             } | ||||
|             else -> true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onSingleTapConfirmed(e: MotionEvent): Boolean { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]") | ||||
|  | ||||
|         if (isDoubleTapping) | ||||
|             return true | ||||
|         super.onSingleTapConfirmed(e) | ||||
|  | ||||
|         if (player.currentState != Player.STATE_BLOCKED) | ||||
|             onSingleTap() | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     private fun onScrollVolume(distanceY: Float) { | ||||
|         // If we just started sliding, change the progress bar to match the system volume | ||||
|         if (binding.volumeRelativeLayout.visibility != View.VISIBLE) { | ||||
|             val volumePercent: Float = player.audioReactor.volume / maxVolume.toFloat() | ||||
|             binding.volumeProgressBar.progress = (volumePercent * MAX_GESTURE_LENGTH).toInt() | ||||
|         } | ||||
|  | ||||
|         binding.volumeProgressBar.incrementProgressBy(distanceY.toInt()) | ||||
|         val currentProgressPercent: Float = | ||||
|             binding.volumeProgressBar.progress.toFloat() / MAX_GESTURE_LENGTH | ||||
|         val currentVolume = (maxVolume * currentProgressPercent).toInt() | ||||
|         player.audioReactor.volume = currentVolume | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume") | ||||
|         } | ||||
|  | ||||
|         binding.volumeImageView.setImageDrawable( | ||||
|             AppCompatResources.getDrawable( | ||||
|                 player.context, | ||||
|                 when { | ||||
|                     currentProgressPercent <= 0 -> R.drawable.ic_volume_off | ||||
|                     currentProgressPercent < 0.25 -> R.drawable.ic_volume_mute | ||||
|                     currentProgressPercent < 0.75 -> R.drawable.ic_volume_down | ||||
|                     else -> R.drawable.ic_volume_up | ||||
|                 } | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         if (binding.volumeRelativeLayout.visibility != View.VISIBLE) { | ||||
|             binding.volumeRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA) | ||||
|         } | ||||
|         if (binding.brightnessRelativeLayout.visibility == View.VISIBLE) { | ||||
|             binding.volumeRelativeLayout.visibility = View.GONE | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun onScrollBrightness(distanceY: Float) { | ||||
|         val parent: Activity = playerUi.parentActivity | ||||
|         val window = parent.window | ||||
|         val layoutParams = window.attributes | ||||
|         val bar: ProgressBar = binding.brightnessProgressBar | ||||
|         val oldBrightness = layoutParams.screenBrightness | ||||
|         bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt() | ||||
|         bar.incrementProgressBy(distanceY.toInt()) | ||||
|         val currentProgressPercent = bar.progress.toFloat() / bar.max | ||||
|         layoutParams.screenBrightness = currentProgressPercent | ||||
|         window.attributes = layoutParams | ||||
|  | ||||
|         // Save current brightness level | ||||
|         PlayerHelper.setScreenBrightness(parent, currentProgressPercent) | ||||
|         if (DEBUG) { | ||||
|             Log.d( | ||||
|                 TAG, | ||||
|                 "onScroll().brightnessControl, " + | ||||
|                     "currentBrightness = " + currentProgressPercent | ||||
|             ) | ||||
|         } | ||||
|         binding.brightnessImageView.setImageDrawable( | ||||
|             AppCompatResources.getDrawable( | ||||
|                 player.context, | ||||
|                 if (currentProgressPercent < 0.25) R.drawable.ic_brightness_low else if (currentProgressPercent < 0.75) R.drawable.ic_brightness_medium else R.drawable.ic_brightness_high | ||||
|             ) | ||||
|         ) | ||||
|         if (binding.brightnessRelativeLayout.visibility != View.VISIBLE) { | ||||
|             binding.brightnessRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA) | ||||
|         } | ||||
|         if (binding.volumeRelativeLayout.visibility == View.VISIBLE) { | ||||
|             binding.volumeRelativeLayout.visibility = View.GONE | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onScrollEnd(event: MotionEvent) { | ||||
|         super.onScrollEnd(event) | ||||
|         if (binding.volumeRelativeLayout.visibility == View.VISIBLE) { | ||||
|             binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200) | ||||
|         } | ||||
|         if (binding.brightnessRelativeLayout.visibility == View.VISIBLE) { | ||||
|             binding.brightnessRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onScroll( | ||||
|         initialEvent: MotionEvent, | ||||
|         movingEvent: MotionEvent, | ||||
|         distanceX: Float, | ||||
|         distanceY: Float | ||||
|     ): Boolean { | ||||
|  | ||||
|         if (!playerUi.isFullscreen) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(player.context) | ||||
|         val isTouchingNavigationBar: Boolean = | ||||
|             initialEvent.y > (binding.root.height - getNavigationBarHeight(player.context)) | ||||
|         if (isTouchingStatusBar || isTouchingNavigationBar) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD | ||||
|         if ( | ||||
|             !isMoving && (insideThreshold || abs(distanceX) > abs(distanceY)) || | ||||
|             player.currentState == Player.STATE_COMPLETED | ||||
|         ) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         isMoving = true | ||||
|  | ||||
|         // -- Brightness and Volume control -- | ||||
|         val isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(player.context) | ||||
|         val isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(player.context) | ||||
|         if (isBrightnessGestureEnabled && isVolumeGestureEnabled) { | ||||
|             if (getDisplayHalfPortion(initialEvent) === DisplayPortion.LEFT_HALF) { | ||||
|                 onScrollBrightness(distanceY) | ||||
|             } else /* DisplayPortion.RIGHT_HALF */ { | ||||
|                 onScrollVolume(distanceY) | ||||
|             } | ||||
|         } else if (isBrightnessGestureEnabled) { | ||||
|             onScrollBrightness(distanceY) | ||||
|         } else if (isVolumeGestureEnabled) { | ||||
|             onScrollVolume(distanceY) | ||||
|         } | ||||
|  | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun getDisplayPortion(e: MotionEvent): DisplayPortion { | ||||
|         return when { | ||||
|             e.x < binding.root.width / 3.0 -> DisplayPortion.LEFT | ||||
|             e.x > binding.root.width * 2.0 / 3.0 -> DisplayPortion.RIGHT | ||||
|             else -> DisplayPortion.MIDDLE | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion { | ||||
|         return when { | ||||
|             e.x < binding.root.width / 2.0 -> DisplayPortion.LEFT_HALF | ||||
|             else -> DisplayPortion.RIGHT_HALF | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         private val TAG = MainPlayerGestureListener::class.java.simpleName | ||||
|         private val DEBUG = MainActivity.DEBUG | ||||
|         private const val MOVEMENT_THRESHOLD = 40 | ||||
|         const val MAX_GESTURE_LENGTH = 0.75f | ||||
|  | ||||
|         private fun getNavigationBarHeight(context: Context): Int { | ||||
|             val resId = context.resources | ||||
|                 .getIdentifier("navigation_bar_height", "dimen", "android") | ||||
|             return if (resId > 0) { | ||||
|                 context.resources.getDimensionPixelSize(resId) | ||||
|             } else 0 | ||||
|         } | ||||
|  | ||||
|         private fun getStatusBarHeight(context: Context): Int { | ||||
|             val resId = context.resources | ||||
|                 .getIdentifier("status_bar_height", "dimen", "android") | ||||
|             return if (resId > 0) { | ||||
|                 context.resources.getDimensionPixelSize(resId) | ||||
|             } else 0 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,287 @@ | ||||
| package org.schabi.newpipe.player.gesture | ||||
|  | ||||
| import android.util.Log | ||||
| import android.view.MotionEvent | ||||
| import android.view.View | ||||
| import android.view.ViewConfiguration | ||||
| import org.schabi.newpipe.MainActivity | ||||
| import org.schabi.newpipe.ktx.AnimationType | ||||
| import org.schabi.newpipe.ktx.animate | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper | ||||
| import org.schabi.newpipe.player.ui.PopupPlayerUi | ||||
| import kotlin.math.abs | ||||
| import kotlin.math.hypot | ||||
| import kotlin.math.max | ||||
| import kotlin.math.min | ||||
|  | ||||
| class PopupPlayerGestureListener( | ||||
|     private val playerUi: PopupPlayerUi, | ||||
| ) : BasePlayerGestureListener(playerUi) { | ||||
|  | ||||
|     private var isMoving = false | ||||
|  | ||||
|     private var initialPopupX: Int = -1 | ||||
|     private var initialPopupY: Int = -1 | ||||
|     private var isResizing = false | ||||
|  | ||||
|     // initial coordinates and distance between fingers | ||||
|     private var initPointerDistance = -1.0 | ||||
|     private var initFirstPointerX = -1f | ||||
|     private var initFirstPointerY = -1f | ||||
|     private var initSecPointerX = -1f | ||||
|     private var initSecPointerY = -1f | ||||
|  | ||||
|     override fun onTouch(v: View, event: MotionEvent): Boolean { | ||||
|         super.onTouch(v, event) | ||||
|         if (event.pointerCount == 2 && !isMoving && !isResizing) { | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.") | ||||
|             } | ||||
|             onPopupResizingStart() | ||||
|  | ||||
|             // record coordinates of fingers | ||||
|             initFirstPointerX = event.getX(0) | ||||
|             initFirstPointerY = event.getY(0) | ||||
|             initSecPointerX = event.getX(1) | ||||
|             initSecPointerY = event.getY(1) | ||||
|             // record distance between fingers | ||||
|             initPointerDistance = hypot( | ||||
|                 initFirstPointerX - initSecPointerX.toDouble(), | ||||
|                 initFirstPointerY - initSecPointerY.toDouble() | ||||
|             ) | ||||
|  | ||||
|             isResizing = true | ||||
|         } | ||||
|         if (event.action == MotionEvent.ACTION_MOVE && !isMoving && isResizing) { | ||||
|             if (DEBUG) { | ||||
|                 Log.d( | ||||
|                     TAG, | ||||
|                     "onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" + | ||||
|                         "[${event.rawX}, ${event.rawY}]" | ||||
|                 ) | ||||
|             } | ||||
|             return handleMultiDrag(event) | ||||
|         } | ||||
|         if (event.action == MotionEvent.ACTION_UP) { | ||||
|             if (DEBUG) { | ||||
|                 Log.d( | ||||
|                     TAG, | ||||
|                     "onTouch() ACTION_UP > v = [$v], e1.getRaw =" + | ||||
|                         " [${event.rawX}, ${event.rawY}]" | ||||
|                 ) | ||||
|             } | ||||
|             if (isMoving) { | ||||
|                 isMoving = false | ||||
|                 onScrollEnd(event) | ||||
|             } | ||||
|             if (isResizing) { | ||||
|                 isResizing = false | ||||
|  | ||||
|                 initPointerDistance = (-1).toDouble() | ||||
|                 initFirstPointerX = (-1).toFloat() | ||||
|                 initFirstPointerY = (-1).toFloat() | ||||
|                 initSecPointerX = (-1).toFloat() | ||||
|                 initSecPointerY = (-1).toFloat() | ||||
|  | ||||
|                 onPopupResizingEnd() | ||||
|                 player.changeState(player.currentState) | ||||
|             } | ||||
|             if (!playerUi.isPopupClosing) { | ||||
|                 PlayerHelper.savePopupPositionAndSizeToPrefs(playerUi) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         v.performClick() | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun onScrollEnd(event: MotionEvent) { | ||||
|         super.onScrollEnd(event) | ||||
|         if (playerUi.isInsideClosingRadius(event)) { | ||||
|             playerUi.closePopup() | ||||
|         } else if (!playerUi.isPopupClosing) { | ||||
|             playerUi.closeOverlayBinding.closeButton.animate(false, 200) | ||||
|             binding.closingOverlay.animate(false, 200) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun handleMultiDrag(event: MotionEvent): Boolean { | ||||
|         if (initPointerDistance != -1.0 && event.pointerCount == 2) { | ||||
|             // get the movements of the fingers | ||||
|             val firstPointerMove = hypot( | ||||
|                 event.getX(0) - initFirstPointerX.toDouble(), | ||||
|                 event.getY(0) - initFirstPointerY.toDouble() | ||||
|             ) | ||||
|             val secPointerMove = hypot( | ||||
|                 event.getX(1) - initSecPointerX.toDouble(), | ||||
|                 event.getY(1) - initSecPointerY.toDouble() | ||||
|             ) | ||||
|  | ||||
|             // minimum threshold beyond which pinch gesture will work | ||||
|             val minimumMove = ViewConfiguration.get(player.context).scaledTouchSlop | ||||
|  | ||||
|             if (max(firstPointerMove, secPointerMove) > minimumMove) { | ||||
|                 // calculate current distance between the pointers | ||||
|                 val currentPointerDistance = hypot( | ||||
|                     event.getX(0) - event.getX(1).toDouble(), | ||||
|                     event.getY(0) - event.getY(1).toDouble() | ||||
|                 ) | ||||
|  | ||||
|                 val popupWidth = playerUi.popupLayoutParams.width.toDouble() | ||||
|                 // change co-ordinates of popup so the center stays at the same position | ||||
|                 val newWidth = popupWidth * currentPointerDistance / initPointerDistance | ||||
|                 initPointerDistance = currentPointerDistance | ||||
|                 playerUi.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt() | ||||
|  | ||||
|                 playerUi.checkPopupPositionBounds() | ||||
|                 playerUi.updateScreenSize() | ||||
|                 playerUi.changePopupSize(min(playerUi.screenWidth.toDouble(), newWidth).toInt()) | ||||
|                 return true | ||||
|             } | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     private fun onPopupResizingStart() { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onPopupResizingStart called") | ||||
|         } | ||||
|         binding.loadingPanel.visibility = View.GONE | ||||
|         playerUi.hideControls(0, 0) | ||||
|         binding.fastSeekOverlay.animate(false, 0) | ||||
|         binding.currentDisplaySeek.animate(false, 0, AnimationType.ALPHA, 0) | ||||
|     } | ||||
|  | ||||
|     private fun onPopupResizingEnd() { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onPopupResizingEnd called") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onLongPress(e: MotionEvent?) { | ||||
|         playerUi.updateScreenSize() | ||||
|         playerUi.checkPopupPositionBounds() | ||||
|         playerUi.changePopupSize(playerUi.screenWidth) | ||||
|     } | ||||
|  | ||||
|     override fun onFling( | ||||
|         e1: MotionEvent?, | ||||
|         e2: MotionEvent?, | ||||
|         velocityX: Float, | ||||
|         velocityY: Float | ||||
|     ): Boolean { | ||||
|         return if (player.popupPlayerSelected()) { | ||||
|             val absVelocityX = abs(velocityX) | ||||
|             val absVelocityY = abs(velocityY) | ||||
|             if (absVelocityX.coerceAtLeast(absVelocityY) > TOSS_FLING_VELOCITY) { | ||||
|                 if (absVelocityX > TOSS_FLING_VELOCITY) { | ||||
|                     playerUi.popupLayoutParams.x = velocityX.toInt() | ||||
|                 } | ||||
|                 if (absVelocityY > TOSS_FLING_VELOCITY) { | ||||
|                     playerUi.popupLayoutParams.y = velocityY.toInt() | ||||
|                 } | ||||
|                 playerUi.checkPopupPositionBounds() | ||||
|                 playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams) | ||||
|                 return true | ||||
|             } | ||||
|             return false | ||||
|         } else { | ||||
|             true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onDownNotDoubleTapping(e: MotionEvent): Boolean { | ||||
|         // 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). | ||||
|         playerUi.updateScreenSize() | ||||
|         playerUi.checkPopupPositionBounds() | ||||
|         playerUi.popupLayoutParams.let { | ||||
|             initialPopupX = it.x | ||||
|             initialPopupY = it.y | ||||
|         } | ||||
|         return true // we want `super.onDown(e)` to be called | ||||
|     } | ||||
|  | ||||
|     override fun onSingleTapConfirmed(e: MotionEvent): Boolean { | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]") | ||||
|  | ||||
|         if (isDoubleTapping) | ||||
|             return true | ||||
|         if (player.exoPlayerIsNull()) | ||||
|             return false | ||||
|  | ||||
|         onSingleTap() | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun onScroll( | ||||
|         initialEvent: MotionEvent, | ||||
|         movingEvent: MotionEvent, | ||||
|         distanceX: Float, | ||||
|         distanceY: Float | ||||
|     ): Boolean { | ||||
|  | ||||
|         if (isResizing) { | ||||
|             return super.onScroll(initialEvent, movingEvent, distanceX, distanceY) | ||||
|         } | ||||
|  | ||||
|         if (!isMoving) { | ||||
|             playerUi.closeOverlayBinding.closeButton.animate(true, 200) | ||||
|         } | ||||
|  | ||||
|         isMoving = true | ||||
|  | ||||
|         val diffX: Float = (movingEvent.rawX - initialEvent.rawX) | ||||
|         var posX: Float = (initialPopupX + diffX) | ||||
|         val diffY: Float = (movingEvent.rawY - initialEvent.rawY) | ||||
|         var posY: Float = (initialPopupY + diffY) | ||||
|  | ||||
|         if (posX > playerUi.screenWidth - playerUi.popupLayoutParams.width) { | ||||
|             posX = (playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat() | ||||
|         } else if (posX < 0) { | ||||
|             posX = 0f | ||||
|         } | ||||
|  | ||||
|         if (posY > playerUi.screenHeight - playerUi.popupLayoutParams.height) { | ||||
|             posY = (playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat() | ||||
|         } else if (posY < 0) { | ||||
|             posY = 0f | ||||
|         } | ||||
|  | ||||
|         playerUi.popupLayoutParams.x = posX.toInt() | ||||
|         playerUi.popupLayoutParams.y = posY.toInt() | ||||
|  | ||||
|         // -- Determine if the ClosingOverlayView (red X) has to be shown or hidden -- | ||||
|         val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent) | ||||
|         // Check if an view is in expected state and if not animate it into the correct state | ||||
|         val expectedVisibility = if (showClosingOverlayView) View.VISIBLE else View.GONE | ||||
|         if (binding.closingOverlay.visibility != expectedVisibility) { | ||||
|             binding.closingOverlay.animate(showClosingOverlayView, 200) | ||||
|         } | ||||
|  | ||||
|         playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams) | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun getDisplayPortion(e: MotionEvent): DisplayPortion { | ||||
|         return when { | ||||
|             e.x < playerUi.popupLayoutParams.width / 3.0 -> DisplayPortion.LEFT | ||||
|             e.x > playerUi.popupLayoutParams.width * 2.0 / 3.0 -> DisplayPortion.RIGHT | ||||
|             else -> DisplayPortion.MIDDLE | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion { | ||||
|         return when { | ||||
|             e.x < playerUi.popupLayoutParams.width / 2.0 -> DisplayPortion.LEFT_HALF | ||||
|             else -> DisplayPortion.RIGHT_HALF | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         private val TAG = PopupPlayerGestureListener::class.java.simpleName | ||||
|         private val DEBUG = MainActivity.DEBUG | ||||
|         private const val TOSS_FLING_VELOCITY = 2500 | ||||
|     } | ||||
| } | ||||
| @@ -26,7 +26,7 @@ import androidx.preference.PreferenceManager; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding; | ||||
| import org.schabi.newpipe.player.Player; | ||||
| import org.schabi.newpipe.player.ui.VideoPlayerUi; | ||||
| import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; | ||||
| import org.schabi.newpipe.util.SliderStrategy; | ||||
|  | ||||
| @@ -207,7 +207,7 @@ public class PlaybackParameterDialog extends DialogFragment { | ||||
|                     ? View.VISIBLE | ||||
|                     : View.GONE); | ||||
|             animateRotation(binding.pitchToogleControlModes, | ||||
|                     Player.DEFAULT_CONTROLS_DURATION, | ||||
|                     VideoPlayerUi.DEFAULT_CONTROLS_DURATION, | ||||
|                     isCurrentlyVisible ? 180 : 0); | ||||
|         }); | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package org.schabi.newpipe.player.helper; | ||||
| import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; | ||||
| import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; | ||||
| import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; | ||||
| import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS; | ||||
| import static org.schabi.newpipe.player.Player.PLAYER_TYPE; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER; | ||||
| @@ -11,6 +10,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLA | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; | ||||
| import static org.schabi.newpipe.player.ui.PopupPlayerUi.IDLE_WINDOW_FLAGS; | ||||
| import static java.lang.annotation.RetentionPolicy.SOURCE; | ||||
|  | ||||
| import android.annotation.SuppressLint; | ||||
| @@ -49,11 +49,12 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.extractor.stream.SubtitlesStream; | ||||
| import org.schabi.newpipe.extractor.utils.Utils; | ||||
| import org.schabi.newpipe.player.MainPlayer; | ||||
| import org.schabi.newpipe.player.PlayerService; | ||||
| import org.schabi.newpipe.player.Player; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||
| import org.schabi.newpipe.player.ui.PopupPlayerUi; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
|  | ||||
| import java.lang.annotation.Retention; | ||||
| @@ -339,10 +340,6 @@ public final class PlayerHelper { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public static int getTossFlingVelocity() { | ||||
|         return 2500; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) { | ||||
|         final CaptioningManager captioningManager = ContextCompat.getSystemService(context, | ||||
| @@ -452,10 +449,10 @@ public final class PlayerHelper { | ||||
|     // Utils used by player | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) { | ||||
|     public static PlayerService.PlayerType retrievePlayerTypeFromIntent(final Intent intent) { | ||||
|         // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra | ||||
|         return MainPlayer.PlayerType.values()[ | ||||
|                 intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())]; | ||||
|         return PlayerService.PlayerType.values()[ | ||||
|                 intent.getIntExtra(PLAYER_TYPE, PlayerService.PlayerType.MAIN.ordinal())]; | ||||
|     } | ||||
|  | ||||
|     public static boolean isPlaybackResumeEnabled(final Player player) { | ||||
| @@ -529,19 +526,20 @@ public final class PlayerHelper { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param player {@code screenWidth} and {@code screenHeight} must have been initialized | ||||
|      * @param playerUi {@code screenWidth} and {@code screenHeight} must have been initialized | ||||
|      * @return the popup starting layout params | ||||
|      */ | ||||
|     @SuppressLint("RtlHardcoded") | ||||
|     public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs( | ||||
|             final Player player) { | ||||
|         final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean( | ||||
|                 player.getContext().getString(R.string.popup_remember_size_pos_key), true); | ||||
|         final float defaultSize = | ||||
|                 player.getContext().getResources().getDimension(R.dimen.popup_default_width); | ||||
|             final PopupPlayerUi playerUi) { | ||||
|         final SharedPreferences prefs = playerUi.getPlayer().getPrefs(); | ||||
|         final Context context = playerUi.getPlayer().getContext(); | ||||
|  | ||||
|         final boolean popupRememberSizeAndPos = prefs.getBoolean( | ||||
|                 context.getString(R.string.popup_remember_size_pos_key), true); | ||||
|         final float defaultSize = context.getResources().getDimension(R.dimen.popup_default_width); | ||||
|         final float popupWidth = popupRememberSizeAndPos | ||||
|                 ? player.getPrefs().getFloat(player.getContext().getString( | ||||
|                 R.string.popup_saved_width_key), defaultSize) | ||||
|                 ? prefs.getFloat(context.getString(R.string.popup_saved_width_key), defaultSize) | ||||
|                 : defaultSize; | ||||
|         final float popupHeight = getMinimumVideoHeight(popupWidth); | ||||
|  | ||||
| @@ -553,27 +551,26 @@ public final class PlayerHelper { | ||||
|         popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; | ||||
|         popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; | ||||
|  | ||||
|         final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f); | ||||
|         final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f); | ||||
|         final int centerX = (int) (playerUi.getScreenWidth() / 2f - popupWidth / 2f); | ||||
|         final int centerY = (int) (playerUi.getScreenHeight() / 2f - popupHeight / 2f); | ||||
|         popupLayoutParams.x = popupRememberSizeAndPos | ||||
|                 ? player.getPrefs().getInt(player.getContext().getString( | ||||
|                 R.string.popup_saved_x_key), centerX) : centerX; | ||||
|                 ? prefs.getInt(context.getString(R.string.popup_saved_x_key), centerX) : centerX; | ||||
|         popupLayoutParams.y = popupRememberSizeAndPos | ||||
|                 ? player.getPrefs().getInt(player.getContext().getString( | ||||
|                 R.string.popup_saved_y_key), centerY) : centerY; | ||||
|                 ? prefs.getInt(context.getString(R.string.popup_saved_y_key), centerY) : centerY; | ||||
|  | ||||
|         return popupLayoutParams; | ||||
|     } | ||||
|  | ||||
|     public static void savePopupPositionAndSizeToPrefs(final Player player) { | ||||
|         if (player.getPopupLayoutParams() != null) { | ||||
|             player.getPrefs().edit() | ||||
|                     .putFloat(player.getContext().getString(R.string.popup_saved_width_key), | ||||
|                             player.getPopupLayoutParams().width) | ||||
|                     .putInt(player.getContext().getString(R.string.popup_saved_x_key), | ||||
|                             player.getPopupLayoutParams().x) | ||||
|                     .putInt(player.getContext().getString(R.string.popup_saved_y_key), | ||||
|                             player.getPopupLayoutParams().y) | ||||
|     public static void savePopupPositionAndSizeToPrefs(final PopupPlayerUi playerUi) { | ||||
|         if (playerUi.getPopupLayoutParams() != null) { | ||||
|             final Context context = playerUi.getPlayer().getContext(); | ||||
|             playerUi.getPlayer().getPrefs().edit() | ||||
|                     .putFloat(context.getString(R.string.popup_saved_width_key), | ||||
|                             playerUi.getPopupLayoutParams().width) | ||||
|                     .putInt(context.getString(R.string.popup_saved_x_key), | ||||
|                             playerUi.getPopupLayoutParams().x) | ||||
|                     .putInt(context.getString(R.string.popup_saved_y_key), | ||||
|                             playerUi.getPopupLayoutParams().y) | ||||
|                     .apply(); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import com.google.android.exoplayer2.PlaybackParameters; | ||||
| import org.schabi.newpipe.App; | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.player.MainPlayer; | ||||
| import org.schabi.newpipe.player.PlayerService; | ||||
| import org.schabi.newpipe.player.Player; | ||||
| import org.schabi.newpipe.player.event.PlayerServiceEventListener; | ||||
| import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; | ||||
| @@ -42,17 +42,17 @@ public final class PlayerHolder { | ||||
|  | ||||
|     private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection(); | ||||
|     private boolean bound; | ||||
|     @Nullable private MainPlayer playerService; | ||||
|     @Nullable private PlayerService playerService; | ||||
|     @Nullable private Player player; | ||||
|  | ||||
|     /** | ||||
|      * Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service, | ||||
|      * Returns the current {@link PlayerService.PlayerType} of the {@link PlayerService} service, | ||||
|      * otherwise `null` if no service running. | ||||
|      * | ||||
|      * @return Current PlayerType | ||||
|      */ | ||||
|     @Nullable | ||||
|     public MainPlayer.PlayerType getType() { | ||||
|     public PlayerService.PlayerType getType() { | ||||
|         if (player == null) { | ||||
|             return null; | ||||
|         } | ||||
| @@ -122,7 +122,7 @@ public final class PlayerHolder { | ||||
|         // and NullPointerExceptions inside the service because the service will be | ||||
|         // bound twice. Prevent it with unbinding first | ||||
|         unbind(context); | ||||
|         ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class)); | ||||
|         ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class)); | ||||
|         serviceConnection.doPlayAfterConnect(playAfterConnect); | ||||
|         bind(context); | ||||
|     } | ||||
| @@ -130,7 +130,7 @@ public final class PlayerHolder { | ||||
|     public void stopService() { | ||||
|         final Context context = getCommonContext(); | ||||
|         unbind(context); | ||||
|         context.stopService(new Intent(context, MainPlayer.class)); | ||||
|         context.stopService(new Intent(context, PlayerService.class)); | ||||
|     } | ||||
|  | ||||
|     class PlayerServiceConnection implements ServiceConnection { | ||||
| @@ -156,7 +156,7 @@ public final class PlayerHolder { | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "Player service is connected"); | ||||
|             } | ||||
|             final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service; | ||||
|             final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service; | ||||
|  | ||||
|             playerService = localBinder.getService(); | ||||
|             player = localBinder.getPlayer(); | ||||
| @@ -172,7 +172,7 @@ public final class PlayerHolder { | ||||
|             Log.d(TAG, "bind() called"); | ||||
|         } | ||||
|  | ||||
|         final Intent serviceIntent = new Intent(context, MainPlayer.class); | ||||
|         final Intent serviceIntent = new Intent(context, PlayerService.class); | ||||
|         bound = context.bindService(serviceIntent, serviceConnection, | ||||
|                 Context.BIND_AUTO_CREATE); | ||||
|         if (!bound) { | ||||
| @@ -211,6 +211,13 @@ public final class PlayerHolder { | ||||
|  | ||||
|     private final PlayerServiceEventListener internalListener = | ||||
|             new PlayerServiceEventListener() { | ||||
|                 @Override | ||||
|                 public void onViewCreated() { | ||||
|                     if (listener != null) { | ||||
|                         listener.onViewCreated(); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 @Override | ||||
|                 public void onFullscreenStateChanged(final boolean fullscreen) { | ||||
|                     if (listener != null) { | ||||
|   | ||||
| @@ -1,47 +0,0 @@ | ||||
| package org.schabi.newpipe.player.listeners.view | ||||
|  | ||||
| import android.util.Log | ||||
| import android.view.View | ||||
| import androidx.appcompat.widget.PopupMenu | ||||
| import org.schabi.newpipe.MainActivity | ||||
| import org.schabi.newpipe.player.Player | ||||
| import org.schabi.newpipe.player.helper.PlaybackParameterDialog | ||||
|  | ||||
| /** | ||||
|  * Click listener for the playbackSpeed textview of the player | ||||
|  */ | ||||
| class PlaybackSpeedClickListener( | ||||
|     private val player: Player, | ||||
|     private val playbackSpeedPopupMenu: PopupMenu | ||||
| ) : View.OnClickListener { | ||||
|  | ||||
|     companion object { | ||||
|         private const val TAG: String = "PlaybSpeedClickListener" | ||||
|     } | ||||
|  | ||||
|     override fun onClick(v: View) { | ||||
|         if (MainActivity.DEBUG) { | ||||
|             Log.d(TAG, "onPlaybackSpeedClicked() called") | ||||
|         } | ||||
|  | ||||
|         if (player.videoPlayerSelected()) { | ||||
|             PlaybackParameterDialog.newInstance( | ||||
|                 player.playbackSpeed.toDouble(), | ||||
|                 player.playbackPitch.toDouble(), | ||||
|                 player.playbackSkipSilence | ||||
|             ) { speed: Float, pitch: Float, skipSilence: Boolean -> | ||||
|                 player.setPlaybackParameters( | ||||
|                     speed, | ||||
|                     pitch, | ||||
|                     skipSilence | ||||
|                 ) | ||||
|             } | ||||
|                 .show(player.parentActivity!!.supportFragmentManager, null) | ||||
|         } else { | ||||
|             playbackSpeedPopupMenu.show() | ||||
|             player.isSomePopupMenuVisible = true | ||||
|         } | ||||
|  | ||||
|         player.manageControlsAfterOnClick(v) | ||||
|     } | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| package org.schabi.newpipe.player.listeners.view | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.util.Log | ||||
| import android.view.View | ||||
| import androidx.appcompat.widget.PopupMenu | ||||
| import org.schabi.newpipe.MainActivity | ||||
| import org.schabi.newpipe.extractor.MediaFormat | ||||
| import org.schabi.newpipe.player.Player | ||||
|  | ||||
| /** | ||||
|  * Click listener for the qualityTextView of the player | ||||
|  */ | ||||
| class QualityClickListener( | ||||
|     private val player: Player, | ||||
|     private val qualityPopupMenu: PopupMenu | ||||
| ) : View.OnClickListener { | ||||
|  | ||||
|     companion object { | ||||
|         private const val TAG: String = "QualityClickListener" | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("SetTextI18n") // we don't need I18N because of a " " | ||||
|     override fun onClick(v: View) { | ||||
|         if (MainActivity.DEBUG) { | ||||
|             Log.d(TAG, "onQualitySelectorClicked() called") | ||||
|         } | ||||
|  | ||||
|         qualityPopupMenu.show() | ||||
|         player.isSomePopupMenuVisible = true | ||||
|  | ||||
|         val videoStream = player.selectedVideoStream | ||||
|         if (videoStream != null) { | ||||
|             player.binding.qualityTextView.text = | ||||
|                 MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution() | ||||
|         } | ||||
|  | ||||
|         player.saveWasPlaying() | ||||
|         player.manageControlsAfterOnClick(v) | ||||
|     } | ||||
| } | ||||
| @@ -8,6 +8,9 @@ import android.support.v4.media.MediaMetadataCompat; | ||||
| import org.schabi.newpipe.player.Player; | ||||
| import org.schabi.newpipe.player.mediasession.MediaSessionCallback; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.player.ui.VideoPlayerUi; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| public class PlayerMediaSession implements MediaSessionCallback { | ||||
|     private final Player player; | ||||
| @@ -89,7 +92,7 @@ public class PlayerMediaSession implements MediaSessionCallback { | ||||
|     public void play() { | ||||
|         player.play(); | ||||
|         // hide the player controls even if the play command came from the media session | ||||
|         player.hideControls(0, 0); | ||||
|         player.UIs().get(VideoPlayerUi.class).ifPresent(playerUi -> playerUi.hideControls(0, 0)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
							
								
								
									
										937
									
								
								app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										937
									
								
								app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,937 @@ | ||||
| package org.schabi.newpipe.player.ui; | ||||
|  | ||||
| import static org.schabi.newpipe.MainActivity.DEBUG; | ||||
| import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu; | ||||
| import static org.schabi.newpipe.ktx.ViewUtils.animate; | ||||
| import static org.schabi.newpipe.player.Player.STATE_COMPLETED; | ||||
| import static org.schabi.newpipe.player.Player.STATE_PAUSED; | ||||
| import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PAUSE; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; | ||||
|  | ||||
| import android.content.Intent; | ||||
| import android.content.res.Resources; | ||||
| import android.database.ContentObserver; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.Color; | ||||
| import android.os.Build; | ||||
| import android.os.Handler; | ||||
| import android.provider.Settings; | ||||
| import android.util.DisplayMetrics; | ||||
| import android.util.Log; | ||||
| import android.util.TypedValue; | ||||
| import android.view.KeyEvent; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.view.ViewParent; | ||||
| import android.view.Window; | ||||
| import android.view.WindowManager; | ||||
| import android.widget.FrameLayout; | ||||
| import android.widget.LinearLayout; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.appcompat.content.res.AppCompatResources; | ||||
| import androidx.recyclerview.widget.ItemTouchHelper; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
|  | ||||
| import com.google.android.exoplayer2.video.VideoSize; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.databinding.PlayerBinding; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamSegment; | ||||
| import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | ||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||
| import org.schabi.newpipe.info_list.StreamSegmentAdapter; | ||||
| import org.schabi.newpipe.ktx.AnimationType; | ||||
| import org.schabi.newpipe.player.Player; | ||||
| import org.schabi.newpipe.player.event.PlayerServiceEventListener; | ||||
| import org.schabi.newpipe.player.gesture.BasePlayerGestureListener; | ||||
| import org.schabi.newpipe.player.gesture.MainPlayerGestureListener; | ||||
| import org.schabi.newpipe.player.helper.PlaybackParameterDialog; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; | ||||
| import org.schabi.newpipe.util.DeviceUtils; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.external_communication.KoreUtils; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
|  | ||||
| public final class MainPlayerUi extends VideoPlayerUi { | ||||
|     private static final String TAG = MainPlayerUi.class.getSimpleName(); | ||||
|  | ||||
|     private boolean isFullscreen = false; | ||||
|     private boolean isVerticalVideo = false; | ||||
|     private boolean fragmentIsVisible = false; | ||||
|  | ||||
|     private ContentObserver settingsContentObserver; | ||||
|  | ||||
|     private PlayQueueAdapter playQueueAdapter; | ||||
|     private StreamSegmentAdapter segmentAdapter; | ||||
|     private boolean isQueueVisible = false; | ||||
|     private boolean areSegmentsVisible = false; | ||||
|  | ||||
|     // fullscreen player | ||||
|     private ItemTouchHelper itemTouchHelper; | ||||
|  | ||||
|     public MainPlayerUi(@NonNull final Player player, | ||||
|                         @NonNull final PlayerBinding playerBinding) { | ||||
|         super(player, playerBinding); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Open fullscreen on tablets where the option to have the main player start automatically in | ||||
|      * fullscreen mode is on. Rotating the device to landscape is already done in {@link | ||||
|      * VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's | ||||
|      * enough for phones, but not for tablets since the mini player can be also shown in landscape. | ||||
|      */ | ||||
|     private void directlyOpenFullscreenIfNeeded() { | ||||
|         if (PlayerHelper.isStartMainPlayerFullscreenEnabled(player.getService()) | ||||
|                 && DeviceUtils.isTablet(player.getService()) | ||||
|                 && PlayerHelper.globalScreenOrientationLocked(player.getService())) { | ||||
|             player.getFragmentListener().ifPresent( | ||||
|                     PlayerServiceEventListener::onScreenRotationButtonClicked); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void setupAfterIntent() { | ||||
|         // needed for tablets, check the function for a better explanation | ||||
|         directlyOpenFullscreenIfNeeded(); | ||||
|  | ||||
|         super.setupAfterIntent(); | ||||
|  | ||||
|         binding.getRoot().setVisibility(View.VISIBLE); | ||||
|         initVideoPlayer(); | ||||
|         // Android TV: without it focus will frame the whole player | ||||
|         binding.playPauseButton.requestFocus(); | ||||
|  | ||||
|         // Note: This is for automatically playing (when "Resume playback" is off), see #6179 | ||||
|         if (player.getPlayWhenReady()) { | ||||
|             player.play(); | ||||
|         } else { | ||||
|             player.pause(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     BasePlayerGestureListener buildGestureListener() { | ||||
|         return new MainPlayerGestureListener(this); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void initListeners() { | ||||
|         super.initListeners(); | ||||
|  | ||||
|         binding.queueButton.setOnClickListener(v -> onQueueClicked()); | ||||
|         binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked()); | ||||
|  | ||||
|         binding.addToPlaylistButton.setOnClickListener(v -> | ||||
|                 player.onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager())); | ||||
|  | ||||
|         settingsContentObserver = new ContentObserver(new Handler()) { | ||||
|             @Override | ||||
|             public void onChange(final boolean selfChange) { | ||||
|                 setupScreenRotationButton(); | ||||
|             } | ||||
|         }; | ||||
|         context.getContentResolver().registerContentObserver( | ||||
|                 Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, | ||||
|                 settingsContentObserver); | ||||
|  | ||||
|         binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void initPlayback() { | ||||
|         super.initPlayback(); | ||||
|  | ||||
|         if (playQueueAdapter != null) { | ||||
|             playQueueAdapter.dispose(); | ||||
|         } | ||||
|         playQueueAdapter = new PlayQueueAdapter(context, | ||||
|                 Objects.requireNonNull(player.getPlayQueue())); | ||||
|         segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void removeViewFromParent() { | ||||
|         // view was added to fragment | ||||
|         final ViewParent parent = binding.getRoot().getParent(); | ||||
|         if (parent instanceof ViewGroup) { | ||||
|             ((ViewGroup) parent).removeView(binding.getRoot()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void destroy() { | ||||
|         super.destroy(); | ||||
|         context.getContentResolver().unregisterContentObserver(settingsContentObserver); | ||||
|  | ||||
|         // Exit from fullscreen when user closes the player via notification | ||||
|         if (isFullscreen) { | ||||
|             toggleFullscreen(); | ||||
|         } | ||||
|  | ||||
|         removeViewFromParent(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void destroyPlayer() { | ||||
|         super.destroyPlayer(); | ||||
|  | ||||
|         if (playQueueAdapter != null) { | ||||
|             playQueueAdapter.unsetSelectedListener(); | ||||
|             playQueueAdapter.dispose(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void smoothStopForImmediateReusing() { | ||||
|         super.smoothStopForImmediateReusing(); | ||||
|         // Android TV will handle back button in case controls will be visible | ||||
|         // (one more additional unneeded click while the player is hidden) | ||||
|         hideControls(0, 0); | ||||
|         closeItemsList(); | ||||
|     } | ||||
|  | ||||
|     private void initVideoPlayer() { | ||||
|         // restore last resize mode | ||||
|         setResizeMode(PlayerHelper.retrieveResizeModeFromPrefs(player)); | ||||
|         binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams( | ||||
|                 FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void setupElementsVisibility() { | ||||
|         super.setupElementsVisibility(); | ||||
|  | ||||
|         closeItemsList(); | ||||
|         showHideKodiButton(); | ||||
|         binding.fullScreenButton.setVisibility(View.GONE); | ||||
|         setupScreenRotationButton(); | ||||
|         binding.resizeTextView.setVisibility(View.VISIBLE); | ||||
|         binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE); | ||||
|         binding.moreOptionsButton.setVisibility(View.VISIBLE); | ||||
|         binding.topControls.setOrientation(LinearLayout.VERTICAL); | ||||
|         binding.primaryControls.getLayoutParams().width | ||||
|                 = LinearLayout.LayoutParams.MATCH_PARENT; | ||||
|         binding.secondaryControls.setVisibility(View.INVISIBLE); | ||||
|         binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(context, | ||||
|                 R.drawable.ic_expand_more)); | ||||
|         binding.share.setVisibility(View.VISIBLE); | ||||
|         binding.openInBrowser.setVisibility(View.VISIBLE); | ||||
|         binding.switchMute.setVisibility(View.VISIBLE); | ||||
|         binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); | ||||
|         // Top controls have a large minHeight which is allows to drag the player | ||||
|         // down in fullscreen mode (just larger area to make easy to locate by finger) | ||||
|         binding.topControls.setClickable(true); | ||||
|         binding.topControls.setFocusable(true); | ||||
|  | ||||
|         if (isFullscreen) { | ||||
|             binding.titleTextView.setVisibility(View.VISIBLE); | ||||
|             binding.channelTextView.setVisibility(View.VISIBLE); | ||||
|         } else { | ||||
|             binding.titleTextView.setVisibility(View.GONE); | ||||
|             binding.channelTextView.setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void setupElementsSize(final Resources resources) { | ||||
|         setupElementsSize( | ||||
|                 resources.getDimensionPixelSize(R.dimen.player_main_buttons_min_width), | ||||
|                 resources.getDimensionPixelSize(R.dimen.player_main_top_padding), | ||||
|                 resources.getDimensionPixelSize(R.dimen.player_main_controls_padding), | ||||
|                 resources.getDimensionPixelSize(R.dimen.player_main_buttons_padding) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Broadcast receiver | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Broadcast receiver | ||||
|     @Override | ||||
|     public void onBroadcastReceived(final Intent intent) { | ||||
|         super.onBroadcastReceived(intent); | ||||
|         if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) { | ||||
|             // Close it because when changing orientation from portrait | ||||
|             // (in fullscreen mode) the size of queue layout can be larger than the screen size | ||||
|             closeItemsList(); | ||||
|         } else if (ACTION_PLAY_PAUSE.equals(intent.getAction())) { | ||||
|             // Ensure that we have audio-only stream playing when a user | ||||
|             // started to play from notification's play button from outside of the app | ||||
|             if (!fragmentIsVisible) { | ||||
|                 onFragmentStopped(); | ||||
|             } | ||||
|         } else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED.equals(intent.getAction())) { | ||||
|             fragmentIsVisible = false; | ||||
|             onFragmentStopped(); | ||||
|         } else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED.equals(intent.getAction())) { | ||||
|             // Restore video source when user returns to the fragment | ||||
|             fragmentIsVisible = true; | ||||
|             player.useVideoSource(true); | ||||
|  | ||||
|             // When a user returns from background, the system UI will always be shown even if | ||||
|             // controls are invisible: hide it in that case | ||||
|             if (!isControlsVisible()) { | ||||
|                 hideSystemUIIfNeeded(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     //endregion | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Fragment binding | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Fragment binding | ||||
|     @Override | ||||
|     public void onFragmentListenerSet() { | ||||
|         super.onFragmentListenerSet(); | ||||
|         fragmentIsVisible = true; | ||||
|         // Apply window insets because Android will not do it when orientation changes | ||||
|         // from landscape to portrait | ||||
|         if (!isFullscreen) { | ||||
|             binding.playbackControlRoot.setPadding(0, 0, 0, 0); | ||||
|         } | ||||
|         binding.itemsListPanel.setPadding(0, 0, 0, 0); | ||||
|         player.getFragmentListener().ifPresent(PlayerServiceEventListener::onViewCreated); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This will be called when a user goes to another app/activity, turns off a screen. | ||||
|      * We don't want to interrupt playback and don't want to see notification so | ||||
|      * next lines of code will enable audio-only playback only if needed | ||||
|      */ | ||||
|     private void onFragmentStopped() { | ||||
|         if (player.isPlaying() || player.isLoading()) { | ||||
|             switch (getMinimizeOnExitAction(context)) { | ||||
|                 case MINIMIZE_ON_EXIT_MODE_BACKGROUND: | ||||
|                     player.useVideoSource(false); | ||||
|                     break; | ||||
|                 case MINIMIZE_ON_EXIT_MODE_POPUP: | ||||
|                     player.setRecovery(); | ||||
|                     NavigationHelper.playOnPopupPlayer(getParentActivity(), | ||||
|                             player.getPlayQueue(), true); | ||||
|                     break; | ||||
|                 case MINIMIZE_ON_EXIT_MODE_NONE: default: | ||||
|                     player.pause(); | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     //endregion | ||||
|  | ||||
|     private void showHideKodiButton() { | ||||
|         // show kodi button if it supports the current service and it is enabled in settings | ||||
|         @Nullable final PlayQueue playQueue = player.getPlayQueue(); | ||||
|         binding.playWithKodi.setVisibility(playQueue != null && playQueue.getItem() != null | ||||
|                 && KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId()) | ||||
|                 ? View.VISIBLE : View.GONE); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onUpdateProgress(final int currentProgress, | ||||
|                                  final int duration, | ||||
|                                  final int bufferPercent) { | ||||
|         super.onUpdateProgress(currentProgress, duration, bufferPercent); | ||||
|  | ||||
|         if (areSegmentsVisible) { | ||||
|             segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress)); | ||||
|         } | ||||
|         if (isQueueVisible) { | ||||
|             updateQueueTime(currentProgress); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Controls showing / hiding | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Controls showing / hiding | ||||
|  | ||||
|     protected void showOrHideButtons() { | ||||
|         super.showOrHideButtons(); | ||||
|         @Nullable final PlayQueue playQueue = player.getPlayQueue(); | ||||
|         if (playQueue == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final boolean showQueue = playQueue.getStreams().size() > 1; | ||||
|         final boolean showSegment = !player.getCurrentStreamInfo() | ||||
|                 .map(StreamInfo::getStreamSegments) | ||||
|                 .map(List::isEmpty) | ||||
|                 .orElse(/*no stream info=*/true); | ||||
|  | ||||
|         binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE); | ||||
|         binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f); | ||||
|         binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE); | ||||
|         binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void showSystemUIPartially() { | ||||
|         if (isFullscreen) { | ||||
|             final Window window = getParentActivity().getWindow(); | ||||
|             window.setStatusBarColor(Color.TRANSPARENT); | ||||
|             window.setNavigationBarColor(Color.TRANSPARENT); | ||||
|             final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | ||||
|                     | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | ||||
|                     | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; | ||||
|             window.getDecorView().setSystemUiVisibility(visibility); | ||||
|             window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void hideSystemUIIfNeeded() { | ||||
|         player.getFragmentListener().ifPresent(PlayerServiceEventListener::hideSystemUiIfNeeded); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Calculate the maximum allowed height for the {@link R.id.endScreen} | ||||
|      * to prevent it from enlarging the player. | ||||
|      * <p> | ||||
|      * The calculating follows these rules: | ||||
|      * <ul> | ||||
|      * <li> | ||||
|      *     Show at least stream title and content creator on TVs and tablets | ||||
|      *     when in landscape (always the case for TVs) and not in fullscreen mode. | ||||
|      *     This requires to have at least <code>85dp</code> free space for {@link R.id.detail_root} | ||||
|      *     and additional space for the stream title text size | ||||
|      *     ({@link R.id.detail_title_root_layout}). | ||||
|      *     The text size is <code>15sp</code> on tablets and <code>16sp</code> on TVs, | ||||
|      *     see {@link R.id.titleTextView}. | ||||
|      * </li> | ||||
|      * <li> | ||||
|      *     Otherwise, the max thumbnail height is the screen height. | ||||
|      *     TODO investigate why this is done on popup player, too | ||||
|      * </li> | ||||
|      * </ul> | ||||
|      * | ||||
|      * @param bitmap the bitmap that needs to be resized to fit the end screen | ||||
|      * @return the maximum height for the end screen thumbnail | ||||
|      */ | ||||
|     @Override | ||||
|     protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) { | ||||
|         final int screenHeight = context.getResources().getDisplayMetrics().heightPixels; | ||||
|  | ||||
|         if (DeviceUtils.isTv(context) && !isFullscreen()) { | ||||
|             final int videoInfoHeight = | ||||
|                     DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(16, context); | ||||
|             return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight); | ||||
|         } else if (DeviceUtils.isTablet(context) && isLandscape() && !isFullscreen()) { | ||||
|             final int videoInfoHeight = | ||||
|                     DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(15, context); | ||||
|             return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight); | ||||
|         } else { // fullscreen player: max height is the device height | ||||
|             return Math.min(bitmap.getHeight(), screenHeight); | ||||
|         } | ||||
|     } | ||||
|     //endregion | ||||
|  | ||||
|     @Override | ||||
|     public void onPlaying() { | ||||
|         super.onPlaying(); | ||||
|         checkLandscape(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onCompleted() { | ||||
|         super.onCompleted(); | ||||
|         if (isFullscreen) { | ||||
|             toggleFullscreen(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @Override | ||||
|     protected void setupSubtitleView(float captionScale) { | ||||
|         final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); | ||||
|         final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); | ||||
|         final float captionRatioInverse = 20f + 4f * (1.0f - captionScale); | ||||
|         binding.subtitleView.setFixedTextSize( | ||||
|                 TypedValue.COMPLEX_UNIT_PX, minimumLength / captionRatioInverse); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Gestures | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Gestures | ||||
|  | ||||
|     @SuppressWarnings("checkstyle:ParameterNumber") | ||||
|     private void onLayoutChange(final View view, final int l, final int t, final int r, final int b, | ||||
|                                 final int ol, final int ot, final int or, final int ob) { | ||||
|         if (l != ol || t != ot || r != or || b != ob) { | ||||
|             // Use smaller value to be consistent between screen orientations | ||||
|             // (and to make usage easier) | ||||
|             final int width = r - l; | ||||
|             final int height = b - t; | ||||
|             final int min = Math.min(width, height); | ||||
|             final int maxGestureLength = (int) (min * MainPlayerGestureListener.MAX_GESTURE_LENGTH); | ||||
|  | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "maxGestureLength = " + maxGestureLength); | ||||
|             } | ||||
|  | ||||
|             binding.volumeProgressBar.setMax(maxGestureLength); | ||||
|             binding.brightnessProgressBar.setMax(maxGestureLength); | ||||
|  | ||||
|             setInitialGestureValues(); | ||||
|             binding.itemsListPanel.getLayoutParams().height | ||||
|                     = height - binding.itemsListPanel.getTop(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void setInitialGestureValues() { | ||||
|         if (player.getAudioReactor() != null) { | ||||
|             final float currentVolumeNormalized = | ||||
|                     (float) player.getAudioReactor().getVolume() | ||||
|                             / player.getAudioReactor().getMaxVolume(); | ||||
|             binding.volumeProgressBar.setProgress( | ||||
|                     (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized)); | ||||
|         } | ||||
|     } | ||||
|     //endregion | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Play queue, segments and streams | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Play queue, segments and streams | ||||
|  | ||||
|     @Override | ||||
|     public void onMetadataChanged(@NonNull final StreamInfo info) { | ||||
|         super.onMetadataChanged(info); | ||||
|         showHideKodiButton(); | ||||
|         if (areSegmentsVisible) { | ||||
|             if (segmentAdapter.setItems(info)) { | ||||
|                 final int adapterPosition = getNearestStreamSegmentPosition( | ||||
|                         player.getExoPlayer().getCurrentPosition()); | ||||
|                 segmentAdapter.selectSegmentAt(adapterPosition); | ||||
|                 binding.itemsList.scrollToPosition(adapterPosition); | ||||
|             } else { | ||||
|                 closeItemsList(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPlayQueueEdited() { | ||||
|         super.onPlayQueueEdited(); | ||||
|         showOrHideButtons(); | ||||
|     } | ||||
|  | ||||
|     private void onQueueClicked() { | ||||
|         isQueueVisible = true; | ||||
|  | ||||
|         hideSystemUIIfNeeded(); | ||||
|         buildQueue(); | ||||
|  | ||||
|         binding.itemsListHeaderTitle.setVisibility(View.GONE); | ||||
|         binding.itemsListHeaderDuration.setVisibility(View.VISIBLE); | ||||
|         binding.shuffleButton.setVisibility(View.VISIBLE); | ||||
|         binding.repeatButton.setVisibility(View.VISIBLE); | ||||
|         binding.addToPlaylistButton.setVisibility(View.VISIBLE); | ||||
|  | ||||
|         hideControls(0, 0); | ||||
|         binding.itemsListPanel.requestFocus(); | ||||
|         animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION, | ||||
|                 AnimationType.SLIDE_AND_ALPHA); | ||||
|  | ||||
|         @Nullable final PlayQueue playQueue = player.getPlayQueue(); | ||||
|         if (playQueue != null) { | ||||
|             binding.itemsList.scrollToPosition(playQueue.getIndex()); | ||||
|         } | ||||
|  | ||||
|         updateQueueTime((int) player.getExoPlayer().getCurrentPosition()); | ||||
|     } | ||||
|  | ||||
|     private void buildQueue() { | ||||
|         binding.itemsList.setAdapter(playQueueAdapter); | ||||
|         binding.itemsList.setClickable(true); | ||||
|         binding.itemsList.setLongClickable(true); | ||||
|  | ||||
|         binding.itemsList.clearOnScrollListeners(); | ||||
|         binding.itemsList.addOnScrollListener(getQueueScrollListener()); | ||||
|  | ||||
|         itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); | ||||
|         itemTouchHelper.attachToRecyclerView(binding.itemsList); | ||||
|  | ||||
|         playQueueAdapter.setSelectedListener(getOnSelectedListener()); | ||||
|  | ||||
|         binding.itemsListClose.setOnClickListener(view -> closeItemsList()); | ||||
|     } | ||||
|  | ||||
|     private void onSegmentsClicked() { | ||||
|         areSegmentsVisible = true; | ||||
|  | ||||
|         hideSystemUIIfNeeded(); | ||||
|         buildSegments(); | ||||
|  | ||||
|         binding.itemsListHeaderTitle.setVisibility(View.VISIBLE); | ||||
|         binding.itemsListHeaderDuration.setVisibility(View.GONE); | ||||
|         binding.shuffleButton.setVisibility(View.GONE); | ||||
|         binding.repeatButton.setVisibility(View.GONE); | ||||
|         binding.addToPlaylistButton.setVisibility(View.GONE); | ||||
|  | ||||
|         hideControls(0, 0); | ||||
|         binding.itemsListPanel.requestFocus(); | ||||
|         animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION, | ||||
|                 AnimationType.SLIDE_AND_ALPHA); | ||||
|  | ||||
|         final int adapterPosition = getNearestStreamSegmentPosition( | ||||
|                 player.getExoPlayer().getCurrentPosition()); | ||||
|         segmentAdapter.selectSegmentAt(adapterPosition); | ||||
|         binding.itemsList.scrollToPosition(adapterPosition); | ||||
|     } | ||||
|  | ||||
|     private void buildSegments() { | ||||
|         binding.itemsList.setAdapter(segmentAdapter); | ||||
|         binding.itemsList.setClickable(true); | ||||
|         binding.itemsList.setLongClickable(false); | ||||
|  | ||||
|         binding.itemsList.clearOnScrollListeners(); | ||||
|         if (itemTouchHelper != null) { | ||||
|             itemTouchHelper.attachToRecyclerView(null); | ||||
|         } | ||||
|  | ||||
|         player.getCurrentStreamInfo().ifPresent(segmentAdapter::setItems); | ||||
|  | ||||
|         binding.shuffleButton.setVisibility(View.GONE); | ||||
|         binding.repeatButton.setVisibility(View.GONE); | ||||
|         binding.addToPlaylistButton.setVisibility(View.GONE); | ||||
|         binding.itemsListClose.setOnClickListener(view -> closeItemsList()); | ||||
|     } | ||||
|  | ||||
|     public void closeItemsList() { | ||||
|         if (isQueueVisible || areSegmentsVisible) { | ||||
|             isQueueVisible = false; | ||||
|             areSegmentsVisible = false; | ||||
|  | ||||
|             if (itemTouchHelper != null) { | ||||
|                 itemTouchHelper.attachToRecyclerView(null); | ||||
|             } | ||||
|  | ||||
|             animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION, | ||||
|                     AnimationType.SLIDE_AND_ALPHA, 0, () -> { | ||||
|                         // Even when queueLayout is GONE it receives touch events | ||||
|                         // and ruins normal behavior of the app. This line fixes it | ||||
|                         binding.itemsListPanel.setTranslationY( | ||||
|                                 -binding.itemsListPanel.getHeight() * 5); | ||||
|                     }); | ||||
|  | ||||
|             // clear focus, otherwise a white rectangle remains on top of the player | ||||
|             binding.itemsListClose.clearFocus(); | ||||
|             binding.playPauseButton.requestFocus(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private OnScrollBelowItemsListener getQueueScrollListener() { | ||||
|         return new OnScrollBelowItemsListener() { | ||||
|             @Override | ||||
|             public void onScrolledDown(final RecyclerView recyclerView) { | ||||
|                 @Nullable final PlayQueue playQueue = player.getPlayQueue(); | ||||
|                 if (playQueue != null && !playQueue.isComplete()) { | ||||
|                     playQueue.fetch(); | ||||
|                 } else if (binding != null) { | ||||
|                     binding.itemsList.clearOnScrollListeners(); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() { | ||||
|         return (item, seconds) -> { | ||||
|             segmentAdapter.selectSegment(item); | ||||
|             player.seekTo(seconds * 1000L); | ||||
|             player.triggerProgressUpdate(); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private int getNearestStreamSegmentPosition(final long playbackPosition) { | ||||
|         //noinspection SimplifyOptionalCallChains | ||||
|         if (!player.getCurrentStreamInfo().isPresent()) { | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|         int nearestPosition = 0; | ||||
|         final List<StreamSegment> segments | ||||
|                 = player.getCurrentStreamInfo().get().getStreamSegments(); | ||||
|  | ||||
|         for (int i = 0; i < segments.size(); i++) { | ||||
|             if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) { | ||||
|                 break; | ||||
|             } | ||||
|             nearestPosition++; | ||||
|         } | ||||
|         return Math.max(0, nearestPosition - 1); | ||||
|     } | ||||
|  | ||||
|     private ItemTouchHelper.SimpleCallback getItemTouchCallback() { | ||||
|         return new PlayQueueItemTouchCallback() { | ||||
|             @Override | ||||
|             public void onMove(final int sourceIndex, final int targetIndex) { | ||||
|                 @Nullable final PlayQueue playQueue = player.getPlayQueue(); | ||||
|                 if (playQueue != null) { | ||||
|                     playQueue.move(sourceIndex, targetIndex); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onSwiped(final int index) { | ||||
|                 @Nullable final PlayQueue playQueue = player.getPlayQueue(); | ||||
|                 if (playQueue != null && index != -1) { | ||||
|                     playQueue.remove(index); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { | ||||
|         return new PlayQueueItemBuilder.OnSelectedListener() { | ||||
|             @Override | ||||
|             public void selected(final PlayQueueItem item, final View view) { | ||||
|                 player.selectQueueItem(item); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void held(final PlayQueueItem item, final View view) { | ||||
|                 @Nullable final PlayQueue playQueue = player.getPlayQueue(); | ||||
|                 @Nullable final AppCompatActivity parentActivity = getParentActivity(); | ||||
|                 if (playQueue != null && parentActivity != null && playQueue.indexOf(item) != -1) { | ||||
|                     openPopupMenu(player.getPlayQueue(), item, view, true, | ||||
|                             parentActivity.getSupportFragmentManager(), context); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onStartDrag(final PlayQueueItemHolder viewHolder) { | ||||
|                 if (itemTouchHelper != null) { | ||||
|                     itemTouchHelper.startDrag(viewHolder); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private void updateQueueTime(final int currentTime) { | ||||
|         @Nullable final PlayQueue playQueue = player.getPlayQueue(); | ||||
|         if (playQueue == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final int currentStream = playQueue.getIndex(); | ||||
|         int before = 0; | ||||
|         int after = 0; | ||||
|  | ||||
|         final List<PlayQueueItem> streams = playQueue.getStreams(); | ||||
|         final int nStreams = streams.size(); | ||||
|  | ||||
|         for (int i = 0; i < nStreams; i++) { | ||||
|             if (i < currentStream) { | ||||
|                 before += streams.get(i).getDuration(); | ||||
|             } else { | ||||
|                 after += streams.get(i).getDuration(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         before *= 1000; | ||||
|         after *= 1000; | ||||
|  | ||||
|         binding.itemsListHeaderDuration.setText( | ||||
|                 String.format("%s/%s", | ||||
|                         getTimeString(currentTime + before), | ||||
|                         getTimeString(before + after) | ||||
|                 )); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected boolean isAnyListViewOpen() { | ||||
|         return isQueueVisible || areSegmentsVisible; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isFullscreen() { | ||||
|         return isFullscreen; | ||||
|     } | ||||
|  | ||||
|     public boolean isVerticalVideo() { | ||||
|         return isVerticalVideo; | ||||
|     } | ||||
|  | ||||
|     //endregion | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Click listeners | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Click listeners | ||||
|     @Override | ||||
|     public void onClick(final View v) { | ||||
|         if (v.getId() == binding.screenRotationButton.getId()) { | ||||
|             // Only if it's not a vertical video or vertical video but in landscape with locked | ||||
|             // orientation a screen orientation can be changed automatically | ||||
|             if (!isVerticalVideo || (isLandscape() && globalScreenOrientationLocked(context))) { | ||||
|                 player.getFragmentListener().ifPresent( | ||||
|                         PlayerServiceEventListener::onScreenRotationButtonClicked); | ||||
|             } else { | ||||
|                 toggleFullscreen(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // call it later since it calls manageControlsAfterOnClick at the end | ||||
|         super.onClick(v); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onPlaybackSpeedClicked() { | ||||
|         PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), | ||||
|                 player.getPlaybackSkipSilence(), (speed, pitch, skipSilence) | ||||
|                         -> player.setPlaybackParameters(speed, pitch, skipSilence)) | ||||
|                 .show(getParentActivity().getSupportFragmentManager(), null); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onLongClick(final View v) { | ||||
|         if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) { | ||||
|             player.getFragmentListener().ifPresent( | ||||
|                     PlayerServiceEventListener::onMoreOptionsLongClicked); | ||||
|             hideControls(0, 0); | ||||
|             hideSystemUIIfNeeded(); | ||||
|             return true; | ||||
|         } | ||||
|         return super.onLongClick(v); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onKeyDown(final int keyCode) { | ||||
|         if (keyCode == KeyEvent.KEYCODE_SPACE && isFullscreen) { | ||||
|             player.playPause(); | ||||
|             if (player.isPlaying()) { | ||||
|                 hideControls(0, 0); | ||||
|             } | ||||
|             return true; | ||||
|         } | ||||
|         return super.onKeyDown(keyCode); | ||||
|     } | ||||
|     //endregion | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Video size, resize, orientation, fullscreen | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Video size, resize, orientation, fullscreen | ||||
|  | ||||
|     private void setupScreenRotationButton() { | ||||
|         binding.screenRotationButton.setVisibility(globalScreenOrientationLocked(context) | ||||
|                 || isVerticalVideo || DeviceUtils.isTablet(context) | ||||
|                 ? View.VISIBLE : View.GONE); | ||||
|         binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context, | ||||
|                 isFullscreen ? R.drawable.ic_fullscreen_exit | ||||
|                         : R.drawable.ic_fullscreen)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { | ||||
|         super.onVideoSizeChanged(videoSize); | ||||
|         isVerticalVideo = videoSize.width < videoSize.height; | ||||
|  | ||||
|         if (globalScreenOrientationLocked(context) | ||||
|                 && isFullscreen | ||||
|                 && isLandscape() == isVerticalVideo | ||||
|                 && !DeviceUtils.isTv(context) | ||||
|                 && !DeviceUtils.isTablet(context)) { | ||||
|             // set correct orientation | ||||
|             player.getFragmentListener().ifPresent( | ||||
|                     PlayerServiceEventListener::onScreenRotationButtonClicked); | ||||
|         } | ||||
|  | ||||
|         setupScreenRotationButton(); | ||||
|     } | ||||
|  | ||||
|     public void toggleFullscreen() { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "toggleFullscreen() called"); | ||||
|         } | ||||
|         final PlayerServiceEventListener fragmentListener | ||||
|                 = player.getFragmentListener().orElse(null); | ||||
|         if (fragmentListener == null || player.exoPlayerIsNull()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         isFullscreen = !isFullscreen; | ||||
|         if (!isFullscreen) { | ||||
|             // Apply window insets because Android will not do it when orientation changes | ||||
|             // from landscape to portrait (open vertical video to reproduce) | ||||
|             binding.playbackControlRoot.setPadding(0, 0, 0, 0); | ||||
|         } else { | ||||
|             // Android needs tens milliseconds to send new insets but a user is able to see | ||||
|             // how controls changes it's position from `0` to `nav bar height` padding. | ||||
|             // So just hide the controls to hide this visual inconsistency | ||||
|             hideControls(0, 0); | ||||
|         } | ||||
|         fragmentListener.onFullscreenStateChanged(isFullscreen); | ||||
|  | ||||
|         if (isFullscreen) { | ||||
|             binding.titleTextView.setVisibility(View.VISIBLE); | ||||
|             binding.channelTextView.setVisibility(View.VISIBLE); | ||||
|             binding.playerCloseButton.setVisibility(View.GONE); | ||||
|         } else { | ||||
|             binding.titleTextView.setVisibility(View.GONE); | ||||
|             binding.channelTextView.setVisibility(View.GONE); | ||||
|             binding.playerCloseButton.setVisibility(View.VISIBLE); | ||||
|         } | ||||
|         setupScreenRotationButton(); | ||||
|     } | ||||
|  | ||||
|     public void checkLandscape() { | ||||
|         // check if landscape is correct | ||||
|         final boolean videoInLandscapeButNotInFullscreen | ||||
|                 = isLandscape() && !isFullscreen && !player.isAudioOnly(); | ||||
|         final boolean notPaused = player.getCurrentState() != STATE_COMPLETED | ||||
|                 && player.getCurrentState() != STATE_PAUSED; | ||||
|  | ||||
|         if (videoInLandscapeButNotInFullscreen | ||||
|                 && notPaused | ||||
|                 && !DeviceUtils.isTablet(context)) { | ||||
|             toggleFullscreen(); | ||||
|         } | ||||
|     } | ||||
|     //endregion | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Getters | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Getters | ||||
|     public PlayerBinding getBinding() { | ||||
|         return binding; | ||||
|     } | ||||
|  | ||||
|     public AppCompatActivity getParentActivity() { | ||||
|         return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext(); | ||||
|     } | ||||
|  | ||||
|     public boolean isLandscape() { | ||||
|         // DisplayMetrics from activity context knows about MultiWindow feature | ||||
|         // while DisplayMetrics from app context doesn't | ||||
|         return DeviceUtils.isLandscape(getParentActivity()); | ||||
|     } | ||||
|     //endregion | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| package org.schabi.newpipe.player.ui; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
|  | ||||
| import org.schabi.newpipe.player.NotificationUtil; | ||||
| import org.schabi.newpipe.player.Player; | ||||
|  | ||||
| public final class NotificationPlayerUi extends PlayerUi { | ||||
|     boolean foregroundNotificationAlreadyCreated = false; | ||||
|  | ||||
|     public NotificationPlayerUi(@NonNull final Player player) { | ||||
|         super(player); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void initPlayer() { | ||||
|         super.initPlayer(); | ||||
|         if (!foregroundNotificationAlreadyCreated) { | ||||
|             NotificationUtil.getInstance() | ||||
|                     .createNotificationAndStartForeground(player, player.getService()); | ||||
|             foregroundNotificationAlreadyCreated = true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // TODO TODO on destroy remove foreground | ||||
| } | ||||
							
								
								
									
										120
									
								
								app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| package org.schabi.newpipe.player.ui; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.graphics.Bitmap; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
|  | ||||
| import com.google.android.exoplayer2.PlaybackParameters; | ||||
| import com.google.android.exoplayer2.Player.RepeatMode; | ||||
| import com.google.android.exoplayer2.Tracks; | ||||
| import com.google.android.exoplayer2.text.Cue; | ||||
| import com.google.android.exoplayer2.video.VideoSize; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.player.Player; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| public abstract class PlayerUi { | ||||
|     private static final String TAG = PlayerUi.class.getSimpleName(); | ||||
|  | ||||
|     @NonNull protected Context context; | ||||
|     @NonNull protected Player player; | ||||
|  | ||||
|     public PlayerUi(@NonNull final Player player) { | ||||
|         this.context = player.getContext(); | ||||
|         this.player = player; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public Player getPlayer() { | ||||
|         return player; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public void setupAfterIntent() { | ||||
|     } | ||||
|  | ||||
|     public void initPlayer() { | ||||
|     } | ||||
|  | ||||
|     public void initPlayback() { | ||||
|     } | ||||
|  | ||||
|     public void destroyPlayer() { | ||||
|     } | ||||
|  | ||||
|     public void destroy() { | ||||
|     } | ||||
|  | ||||
|     public void smoothStopForImmediateReusing() { | ||||
|     } | ||||
|  | ||||
|     public void onFragmentListenerSet() { | ||||
|     } | ||||
|  | ||||
|     public void onBroadcastReceived(final Intent intent) { | ||||
|     } | ||||
|  | ||||
|     public void onUpdateProgress(final int currentProgress, | ||||
|                                  final int duration, | ||||
|                                  final int bufferPercent) { | ||||
|     } | ||||
|  | ||||
|     public void onPrepared() { | ||||
|     } | ||||
|  | ||||
|     public void onBlocked() { | ||||
|     } | ||||
|  | ||||
|     public void onPlaying() { | ||||
|     } | ||||
|  | ||||
|     public void onBuffering() { | ||||
|     } | ||||
|  | ||||
|     public void onPaused() { | ||||
|     } | ||||
|  | ||||
|     public void onPausedSeek() { | ||||
|     } | ||||
|  | ||||
|     public void onCompleted() { | ||||
|     } | ||||
|  | ||||
|     public void onRepeatModeChanged(@RepeatMode final int repeatMode) { | ||||
|     } | ||||
|  | ||||
|     public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { | ||||
|     } | ||||
|  | ||||
|     public void onMuteUnmuteChanged(final boolean isMuted) { | ||||
|     } | ||||
|  | ||||
|     public void onTextTracksChanged(@NonNull final Tracks currentTracks) { | ||||
|     } | ||||
|  | ||||
|     public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) { | ||||
|     } | ||||
|  | ||||
|     public void onRenderedFirstFrame() { | ||||
|     } | ||||
|  | ||||
|     public void onCues(@NonNull final List<Cue> cues) { | ||||
|     } | ||||
|  | ||||
|     public void onMetadataChanged(@NonNull final StreamInfo info) { | ||||
|     } | ||||
|  | ||||
|     public void onThumbnailLoaded(@Nullable final Bitmap bitmap) { | ||||
|     } | ||||
|  | ||||
|     public void onPlayQueueEdited() { | ||||
|     } | ||||
|  | ||||
|     public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,36 @@ | ||||
| package org.schabi.newpipe.player.ui; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
| import java.util.function.Consumer; | ||||
|  | ||||
| public final class PlayerUiList { | ||||
|     final List<PlayerUi> playerUis = new ArrayList<>(); | ||||
|  | ||||
|     public void add(final PlayerUi playerUi) { | ||||
|         playerUis.add(playerUi); | ||||
|     } | ||||
|  | ||||
|     public <T> void destroyAll(final Class<T> playerUiType) { | ||||
|         playerUis.stream() | ||||
|                 .filter(playerUiType::isInstance) | ||||
|                 .forEach(playerUi -> { | ||||
|                     playerUi.destroyPlayer(); | ||||
|                     playerUi.destroy(); | ||||
|                 }); | ||||
|         playerUis.removeIf(playerUiType::isInstance); | ||||
|     } | ||||
|  | ||||
|     public <T> Optional<T> get(final Class<T> playerUiType) { | ||||
|         return playerUis.stream() | ||||
|                 .filter(playerUiType::isInstance) | ||||
|                 .map(playerUiType::cast) | ||||
|                 .findFirst(); | ||||
|     } | ||||
|  | ||||
|     public void call(final Consumer<PlayerUi> consumer) { | ||||
|         //noinspection SimplifyStreamApiCallChains | ||||
|         playerUis.stream().forEach(consumer); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,460 @@ | ||||
| package org.schabi.newpipe.player.ui; | ||||
|  | ||||
| import static org.schabi.newpipe.MainActivity.DEBUG; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs; | ||||
|  | ||||
| import android.animation.Animator; | ||||
| import android.animation.AnimatorListenerAdapter; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.res.Resources; | ||||
| import android.graphics.Bitmap; | ||||
| import android.util.DisplayMetrics; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| import android.view.WindowManager; | ||||
| import android.view.animation.AnticipateInterpolator; | ||||
| import android.widget.LinearLayout; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.core.content.ContextCompat; | ||||
|  | ||||
| import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | ||||
| import com.google.android.exoplayer2.ui.SubtitleView; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.databinding.PlayerBinding; | ||||
| import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding; | ||||
| import org.schabi.newpipe.player.Player; | ||||
| import org.schabi.newpipe.player.gesture.BasePlayerGestureListener; | ||||
| import org.schabi.newpipe.player.gesture.PopupPlayerGestureListener; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
|  | ||||
| public final class PopupPlayerUi extends VideoPlayerUi { | ||||
|     private static final String TAG = PopupPlayerUi.class.getSimpleName(); | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Popup player | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private PlayerPopupCloseOverlayBinding closeOverlayBinding; | ||||
|  | ||||
|     private boolean isPopupClosing = false; | ||||
|  | ||||
|     private int screenWidth; | ||||
|     private int screenHeight; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Popup player window manager | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     public static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | ||||
|             | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; | ||||
|     public static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS | ||||
|             | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; | ||||
|  | ||||
|     private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup | ||||
|     private final WindowManager windowManager; | ||||
|  | ||||
|     public PopupPlayerUi(@NonNull final Player player, | ||||
|                          @NonNull final PlayerBinding playerBinding) { | ||||
|         super(player, playerBinding); | ||||
|         windowManager = ContextCompat.getSystemService(context, WindowManager.class); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void setupAfterIntent() { | ||||
|         setupElementsVisibility(); | ||||
|         binding.getRoot().setVisibility(View.VISIBLE); | ||||
|         initPopup(); | ||||
|         initPopupCloseOverlay(); | ||||
|         binding.playPauseButton.requestFocus(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     BasePlayerGestureListener buildGestureListener() { | ||||
|         return new PopupPlayerGestureListener(this); | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("RtlHardcoded") | ||||
|     private void initPopup() { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "initPopup() called"); | ||||
|         } | ||||
|  | ||||
|         // Popup is already added to windowManager | ||||
|         if (popupHasParent()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         updateScreenSize(); | ||||
|  | ||||
|         popupLayoutParams = retrievePopupLayoutParamsFromPrefs(this); | ||||
|         binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height); | ||||
|  | ||||
|         checkPopupPositionBounds(); | ||||
|  | ||||
|         binding.loadingPanel.setMinimumWidth(popupLayoutParams.width); | ||||
|         binding.loadingPanel.setMinimumHeight(popupLayoutParams.height); | ||||
|  | ||||
|         windowManager.addView(binding.getRoot(), popupLayoutParams); | ||||
|  | ||||
|         // Popup doesn't have aspectRatio selector, using FIT automatically | ||||
|         setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("RtlHardcoded") | ||||
|     private void initPopupCloseOverlay() { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "initPopupCloseOverlay() called"); | ||||
|         } | ||||
|  | ||||
|         // closeOverlayView is already added to windowManager | ||||
|         if (closeOverlayBinding != null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(context)); | ||||
|  | ||||
|         final WindowManager.LayoutParams closeOverlayLayoutParams = buildCloseOverlayLayoutParams(); | ||||
|         closeOverlayBinding.closeButton.setVisibility(View.GONE); | ||||
|         windowManager.addView(closeOverlayBinding.getRoot(), closeOverlayLayoutParams); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void setupElementsVisibility() { | ||||
|         binding.fullScreenButton.setVisibility(View.VISIBLE); | ||||
|         binding.screenRotationButton.setVisibility(View.GONE); | ||||
|         binding.resizeTextView.setVisibility(View.GONE); | ||||
|         binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE); | ||||
|         binding.queueButton.setVisibility(View.GONE); | ||||
|         binding.segmentsButton.setVisibility(View.GONE); | ||||
|         binding.moreOptionsButton.setVisibility(View.GONE); | ||||
|         binding.topControls.setOrientation(LinearLayout.HORIZONTAL); | ||||
|         binding.primaryControls.getLayoutParams().width | ||||
|                 = LinearLayout.LayoutParams.WRAP_CONTENT; | ||||
|         binding.secondaryControls.setAlpha(1.0f); | ||||
|         binding.secondaryControls.setVisibility(View.VISIBLE); | ||||
|         binding.secondaryControls.setTranslationY(0); | ||||
|         binding.share.setVisibility(View.GONE); | ||||
|         binding.playWithKodi.setVisibility(View.GONE); | ||||
|         binding.openInBrowser.setVisibility(View.GONE); | ||||
|         binding.switchMute.setVisibility(View.GONE); | ||||
|         binding.playerCloseButton.setVisibility(View.GONE); | ||||
|         binding.topControls.bringToFront(); | ||||
|         binding.topControls.setClickable(false); | ||||
|         binding.topControls.setFocusable(false); | ||||
|         binding.bottomControls.bringToFront(); | ||||
|         super.setupElementsVisibility(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void setupElementsSize(final Resources resources) { | ||||
|         setupElementsSize( | ||||
|                 0, | ||||
|                 0, | ||||
|                 resources.getDimensionPixelSize(R.dimen.player_popup_controls_padding), | ||||
|                 resources.getDimensionPixelSize(R.dimen.player_popup_buttons_padding) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void removeViewFromParent() { | ||||
|         // view was added by windowManager for popup player | ||||
|         windowManager.removeViewImmediate(binding.getRoot()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void destroy() { | ||||
|         super.destroy(); | ||||
|         removePopupFromView(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Broadcast receiver | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Broadcast receiver | ||||
|     @Override | ||||
|     public void onBroadcastReceived(final Intent intent) { | ||||
|         super.onBroadcastReceived(intent); | ||||
|         if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) { | ||||
|             updateScreenSize(); | ||||
|             changePopupSize(popupLayoutParams.width); | ||||
|             checkPopupPositionBounds(); | ||||
|         } else if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { | ||||
|             // Use only audio source when screen turns off while popup player is playing | ||||
|             if (player.isPlaying() || player.isLoading()) { | ||||
|                 player.useVideoSource(false); | ||||
|             } | ||||
|         } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) { | ||||
|             // Restore video source when screen turns on and user is watching video in popup player | ||||
|             if (player.isPlaying() || player.isLoading()) { | ||||
|                 player.useVideoSource(true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     //endregion | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary | ||||
|      * that goes from (0, 0) to (screenWidth, screenHeight). | ||||
|      * <p> | ||||
|      * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed | ||||
|      * and {@code true} is returned to represent this change. | ||||
|      * </p> | ||||
|      */ | ||||
|     public void checkPopupPositionBounds() { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "checkPopupPositionBounds() called with: " | ||||
|                     + "screenWidth = [" + screenWidth + "], " | ||||
|                     + "screenHeight = [" + screenHeight + "]"); | ||||
|         } | ||||
|         if (popupLayoutParams == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (popupLayoutParams.x < 0) { | ||||
|             popupLayoutParams.x = 0; | ||||
|         } else if (popupLayoutParams.x > screenWidth - popupLayoutParams.width) { | ||||
|             popupLayoutParams.x = screenWidth - popupLayoutParams.width; | ||||
|         } | ||||
|  | ||||
|         if (popupLayoutParams.y < 0) { | ||||
|             popupLayoutParams.y = 0; | ||||
|         } else if (popupLayoutParams.y > screenHeight - popupLayoutParams.height) { | ||||
|             popupLayoutParams.y = screenHeight - popupLayoutParams.height; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void updateScreenSize() { | ||||
|         final 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 + "]"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Changes the size of the popup based on the width. | ||||
|      * @param width the new width, height is calculated with | ||||
|      *              {@link PlayerHelper#getMinimumVideoHeight(float)} | ||||
|      */ | ||||
|     public void changePopupSize(final int width) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "changePopupSize() called with: width = [" + width + "]"); | ||||
|         } | ||||
|  | ||||
|         if (anyPopupViewIsNull()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width); | ||||
|         final int actualWidth = (int) (width > screenWidth ? screenWidth | ||||
|                 : (width < minimumWidth ? minimumWidth : width)); | ||||
|         final int actualHeight = (int) getMinimumVideoHeight(width); | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "updatePopupSize() updated values:" | ||||
|                     + "  width = [" + actualWidth + "], height = [" + actualHeight + "]"); | ||||
|         } | ||||
|  | ||||
|         popupLayoutParams.width = actualWidth; | ||||
|         popupLayoutParams.height = actualHeight; | ||||
|         binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height); | ||||
|         windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams); | ||||
|     } | ||||
|  | ||||
|     private void changePopupWindowFlags(final int flags) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]"); | ||||
|         } | ||||
|  | ||||
|         if (!anyPopupViewIsNull()) { | ||||
|             popupLayoutParams.flags = flags; | ||||
|             windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void closePopup() { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); | ||||
|         } | ||||
|         if (isPopupClosing) { | ||||
|             return; | ||||
|         } | ||||
|         isPopupClosing = true; | ||||
|  | ||||
|         player.saveStreamProgressState(); | ||||
|         windowManager.removeView(binding.getRoot()); | ||||
|  | ||||
|         animatePopupOverlayAndFinishService(); | ||||
|     } | ||||
|  | ||||
|     public boolean isPopupClosing() { | ||||
|         return isPopupClosing; | ||||
|     } | ||||
|  | ||||
|     public void removePopupFromView() { | ||||
|         if (windowManager != null) { | ||||
|             // wrap in try-catch since it could sometimes generate errors randomly | ||||
|             try { | ||||
|                 if (popupHasParent()) { | ||||
|                     windowManager.removeView(binding.getRoot()); | ||||
|                 } | ||||
|             } catch (final IllegalArgumentException e) { | ||||
|                 Log.w(TAG, "Failed to remove popup from window manager", e); | ||||
|             } | ||||
|  | ||||
|             try { | ||||
|                 final boolean closeOverlayHasParent = closeOverlayBinding != null | ||||
|                         && closeOverlayBinding.getRoot().getParent() != null; | ||||
|                 if (closeOverlayHasParent) { | ||||
|                     windowManager.removeView(closeOverlayBinding.getRoot()); | ||||
|                 } | ||||
|             } catch (final IllegalArgumentException e) { | ||||
|                 Log.w(TAG, "Failed to remove popup overlay from window manager", e); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void animatePopupOverlayAndFinishService() { | ||||
|         final int targetTranslationY = | ||||
|                 (int) (closeOverlayBinding.closeButton.getRootView().getHeight() | ||||
|                         - closeOverlayBinding.closeButton.getY()); | ||||
|  | ||||
|         closeOverlayBinding.closeButton.animate().setListener(null).cancel(); | ||||
|         closeOverlayBinding.closeButton.animate() | ||||
|                 .setInterpolator(new AnticipateInterpolator()) | ||||
|                 .translationY(targetTranslationY) | ||||
|                 .setDuration(400) | ||||
|                 .setListener(new AnimatorListenerAdapter() { | ||||
|                     @Override | ||||
|                     public void onAnimationCancel(final Animator animation) { | ||||
|                         end(); | ||||
|                     } | ||||
|  | ||||
|                     @Override | ||||
|                     public void onAnimationEnd(final Animator animation) { | ||||
|                         end(); | ||||
|                     } | ||||
|  | ||||
|                     private void end() { | ||||
|                         windowManager.removeView(closeOverlayBinding.getRoot()); | ||||
|                         closeOverlayBinding = null; | ||||
|                         player.getService().stopService(); | ||||
|                     } | ||||
|                 }).start(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) { | ||||
|         // no need for the end screen thumbnail to be resized on popup player: it's only needed | ||||
|         // for the main player so that it is enlarged correctly inside the fragment | ||||
|         return bitmap.getHeight(); | ||||
|     } | ||||
|  | ||||
|     private boolean popupHasParent() { | ||||
|         return binding != null | ||||
|                 && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams | ||||
|                 && binding.getRoot().getParent() != null; | ||||
|     } | ||||
|  | ||||
|     private boolean anyPopupViewIsNull() { | ||||
|         return popupLayoutParams == null || windowManager == null | ||||
|                 || binding.getRoot().getParent() == null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPlaying() { | ||||
|         super.onPlaying(); | ||||
|         changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPaused() { | ||||
|         super.onPaused(); | ||||
|         changePopupWindowFlags(IDLE_WINDOW_FLAGS); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onCompleted() { | ||||
|         super.onCompleted(); | ||||
|         changePopupWindowFlags(IDLE_WINDOW_FLAGS); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void setupSubtitleView(final float captionScale) { | ||||
|         final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f; | ||||
|         binding.subtitleView.setFractionalTextSize( | ||||
|                 SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onPlaybackSpeedClicked() { | ||||
|         playbackSpeedPopupMenu.show(); | ||||
|         isSomePopupMenuVisible = true; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Gestures | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Gestures | ||||
|     private int distanceFromCloseButton(@NonNull final MotionEvent popupMotionEvent) { | ||||
|         final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft() | ||||
|                 + closeOverlayBinding.closeButton.getWidth() / 2; | ||||
|         final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop() | ||||
|                 + closeOverlayBinding.closeButton.getHeight() / 2; | ||||
|  | ||||
|         final float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); | ||||
|         final 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 = closeOverlayBinding.closeButton.getWidth() / 2; | ||||
|         // 20% wider than the button itself | ||||
|         return buttonRadius * 1.2f; | ||||
|     } | ||||
|  | ||||
|     public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent) { | ||||
|         return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); | ||||
|     } | ||||
|     //endregion | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Getters | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Gestures | ||||
|     public PlayerPopupCloseOverlayBinding getCloseOverlayBinding() { | ||||
|         return closeOverlayBinding; | ||||
|     } | ||||
|  | ||||
|     public WindowManager.LayoutParams getPopupLayoutParams() { | ||||
|         return popupLayoutParams; | ||||
|     } | ||||
|  | ||||
|     public WindowManager getWindowManager() { | ||||
|         return windowManager; | ||||
|     } | ||||
|  | ||||
|     public int getScreenHeight() { | ||||
|         return screenHeight; | ||||
|     } | ||||
|  | ||||
|     public int getScreenWidth() { | ||||
|         return screenWidth; | ||||
|     } | ||||
|     //endregion | ||||
| } | ||||
							
								
								
									
										1523
									
								
								app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1523
									
								
								app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -26,7 +26,7 @@ import androidx.preference.PreferenceViewHolder; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.databinding.ListRadioIconItemBinding; | ||||
| import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; | ||||
| import org.schabi.newpipe.player.MainPlayer; | ||||
| import org.schabi.newpipe.player.PlayerService; | ||||
| import org.schabi.newpipe.player.NotificationConstants; | ||||
| import org.schabi.newpipe.util.DeviceUtils; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
| @@ -61,7 +61,7 @@ public class NotificationActionsPreference extends Preference { | ||||
|     public void onDetached() { | ||||
|         super.onDetached(); | ||||
|         saveChanges(); | ||||
|         getContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION)); | ||||
|         getContext().sendBroadcast(new Intent(PlayerService.ACTION_RECREATE_NOTIFICATION)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -50,8 +50,8 @@ 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.MainPlayer; | ||||
| import org.schabi.newpipe.player.MainPlayer.PlayerType; | ||||
| import org.schabi.newpipe.player.PlayerService; | ||||
| import org.schabi.newpipe.player.PlayerService.PlayerType; | ||||
| import org.schabi.newpipe.player.PlayQueueActivity; | ||||
| import org.schabi.newpipe.player.Player; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| @@ -91,7 +91,7 @@ public final class NavigationHelper { | ||||
|                 intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey); | ||||
|             } | ||||
|         } | ||||
|         intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal()); | ||||
|         intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.MAIN.ordinal()); | ||||
|         intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback); | ||||
|  | ||||
|         return intent; | ||||
| @@ -163,8 +163,8 @@ public final class NavigationHelper { | ||||
|  | ||||
|         Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); | ||||
|  | ||||
|         final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); | ||||
|         intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal()); | ||||
|         final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback); | ||||
|         intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.POPUP.ordinal()); | ||||
|         ContextCompat.startForegroundService(context, intent); | ||||
|     } | ||||
|  | ||||
| @@ -174,8 +174,8 @@ public final class NavigationHelper { | ||||
|         Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) | ||||
|                 .show(); | ||||
|  | ||||
|         final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); | ||||
|         intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal()); | ||||
|         final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback); | ||||
|         intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.AUDIO.ordinal()); | ||||
|         ContextCompat.startForegroundService(context, intent); | ||||
|     } | ||||
|  | ||||
| @@ -184,7 +184,7 @@ public final class NavigationHelper { | ||||
|                                        final PlayQueue queue, | ||||
|                                        final PlayerType playerType) { | ||||
|         Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); | ||||
|         final Intent intent = getPlayerEnqueueIntent(context, MainPlayer.class, queue); | ||||
|         final Intent intent = getPlayerEnqueueIntent(context, PlayerService.class, queue); | ||||
|  | ||||
|         intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal()); | ||||
|         ContextCompat.startForegroundService(context, intent); | ||||
| @@ -194,7 +194,7 @@ public final class NavigationHelper { | ||||
|         PlayerType playerType = PlayerHolder.getInstance().getType(); | ||||
|         if (!PlayerHolder.getInstance().isPlayerOpen()) { | ||||
|             Log.e(TAG, "Enqueueing but no player is open; defaulting to background player"); | ||||
|             playerType = MainPlayer.PlayerType.AUDIO; | ||||
|             playerType = PlayerService.PlayerType.AUDIO; | ||||
|         } | ||||
|  | ||||
|         enqueueOnPlayer(context, queue, playerType); | ||||
| @@ -205,10 +205,10 @@ public final class NavigationHelper { | ||||
|         PlayerType playerType = PlayerHolder.getInstance().getType(); | ||||
|         if (!PlayerHolder.getInstance().isPlayerOpen()) { | ||||
|             Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player"); | ||||
|             playerType = MainPlayer.PlayerType.AUDIO; | ||||
|             playerType = PlayerService.PlayerType.AUDIO; | ||||
|         } | ||||
|         Toast.makeText(context, R.string.enqueued_next, Toast.LENGTH_SHORT).show(); | ||||
|         final Intent intent = getPlayerEnqueueNextIntent(context, MainPlayer.class, queue); | ||||
|         final Intent intent = getPlayerEnqueueNextIntent(context, PlayerService.class, queue); | ||||
|  | ||||
|         intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal()); | ||||
|         ContextCompat.startForegroundService(context, intent); | ||||
| @@ -414,14 +414,14 @@ public final class NavigationHelper { | ||||
|                                                final boolean switchingPlayers) { | ||||
|  | ||||
|         final boolean autoPlay; | ||||
|         @Nullable final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType(); | ||||
|         @Nullable final PlayerService.PlayerType playerType = PlayerHolder.getInstance().getType(); | ||||
|         if (!PlayerHolder.getInstance().isPlayerOpen()) { | ||||
|             // no player open | ||||
|             autoPlay = PlayerHelper.isAutoplayAllowedByUser(context); | ||||
|         } else if (switchingPlayers) { | ||||
|             // switching player to main player | ||||
|             autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state | ||||
|         } else if (playerType == MainPlayer.PlayerType.VIDEO) { | ||||
|         } else if (playerType == PlayerService.PlayerType.MAIN) { | ||||
|             // opening new stream while already playing in main player | ||||
|             autoPlay = PlayerHelper.isAutoplayAllowedByUser(context); | ||||
|         } else { | ||||
| @@ -436,7 +436,7 @@ public final class NavigationHelper { | ||||
|                 // Situation when user switches from players to main player. All needed data is | ||||
|                 // here, we can start watching (assuming newQueue equals playQueue). | ||||
|                 // Starting directly in fullscreen if the previous player type was popup. | ||||
|                 detailFragment.openVideoPlayer(playerType == MainPlayer.PlayerType.POPUP | ||||
|                 detailFragment.openVideoPlayer(playerType == PlayerService.PlayerType.POPUP | ||||
|                         || PlayerHelper.isStartMainPlayerFullscreenEnabled(context)); | ||||
|             } else { | ||||
|                 detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue); | ||||
|   | ||||
| @@ -12,8 +12,8 @@ import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.START | ||||
| import androidx.constraintlayout.widget.ConstraintSet | ||||
| import org.schabi.newpipe.MainActivity | ||||
| import org.schabi.newpipe.R | ||||
| import org.schabi.newpipe.player.event.DisplayPortion | ||||
| import org.schabi.newpipe.player.event.DoubleTapListener | ||||
| import org.schabi.newpipe.player.gesture.DisplayPortion | ||||
| import org.schabi.newpipe.player.gesture.DoubleTapListener | ||||
|  | ||||
| class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : | ||||
|     ConstraintLayout(context, attrs), DoubleTapListener { | ||||
|   | ||||
| @@ -25,7 +25,7 @@ | ||||
|             android:layout_gravity="center_horizontal" | ||||
|             app:behavior_hideable="true" | ||||
|             app:behavior_peekHeight="0dp" | ||||
|             app:layout_behavior="org.schabi.newpipe.player.event.CustomBottomSheetBehavior" /> | ||||
|             app:layout_behavior="org.schabi.newpipe.player.gesture.CustomBottomSheetBehavior" /> | ||||
|  | ||||
|     </org.schabi.newpipe.views.FocusAwareCoordinator> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Stypox
					Stypox