mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-11-05 01:26:23 +00:00
Merge pull request #8170 from Stypox/player-refactor
Refactor player and extract UI components
This commit is contained in:
commit
b7a44560f5
@ -44,7 +44,7 @@
|
|||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".player.MainPlayer"
|
android:name=".player.PlayerService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="mediaPlayback">
|
android:foregroundServiceType="mediaPlayback">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -60,7 +60,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
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
|
// ...the player is not running or in normal Video-mode/type
|
||||||
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
|
final PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||||
return playerType == null || playerType == MainPlayer.PlayerType.VIDEO;
|
return playerType == null || playerType == PlayerType.MAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openAddToPlaylistDialog() {
|
private void openAddToPlaylistDialog() {
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
package org.schabi.newpipe.fragments.detail;
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
|
import static android.text.TextUtils.isEmpty;
|
||||||
|
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||||
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||||
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
|
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||||
|
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||||
|
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||||
|
|
||||||
import android.animation.ValueAnimator;
|
import android.animation.ValueAnimator;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
@ -77,9 +88,9 @@ import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
|
|||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
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.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
@ -87,6 +98,8 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
|
|||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.player.ui.MainPlayerUi;
|
||||||
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
@ -106,6 +119,7 @@ import java.util.Iterator;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
@ -114,17 +128,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
|
||||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
|
||||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
|
||||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
|
||||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
|
||||||
|
|
||||||
public final class VideoDetailFragment
|
public final class VideoDetailFragment
|
||||||
extends BaseStateFragment<StreamInfo>
|
extends BaseStateFragment<StreamInfo>
|
||||||
implements BackPressable,
|
implements BackPressable,
|
||||||
@ -202,7 +205,7 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
private ContentObserver settingsContentObserver;
|
private ContentObserver settingsContentObserver;
|
||||||
@Nullable
|
@Nullable
|
||||||
private MainPlayer playerService;
|
private PlayerService playerService;
|
||||||
private Player player;
|
private Player player;
|
||||||
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
|
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
|
||||||
|
|
||||||
@ -211,7 +214,7 @@ public final class VideoDetailFragment
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@Override
|
@Override
|
||||||
public void onServiceConnected(final Player connectedPlayer,
|
public void onServiceConnected(final Player connectedPlayer,
|
||||||
final MainPlayer connectedPlayerService,
|
final PlayerService connectedPlayerService,
|
||||||
final boolean playAfterConnect) {
|
final boolean playAfterConnect) {
|
||||||
player = connectedPlayer;
|
player = connectedPlayer;
|
||||||
playerService = connectedPlayerService;
|
playerService = connectedPlayerService;
|
||||||
@ -219,6 +222,7 @@ public final class VideoDetailFragment
|
|||||||
// It will do nothing if the player is not in fullscreen mode
|
// It will do nothing if the player is not in fullscreen mode
|
||||||
hideSystemUiIfNeeded();
|
hideSystemUiIfNeeded();
|
||||||
|
|
||||||
|
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
|
||||||
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -227,22 +231,19 @@ public final class VideoDetailFragment
|
|||||||
// If the video is playing but orientation changed
|
// If the video is playing but orientation changed
|
||||||
// let's make the video in fullscreen again
|
// let's make the video in fullscreen again
|
||||||
checkLandscape();
|
checkLandscape();
|
||||||
} else if (player.isFullscreen() && !player.isVerticalVideo()
|
} else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false)
|
||||||
// Tablet UI has orientation-independent fullscreen
|
// Tablet UI has orientation-independent fullscreen
|
||||||
&& !DeviceUtils.isTablet(activity)) {
|
&& !DeviceUtils.isTablet(activity)) {
|
||||||
// Device is in portrait orientation after rotation but UI is in fullscreen.
|
// Device is in portrait orientation after rotation but UI is in fullscreen.
|
||||||
// Return back to non-fullscreen state
|
// Return back to non-fullscreen state
|
||||||
player.toggleFullscreen();
|
playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
}
|
|
||||||
|
|
||||||
if (playerIsNotStopped() && player.videoPlayerSelected()) {
|
|
||||||
addVideoPlayerView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//noinspection SimplifyOptionalCallChains
|
||||||
if (playAfterConnect
|
if (playAfterConnect
|
||||||
|| (currentInfo != null
|
|| (currentInfo != null
|
||||||
&& isAutoplayEnabled()
|
&& isAutoplayEnabled()
|
||||||
&& player.getParentActivity() == null)) {
|
&& !playerUi.isPresent())) {
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
openVideoPlayerAutoFullscreen();
|
openVideoPlayerAutoFullscreen();
|
||||||
}
|
}
|
||||||
@ -329,6 +330,9 @@ public final class VideoDetailFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onResume() called");
|
||||||
|
}
|
||||||
|
|
||||||
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
|
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
|
||||||
|
|
||||||
@ -518,7 +522,7 @@ public final class VideoDetailFragment
|
|||||||
case R.id.overlay_play_pause_button:
|
case R.id.overlay_play_pause_button:
|
||||||
if (playerIsNotStopped()) {
|
if (playerIsNotStopped()) {
|
||||||
player.playPause();
|
player.playPause();
|
||||||
player.hideControls(0, 0);
|
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||||
showSystemUi();
|
showSystemUi();
|
||||||
} else {
|
} else {
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
@ -583,12 +587,12 @@ public final class VideoDetailFragment
|
|||||||
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
|
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
|
||||||
binding.detailVideoTitleView.setMaxLines(10);
|
binding.detailVideoTitleView.setMaxLines(10);
|
||||||
animateRotation(binding.detailToggleSecondaryControlsView,
|
animateRotation(binding.detailToggleSecondaryControlsView,
|
||||||
Player.DEFAULT_CONTROLS_DURATION, 180);
|
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180);
|
||||||
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
|
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
binding.detailVideoTitleView.setMaxLines(1);
|
binding.detailVideoTitleView.setMaxLines(1);
|
||||||
animateRotation(binding.detailToggleSecondaryControlsView,
|
animateRotation(binding.detailToggleSecondaryControlsView,
|
||||||
Player.DEFAULT_CONTROLS_DURATION, 0);
|
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0);
|
||||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
// view pager height has changed, update the tab layout
|
// view pager height has changed, update the tab layout
|
||||||
@ -746,7 +750,9 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(final int keyCode) {
|
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
|
@Override
|
||||||
@ -756,7 +762,7 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we are in fullscreen mode just exit from it via first back press
|
// If we are in fullscreen mode just exit from it via first back press
|
||||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
if (isFullscreen()) {
|
||||||
if (!DeviceUtils.isTablet(activity)) {
|
if (!DeviceUtils.isTablet(activity)) {
|
||||||
player.pause();
|
player.pause();
|
||||||
}
|
}
|
||||||
@ -1006,8 +1012,7 @@ public final class VideoDetailFragment
|
|||||||
getChildFragmentManager().beginTransaction()
|
getChildFragmentManager().beginTransaction()
|
||||||
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
|
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
|
||||||
.commitAllowingStateLoss();
|
.commitAllowingStateLoss();
|
||||||
binding.relatedItemsLayout.setVisibility(
|
binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE);
|
||||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1087,8 +1092,12 @@ public final class VideoDetailFragment
|
|||||||
private void toggleFullscreenIfInFullscreenMode() {
|
private void toggleFullscreenIfInFullscreenMode() {
|
||||||
// If a user watched video inside fullscreen mode and than chose another player
|
// If a user watched video inside fullscreen mode and than chose another player
|
||||||
// return to non-fullscreen mode
|
// return to non-fullscreen mode
|
||||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
if (isPlayerAvailable()) {
|
||||||
player.toggleFullscreen();
|
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||||
|
if (playerUi.isFullscreen()) {
|
||||||
|
playerUi.toggleFullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1214,16 +1223,10 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
final PlayQueue queue = setupPlayQueueForIntent(false);
|
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();
|
addVideoPlayerView();
|
||||||
|
|
||||||
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
||||||
MainPlayer.class, queue, true, autoPlayEnabled);
|
PlayerService.class, queue, true, autoPlayEnabled);
|
||||||
ContextCompat.startForegroundService(activity, playerIntent);
|
ContextCompat.startForegroundService(activity, playerIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1235,8 +1238,8 @@ public final class VideoDetailFragment
|
|||||||
* be reused in a few milliseconds and the flickering would be annoying.
|
* be reused in a few milliseconds and the flickering would be annoying.
|
||||||
*/
|
*/
|
||||||
private void hideMainPlayerOnLoadingNewStream() {
|
private void hideMainPlayerOnLoadingNewStream() {
|
||||||
if (!isPlayerServiceAvailable()
|
//noinspection SimplifyOptionalCallChains
|
||||||
|| playerService.getView() == null
|
if (!isPlayerServiceAvailable() || !getRoot().isPresent()
|
||||||
|| !player.videoPlayerSelected()) {
|
|| !player.videoPlayerSelected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1244,7 +1247,7 @@ public final class VideoDetailFragment
|
|||||||
removeVideoPlayerView();
|
removeVideoPlayerView();
|
||||||
if (isAutoplayEnabled()) {
|
if (isAutoplayEnabled()) {
|
||||||
playerService.stopForImmediateReusing();
|
playerService.stopForImmediateReusing();
|
||||||
playerService.getView().setVisibility(View.GONE);
|
getRoot().ifPresent(view -> view.setVisibility(View.GONE));
|
||||||
} else {
|
} else {
|
||||||
playerHolder.stopService();
|
playerHolder.stopService();
|
||||||
}
|
}
|
||||||
@ -1305,23 +1308,23 @@ public final class VideoDetailFragment
|
|||||||
if (!isPlayerAvailable() || getView() == null) {
|
if (!isPlayerAvailable() || getView() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if viewHolder already contains a child
|
|
||||||
if (player.getRootView().getParent() != binding.playerPlaceholder) {
|
|
||||||
playerService.removeViewFromParent();
|
|
||||||
}
|
|
||||||
setHeightThumbnail();
|
setHeightThumbnail();
|
||||||
|
|
||||||
// Prevent from re-adding a view multiple times
|
// Prevent from re-adding a view multiple times
|
||||||
if (player.getRootView().getParent() == null) {
|
new Handler(Looper.getMainLooper()).post(() ->
|
||||||
binding.playerPlaceholder.addView(player.getRootView());
|
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||||
}
|
playerUi.removeViewFromParent();
|
||||||
|
binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
|
||||||
|
playerUi.setupVideoSurfaceIfNeeded();
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeVideoPlayerView() {
|
private void removeVideoPlayerView() {
|
||||||
makeDefaultHeightForVideoPlaceholder();
|
makeDefaultHeightForVideoPlaceholder();
|
||||||
|
|
||||||
playerService.removeViewFromParent();
|
if (player != null) {
|
||||||
|
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void makeDefaultHeightForVideoPlaceholder() {
|
private void makeDefaultHeightForVideoPlaceholder() {
|
||||||
@ -1362,7 +1365,7 @@ public final class VideoDetailFragment
|
|||||||
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
||||||
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
||||||
|
|
||||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
if (isFullscreen()) {
|
||||||
final int height = (DeviceUtils.isInMultiWindow(activity)
|
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||||
? requireView()
|
? requireView()
|
||||||
: activity.getWindow().getDecorView()).getHeight();
|
: activity.getWindow().getDecorView()).getHeight();
|
||||||
@ -1387,8 +1390,9 @@ public final class VideoDetailFragment
|
|||||||
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
||||||
player.getSurfaceView()
|
player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
|
||||||
.setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight);
|
ui.getBinding().surfaceView.setHeights(newHeight,
|
||||||
|
ui.isFullscreen() ? newHeight : maxHeight));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1517,7 +1521,7 @@ public final class VideoDetailFragment
|
|||||||
if (binding.relatedItemsLayout != null) {
|
if (binding.relatedItemsLayout != null) {
|
||||||
if (showRelatedItems) {
|
if (showRelatedItems) {
|
||||||
binding.relatedItemsLayout.setVisibility(
|
binding.relatedItemsLayout.setVisibility(
|
||||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE);
|
isFullscreen() ? View.GONE : View.INVISIBLE);
|
||||||
} else {
|
} else {
|
||||||
binding.relatedItemsLayout.setVisibility(View.GONE);
|
binding.relatedItemsLayout.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
@ -1779,6 +1783,11 @@ public final class VideoDetailFragment
|
|||||||
// Player event listener
|
// Player event listener
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated() {
|
||||||
|
addVideoPlayerView();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onQueueUpdate(final PlayQueue queue) {
|
public void onQueueUpdate(final PlayQueue queue) {
|
||||||
playQueue = queue;
|
playQueue = queue;
|
||||||
@ -1899,15 +1908,10 @@ public final class VideoDetailFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||||
setupBrightness();
|
setupBrightness();
|
||||||
|
//noinspection SimplifyOptionalCallChains
|
||||||
if (!isPlayerAndPlayerServiceAvailable()
|
if (!isPlayerAndPlayerServiceAvailable()
|
||||||
|| playerService.getView() == null
|
|| !player.UIs().get(MainPlayerUi.class).isPresent()
|
||||||
|| player.getParentActivity() == null) {
|
|| getRoot().map(View::getParent).orElse(null) == null) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final View view = playerService.getView();
|
|
||||||
final ViewGroup parent = (ViewGroup) view.getParent();
|
|
||||||
if (parent == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1935,7 +1939,7 @@ public final class VideoDetailFragment
|
|||||||
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||||
if (DeviceUtils.isTablet(activity)
|
if (DeviceUtils.isTablet(activity)
|
||||||
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||||
player.toggleFullscreen();
|
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2018,7 +2022,7 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||||
|
|
||||||
if (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen())) {
|
if (isInMultiWindow || isFullscreen()) {
|
||||||
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||||
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||||
}
|
}
|
||||||
@ -2027,13 +2031,17 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
// Listener implementation
|
// Listener implementation
|
||||||
public void hideSystemUiIfNeeded() {
|
public void hideSystemUiIfNeeded() {
|
||||||
if (isPlayerAvailable()
|
if (isFullscreen()
|
||||||
&& player.isFullscreen()
|
|
||||||
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
hideSystemUi();
|
hideSystemUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isFullscreen() {
|
||||||
|
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
|
||||||
|
.map(VideoPlayerUi::isFullscreen).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean playerIsNotStopped() {
|
private boolean playerIsNotStopped() {
|
||||||
return isPlayerAvailable() && !player.isStopped();
|
return isPlayerAvailable() && !player.isStopped();
|
||||||
}
|
}
|
||||||
@ -2056,10 +2064,7 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
|
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
|
||||||
if (!isPlayerAvailable()
|
if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|| !player.videoPlayerSelected()
|
|
||||||
|| !player.isFullscreen()
|
|
||||||
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|
|
||||||
// Apply system brightness when the player is not in fullscreen
|
// Apply system brightness when the player is not in fullscreen
|
||||||
restoreDefaultBrightness();
|
restoreDefaultBrightness();
|
||||||
} else {
|
} else {
|
||||||
@ -2083,7 +2088,7 @@ public final class VideoDetailFragment
|
|||||||
setAutoPlay(true);
|
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
|
// Let's give a user time to look at video information page if video is not playing
|
||||||
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
||||||
player.play();
|
player.play();
|
||||||
@ -2310,10 +2315,10 @@ public final class VideoDetailFragment
|
|||||||
if (DeviceUtils.isLandscape(requireContext())
|
if (DeviceUtils.isLandscape(requireContext())
|
||||||
&& isPlayerAvailable()
|
&& isPlayerAvailable()
|
||||||
&& player.isPlaying()
|
&& player.isPlaying()
|
||||||
&& !player.isFullscreen()
|
&& !isFullscreen()
|
||||||
&& !DeviceUtils.isTablet(activity)
|
&& !DeviceUtils.isTablet(activity)) {
|
||||||
&& player.videoPlayerSelected()) {
|
player.UIs().get(MainPlayerUi.class)
|
||||||
player.toggleFullscreen();
|
.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
}
|
}
|
||||||
setOverlayLook(binding.appBarLayout, behavior, 1);
|
setOverlayLook(binding.appBarLayout, behavior, 1);
|
||||||
break;
|
break;
|
||||||
@ -2326,17 +2331,22 @@ public final class VideoDetailFragment
|
|||||||
// Re-enable clicks
|
// Re-enable clicks
|
||||||
setOverlayElementsClickable(true);
|
setOverlayElementsClickable(true);
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
player.closeItemsList();
|
player.UIs().get(MainPlayerUi.class)
|
||||||
|
.ifPresent(MainPlayerUi::closeItemsList);
|
||||||
}
|
}
|
||||||
setOverlayLook(binding.appBarLayout, behavior, 0);
|
setOverlayLook(binding.appBarLayout, behavior, 0);
|
||||||
break;
|
break;
|
||||||
case BottomSheetBehavior.STATE_DRAGGING:
|
case BottomSheetBehavior.STATE_DRAGGING:
|
||||||
case BottomSheetBehavior.STATE_SETTLING:
|
case BottomSheetBehavior.STATE_SETTLING:
|
||||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
if (isFullscreen()) {
|
||||||
showSystemUi();
|
showSystemUi();
|
||||||
}
|
}
|
||||||
if (isPlayerAvailable() && player.isControlsVisible()) {
|
if (isPlayerAvailable()) {
|
||||||
player.hideControls(0, 0);
|
player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
|
||||||
|
if (ui.isControlsVisible()) {
|
||||||
|
ui.hideControls(0, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -2410,4 +2420,13 @@ public final class VideoDetailFragment
|
|||||||
boolean isPlayerAndPlayerServiceAvailable() {
|
boolean isPlayerAndPlayerServiceAvailable() {
|
||||||
return (player != null && playerService != null);
|
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.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
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.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
@ -9,15 +9,20 @@ import android.view.Window;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
@ -131,13 +136,13 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
|||||||
* @param context context used for accessing the database
|
* @param context context used for accessing the database
|
||||||
* @param streamEntities used for crating the dialog
|
* @param streamEntities used for crating the dialog
|
||||||
* @param onExec execution that should occur after a dialog got created, e.g. showing it
|
* @param onExec execution that should occur after a dialog got created, e.g. showing it
|
||||||
* @return Disposable
|
* @return the disposable that was created
|
||||||
*/
|
*/
|
||||||
public static Disposable createCorrespondingDialog(
|
public static Disposable createCorrespondingDialog(
|
||||||
final Context context,
|
final Context context,
|
||||||
final List<StreamEntity> streamEntities,
|
final List<StreamEntity> streamEntities,
|
||||||
final Consumer<PlaylistDialog> onExec
|
final Consumer<PlaylistDialog> onExec) {
|
||||||
) {
|
|
||||||
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
|
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
|
||||||
.hasPlaylists()
|
.hasPlaylists()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
@ -147,4 +152,30 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
|||||||
: PlaylistCreationDialog.newInstance(streamEntities))
|
: PlaylistCreationDialog.newInstance(streamEntities))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link PlaylistAppendDialog} when playlists exists,
|
||||||
|
* otherwise a {@link PlaylistCreationDialog}. If the player's play queue is null or empty, no
|
||||||
|
* dialog will be created.
|
||||||
|
*
|
||||||
|
* @param player the player from which to extract the context and the play queue
|
||||||
|
* @param fragmentManager the fragment manager to use to show the dialog
|
||||||
|
* @return the disposable that was created
|
||||||
|
*/
|
||||||
|
public static Disposable showForPlayQueue(
|
||||||
|
final Player player,
|
||||||
|
@NonNull final FragmentManager fragmentManager) {
|
||||||
|
|
||||||
|
final List<StreamEntity> streamEntities = Stream.of(player.getPlayQueue())
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.flatMap(playQueue -> playQueue.getStreams().stream())
|
||||||
|
.map(StreamEntity::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (streamEntities.isEmpty()) {
|
||||||
|
return Disposable.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlaylistDialog.createCorrespondingDialog(player.getContext(), streamEntities,
|
||||||
|
dialog -> dialog.show(fragmentManager, "PlaylistDialog"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
@ -1,259 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
|
||||||
* Part of NewPipe
|
|
||||||
*
|
|
||||||
* License: GPL-3.0+
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.player;
|
|
||||||
|
|
||||||
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.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";
|
|
||||||
private static final boolean DEBUG = Player.DEBUG;
|
|
||||||
|
|
||||||
private Player player;
|
|
||||||
private WindowManager windowManager;
|
|
||||||
|
|
||||||
private final IBinder mBinder = new MainPlayer.LocalBinder();
|
|
||||||
|
|
||||||
public enum PlayerType {
|
|
||||||
VIDEO,
|
|
||||||
AUDIO,
|
|
||||||
POPUP
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Notification
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
static final String ACTION_CLOSE
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
|
|
||||||
static final String ACTION_PLAY_PAUSE
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
|
|
||||||
static final String ACTION_REPEAT
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
|
|
||||||
static final String ACTION_PLAY_NEXT
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
|
|
||||||
static final String ACTION_PLAY_PREVIOUS
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
|
|
||||||
static final String ACTION_FAST_REWIND
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
|
|
||||||
static final String ACTION_FAST_FORWARD
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
|
|
||||||
static final String ACTION_SHUFFLE
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
|
|
||||||
public static final String ACTION_RECREATE_NOTIFICATION
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Service's LifeCycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
if (DEBUG) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
|
|
||||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
|
||||||
}
|
|
||||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
|
||||||
&& player.getPlayQueue() == null) {
|
|
||||||
// Player is not working, no need to process media button's action
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopForImmediateReusing() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "stopForImmediateReusing() called");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!player.exoPlayerIsNull()) {
|
|
||||||
player.saveWasPlaying();
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
// Notification shows information about old stream but if a user selects
|
|
||||||
// a stream from backStack it's not actual anymore
|
|
||||||
// So we should hide the notification at all.
|
|
||||||
// When autoplay enabled such notification flashing is annoying so skip this case
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTaskRemoved(final Intent rootIntent) {
|
|
||||||
super.onTaskRemoved(rootIntent);
|
|
||||||
if (!player.videoPlayerSelected()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onDestroy();
|
|
||||||
// Unload from memory completely
|
|
||||||
Runtime.getRuntime().halt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "destroy() called");
|
|
||||||
}
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopService() {
|
|
||||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
|
||||||
cleanup();
|
|
||||||
stopSelf();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(final Context base) {
|
|
||||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(final Intent intent) {
|
|
||||||
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 Player getPlayer() {
|
|
||||||
return MainPlayer.this.player;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -29,6 +29,7 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
@ -51,7 +52,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
|
|
||||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||||
|
|
||||||
protected Player player;
|
private Player player;
|
||||||
|
|
||||||
private boolean serviceBound;
|
private boolean serviceBound;
|
||||||
private ServiceConnection serviceConnection;
|
private ServiceConnection serviceConnection;
|
||||||
@ -126,13 +127,13 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
NavigationHelper.openSettings(this);
|
NavigationHelper.openSettings(this);
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_append_playlist:
|
case R.id.action_append_playlist:
|
||||||
player.onAddToPlaylistClicked(getSupportFragmentManager());
|
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_playback_speed:
|
case R.id.action_playback_speed:
|
||||||
openPlaybackParameterDialog();
|
openPlaybackParameterDialog();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_mute:
|
case R.id.action_mute:
|
||||||
player.onMuteUnmuteButtonClicked();
|
player.toggleMute();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_system_audio:
|
case R.id.action_system_audio:
|
||||||
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
|
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
|
||||||
@ -168,7 +169,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void bind() {
|
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);
|
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
unbindService(serviceConnection);
|
unbindService(serviceConnection);
|
||||||
@ -184,10 +185,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
player.removeActivityListener(this);
|
player.removeActivityListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player != null && player.getPlayQueueAdapter() != null) {
|
onQueueUpdate(null);
|
||||||
player.getPlayQueueAdapter().unsetSelectedListener();
|
|
||||||
}
|
|
||||||
queueControlBinding.playQueue.setAdapter(null);
|
|
||||||
if (itemTouchHelper != null) {
|
if (itemTouchHelper != null) {
|
||||||
itemTouchHelper.attachToRecyclerView(null);
|
itemTouchHelper.attachToRecyclerView(null);
|
||||||
}
|
}
|
||||||
@ -208,17 +206,15 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
public void onServiceConnected(final ComponentName name, final IBinder service) {
|
public void onServiceConnected(final ComponentName name, final IBinder service) {
|
||||||
Log.d(TAG, "Player service is connected");
|
Log.d(TAG, "Player service is connected");
|
||||||
|
|
||||||
if (service instanceof PlayerServiceBinder) {
|
if (service instanceof PlayerService.LocalBinder) {
|
||||||
player = ((PlayerServiceBinder) service).getPlayerInstance();
|
player = ((PlayerService.LocalBinder) service).getPlayer();
|
||||||
} else if (service instanceof MainPlayer.LocalBinder) {
|
|
||||||
player = ((MainPlayer.LocalBinder) service).getPlayer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player == null || player.getPlayQueue() == null
|
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
|
||||||
|| player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) {
|
|
||||||
unbind();
|
unbind();
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
|
onQueueUpdate(player.getPlayQueue());
|
||||||
buildComponents();
|
buildComponents();
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
player.setActivityListener(PlayQueueActivity.this);
|
player.setActivityListener(PlayQueueActivity.this);
|
||||||
@ -241,7 +237,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
|
|
||||||
private void buildQueue() {
|
private void buildQueue() {
|
||||||
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
|
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
|
||||||
queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter());
|
|
||||||
queueControlBinding.playQueue.setClickable(true);
|
queueControlBinding.playQueue.setClickable(true);
|
||||||
queueControlBinding.playQueue.setLongClickable(true);
|
queueControlBinding.playQueue.setLongClickable(true);
|
||||||
queueControlBinding.playQueue.clearOnScrollListeners();
|
queueControlBinding.playQueue.clearOnScrollListeners();
|
||||||
@ -249,8 +244,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
|
|
||||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||||
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
|
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
|
||||||
|
|
||||||
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildMetadata() {
|
private void buildMetadata() {
|
||||||
@ -370,7 +363,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (view.getId() == queueControlBinding.controlRepeat.getId()) {
|
if (view.getId() == queueControlBinding.controlRepeat.getId()) {
|
||||||
player.onRepeatClicked();
|
player.cycleNextRepeatMode();
|
||||||
} else if (view.getId() == queueControlBinding.controlBackward.getId()) {
|
} else if (view.getId() == queueControlBinding.controlBackward.getId()) {
|
||||||
player.playPrevious();
|
player.playPrevious();
|
||||||
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
|
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
|
||||||
@ -382,7 +375,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
} else if (view.getId() == queueControlBinding.controlForward.getId()) {
|
} else if (view.getId() == queueControlBinding.controlForward.getId()) {
|
||||||
player.playNext();
|
player.playNext();
|
||||||
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
|
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
|
||||||
player.onShuffleClicked();
|
player.toggleShuffleModeEnabled();
|
||||||
} else if (view.getId() == queueControlBinding.metadata.getId()) {
|
} else if (view.getId() == queueControlBinding.metadata.getId()) {
|
||||||
scrollToSelected();
|
scrollToSelected();
|
||||||
} else if (view.getId() == queueControlBinding.liveSync.getId()) {
|
} else if (view.getId() == queueControlBinding.liveSync.getId()) {
|
||||||
@ -445,7 +438,14 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onQueueUpdate(final PlayQueue queue) {
|
public void onQueueUpdate(@Nullable final PlayQueue queue) {
|
||||||
|
if (queue == null) {
|
||||||
|
queueControlBinding.playQueue.setAdapter(null);
|
||||||
|
} else {
|
||||||
|
final PlayQueueAdapter adapter = new PlayQueueAdapter(this, queue);
|
||||||
|
adapter.setSelectedListener(getOnSelectedListener());
|
||||||
|
queueControlBinding.playQueue.setAdapter(adapter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -454,7 +454,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
onStateChanged(state);
|
onStateChanged(state);
|
||||||
onPlayModeChanged(repeatMode, shuffled);
|
onPlayModeChanged(repeatMode, shuffled);
|
||||||
onPlaybackParameterChanged(parameters);
|
onPlaybackParameterChanged(parameters);
|
||||||
onMaybePlaybackAdapterChanged();
|
|
||||||
onMaybeMuteChanged();
|
onMaybeMuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -582,17 +581,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() {
|
private void onMaybeMuteChanged() {
|
||||||
if (menu != null && player != null) {
|
if (menu != null && player != null) {
|
||||||
final MenuItem item = menu.findItem(R.id.action_mute);
|
final MenuItem item = menu.findItem(R.id.action_mute);
|
||||||
|
File diff suppressed because it is too large
Load Diff
149
app/src/main/java/org/schabi/newpipe/player/PlayerService.java
Normal file
149
app/src/main/java/org/schabi/newpipe/player/PlayerService.java
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
||||||
|
* Part of NewPipe
|
||||||
|
*
|
||||||
|
* License: GPL-3.0+
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One service for all players.
|
||||||
|
*/
|
||||||
|
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 final IBinder mBinder = new PlayerService.LocalBinder();
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Service's LifeCycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreate() called");
|
||||||
|
}
|
||||||
|
assureCorrectAppLanguage(this);
|
||||||
|
ThemeHelper.setTheme(this);
|
||||||
|
|
||||||
|
player = new Player(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
|
||||||
|
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||||
|
&& player.getPlayQueue() == null) {
|
||||||
|
// No need to process media button's actions if the player is not working, otherwise the
|
||||||
|
// player service would strangely start with nothing to play
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.handleIntent(intent);
|
||||||
|
if (player.getMediaSessionManager() != null) {
|
||||||
|
player.getMediaSessionManager().handleMediaButtonIntent(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopForImmediateReusing() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "stopForImmediateReusing() called");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player.exoPlayerIsNull()) {
|
||||||
|
player.saveWasPlaying();
|
||||||
|
|
||||||
|
// 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.smoothStopForImmediateReusing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTaskRemoved(final Intent rootIntent) {
|
||||||
|
super.onTaskRemoved(rootIntent);
|
||||||
|
if (!player.videoPlayerSelected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onDestroy();
|
||||||
|
// Unload from memory completely
|
||||||
|
Runtime.getRuntime().halt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "destroy() called");
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanup() {
|
||||||
|
if (player != null) {
|
||||||
|
player.destroy();
|
||||||
|
player = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopService() {
|
||||||
|
cleanup();
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attachBaseContext(final Context base) {
|
||||||
|
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(final Intent intent) {
|
||||||
|
return mBinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LocalBinder extends Binder {
|
||||||
|
|
||||||
|
public PlayerService getService() {
|
||||||
|
return PlayerService.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Player getPlayer() {
|
||||||
|
return PlayerService.this.player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +0,0 @@
|
|||||||
package org.schabi.newpipe.player;
|
|
||||||
|
|
||||||
import android.os.Binder;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
class PlayerServiceBinder extends Binder {
|
|
||||||
private final Player player;
|
|
||||||
|
|
||||||
PlayerServiceBinder(@NonNull final Player player) {
|
|
||||||
this.player = player;
|
|
||||||
}
|
|
||||||
|
|
||||||
Player getPlayerInstance() {
|
|
||||||
return player;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
package org.schabi.newpipe.player;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
public class PlayerState implements Serializable {
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final PlayQueue playQueue;
|
|
||||||
private final int repeatMode;
|
|
||||||
private final float playbackSpeed;
|
|
||||||
private final float playbackPitch;
|
|
||||||
@Nullable
|
|
||||||
private final String playbackQuality;
|
|
||||||
private final boolean playbackSkipSilence;
|
|
||||||
private final boolean wasPlaying;
|
|
||||||
|
|
||||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
|
||||||
final float playbackSpeed, final float playbackPitch,
|
|
||||||
final boolean playbackSkipSilence, final boolean wasPlaying) {
|
|
||||||
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null,
|
|
||||||
playbackSkipSilence, wasPlaying);
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
|
||||||
final float playbackSpeed, final float playbackPitch,
|
|
||||||
@Nullable final String playbackQuality, final boolean playbackSkipSilence,
|
|
||||||
final boolean wasPlaying) {
|
|
||||||
this.playQueue = playQueue;
|
|
||||||
this.repeatMode = repeatMode;
|
|
||||||
this.playbackSpeed = playbackSpeed;
|
|
||||||
this.playbackPitch = playbackPitch;
|
|
||||||
this.playbackQuality = playbackQuality;
|
|
||||||
this.playbackSkipSilence = playbackSkipSilence;
|
|
||||||
this.wasPlaying = wasPlaying;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Serdes
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Getters
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public PlayQueue getPlayQueue() {
|
|
||||||
return playQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getRepeatMode() {
|
|
||||||
return repeatMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getPlaybackSpeed() {
|
|
||||||
return playbackSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getPlaybackPitch() {
|
|
||||||
return playbackPitch;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getPlaybackQuality() {
|
|
||||||
return playbackQuality;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPlaybackSkipSilence() {
|
|
||||||
return playbackSkipSilence;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean wasPlaying() {
|
|
||||||
return wasPlaying;
|
|
||||||
}
|
|
||||||
}
|
|
32
app/src/main/java/org/schabi/newpipe/player/PlayerType.java
Normal file
32
app/src/main/java/org/schabi/newpipe/player/PlayerType.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
public enum PlayerType {
|
||||||
|
MAIN,
|
||||||
|
AUDIO,
|
||||||
|
POPUP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an integer representing this {@link PlayerType}, to be used to save it in intents
|
||||||
|
* @see #retrieveFromIntent(Intent) Use retrieveFromIntent() to retrieve and convert player type
|
||||||
|
* integers from an intent
|
||||||
|
*/
|
||||||
|
public int valueForIntent() {
|
||||||
|
return ordinal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param intent the intent to retrieve a player type from
|
||||||
|
* @return the player type integer retrieved from the intent, converted back into a {@link
|
||||||
|
* PlayerType}, or {@link PlayerType#MAIN} if there is no player type extra in the
|
||||||
|
* intent
|
||||||
|
* @throws ArrayIndexOutOfBoundsException if the intent contains an invalid player type integer
|
||||||
|
* @see #valueForIntent() Use valueForIntent() to obtain valid player type integers
|
||||||
|
*/
|
||||||
|
public static PlayerType retrieveFromIntent(final Intent intent) {
|
||||||
|
return values()[intent.getIntExtra(PLAYER_TYPE, MAIN.valueForIntent())];
|
||||||
|
}
|
||||||
|
}
|
@ -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,7 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.event
|
|
||||||
|
|
||||||
interface DoubleTapListener {
|
|
||||||
fun onDoubleTapStarted(portion: DisplayPortion) {}
|
|
||||||
fun onDoubleTapProgressDown(portion: DisplayPortion) {}
|
|
||||||
fun onDoubleTapFinished() {}
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
package org.schabi.newpipe.player.event;
|
package org.schabi.newpipe.player.event;
|
||||||
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
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;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
|
|
||||||
public interface PlayerServiceEventListener extends PlayerEventListener {
|
public interface PlayerServiceEventListener extends PlayerEventListener {
|
||||||
|
void onViewCreated();
|
||||||
|
|
||||||
void onFullscreenStateChanged(boolean fullscreen);
|
void onFullscreenStateChanged(boolean fullscreen);
|
||||||
|
|
||||||
void onScreenRotationButtonClicked();
|
void onScreenRotationButtonClicked();
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package org.schabi.newpipe.player.event;
|
package org.schabi.newpipe.player.event;
|
||||||
|
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
|
||||||
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
|
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
|
||||||
void onServiceConnected(Player player,
|
void onServiceConnected(Player player,
|
||||||
MainPlayer playerService,
|
PlayerService playerService,
|
||||||
boolean playAfterConnect);
|
boolean playAfterConnect);
|
||||||
void onServiceDisconnected();
|
void onServiceDisconnected();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,186 @@
|
|||||||
|
package org.schabi.newpipe.player.gesture
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDownNotDoubleTapping(e)) {
|
||||||
|
return super.onDown(e)
|
||||||
|
}
|
||||||
|
return 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(Looper.getMainLooper())
|
||||||
|
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.content.Context;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
@ -1,4 +1,4 @@
|
|||||||
package org.schabi.newpipe.player.event
|
package org.schabi.newpipe.player.gesture
|
||||||
|
|
||||||
enum class DisplayPortion {
|
enum class DisplayPortion {
|
||||||
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF
|
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.schabi.newpipe.player.gesture
|
||||||
|
|
||||||
|
interface DoubleTapListener {
|
||||||
|
fun onDoubleTapStarted(portion: DisplayPortion)
|
||||||
|
fun onDoubleTapProgressDown(portion: DisplayPortion)
|
||||||
|
fun onDoubleTapFinished()
|
||||||
|
}
|
@ -0,0 +1,234 @@
|
|||||||
|
package org.schabi.newpipe.player.gesture
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.OnTouchListener
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
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.AudioReactor
|
||||||
|
import org.schabi.newpipe.player.helper.PlayerHelper
|
||||||
|
import org.schabi.newpipe.player.ui.MainPlayerUi
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx
|
||||||
|
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 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) {
|
||||||
|
val bar: ProgressBar = binding.volumeProgressBar
|
||||||
|
val audioReactor: AudioReactor = player.audioReactor
|
||||||
|
|
||||||
|
// If we just started sliding, change the progress bar to match the system volume
|
||||||
|
if (!binding.volumeRelativeLayout.isVisible) {
|
||||||
|
val volumePercent: Float = audioReactor.volume / audioReactor.maxVolume.toFloat()
|
||||||
|
bar.progress = (volumePercent * bar.max).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
binding.volumeProgressBar.incrementProgressBy(distanceY.toInt())
|
||||||
|
|
||||||
|
// Update volume
|
||||||
|
val currentProgressPercent: Float = bar.progress / bar.max.toFloat()
|
||||||
|
val currentVolume = (audioReactor.maxVolume * currentProgressPercent).toInt()
|
||||||
|
audioReactor.volume = currentVolume
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player center image
|
||||||
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make sure the correct layout is visible
|
||||||
|
if (!binding.volumeRelativeLayout.isVisible) {
|
||||||
|
binding.volumeRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
|
||||||
|
}
|
||||||
|
binding.brightnessRelativeLayout.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onScrollBrightness(distanceY: Float) {
|
||||||
|
val parent: AppCompatActivity = playerUi.parentActivity.orElse(null) ?: return
|
||||||
|
val window = parent.window
|
||||||
|
val layoutParams = window.attributes
|
||||||
|
val bar: ProgressBar = binding.brightnessProgressBar
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
val oldBrightness = layoutParams.screenBrightness
|
||||||
|
bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt()
|
||||||
|
bar.incrementProgressBy(distanceY.toInt())
|
||||||
|
|
||||||
|
// Update brightness
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player center image
|
||||||
|
binding.brightnessImageView.setImageDrawable(
|
||||||
|
AppCompatResources.getDrawable(
|
||||||
|
player.context,
|
||||||
|
when {
|
||||||
|
currentProgressPercent < 0.25 -> R.drawable.ic_brightness_low
|
||||||
|
currentProgressPercent < 0.75 -> R.drawable.ic_brightness_medium
|
||||||
|
else -> R.drawable.ic_brightness_high
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make sure the correct layout is visible
|
||||||
|
if (!binding.brightnessRelativeLayout.isVisible) {
|
||||||
|
binding.brightnessRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
|
||||||
|
}
|
||||||
|
binding.volumeRelativeLayout.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrollEnd(event: MotionEvent) {
|
||||||
|
super.onScrollEnd(event)
|
||||||
|
if (binding.volumeRelativeLayout.isVisible) {
|
||||||
|
binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
|
||||||
|
}
|
||||||
|
if (binding.brightnessRelativeLayout.isVisible) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate heights of status and navigation bars
|
||||||
|
val statusBarHeight = getAndroidDimenPx(player.context, "status_bar_height")
|
||||||
|
val navigationBarHeight = getAndroidDimenPx(player.context, "navigation_bar_height")
|
||||||
|
|
||||||
|
// Do not handle this event if initially it started from status or navigation bars
|
||||||
|
val isTouchingStatusBar = initialEvent.y < statusBarHeight
|
||||||
|
val isTouchingNavigationBar = initialEvent.y > (binding.root.height - navigationBarHeight)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,288 @@
|
|||||||
|
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.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) {
|
||||||
|
playerUi.savePopupPositionAndSizeToPrefs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.R;
|
||||||
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
|
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.SimpleOnSeekBarChangeListener;
|
||||||
import org.schabi.newpipe.util.SliderStrategy;
|
import org.schabi.newpipe.util.SliderStrategy;
|
||||||
|
|
||||||
@ -207,7 +207,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||||||
? View.VISIBLE
|
? View.VISIBLE
|
||||||
: View.GONE);
|
: View.GONE);
|
||||||
animateRotation(binding.pitchToogleControlModes,
|
animateRotation(binding.pitchToogleControlModes,
|
||||||
Player.DEFAULT_CONTROLS_DURATION,
|
VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
|
||||||
isCurrentlyVisible ? 180 : 0);
|
isCurrentlyVisible ? 180 : 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,8 +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_ALL;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
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_ALWAYS;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
||||||
@ -15,14 +13,8 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.PixelFormat;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.view.accessibility.CaptioningManager;
|
import android.view.accessibility.CaptioningManager;
|
||||||
|
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
@ -49,7 +41,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
@ -76,20 +67,6 @@ public final class PlayerHelper {
|
|||||||
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
|
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
|
||||||
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
|
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
|
|
||||||
* NewPipe's popup player.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* This value is hardcoded instead of being get dynamically with the method linked of the
|
|
||||||
* constant documentation below, because it is not static and popup player layout parameters
|
|
||||||
* are generated with static methods.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
|
|
||||||
*/
|
|
||||||
private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;
|
|
||||||
|
|
||||||
@Retention(SOURCE)
|
@Retention(SOURCE)
|
||||||
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
|
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
|
||||||
AUTOPLAY_TYPE_NEVER})
|
AUTOPLAY_TYPE_NEVER})
|
||||||
@ -339,10 +316,6 @@ public final class PlayerHelper {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getTossFlingVelocity() {
|
|
||||||
return 2500;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
|
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
|
||||||
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
|
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
|
||||||
@ -452,12 +425,6 @@ public final class PlayerHelper {
|
|||||||
// Utils used by player
|
// Utils used by player
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public static MainPlayer.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())];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPlaybackResumeEnabled(final Player player) {
|
public static boolean isPlaybackResumeEnabled(final Player player) {
|
||||||
return player.getPrefs().getBoolean(
|
return player.getPrefs().getBoolean(
|
||||||
player.getContext().getString(R.string.enable_watch_history_key), true)
|
player.getContext().getString(R.string.enable_watch_history_key), true)
|
||||||
@ -528,90 +495,10 @@ public final class PlayerHelper {
|
|||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param player {@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 float popupWidth = popupRememberSizeAndPos
|
|
||||||
? player.getPrefs().getFloat(player.getContext().getString(
|
|
||||||
R.string.popup_saved_width_key), defaultSize)
|
|
||||||
: defaultSize;
|
|
||||||
final float popupHeight = getMinimumVideoHeight(popupWidth);
|
|
||||||
|
|
||||||
final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams(
|
|
||||||
(int) popupWidth, (int) popupHeight,
|
|
||||||
popupLayoutParamType(),
|
|
||||||
IDLE_WINDOW_FLAGS,
|
|
||||||
PixelFormat.TRANSLUCENT);
|
|
||||||
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);
|
|
||||||
popupLayoutParams.x = popupRememberSizeAndPos
|
|
||||||
? player.getPrefs().getInt(player.getContext().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;
|
|
||||||
|
|
||||||
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)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static float getMinimumVideoHeight(final float width) {
|
public static float getMinimumVideoHeight(final float width) {
|
||||||
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
|
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RtlHardcoded")
|
|
||||||
public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
|
|
||||||
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
||||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
|
||||||
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
|
|
||||||
|
|
||||||
final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
popupLayoutParamType(),
|
|
||||||
flags,
|
|
||||||
PixelFormat.TRANSLUCENT);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
// Setting maximum opacity allowed for touch events to other apps for Android 12 and
|
|
||||||
// higher to prevent non interaction when using other apps with the popup player
|
|
||||||
closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER;
|
|
||||||
}
|
|
||||||
|
|
||||||
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
|
||||||
closeOverlayLayoutParams.softInputMode =
|
|
||||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
|
||||||
return closeOverlayLayoutParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int popupLayoutParamType() {
|
|
||||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
|
||||||
? WindowManager.LayoutParams.TYPE_PHONE
|
|
||||||
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int retrieveSeekDurationFromPreferences(final Player player) {
|
public static int retrieveSeekDurationFromPreferences(final Player player) {
|
||||||
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
|
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
|
||||||
player.getContext().getString(R.string.seek_duration_key),
|
player.getContext().getString(R.string.seek_duration_key),
|
||||||
|
@ -16,8 +16,9 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
|||||||
import org.schabi.newpipe.App;
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
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.Player;
|
||||||
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
@ -42,17 +43,17 @@ public final class PlayerHolder {
|
|||||||
|
|
||||||
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
|
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
|
||||||
private boolean bound;
|
private boolean bound;
|
||||||
@Nullable private MainPlayer playerService;
|
@Nullable private PlayerService playerService;
|
||||||
@Nullable private Player player;
|
@Nullable private Player player;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,
|
* Returns the current {@link PlayerType} of the {@link PlayerService} service,
|
||||||
* otherwise `null` if no service running.
|
* otherwise `null` if no service is running.
|
||||||
*
|
*
|
||||||
* @return Current PlayerType
|
* @return Current PlayerType
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public MainPlayer.PlayerType getType() {
|
public PlayerType getType() {
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -122,7 +123,7 @@ public final class PlayerHolder {
|
|||||||
// and NullPointerExceptions inside the service because the service will be
|
// and NullPointerExceptions inside the service because the service will be
|
||||||
// bound twice. Prevent it with unbinding first
|
// bound twice. Prevent it with unbinding first
|
||||||
unbind(context);
|
unbind(context);
|
||||||
ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class));
|
ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class));
|
||||||
serviceConnection.doPlayAfterConnect(playAfterConnect);
|
serviceConnection.doPlayAfterConnect(playAfterConnect);
|
||||||
bind(context);
|
bind(context);
|
||||||
}
|
}
|
||||||
@ -130,7 +131,7 @@ public final class PlayerHolder {
|
|||||||
public void stopService() {
|
public void stopService() {
|
||||||
final Context context = getCommonContext();
|
final Context context = getCommonContext();
|
||||||
unbind(context);
|
unbind(context);
|
||||||
context.stopService(new Intent(context, MainPlayer.class));
|
context.stopService(new Intent(context, PlayerService.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlayerServiceConnection implements ServiceConnection {
|
class PlayerServiceConnection implements ServiceConnection {
|
||||||
@ -156,7 +157,7 @@ public final class PlayerHolder {
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Player service is connected");
|
Log.d(TAG, "Player service is connected");
|
||||||
}
|
}
|
||||||
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
|
final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
|
||||||
|
|
||||||
playerService = localBinder.getService();
|
playerService = localBinder.getService();
|
||||||
player = localBinder.getPlayer();
|
player = localBinder.getPlayer();
|
||||||
@ -172,7 +173,7 @@ public final class PlayerHolder {
|
|||||||
Log.d(TAG, "bind() called");
|
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,
|
bound = context.bindService(serviceIntent, serviceConnection,
|
||||||
Context.BIND_AUTO_CREATE);
|
Context.BIND_AUTO_CREATE);
|
||||||
if (!bound) {
|
if (!bound) {
|
||||||
@ -211,6 +212,13 @@ public final class PlayerHolder {
|
|||||||
|
|
||||||
private final PlayerServiceEventListener internalListener =
|
private final PlayerServiceEventListener internalListener =
|
||||||
new PlayerServiceEventListener() {
|
new PlayerServiceEventListener() {
|
||||||
|
@Override
|
||||||
|
public void onViewCreated() {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onViewCreated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||||
if (listener != null) {
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player.notification;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
@ -7,6 +7,7 @@ import androidx.annotation.DrawableRes;
|
|||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
@ -20,7 +21,34 @@ import java.util.TreeSet;
|
|||||||
|
|
||||||
public final class NotificationConstants {
|
public final class NotificationConstants {
|
||||||
|
|
||||||
private NotificationConstants() { }
|
private NotificationConstants() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Intent actions
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
public static final String ACTION_CLOSE
|
||||||
|
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
|
||||||
|
public static final String ACTION_PLAY_PAUSE
|
||||||
|
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
|
||||||
|
public static final String ACTION_REPEAT
|
||||||
|
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
|
||||||
|
public static final String ACTION_PLAY_NEXT
|
||||||
|
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
|
||||||
|
public static final String ACTION_PLAY_PREVIOUS
|
||||||
|
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
|
||||||
|
public static final String ACTION_FAST_REWIND
|
||||||
|
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
|
||||||
|
public static final String ACTION_FAST_FORWARD
|
||||||
|
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
|
||||||
|
public static final String ACTION_SHUFFLE
|
||||||
|
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
|
||||||
|
public static final String ACTION_RECREATE_NOTIFICATION
|
||||||
|
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static final int NOTHING = 0;
|
public static final int NOTHING = 0;
|
@ -0,0 +1,125 @@
|
|||||||
|
package org.schabi.newpipe.player.notification;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
|
import org.schabi.newpipe.player.ui.PlayerUi;
|
||||||
|
|
||||||
|
public final class NotificationPlayerUi extends PlayerUi {
|
||||||
|
private boolean foregroundNotificationAlreadyCreated = false;
|
||||||
|
private final NotificationUtil notificationUtil;
|
||||||
|
|
||||||
|
public NotificationPlayerUi(@NonNull final Player player) {
|
||||||
|
super(player);
|
||||||
|
notificationUtil = new NotificationUtil(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initPlayer() {
|
||||||
|
super.initPlayer();
|
||||||
|
if (!foregroundNotificationAlreadyCreated) {
|
||||||
|
notificationUtil.createNotificationAndStartForeground();
|
||||||
|
foregroundNotificationAlreadyCreated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
super.destroy();
|
||||||
|
notificationUtil.cancelNotificationAndStopForeground();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
|
||||||
|
super.onThumbnailLoaded(bitmap);
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlocked() {
|
||||||
|
super.onBlocked();
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaying() {
|
||||||
|
super.onPlaying();
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBuffering() {
|
||||||
|
super.onBuffering();
|
||||||
|
if (notificationUtil.shouldUpdateBufferingSlot()) {
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPaused() {
|
||||||
|
super.onPaused();
|
||||||
|
|
||||||
|
// Remove running notification when user does not want minimization to background or popup
|
||||||
|
if (PlayerHelper.getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE
|
||||||
|
&& player.videoPlayerSelected()) {
|
||||||
|
notificationUtil.cancelNotificationAndStopForeground();
|
||||||
|
} else {
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPausedSeek() {
|
||||||
|
super.onPausedSeek();
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompleted() {
|
||||||
|
super.onCompleted();
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
|
||||||
|
super.onRepeatModeChanged(repeatMode);
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
|
||||||
|
super.onShuffleModeEnabledChanged(shuffleModeEnabled);
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBroadcastReceived(final Intent intent) {
|
||||||
|
super.onBroadcastReceived(intent);
|
||||||
|
if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) {
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMetadataChanged(@NonNull final StreamInfo info) {
|
||||||
|
super.onMetadataChanged(info);
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayQueueEdited() {
|
||||||
|
super.onPlayQueueEdited();
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,7 @@
|
|||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player.notification;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ServiceInfo;
|
import android.content.pm.ServiceInfo;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
@ -19,6 +18,7 @@ import androidx.core.content.ContextCompat;
|
|||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -26,14 +26,14 @@ import java.util.List;
|
|||||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
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_ALL;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
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.notification.NotificationConstants.ACTION_CLOSE;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a utility class for player notifications.
|
* This is a utility class for player notifications.
|
||||||
@ -45,22 +45,16 @@ public final class NotificationUtil {
|
|||||||
private static final boolean DEBUG = Player.DEBUG;
|
private static final boolean DEBUG = Player.DEBUG;
|
||||||
private static final int NOTIFICATION_ID = 123789;
|
private static final int NOTIFICATION_ID = 123789;
|
||||||
|
|
||||||
@Nullable private static NotificationUtil instance = null;
|
|
||||||
|
|
||||||
@NotificationConstants.Action
|
@NotificationConstants.Action
|
||||||
private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
|
private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
|
||||||
|
|
||||||
private NotificationManagerCompat notificationManager;
|
private NotificationManagerCompat notificationManager;
|
||||||
private NotificationCompat.Builder notificationBuilder;
|
private NotificationCompat.Builder notificationBuilder;
|
||||||
|
|
||||||
private NotificationUtil() {
|
private final Player player;
|
||||||
}
|
|
||||||
|
|
||||||
public static NotificationUtil getInstance() {
|
public NotificationUtil(final Player player) {
|
||||||
if (instance == null) {
|
this.player = player;
|
||||||
instance = new NotificationUtil();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -71,20 +65,18 @@ public final class NotificationUtil {
|
|||||||
/**
|
/**
|
||||||
* Creates the notification if it does not exist already and recreates it if forceRecreate is
|
* Creates the notification if it does not exist already and recreates it if forceRecreate is
|
||||||
* true. Updates the notification with the data in the player.
|
* true. Updates the notification with the data in the player.
|
||||||
* @param player the player currently open, to take data from
|
|
||||||
* @param forceRecreate whether to force the recreation of the notification even if it already
|
* @param forceRecreate whether to force the recreation of the notification even if it already
|
||||||
* exists
|
* exists
|
||||||
*/
|
*/
|
||||||
synchronized void createNotificationIfNeededAndUpdate(final Player player,
|
public synchronized void createNotificationIfNeededAndUpdate(final boolean forceRecreate) {
|
||||||
final boolean forceRecreate) {
|
|
||||||
if (forceRecreate || notificationBuilder == null) {
|
if (forceRecreate || notificationBuilder == null) {
|
||||||
notificationBuilder = createNotification(player);
|
notificationBuilder = createNotification();
|
||||||
}
|
}
|
||||||
updateNotification(player);
|
updateNotification();
|
||||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized NotificationCompat.Builder createNotification(final Player player) {
|
private synchronized NotificationCompat.Builder createNotification() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "createNotification()");
|
Log.d(TAG, "createNotification()");
|
||||||
}
|
}
|
||||||
@ -93,7 +85,7 @@ public final class NotificationUtil {
|
|||||||
new NotificationCompat.Builder(player.getContext(),
|
new NotificationCompat.Builder(player.getContext(),
|
||||||
player.getContext().getString(R.string.notification_channel_id));
|
player.getContext().getString(R.string.notification_channel_id));
|
||||||
|
|
||||||
initializeNotificationSlots(player);
|
initializeNotificationSlots();
|
||||||
|
|
||||||
// count the number of real slots, to make sure compact slots indices are not out of bound
|
// count the number of real slots, to make sure compact slots indices are not out of bound
|
||||||
int nonNothingSlotCount = 5;
|
int nonNothingSlotCount = 5;
|
||||||
@ -132,30 +124,29 @@ public final class NotificationUtil {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the notification builder and the button icons depending on the playback state.
|
* Updates the notification builder and the button icons depending on the playback state.
|
||||||
* @param player the player currently open, to take data from
|
|
||||||
*/
|
*/
|
||||||
private synchronized void updateNotification(final Player player) {
|
private synchronized void updateNotification() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "updateNotification()");
|
Log.d(TAG, "updateNotification()");
|
||||||
}
|
}
|
||||||
|
|
||||||
// also update content intent, in case the user switched players
|
// also update content intent, in case the user switched players
|
||||||
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
|
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
|
||||||
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
|
NOTIFICATION_ID, getIntentForNotification(), FLAG_UPDATE_CURRENT));
|
||||||
notificationBuilder.setContentTitle(player.getVideoTitle());
|
notificationBuilder.setContentTitle(player.getVideoTitle());
|
||||||
notificationBuilder.setContentText(player.getUploaderName());
|
notificationBuilder.setContentText(player.getUploaderName());
|
||||||
notificationBuilder.setTicker(player.getVideoTitle());
|
notificationBuilder.setTicker(player.getVideoTitle());
|
||||||
updateActions(notificationBuilder, player);
|
updateActions(notificationBuilder);
|
||||||
final boolean showThumbnail = player.getPrefs().getBoolean(
|
final boolean showThumbnail = player.getPrefs().getBoolean(
|
||||||
player.getContext().getString(R.string.show_thumbnail_key), true);
|
player.getContext().getString(R.string.show_thumbnail_key), true);
|
||||||
if (showThumbnail) {
|
if (showThumbnail) {
|
||||||
setLargeIcon(notificationBuilder, player);
|
setLargeIcon(notificationBuilder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
boolean shouldUpdateBufferingSlot() {
|
public boolean shouldUpdateBufferingSlot() {
|
||||||
if (notificationBuilder == null) {
|
if (notificationBuilder == null) {
|
||||||
// if there is no notification active, there is no point in updating it
|
// if there is no notification active, there is no point in updating it
|
||||||
return false;
|
return false;
|
||||||
@ -173,22 +164,22 @@ public final class NotificationUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void createNotificationAndStartForeground(final Player player, final Service service) {
|
public void createNotificationAndStartForeground() {
|
||||||
if (notificationBuilder == null) {
|
if (notificationBuilder == null) {
|
||||||
notificationBuilder = createNotification(player);
|
notificationBuilder = createNotification();
|
||||||
}
|
}
|
||||||
updateNotification(player);
|
updateNotification();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
service.startForeground(NOTIFICATION_ID, notificationBuilder.build(),
|
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build(),
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||||
} else {
|
} else {
|
||||||
service.startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancelNotificationAndStopForeground(final Service service) {
|
public void cancelNotificationAndStopForeground() {
|
||||||
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
ServiceCompat.stopForeground(player.getService(), ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||||
|
|
||||||
if (notificationManager != null) {
|
if (notificationManager != null) {
|
||||||
notificationManager.cancel(NOTIFICATION_ID);
|
notificationManager.cancel(NOTIFICATION_ID);
|
||||||
@ -202,7 +193,7 @@ public final class NotificationUtil {
|
|||||||
// ACTIONS
|
// ACTIONS
|
||||||
/////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void initializeNotificationSlots(final Player player) {
|
private void initializeNotificationSlots() {
|
||||||
for (int i = 0; i < 5; ++i) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
notificationSlots[i] = player.getPrefs().getInt(
|
notificationSlots[i] = player.getPrefs().getInt(
|
||||||
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||||
@ -211,17 +202,16 @@ public final class NotificationUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private void updateActions(final NotificationCompat.Builder builder, final Player player) {
|
private void updateActions(final NotificationCompat.Builder builder) {
|
||||||
builder.mActions.clear();
|
builder.mActions.clear();
|
||||||
for (int i = 0; i < 5; ++i) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
addAction(builder, player, notificationSlots[i]);
|
addAction(builder, notificationSlots[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addAction(final NotificationCompat.Builder builder,
|
private void addAction(final NotificationCompat.Builder builder,
|
||||||
final Player player,
|
|
||||||
@NotificationConstants.Action final int slot) {
|
@NotificationConstants.Action final int slot) {
|
||||||
final NotificationCompat.Action action = getAction(player, slot);
|
final NotificationCompat.Action action = getAction(slot);
|
||||||
if (action != null) {
|
if (action != null) {
|
||||||
builder.addAction(action);
|
builder.addAction(action);
|
||||||
}
|
}
|
||||||
@ -229,41 +219,40 @@ public final class NotificationUtil {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private NotificationCompat.Action getAction(
|
private NotificationCompat.Action getAction(
|
||||||
final Player player,
|
|
||||||
@NotificationConstants.Action final int selectedAction) {
|
@NotificationConstants.Action final int selectedAction) {
|
||||||
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
||||||
switch (selectedAction) {
|
switch (selectedAction) {
|
||||||
case NotificationConstants.PREVIOUS:
|
case NotificationConstants.PREVIOUS:
|
||||||
return getAction(player, baseActionIcon,
|
return getAction(baseActionIcon,
|
||||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||||
|
|
||||||
case NotificationConstants.NEXT:
|
case NotificationConstants.NEXT:
|
||||||
return getAction(player, baseActionIcon,
|
return getAction(baseActionIcon,
|
||||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||||
|
|
||||||
case NotificationConstants.REWIND:
|
case NotificationConstants.REWIND:
|
||||||
return getAction(player, baseActionIcon,
|
return getAction(baseActionIcon,
|
||||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||||
|
|
||||||
case NotificationConstants.FORWARD:
|
case NotificationConstants.FORWARD:
|
||||||
return getAction(player, baseActionIcon,
|
return getAction(baseActionIcon,
|
||||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||||
|
|
||||||
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||||
return getAction(player, R.drawable.exo_notification_previous,
|
return getAction(R.drawable.exo_notification_previous,
|
||||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||||
} else {
|
} else {
|
||||||
return getAction(player, R.drawable.exo_controls_rewind,
|
return getAction(R.drawable.exo_controls_rewind,
|
||||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||||
}
|
}
|
||||||
|
|
||||||
case NotificationConstants.SMART_FORWARD_NEXT:
|
case NotificationConstants.SMART_FORWARD_NEXT:
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||||
return getAction(player, R.drawable.exo_notification_next,
|
return getAction(R.drawable.exo_notification_next,
|
||||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||||
} else {
|
} else {
|
||||||
return getAction(player, R.drawable.exo_controls_fastforward,
|
return getAction(R.drawable.exo_controls_fastforward,
|
||||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,44 +266,45 @@ public final class NotificationUtil {
|
|||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fallthrough
|
||||||
case NotificationConstants.PLAY_PAUSE:
|
case NotificationConstants.PLAY_PAUSE:
|
||||||
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
||||||
return getAction(player, R.drawable.ic_replay,
|
return getAction(R.drawable.ic_replay,
|
||||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||||
} else if (player.isPlaying()
|
} else if (player.isPlaying()
|
||||||
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
||||||
|| player.getCurrentState() == Player.STATE_BLOCKED
|
|| player.getCurrentState() == Player.STATE_BLOCKED
|
||||||
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
||||||
return getAction(player, R.drawable.exo_notification_pause,
|
return getAction(R.drawable.exo_notification_pause,
|
||||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||||
} else {
|
} else {
|
||||||
return getAction(player, R.drawable.exo_notification_play,
|
return getAction(R.drawable.exo_notification_play,
|
||||||
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
|
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
case NotificationConstants.REPEAT:
|
case NotificationConstants.REPEAT:
|
||||||
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
||||||
return getAction(player, R.drawable.exo_media_action_repeat_all,
|
return getAction(R.drawable.exo_media_action_repeat_all,
|
||||||
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
|
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
|
||||||
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
||||||
return getAction(player, R.drawable.exo_media_action_repeat_one,
|
return getAction(R.drawable.exo_media_action_repeat_one,
|
||||||
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
|
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
|
||||||
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
||||||
return getAction(player, R.drawable.exo_media_action_repeat_off,
|
return getAction(R.drawable.exo_media_action_repeat_off,
|
||||||
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
|
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
case NotificationConstants.SHUFFLE:
|
case NotificationConstants.SHUFFLE:
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
||||||
return getAction(player, R.drawable.exo_controls_shuffle_on,
|
return getAction(R.drawable.exo_controls_shuffle_on,
|
||||||
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
|
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
|
||||||
} else {
|
} else {
|
||||||
return getAction(player, R.drawable.exo_controls_shuffle_off,
|
return getAction(R.drawable.exo_controls_shuffle_off,
|
||||||
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
|
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
case NotificationConstants.CLOSE:
|
case NotificationConstants.CLOSE:
|
||||||
return getAction(player, R.drawable.ic_close,
|
return getAction(R.drawable.ic_close,
|
||||||
R.string.close, ACTION_CLOSE);
|
R.string.close, ACTION_CLOSE);
|
||||||
|
|
||||||
case NotificationConstants.NOTHING:
|
case NotificationConstants.NOTHING:
|
||||||
@ -324,8 +314,7 @@ public final class NotificationUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private NotificationCompat.Action getAction(final Player player,
|
private NotificationCompat.Action getAction(@DrawableRes final int drawable,
|
||||||
@DrawableRes final int drawable,
|
|
||||||
@StringRes final int title,
|
@StringRes final int title,
|
||||||
final String intentAction) {
|
final String intentAction) {
|
||||||
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
|
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
|
||||||
@ -333,7 +322,7 @@ public final class NotificationUtil {
|
|||||||
new Intent(intentAction), FLAG_UPDATE_CURRENT));
|
new Intent(intentAction), FLAG_UPDATE_CURRENT));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getIntentForNotification(final Player player) {
|
private Intent getIntentForNotification() {
|
||||||
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
|
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
|
||||||
// Means we play in popup or audio only. Let's show the play queue
|
// Means we play in popup or audio only. Let's show the play queue
|
||||||
return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
|
return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
|
||||||
@ -353,7 +342,7 @@ public final class NotificationUtil {
|
|||||||
// BITMAP
|
// BITMAP
|
||||||
/////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) {
|
private void setLargeIcon(final NotificationCompat.Builder builder) {
|
||||||
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
|
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
|
||||||
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
|
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
|
||||||
false);
|
false);
|
@ -8,6 +8,7 @@ import android.support.v4.media.MediaMetadataCompat;
|
|||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
|
|
||||||
public class PlayerMediaSession implements MediaSessionCallback {
|
public class PlayerMediaSession implements MediaSessionCallback {
|
||||||
private final Player player;
|
private final Player player;
|
||||||
@ -89,7 +90,7 @@ public class PlayerMediaSession implements MediaSessionCallback {
|
|||||||
public void play() {
|
public void play() {
|
||||||
player.play();
|
player.play();
|
||||||
// hide the player controls even if the play command came from the media session
|
// 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
|
@Override
|
||||||
|
979
app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
Normal file
979
app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
Normal file
@ -0,0 +1,979 @@
|
|||||||
|
package org.schabi.newpipe.player.ui;
|
||||||
|
|
||||||
|
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
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.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 static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.database.ContentObserver;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
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.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.fragment.app.FragmentActivity;
|
||||||
|
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.local.dialog.PlaylistDialog;
|
||||||
|
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;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener {
|
||||||
|
private static final String TAG = MainPlayerUi.class.getSimpleName();
|
||||||
|
|
||||||
|
// see the Javadoc of calculateMaxEndScreenThumbnailHeight for information
|
||||||
|
private static final int DETAIL_ROOT_MINIMUM_HEIGHT = 85; // dp
|
||||||
|
private static final int DETAIL_TITLE_TEXT_SIZE_TV = 16; // sp
|
||||||
|
private static final int DETAIL_TITLE_TEXT_SIZE_TABLET = 15; // sp
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Constructor, setup, destroy
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Constructor, setup, destroy
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
getParentActivity().map(FragmentActivity::getSupportFragmentManager)
|
||||||
|
.ifPresent(fragmentManager ->
|
||||||
|
PlaylistDialog.showForPlayQueue(player, fragmentManager)));
|
||||||
|
|
||||||
|
settingsContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
|
||||||
|
@Override
|
||||||
|
public void onChange(final boolean selfChange) {
|
||||||
|
setupScreenRotationButton();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
context.getContentResolver().registerContentObserver(
|
||||||
|
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
|
||||||
|
settingsContentObserver);
|
||||||
|
|
||||||
|
binding.getRoot().addOnLayoutChangeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void deinitListeners() {
|
||||||
|
super.deinitListeners();
|
||||||
|
|
||||||
|
binding.queueButton.setOnClickListener(null);
|
||||||
|
binding.segmentsButton.setOnClickListener(null);
|
||||||
|
binding.addToPlaylistButton.setOnClickListener(null);
|
||||||
|
|
||||||
|
context.getContentResolver().unregisterContentObserver(settingsContentObserver);
|
||||||
|
|
||||||
|
binding.getRoot().removeOnLayoutChangeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
|
||||||
|
// 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(MATCH_PARENT, 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 = 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);
|
||||||
|
|
||||||
|
binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||||
|
binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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:
|
||||||
|
getParentActivity().ifPresent(activity -> {
|
||||||
|
player.setRecovery();
|
||||||
|
NavigationHelper.playOnPopupPlayer(activity, player.getPlayQueue(), true);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case MINIMIZE_ON_EXIT_MODE_NONE: default:
|
||||||
|
player.pause();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playback states
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Playback states
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaying() {
|
||||||
|
super.onPlaying();
|
||||||
|
checkLandscape();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompleted() {
|
||||||
|
super.onCompleted();
|
||||||
|
if (isFullscreen) {
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Controls showing / hiding
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Controls showing / hiding
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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) {
|
||||||
|
getParentActivity().map(Activity::getWindow).ifPresent(window -> {
|
||||||
|
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
|
||||||
|
* {@link #DETAIL_ROOT_MINIMUM_HEIGHT} 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 {@link #DETAIL_TITLE_TEXT_SIZE_TABLET} on tablets and
|
||||||
|
* {@link #DETAIL_TITLE_TEXT_SIZE_TV} on TVs, see {@link R.id.titleTextView}.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* Otherwise, the max thumbnail height is the screen height.
|
||||||
|
* </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(DETAIL_ROOT_MINIMUM_HEIGHT, context)
|
||||||
|
+ DeviceUtils.spToPx(DETAIL_TITLE_TEXT_SIZE_TV, context);
|
||||||
|
return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
|
||||||
|
} else if (DeviceUtils.isTablet(context) && isLandscape() && !isFullscreen()) {
|
||||||
|
final int videoInfoHeight = DeviceUtils.dpToPx(DETAIL_ROOT_MINIMUM_HEIGHT, context)
|
||||||
|
+ DeviceUtils.spToPx(DETAIL_TITLE_TEXT_SIZE_TABLET, context);
|
||||||
|
return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
|
||||||
|
} else { // fullscreen player: max height is the device height
|
||||||
|
return Math.min(bitmap.getHeight(), screenHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Captions (text tracks)
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Captions (text tracks)
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupSubtitleView(final 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);
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Gestures
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Gestures
|
||||||
|
|
||||||
|
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||||
|
@Override
|
||||||
|
public 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 a smaller value to be consistent across screen orientations, and to make usage
|
||||||
|
// easier. Multiply by 3/4 to ensure the user does not need to move the finger up to the
|
||||||
|
// screen border, in order to reach the maximum volume/brightness.
|
||||||
|
final int width = r - l;
|
||||||
|
final int height = b - t;
|
||||||
|
final int min = Math.min(width, height);
|
||||||
|
final int maxGestureLength = (int) (min * 0.75);
|
||||||
|
|
||||||
|
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.0f));
|
||||||
|
|
||||||
|
// 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().orElse(null);
|
||||||
|
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() {
|
||||||
|
final AppCompatActivity activity = getParentActivity().orElse(null);
|
||||||
|
if (activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
|
||||||
|
player.getPlaybackSkipSilence(), player::setPlaybackParameters)
|
||||||
|
.show(activity.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, orientation, fullscreen
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Video size, 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) {
|
||||||
|
// 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);
|
||||||
|
} else {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
fragmentListener.onFullscreenStateChanged(isFullscreen);
|
||||||
|
|
||||||
|
binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||||
|
binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||||
|
binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : 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 Optional<AppCompatActivity> getParentActivity() {
|
||||||
|
final ViewParent rootParent = binding.getRoot().getParent();
|
||||||
|
if (rootParent instanceof ViewGroup) {
|
||||||
|
final Context activity = ((ViewGroup) rootParent).getContext();
|
||||||
|
if (activity instanceof AppCompatActivity) {
|
||||||
|
return Optional.of((AppCompatActivity) activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLandscape() {
|
||||||
|
// DisplayMetrics from activity context knows about MultiWindow feature
|
||||||
|
// while DisplayMetrics from app context doesn't
|
||||||
|
return DeviceUtils.isLandscape(
|
||||||
|
getParentActivity().map(Context.class::cast).orElse(player.getService()));
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
}
|
211
app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
Normal file
211
app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A player UI is a component that can seamlessly connect and disconnect from the {@link Player} and
|
||||||
|
* provide a user interface of some sort. Try to extend this class instead of adding more code to
|
||||||
|
* {@link Player}!
|
||||||
|
*/
|
||||||
|
public abstract class PlayerUi {
|
||||||
|
|
||||||
|
@NonNull protected final Context context;
|
||||||
|
@NonNull protected final Player player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param player the player instance that will be usable throughout the lifetime of this UI
|
||||||
|
*/
|
||||||
|
protected PlayerUi(@NonNull final Player player) {
|
||||||
|
this.context = player.getContext();
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the player instance this UI was constructed with
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public Player getPlayer() {
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the player received an intent and processed it.
|
||||||
|
*/
|
||||||
|
public void setupAfterIntent() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called right after the exoplayer instance is constructed, or right after this UI is
|
||||||
|
* constructed if the exoplayer is already available then. Note that the exoplayer instance
|
||||||
|
* could be built and destroyed multiple times during the lifetime of the player, so this method
|
||||||
|
* might be called multiple times.
|
||||||
|
*/
|
||||||
|
public void initPlayer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when playback in the exoplayer is about to start, or right after this UI is
|
||||||
|
* constructed if the exoplayer and the play queue are already available then. The play queue
|
||||||
|
* will therefore always be not null.
|
||||||
|
*/
|
||||||
|
public void initPlayback() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the exoplayer instance is about to be destroyed. Note that the exoplayer instance
|
||||||
|
* could be built and destroyed multiple times during the lifetime of the player, so this method
|
||||||
|
* might be called multiple times. Be sure to unset any video surface view or play queue
|
||||||
|
* listeners! This will also be called when this UI is being discarded, just before {@link
|
||||||
|
* #destroy()}.
|
||||||
|
*/
|
||||||
|
public void destroyPlayer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when this UI is being discarded, either because the player is switching to a different
|
||||||
|
* UI or because the player is shutting down completely.
|
||||||
|
*/
|
||||||
|
public void destroy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the player is smooth-stopping, that is, transitioning smoothly to a new play
|
||||||
|
* queue after the user tapped on a new video stream while a stream was playing in the video
|
||||||
|
* detail fragment.
|
||||||
|
*/
|
||||||
|
public void smoothStopForImmediateReusing() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the video detail fragment listener is connected with the player, or right after
|
||||||
|
* this UI is constructed if the listener is already connected then.
|
||||||
|
*/
|
||||||
|
public void onFragmentListenerSet() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts that the player receives will also be notified to UIs here. If you want to
|
||||||
|
* register new broadcast actions to receive here, add them to {@link
|
||||||
|
* Player#setupBroadcastReceiver()}.
|
||||||
|
* @param intent the broadcast intent received by the player
|
||||||
|
*/
|
||||||
|
public void onBroadcastReceived(final Intent intent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when stream progress (i.e. the current time in the seekbar) or stream duration change.
|
||||||
|
* Will surely be called every {@link Player#PROGRESS_LOOP_INTERVAL_MILLIS} while a stream is
|
||||||
|
* playing.
|
||||||
|
* @param currentProgress the current progress in milliseconds
|
||||||
|
* @param duration the duration of the stream being played
|
||||||
|
* @param bufferPercent the percentage of stream already buffered, see {@link
|
||||||
|
* com.google.android.exoplayer2.BasePlayer#getBufferedPercentage()}
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see com.google.android.exoplayer2.Player.Listener#onTracksChanged(Tracks)
|
||||||
|
* @param currentTracks the available tracks information
|
||||||
|
*/
|
||||||
|
public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see com.google.android.exoplayer2.Player.Listener#onPlaybackParametersChanged
|
||||||
|
* @param playbackParameters the new playback parameters
|
||||||
|
*/
|
||||||
|
public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see com.google.android.exoplayer2.Player.Listener#onRenderedFirstFrame
|
||||||
|
*/
|
||||||
|
public void onRenderedFirstFrame() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see com.google.android.exoplayer2.text.TextOutput#onCues
|
||||||
|
* @param cues the cues to pass to the subtitle view
|
||||||
|
*/
|
||||||
|
public void onCues(@NonNull final List<Cue> cues) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the stream being played changes.
|
||||||
|
* @param info the {@link StreamInfo} metadata object, along with data about the selected and
|
||||||
|
* available video streams (to be used to build the resolution menus, for example)
|
||||||
|
*/
|
||||||
|
public void onMetadataChanged(@NonNull final StreamInfo info) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the thumbnail for the current metadata was loaded.
|
||||||
|
* @param bitmap the thumbnail to process, or null if there is no thumbnail or there was an
|
||||||
|
* error when loading the thumbnail
|
||||||
|
*/
|
||||||
|
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the play queue was edited: a stream was appended, moved or removed.
|
||||||
|
*/
|
||||||
|
public void onPlayQueueEdited() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param videoSize the new video size, useful to set the surface aspect ratio
|
||||||
|
* @see com.google.android.exoplayer2.Player.Listener#onVideoSizeChanged
|
||||||
|
*/
|
||||||
|
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
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<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the provided player ui to the list and calls on it the initialization functions that
|
||||||
|
* apply based on the current player state. The preparation step needs to be done since when UIs
|
||||||
|
* are removed and re-added, the player will not call e.g. initPlayer again since the exoplayer
|
||||||
|
* is already initialized, but we need to notify the newly built UI that the player is ready
|
||||||
|
* nonetheless.
|
||||||
|
* @param playerUi the player ui to prepare and add to the list; its {@link
|
||||||
|
* PlayerUi#getPlayer()} will be used to query information about the player
|
||||||
|
* state
|
||||||
|
*/
|
||||||
|
public void addAndPrepare(final PlayerUi playerUi) {
|
||||||
|
if (playerUi.getPlayer().getFragmentListener().isPresent()) {
|
||||||
|
// make sure UIs know whether a service is connected or not
|
||||||
|
playerUi.onFragmentListenerSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playerUi.getPlayer().exoPlayerIsNull()) {
|
||||||
|
playerUi.initPlayer();
|
||||||
|
if (playerUi.getPlayer().getPlayQueue() != null) {
|
||||||
|
playerUi.initPlayback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerUis.add(playerUi);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys all matching player UIs and removes them from the list.
|
||||||
|
* @param playerUiType the class of the player UI to destroy; the {@link
|
||||||
|
* Class#isInstance(Object)} method will be used, so even subclasses will be
|
||||||
|
* destroyed and removed
|
||||||
|
* @param <T> the class type parameter
|
||||||
|
*/
|
||||||
|
public <T> void destroyAll(final Class<T> playerUiType) {
|
||||||
|
playerUis.stream()
|
||||||
|
.filter(playerUiType::isInstance)
|
||||||
|
.forEach(playerUi -> {
|
||||||
|
playerUi.destroyPlayer();
|
||||||
|
playerUi.destroy();
|
||||||
|
});
|
||||||
|
playerUis.removeIf(playerUiType::isInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param playerUiType the class of the player UI to return; the {@link
|
||||||
|
* Class#isInstance(Object)} method will be used, so even subclasses could
|
||||||
|
* be returned
|
||||||
|
* @param <T> the class type parameter
|
||||||
|
* @return the first player UI of the required type found in the list, or an empty {@link
|
||||||
|
* Optional} otherwise
|
||||||
|
*/
|
||||||
|
public <T> Optional<T> get(final Class<T> playerUiType) {
|
||||||
|
return playerUis.stream()
|
||||||
|
.filter(playerUiType::isInstance)
|
||||||
|
.map(playerUiType::cast)
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the provided consumer on all player UIs in the list.
|
||||||
|
* @param consumer the consumer to call with player UIs
|
||||||
|
*/
|
||||||
|
public void call(final Consumer<PlayerUi> consumer) {
|
||||||
|
//noinspection SimplifyStreamApiCallChains
|
||||||
|
playerUis.stream().forEach(consumer);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,588 @@
|
|||||||
|
package org.schabi.newpipe.player.ui;
|
||||||
|
|
||||||
|
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
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();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
|
||||||
|
* NewPipe's popup player.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This value is hardcoded instead of being get dynamically with the method linked of the
|
||||||
|
* constant documentation below, because it is not static and popup player layout parameters
|
||||||
|
* are generated with static methods.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
|
||||||
|
*/
|
||||||
|
private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Constructor, setup, destroy
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Constructor, setup, destroy
|
||||||
|
|
||||||
|
public PopupPlayerUi(@NonNull final Player player,
|
||||||
|
@NonNull final PlayerBinding playerBinding) {
|
||||||
|
super(player, playerBinding);
|
||||||
|
windowManager = ContextCompat.getSystemService(context, WindowManager.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setupAfterIntent() {
|
||||||
|
super.setupAfterIntent();
|
||||||
|
initPopup();
|
||||||
|
initPopupCloseOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
|
||||||
|
|
||||||
|
checkPopupPositionBounds();
|
||||||
|
|
||||||
|
binding.loadingPanel.setMinimumWidth(popupLayoutParams.width);
|
||||||
|
binding.loadingPanel.setMinimumHeight(popupLayoutParams.height);
|
||||||
|
|
||||||
|
windowManager.addView(binding.getRoot(), popupLayoutParams);
|
||||||
|
setupVideoSurfaceIfNeeded(); // now there is a parent, we can setup video surface
|
||||||
|
|
||||||
|
// 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 = 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();
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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 (player.isPlaying() || player.isLoading()) {
|
||||||
|
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
|
||||||
|
// Use only audio source when screen turns off while popup player is playing
|
||||||
|
player.useVideoSource(false);
|
||||||
|
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
|
||||||
|
// Restore video source when screen turns on and user was watching video in popup
|
||||||
|
player.useVideoSource(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Popup position and size
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Popup position and size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = Math.min((int) Math.max(width, minimumWidth), screenWidth);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Popup closing
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Popup closing
|
||||||
|
|
||||||
|
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() {
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playback states
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Playback states
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Popup & closing overlay layout params + saving popup position and size
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Popup & closing overlay layout params + saving popup position and size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code screenWidth} and {@code screenHeight} must have been initialized.
|
||||||
|
* @return the popup starting layout params
|
||||||
|
*/
|
||||||
|
@SuppressLint("RtlHardcoded")
|
||||||
|
public WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs() {
|
||||||
|
final SharedPreferences prefs = getPlayer().getPrefs();
|
||||||
|
final Context context = 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
|
||||||
|
? prefs.getFloat(context.getString(R.string.popup_saved_width_key), defaultSize)
|
||||||
|
: defaultSize;
|
||||||
|
final float popupHeight = getMinimumVideoHeight(popupWidth);
|
||||||
|
|
||||||
|
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
|
||||||
|
(int) popupWidth, (int) popupHeight,
|
||||||
|
popupLayoutParamType(),
|
||||||
|
IDLE_WINDOW_FLAGS,
|
||||||
|
PixelFormat.TRANSLUCENT);
|
||||||
|
params.gravity = Gravity.LEFT | Gravity.TOP;
|
||||||
|
params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||||
|
|
||||||
|
final int centerX = (int) (screenWidth / 2f - popupWidth / 2f);
|
||||||
|
final int centerY = (int) (screenHeight / 2f - popupHeight / 2f);
|
||||||
|
params.x = popupRememberSizeAndPos
|
||||||
|
? prefs.getInt(context.getString(R.string.popup_saved_x_key), centerX) : centerX;
|
||||||
|
params.y = popupRememberSizeAndPos
|
||||||
|
? prefs.getInt(context.getString(R.string.popup_saved_y_key), centerY) : centerY;
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void savePopupPositionAndSizeToPrefs() {
|
||||||
|
if (getPopupLayoutParams() != null) {
|
||||||
|
final Context context = getPlayer().getContext();
|
||||||
|
getPlayer().getPrefs().edit()
|
||||||
|
.putFloat(context.getString(R.string.popup_saved_width_key),
|
||||||
|
popupLayoutParams.width)
|
||||||
|
.putInt(context.getString(R.string.popup_saved_x_key),
|
||||||
|
popupLayoutParams.x)
|
||||||
|
.putInt(context.getString(R.string.popup_saved_y_key),
|
||||||
|
popupLayoutParams.y)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RtlHardcoded")
|
||||||
|
public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
|
||||||
|
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||||
|
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||||
|
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
|
||||||
|
|
||||||
|
final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
popupLayoutParamType(),
|
||||||
|
flags,
|
||||||
|
PixelFormat.TRANSLUCENT);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
// Setting maximum opacity allowed for touch events to other apps for Android 12 and
|
||||||
|
// higher to prevent non interaction when using other apps with the popup player
|
||||||
|
closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||||
|
closeOverlayLayoutParams.softInputMode =
|
||||||
|
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||||
|
return closeOverlayLayoutParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int popupLayoutParamType() {
|
||||||
|
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||||
|
? WindowManager.LayoutParams.TYPE_PHONE
|
||||||
|
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Getters
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Getters
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
1591
app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
Normal file
1591
app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,7 @@
|
|||||||
package org.schabi.newpipe.settings.custom;
|
package org.schabi.newpipe.settings.custom;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
@ -23,11 +25,11 @@ import androidx.core.graphics.drawable.DrawableCompat;
|
|||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceViewHolder;
|
import androidx.preference.PreferenceViewHolder;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
import org.schabi.newpipe.player.notification.NotificationConstants;
|
||||||
import org.schabi.newpipe.player.NotificationConstants;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.views.FocusOverlayView;
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
@ -61,7 +63,9 @@ public class NotificationActionsPreference extends Preference {
|
|||||||
public void onDetached() {
|
public void onDetached() {
|
||||||
super.onDetached();
|
super.onDetached();
|
||||||
saveChanges();
|
saveChanges();
|
||||||
getContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION));
|
// set package to this app's package to prevent the intent from being seen outside
|
||||||
|
getContext().sendBroadcast(new Intent(ACTION_RECREATE_NOTIFICATION)
|
||||||
|
.setPackage(App.PACKAGE_NAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,10 +50,10 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
|
|||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
|
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
|
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
|
||||||
import org.schabi.newpipe.player.PlayQueueActivity;
|
import org.schabi.newpipe.player.PlayQueueActivity;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
@ -91,7 +91,7 @@ public final class NavigationHelper {
|
|||||||
intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey);
|
intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal());
|
intent.putExtra(Player.PLAYER_TYPE, PlayerType.MAIN.valueForIntent());
|
||||||
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
|
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
|
||||||
|
|
||||||
return intent;
|
return intent;
|
||||||
@ -163,8 +163,8 @@ public final class NavigationHelper {
|
|||||||
|
|
||||||
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
|
final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
|
||||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal());
|
intent.putExtra(Player.PLAYER_TYPE, PlayerType.POPUP.valueForIntent());
|
||||||
ContextCompat.startForegroundService(context, intent);
|
ContextCompat.startForegroundService(context, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,8 +174,8 @@ public final class NavigationHelper {
|
|||||||
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
|
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
|
||||||
.show();
|
.show();
|
||||||
|
|
||||||
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
|
final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
|
||||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal());
|
intent.putExtra(Player.PLAYER_TYPE, PlayerType.AUDIO.valueForIntent());
|
||||||
ContextCompat.startForegroundService(context, intent);
|
ContextCompat.startForegroundService(context, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,17 +184,17 @@ public final class NavigationHelper {
|
|||||||
final PlayQueue queue,
|
final PlayQueue queue,
|
||||||
final PlayerType playerType) {
|
final PlayerType playerType) {
|
||||||
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
|
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());
|
intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent());
|
||||||
ContextCompat.startForegroundService(context, intent);
|
ContextCompat.startForegroundService(context, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void enqueueOnPlayer(final Context context, final PlayQueue queue) {
|
public static void enqueueOnPlayer(final Context context, final PlayQueue queue) {
|
||||||
PlayerType playerType = PlayerHolder.getInstance().getType();
|
PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
if (playerType == null) {
|
||||||
Log.e(TAG, "Enqueueing but no player is open; defaulting to background player");
|
Log.e(TAG, "Enqueueing but no player is open; defaulting to background player");
|
||||||
playerType = MainPlayer.PlayerType.AUDIO;
|
playerType = PlayerType.AUDIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueueOnPlayer(context, queue, playerType);
|
enqueueOnPlayer(context, queue, playerType);
|
||||||
@ -203,14 +203,14 @@ public final class NavigationHelper {
|
|||||||
/* ENQUEUE NEXT */
|
/* ENQUEUE NEXT */
|
||||||
public static void enqueueNextOnPlayer(final Context context, final PlayQueue queue) {
|
public static void enqueueNextOnPlayer(final Context context, final PlayQueue queue) {
|
||||||
PlayerType playerType = PlayerHolder.getInstance().getType();
|
PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
if (playerType == null) {
|
||||||
Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player");
|
Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player");
|
||||||
playerType = MainPlayer.PlayerType.AUDIO;
|
playerType = PlayerType.AUDIO;
|
||||||
}
|
}
|
||||||
Toast.makeText(context, R.string.enqueued_next, Toast.LENGTH_SHORT).show();
|
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());
|
intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent());
|
||||||
ContextCompat.startForegroundService(context, intent);
|
ContextCompat.startForegroundService(context, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,14 +414,14 @@ public final class NavigationHelper {
|
|||||||
final boolean switchingPlayers) {
|
final boolean switchingPlayers) {
|
||||||
|
|
||||||
final boolean autoPlay;
|
final boolean autoPlay;
|
||||||
@Nullable final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
|
@Nullable final PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
if (playerType == null) {
|
||||||
// no player open
|
// no player open
|
||||||
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
||||||
} else if (switchingPlayers) {
|
} else if (switchingPlayers) {
|
||||||
// switching player to main player
|
// switching player to main player
|
||||||
autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state
|
autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state
|
||||||
} else if (playerType == MainPlayer.PlayerType.VIDEO) {
|
} else if (playerType == PlayerType.MAIN) {
|
||||||
// opening new stream while already playing in main player
|
// opening new stream while already playing in main player
|
||||||
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
||||||
} else {
|
} else {
|
||||||
@ -436,7 +436,7 @@ public final class NavigationHelper {
|
|||||||
// Situation when user switches from players to main player. All needed data is
|
// Situation when user switches from players to main player. All needed data is
|
||||||
// here, we can start watching (assuming newQueue equals playQueue).
|
// here, we can start watching (assuming newQueue equals playQueue).
|
||||||
// Starting directly in fullscreen if the previous player type was popup.
|
// Starting directly in fullscreen if the previous player type was popup.
|
||||||
detailFragment.openVideoPlayer(playerType == MainPlayer.PlayerType.POPUP
|
detailFragment.openVideoPlayer(playerType == PlayerType.POPUP
|
||||||
|| PlayerHelper.isStartMainPlayerFullscreenEnabled(context));
|
|| PlayerHelper.isStartMainPlayerFullscreenEnabled(context));
|
||||||
} else {
|
} else {
|
||||||
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
|
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
|
||||||
|
@ -244,6 +244,22 @@ public final class ThemeHelper {
|
|||||||
return AppCompatResources.getDrawable(context, typedValue.resourceId);
|
return AppCompatResources.getDrawable(context, typedValue.resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a runtime dimen from the {@code android} package. Should be used for dimens for which
|
||||||
|
* normal accessing with {@code R.dimen.} is not available.
|
||||||
|
*
|
||||||
|
* @param context context
|
||||||
|
* @param name dimen resource name (e.g. navigation_bar_height)
|
||||||
|
* @return the obtained dimension, in pixels, or 0 if the resource could not be resolved
|
||||||
|
*/
|
||||||
|
public static int getAndroidDimenPx(@NonNull final Context context, final String name) {
|
||||||
|
final int resId = context.getResources().getIdentifier(name, "dimen", "android");
|
||||||
|
if (resId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return context.getResources().getDimensionPixelSize(resId);
|
||||||
|
}
|
||||||
|
|
||||||
private static String getSelectedThemeKey(final Context context) {
|
private static String getSelectedThemeKey(final Context context) {
|
||||||
final String themeKey = context.getString(R.string.theme_key);
|
final String themeKey = context.getString(R.string.theme_key);
|
||||||
final String defaultTheme = context.getResources().getString(R.string.default_theme_value);
|
final String defaultTheme = context.getResources().getString(R.string.default_theme_value);
|
||||||
|
@ -12,8 +12,8 @@ import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.START
|
|||||||
import androidx.constraintlayout.widget.ConstraintSet
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
import org.schabi.newpipe.MainActivity
|
import org.schabi.newpipe.MainActivity
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.player.event.DisplayPortion
|
import org.schabi.newpipe.player.gesture.DisplayPortion
|
||||||
import org.schabi.newpipe.player.event.DoubleTapListener
|
import org.schabi.newpipe.player.gesture.DoubleTapListener
|
||||||
|
|
||||||
class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||||
ConstraintLayout(context, attrs), DoubleTapListener {
|
ConstraintLayout(context, attrs), DoubleTapListener {
|
||||||
@ -38,14 +38,14 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
|||||||
|
|
||||||
private var performListener: PerformListener? = null
|
private var performListener: PerformListener? = null
|
||||||
|
|
||||||
fun performListener(listener: PerformListener) = apply {
|
fun performListener(listener: PerformListener?) = apply {
|
||||||
performListener = listener
|
performListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
private var seekSecondsSupplier: () -> Int = { 0 }
|
private var seekSecondsSupplier: () -> Int = { 0 }
|
||||||
|
|
||||||
fun seekSecondsSupplier(supplier: () -> Int) = apply {
|
fun seekSecondsSupplier(supplier: (() -> Int)?) = apply {
|
||||||
seekSecondsSupplier = supplier
|
seekSecondsSupplier = supplier ?: { 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Indicates whether this (double) tap is the first of a series
|
// Indicates whether this (double) tap is the first of a series
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
app:behavior_hideable="true"
|
app:behavior_hideable="true"
|
||||||
app:behavior_peekHeight="0dp"
|
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>
|
</org.schabi.newpipe.views.FocusAwareCoordinator>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user