diff --git a/app/build.gradle b/app/build.gradle index 95d35eeac..2129007ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -164,7 +164,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:6633f26ec5a73a8e932de575b7a0643b6ad6c890' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:2463884aa8b696df5812f7feff553008bbd2f888' implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" implementation "org.jsoup:jsoup:1.13.1" @@ -209,7 +209,7 @@ dependencies { implementation "io.reactivex.rxjava2:rxandroid:2.1.1" implementation "com.jakewharton.rxbinding2:rxbinding:2.2.0" - implementation "org.ocpsoft.prettytime:prettytime:4.0.5.Final" + implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final" } static String getGitWorkingBranch() { diff --git a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java index b5c7fc564..3518aa139 100644 --- a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java +++ b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java @@ -5,7 +5,6 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.widget.OverScroller; import androidx.annotation.NonNull; @@ -14,6 +13,8 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout; import org.schabi.newpipe.R; import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; // See https://stackoverflow.com/questions/56849221#57997489 public final class FlingBehavior extends AppBarLayout.Behavior { @@ -25,6 +26,9 @@ public final class FlingBehavior extends AppBarLayout.Behavior { private boolean allowScroll = true; private final Rect globalRect = new Rect(); + private final List skipInterceptionOfElements = Arrays.asList( + R.id.playQueuePanel, R.id.playbackSeekBar, + R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton); @Override public boolean onRequestChildRectangleOnScreen( @@ -60,20 +64,14 @@ public final class FlingBehavior extends AppBarLayout.Behavior { public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child, final MotionEvent ev) { - final ViewGroup playQueue = child.findViewById(R.id.playQueuePanel); - if (playQueue != null) { - final boolean visible = playQueue.getGlobalVisibleRect(globalRect); - if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) { - allowScroll = false; - return false; - } - } - final View seekBar = child.findViewById(R.id.playbackSeekBar); - if (seekBar != null) { - final boolean visible = seekBar.getGlobalVisibleRect(globalRect); - if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) { - allowScroll = false; - return false; + for (final Integer element : skipInterceptionOfElements) { + final View view = child.findViewById(element); + if (view != null) { + final boolean visible = view.getGlobalVisibleRect(globalRect); + if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) { + allowScroll = false; + return false; + } } } allowScroll = true; diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index aedf64231..e72d4609e 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -20,7 +20,10 @@ package org.schabi.newpipe; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; @@ -101,6 +104,8 @@ public class MainActivity extends AppCompatActivity { private boolean servicesShown = false; private ImageView serviceArrow; + private BroadcastReceiver broadcastReceiver; + private static final int ITEM_ID_SUBSCRIPTIONS = -1; private static final int ITEM_ID_FEED = -2; private static final int ITEM_ID_BOOKMARKS = -3; @@ -147,6 +152,7 @@ public class MainActivity extends AppCompatActivity { if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(this); } + setupBroadcastReceiver(); } private void setupDrawer() throws Exception { @@ -454,6 +460,9 @@ public class MainActivity extends AppCompatActivity { if (!isChangingConfigurations()) { StateSaver.clearStateFiles(); } + if (broadcastReceiver != null) { + unregisterReceiver(broadcastReceiver); + } } @Override @@ -795,9 +804,36 @@ public class MainActivity extends AppCompatActivity { ErrorActivity.reportUiError(this, e); } } - /* - * Utils - * */ + + private void setupBroadcastReceiver() { + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + if (intent.getAction().equals(VideoDetailFragment.ACTION_PLAYER_STARTED)) { + final Fragment fragmentPlayer = getSupportFragmentManager() + .findFragmentById(R.id.fragment_player_holder); + if (fragmentPlayer == null) { + /* + * We still don't have a fragment attached to the activity. + * It can happen when a user started popup or background players + * without opening a stream inside the fragment. + * Adding it in a collapsed state (only mini player will be visible) + * */ + NavigationHelper.showMiniPlayer(getSupportFragmentManager()); + } + /* + * At this point the player is added 100%, we can unregister. + * Other actions are useless since the fragment will not be removed after that + * */ + unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + } + }; + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED); + registerReceiver(broadcastReceiver, intentFilter); + } private boolean bottomSheetHiddenOrCollapsed() { final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 222fc4687..28a67173b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -3,11 +3,9 @@ package org.schabi.newpipe.fragments.detail; import android.animation.ValueAnimator; import android.app.Activity; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.database.ContentObserver; @@ -16,7 +14,8 @@ import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.os.IBinder; +import android.os.Looper; +import android.view.ViewTreeObserver; import androidx.core.text.HtmlCompat; import androidx.preference.PreferenceManager; import android.provider.Settings; @@ -60,6 +59,7 @@ import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; @@ -83,12 +83,11 @@ import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.BasePlayer; import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.VideoPlayer; import org.schabi.newpipe.player.VideoPlayerImpl; import org.schabi.newpipe.player.event.OnKeyDownListener; -import org.schabi.newpipe.player.event.PlayerEventListener; -import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; @@ -98,12 +97,10 @@ import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; -import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.AnimatedProgressBar; @@ -136,8 +133,7 @@ public class VideoDetailFragment SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener, View.OnLongClickListener, - PlayerEventListener, - PlayerServiceEventListener, + PlayerServiceExtendedEventListener, OnKeyDownListener { public static final String AUTO_PLAY = "auto_play"; @@ -150,6 +146,8 @@ public class VideoDetailFragment "org.schabi.newpipe.VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER"; public static final String ACTION_HIDE_MAIN_PLAYER = "org.schabi.newpipe.VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER"; + public static final String ACTION_PLAYER_STARTED = + "org.schabi.newpipe.VideoDetailFragment.ACTION_PLAYER_STARTED"; public static final String ACTION_VIDEO_FRAGMENT_RESUMED = "org.schabi.newpipe.VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED"; public static final String ACTION_VIDEO_FRAGMENT_STOPPED = @@ -159,9 +157,6 @@ public class VideoDetailFragment private static final String RELATED_TAB_TAG = "NEXT VIDEO"; private static final String EMPTY_TAB_TAG = "EMPTY TAB"; - private static final String INFO_KEY = "info_key"; - private static final String STACK_KEY = "stack_key"; - private boolean showRelatedStreams; private boolean showComments; private String selectedTabTag; @@ -174,14 +169,13 @@ public class VideoDetailFragment protected String name; @State protected String url; - @State - protected PlayQueue playQueue; + protected static PlayQueue playQueue; @State int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; @State protected boolean autoPlayEnabled = true; - private StreamInfo currentInfo; + private static StreamInfo currentInfo; private Disposable currentWorker; @NonNull private CompositeDisposable disposables = new CompositeDisposable(); @@ -250,8 +244,6 @@ public class VideoDetailFragment private FrameLayout relatedStreamsLayout; private ContentObserver settingsContentObserver; - private ServiceConnection serviceConnection; - private boolean bound; private MainPlayer playerService; private VideoPlayerImpl player; @@ -259,123 +251,62 @@ public class VideoDetailFragment /*////////////////////////////////////////////////////////////////////////// // Service management //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onServiceConnected(final VideoPlayerImpl connectedPlayer, + final MainPlayer connectedPlayerService, + final boolean playAfterConnect) { + player = connectedPlayer; + playerService = connectedPlayerService; - private ServiceConnection getServiceConnection(final Context context, - final boolean playAfterConnect) { - return new ServiceConnection() { - @Override - public void onServiceDisconnected(final ComponentName compName) { - if (DEBUG) { - Log.d(TAG, "Player service is disconnected"); - } + // It will do nothing if the player is not in fullscreen mode + hideSystemUiIfNeeded(); - unbind(context); - } - - @Override - public void onServiceConnected(final ComponentName compName, final IBinder service) { - if (DEBUG) { - Log.d(TAG, "Player service is connected"); - } - final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service; - - playerService = localBinder.getService(); - player = localBinder.getPlayer(); - - startPlayerListener(); - - // It will do nothing if the player is not in fullscreen mode - hideSystemUiIfNeeded(); - - if (!player.videoPlayerSelected() && !playAfterConnect) { - return; - } - - if (playerIsNotStopped() && player.videoPlayerSelected()) { - addVideoPlayerView(); - } - - if (isLandscape()) { - // If the video is playing but orientation changed - // let's make the video in fullscreen again - checkLandscape(); - } else if (player.isFullscreen()) { - // Device is in portrait orientation after rotation but UI is in fullscreen. - // Return back to non-fullscreen state - player.toggleFullscreen(); - } - - if (playAfterConnect - || (currentInfo != null - && isAutoplayEnabled() - && player.getParentActivity() == null)) { - openVideoPlayer(); - } - } - }; - } - - private void bind(final Context context) { - if (DEBUG) { - Log.d(TAG, "bind() called"); + if (!player.videoPlayerSelected() && !playAfterConnect) { + return; } - final Intent serviceIntent = new Intent(context, MainPlayer.class); - bound = context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); - if (!bound) { - context.unbindService(serviceConnection); + if (isLandscape()) { + // If the video is playing but orientation changed + // let's make the video in fullscreen again + checkLandscape(); + } else if (player.isFullscreen() && !player.isVerticalVideo()) { + // Device is in portrait orientation after rotation but UI is in fullscreen. + // Return back to non-fullscreen state + player.toggleFullscreen(); + } + + if (playerIsNotStopped() && player.videoPlayerSelected()) { + addVideoPlayerView(); + } + + if (playAfterConnect + || (currentInfo != null + && isAutoplayEnabled() + && player.getParentActivity() == null)) { + openVideoPlayer(); } } - private void unbind(final Context context) { - if (DEBUG) { - Log.d(TAG, "unbind() called"); - } - - if (bound) { - context.unbindService(serviceConnection); - bound = false; - stopPlayerListener(); - playerService = null; - player = null; - restoreDefaultBrightness(); - } - } - - private void startPlayerListener() { - if (player != null) { - player.setFragmentListener(this); - } - } - - private void stopPlayerListener() { - if (player != null) { - player.removeFragmentListener(this); - } - } - - private void startService(final Context context, final boolean playAfterConnect) { - // startService() can be called concurrently and it will give a random crashes - // and NullPointerExceptions inside the service because the service will be - // bound twice. Prevent it with unbinding first - unbind(context); - context.startService(new Intent(context, MainPlayer.class)); - serviceConnection = getServiceConnection(context, playAfterConnect); - bind(context); - } - - private void stopService(final Context context) { - unbind(context); - context.stopService(new Intent(context, MainPlayer.class)); + @Override + public void onServiceDisconnected() { + playerService = null; + player = null; + restoreDefaultBrightness(); } /*////////////////////////////////////////////////////////////////////////*/ public static VideoDetailFragment getInstance(final int serviceId, final String videoUrl, - final String name, final PlayQueue playQueue) { + final String name, final PlayQueue queue) { final VideoDetailFragment instance = new VideoDetailFragment(); - instance.setInitialData(serviceId, videoUrl, name, playQueue); + instance.setInitialData(serviceId, videoUrl, name, queue); + return instance; + } + + public static VideoDetailFragment getInstanceInCollapsedState() { + final VideoDetailFragment instance = new VideoDetailFragment(); + instance.bottomSheetState = BottomSheetBehavior.STATE_COLLAPSED; return instance; } @@ -478,9 +409,9 @@ public class VideoDetailFragment // Stop the service when user leaves the app with double back press // if video player is selected. Otherwise unbind if (activity.isFinishing() && player != null && player.videoPlayerSelected()) { - stopService(requireContext()); + PlayerHolder.stopService(App.getApp()); } else { - unbind(requireContext()); + PlayerHolder.removeListener(); } PreferenceManager.getDefaultSharedPreferences(activity) @@ -498,6 +429,12 @@ public class VideoDetailFragment positionSubscriber = null; currentWorker = null; bottomSheetBehavior.setBottomSheetCallback(null); + + if (activity.isFinishing()) { + playQueue = null; + currentInfo = null; + stack = new LinkedList<>(); + } } @Override @@ -530,62 +467,6 @@ public class VideoDetailFragment } } - /*////////////////////////////////////////////////////////////////////////// - // State Saving - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onSaveInstanceState(final Bundle outState) { - super.onSaveInstanceState(outState); - - if (!isLoading.get() && currentInfo != null && isVisible()) { - final String infoCacheKey = SerializedCache.getInstance() - .put(currentInfo, StreamInfo.class); - if (infoCacheKey != null) { - outState.putString(INFO_KEY, infoCacheKey); - } - } - - if (playQueue != null) { - final String queueCacheKey = SerializedCache.getInstance() - .put(playQueue, PlayQueue.class); - if (queueCacheKey != null) { - outState.putString(VideoPlayer.PLAY_QUEUE_KEY, queueCacheKey); - } - } - final String stackCacheKey = SerializedCache.getInstance().put(stack, LinkedList.class); - if (stackCacheKey != null) { - outState.putString(STACK_KEY, stackCacheKey); - } - } - - @Override - protected void onRestoreInstanceState(@NonNull final Bundle savedState) { - super.onRestoreInstanceState(savedState); - - final String infoCacheKey = savedState.getString(INFO_KEY); - if (infoCacheKey != null) { - currentInfo = SerializedCache.getInstance().take(infoCacheKey, StreamInfo.class); - if (currentInfo != null) { - InfoCache.getInstance() - .putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM); - } - } - - final String stackCacheKey = savedState.getString(STACK_KEY); - if (stackCacheKey != null) { - final LinkedList cachedStack = - SerializedCache.getInstance().take(stackCacheKey, LinkedList.class); - if (cachedStack != null) { - stack.addAll(cachedStack); - } - } - final String queueCacheKey = savedState.getString(VideoPlayer.PLAY_QUEUE_KEY); - if (queueCacheKey != null) { - playQueue = SerializedCache.getInstance().take(queueCacheKey, PlayQueue.class); - } - } - /*////////////////////////////////////////////////////////////////////////// // OnClick //////////////////////////////////////////////////////////////////////////*/ @@ -645,7 +526,7 @@ public class VideoDetailFragment openVideoPlayer(); } - setOverlayPlayPauseImage(); + setOverlayPlayPauseImage(player != null && player.isPlaying()); break; case R.id.overlay_close_button: bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); @@ -780,8 +661,6 @@ public class VideoDetailFragment relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout); - setHeightThumbnail(); - thumbnailBackgroundButton.requestFocus(); if (DeviceUtils.isTv(getContext())) { @@ -827,7 +706,11 @@ public class VideoDetailFragment detailControlsPopup.setOnTouchListener(getOnControlsTouchListener()); setupBottomPlayer(); - startService(requireContext(), false); + if (!PlayerHolder.bound) { + setHeightThumbnail(); + } else { + PlayerHolder.startService(App.getApp(), false, this); + } } private View.OnTouchListener getOnControlsTouchListener() { @@ -882,7 +765,7 @@ public class VideoDetailFragment * Stack that contains the "navigation history".
* The peek is the current video. */ - protected final LinkedList stack = new LinkedList<>(); + private static LinkedList stack = new LinkedList<>(); @Override public boolean onKeyDown(final int keyCode) { @@ -966,7 +849,7 @@ public class VideoDetailFragment if (currentInfo == null) { prepareAndLoadInfo(); } else { - prepareAndHandleInfo(currentInfo, false); + prepareAndHandleInfoIfNeededAfterDelay(currentInfo, false, 50); } } @@ -985,6 +868,21 @@ public class VideoDetailFragment startLoading(false, true); } + private void prepareAndHandleInfoIfNeededAfterDelay(final StreamInfo info, + final boolean scrollToTop, + final long delay) { + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (activity == null) { + return; + } + // Data can already be drawn, don't spend time twice + if (info.getName().equals(videoTitleTextView.getText().toString())) { + return; + } + prepareAndHandleInfo(info, scrollToTop); + }, delay); + } + private void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) { if (DEBUG) { Log.d(TAG, "prepareAndHandleInfo() called with: " @@ -1144,8 +1042,8 @@ public class VideoDetailFragment } // See UI changes while remote playQueue changes - if (!bound) { - startService(requireContext(), false); + if (player == null) { + PlayerHolder.startService(App.getApp(), false, this); } // If a user watched video inside fullscreen mode and than chose another player @@ -1174,8 +1072,8 @@ public class VideoDetailFragment private void openNormalBackgroundPlayer(final boolean append) { // See UI changes while remote playQueue changes - if (!bound) { - startService(requireContext(), false); + if (player == null) { + PlayerHolder.startService(App.getApp(), false, this); } final PlayQueue queue = setupPlayQueueForIntent(append); @@ -1189,7 +1087,7 @@ public class VideoDetailFragment private void openMainPlayer() { if (playerService == null) { - startService(requireContext(), true); + PlayerHolder.startService(App.getApp(), true, this); return; } if (currentInfo == null) { @@ -1278,7 +1176,7 @@ public class VideoDetailFragment // Check if viewHolder already contains a child if (player.getRootView().getParent() != playerPlaceholder) { - removeVideoPlayerView(); + playerService.removeViewFromParent(); } setHeightThumbnail(); @@ -1334,6 +1232,23 @@ public class VideoDetailFragment } } + private final ViewTreeObserver.OnPreDrawListener preDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + + if (getView() != null) { + final int height = isInMultiWindow() + ? requireView().getHeight() + : activity.getWindow().getDecorView().getHeight(); + setHeightThumbnail(height, metrics); + getView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener); + } + return false; + } + }; + /** * Method which controls the size of thumbnail and the size of main player inside * a layout with thumbnail. It decides what height the player should have in both @@ -1344,24 +1259,35 @@ public class VideoDetailFragment private void setHeightThumbnail() { final DisplayMetrics metrics = getResources().getDisplayMetrics(); final boolean isPortrait = metrics.heightPixels > metrics.widthPixels; + requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener); - final int height; if (player != null && player.isFullscreen()) { - height = isInMultiWindow() + final int height = isInMultiWindow() ? requireView().getHeight() : activity.getWindow().getDecorView().getHeight(); + // Height is zero when the view is not yet displayed like after orientation change + if (height != 0) { + setHeightThumbnail(height, metrics); + } else { + requireView().getViewTreeObserver().addOnPreDrawListener(preDrawListener); + } } else { - height = isPortrait + final int height = isPortrait ? (int) (metrics.widthPixels / (16.0f / 9.0f)) : (int) (metrics.heightPixels / 2.0f); + setHeightThumbnail(height, metrics); } + } + private void setHeightThumbnail(final int newHeight, final DisplayMetrics metrics) { thumbnailImageView.setLayoutParams( - new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); - thumbnailImageView.setMinimumHeight(height); + new FrameLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, newHeight)); + thumbnailImageView.setMinimumHeight(newHeight); if (player != null) { final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT); - player.getSurfaceView().setHeights(height, player.isFullscreen() ? height : maxHeight); + player.getSurfaceView() + .setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight); } } @@ -1407,12 +1333,22 @@ public class VideoDetailFragment bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); } else if (intent.getAction().equals(ACTION_HIDE_MAIN_PLAYER)) { bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } else if (intent.getAction().equals(ACTION_PLAYER_STARTED)) { + // If the state is not hidden we don't need to show the mini player + if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + // Rebound to the service if it was closed via notification or mini player + if (!PlayerHolder.bound) { + PlayerHolder.startService(App.getApp(), false, VideoDetailFragment.this); + } } } }; final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_SHOW_MAIN_PLAYER); intentFilter.addAction(ACTION_HIDE_MAIN_PLAYER); + intentFilter.addAction(ACTION_PLAYER_STARTED); activity.registerReceiver(broadcastReceiver, intentFilter); } @@ -1614,22 +1550,16 @@ public class VideoDetailFragment 0); } - switch (info.getStreamType()) { - case LIVE_STREAM: - case AUDIO_LIVE_STREAM: - detailControlsDownload.setVisibility(View.GONE); - break; - default: - if (info.getAudioStreams().isEmpty()) { - detailControlsBackground.setVisibility(View.GONE); - } - if (!info.getVideoStreams().isEmpty() || !info.getVideoOnlyStreams().isEmpty()) { - break; - } - detailControlsPopup.setVisibility(View.GONE); - thumbnailPlayButton.setImageResource(R.drawable.ic_headset_shadow); - break; - } + detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM + || info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE); + detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty() + ? View.GONE : View.VISIBLE); + + final boolean noVideoStreams = + info.getVideoStreams().isEmpty() && info.getVideoOnlyStreams().isEmpty(); + detailControlsPopup.setVisibility(noVideoStreams ? View.GONE : View.VISIBLE); + thumbnailPlayButton.setImageResource( + noVideoStreams ? R.drawable.ic_headset_shadow : R.drawable.ic_play_arrow_shadow); } private void hideAgeRestrictedContent() { @@ -1801,8 +1731,12 @@ public class VideoDetailFragment // It will allow to have live instance of PlayQueue with actual information about // deleted/added items inside Channel/Playlist queue and makes possible to have // a history of played items - if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(queue)) { - stack.push(new StackItem(serviceId, url, name, playQueue)); + if ((stack.isEmpty() || !stack.peek().getPlayQueue().equals(queue) + && queue.getItem() != null)) { + stack.push(new StackItem(queue.getItem().getServiceId(), + queue.getItem().getUrl(), + queue.getItem().getTitle(), + queue)); } else { final StackItem stackWithQueue = findQueueInStack(queue); if (stackWithQueue != null) { @@ -1826,7 +1760,7 @@ public class VideoDetailFragment final int repeatMode, final boolean shuffled, final PlaybackParameters parameters) { - setOverlayPlayPauseImage(); + setOverlayPlayPauseImage(player != null && player.isPlaying()); switch (state) { case BasePlayer.STATE_PLAYING: @@ -1880,7 +1814,10 @@ public class VideoDetailFragment currentInfo = info; setInitialData(info.getServiceId(), info.getUrl(), info.getName(), queue); setAutoplay(false); - prepareAndHandleInfo(info, true); + // Delay execution just because it freezes the main thread, and while playing + // next/previous video you see visual glitches + // (when non-vertical video goes after vertical video) + prepareAndHandleInfoIfNeededAfterDelay(info, true, 200); } @Override @@ -1897,8 +1834,7 @@ public class VideoDetailFragment @Override public void onServiceStopped() { - unbind(requireContext()); - setOverlayPlayPauseImage(); + setOverlayPlayPauseImage(false); if (currentInfo != null) { updateOverlayData(currentInfo.getName(), currentInfo.getUploaderName(), @@ -2189,7 +2125,7 @@ public class VideoDetailFragment if (currentWorker != null) { currentWorker.dispose(); } - stopService(requireContext()); + PlayerHolder.stopService(App.getApp()); setInitialData(0, null, "", null); currentInfo = null; updateOverlayData(null, null, null); @@ -2304,6 +2240,9 @@ public class VideoDetailFragment break; case BottomSheetBehavior.STATE_COLLAPSED: moveFocusToMainFragment(true); + manageSpaceAtTheBottom(false); + + bottomSheetBehavior.setPeekHeight(peekHeight); // Re-enable clicks setOverlayElementsClickable(true); @@ -2350,8 +2289,8 @@ public class VideoDetailFragment } } - private void setOverlayPlayPauseImage() { - final int attr = player != null && player.isPlaying() + private void setOverlayPlayPauseImage(final boolean playerIsPlaying) { + final int attr = playerIsPlaying ? R.attr.ic_pause : R.attr.ic_play_arrow; overlayPlayPauseButton.setImageResource( diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index 944b578f5..878b7af7d 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -1,13 +1,18 @@ package org.schabi.newpipe.info_list.holder; +import android.content.SharedPreferences; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.style.URLSpan; import android.text.util.Linkify; +import android.view.View; import android.view.ViewGroup; +import android.widget.RelativeLayout; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceManager; + import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; @@ -31,7 +36,12 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { private static final int COMMENT_DEFAULT_LINES = 2; private static final int COMMENT_EXPANDED_LINES = 1000; private static final Pattern PATTERN = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)"); + private final String downloadThumbnailKey; + private final int commentHorizontalPadding; + private final int commentVerticalPadding; + private SharedPreferences preferences = null; + private final RelativeLayout itemRoot; public final CircleImageView itemThumbnailView; private final TextView itemContentView; private final TextView itemLikesCountView; @@ -65,11 +75,20 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); + itemRoot = itemView.findViewById(R.id.itemRoot); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view); itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view); itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime); itemContentView = itemView.findViewById(R.id.itemCommentContentView); + + downloadThumbnailKey = infoItemBuilder.getContext(). + getString(R.string.download_thumbnail_key); + + commentHorizontalPadding = (int) infoItemBuilder.getContext() + .getResources().getDimension(R.dimen.comments_horizontal_padding); + commentVerticalPadding = (int) infoItemBuilder.getContext() + .getResources().getDimension(R.dimen.comments_vertical_padding); } public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, @@ -85,11 +104,24 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { } final CommentsInfoItem item = (CommentsInfoItem) infoItem; + preferences = PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext()); + itemBuilder.getImageLoader() .displayImage(item.getUploaderAvatarUrl(), itemThumbnailView, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); + if (preferences.getBoolean(downloadThumbnailKey, true)) { + itemThumbnailView.setVisibility(View.VISIBLE); + itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding, + commentVerticalPadding, commentVerticalPadding); + } else { + itemThumbnailView.setVisibility(View.GONE); + itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding, + commentHorizontalPadding, commentVerticalPadding); + } + + itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item)); streamUrl = item.getUrl(); diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 97ff523df..824690b1d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -514,12 +514,19 @@ public abstract class BasePlayer implements @Override public void onLoadingComplete(final String imageUri, final View view, final Bitmap loadedImage) { + final float width = Math.min( + context.getResources().getDimension(R.dimen.player_notification_thumbnail_width), + loadedImage.getWidth()); + currentThumbnail = Bitmap.createScaledBitmap(loadedImage, + (int) width, + (int) (loadedImage.getHeight() / (loadedImage.getWidth() / width)), true); if (DEBUG) { Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + "imageUri = [" + imageUri + "], view = [" + view + "], " - + "loadedImage = [" + loadedImage + "]"); + + "loadedImage = [" + loadedImage + "], " + + loadedImage.getWidth() + "x" + loadedImage.getHeight() + + ", scaled width = " + width); } - currentThumbnail = loadedImage; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java index 0aed3469f..c7fbb444b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -103,6 +103,8 @@ public final class MainPlayer extends Service { playerImpl = new VideoPlayerImpl(this); playerImpl.setup(layout); playerImpl.shouldUpdateOnProgress = true; + + NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java index b327d619a..7e3f8f401 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -176,6 +176,8 @@ public class VideoPlayerImpl extends VideoPlayer private RecyclerView itemsList; private ItemTouchHelper itemTouchHelper; + private RelativeLayout playerOverlays; + private boolean queueVisible; private MainPlayer.PlayerType playerType = MainPlayer.PlayerType.VIDEO; @@ -259,9 +261,9 @@ public class VideoPlayerImpl extends VideoPlayer onQueueClosed(); // Android TV: without it focus will frame the whole player playPauseButton.requestFocus(); + onPlay(); } - - onPlay(); + NavigationHelper.sendPlayerStartedEvent(service); } VideoPlayerImpl(final MainPlayer service) { @@ -310,6 +312,8 @@ public class VideoPlayerImpl extends VideoPlayer this.itemsListCloseButton = view.findViewById(R.id.playQueueClose); this.itemsList = view.findViewById(R.id.playQueue); + this.playerOverlays = view.findViewById(R.id.player_overlays); + closingOverlayView = view.findViewById(R.id.closingOverlay); titleTextView.setSelected(true); @@ -504,6 +508,16 @@ public class VideoPlayerImpl extends VideoPlayer return windowInsets; }); } + + // PlaybackControlRoot already consumed window insets but we should pass them to + // player_overlays too. Without it they will be off-centered + getControlsRoot().addOnLayoutChangeListener((v, left, top, right, bottom, + oldLeft, oldTop, oldRight, oldBottom) -> + playerOverlays.setPadding( + v.getPaddingLeft(), + v.getPaddingTop(), + v.getPaddingRight(), + v.getPaddingBottom())); } public boolean onKeyDown(final int keyCode) { @@ -706,6 +720,7 @@ public class VideoPlayerImpl extends VideoPlayer @Override public void onPlayQueueEdited() { updatePlayback(); + showOrHideButtons(); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); } @@ -1540,15 +1555,16 @@ public class VideoPlayerImpl extends VideoPlayer return; } - playPreviousButton.setVisibility(playQueue.getIndex() == 0 - ? View.INVISIBLE - : View.VISIBLE); - playNextButton.setVisibility(playQueue.getIndex() + 1 == playQueue.getStreams().size() - ? View.INVISIBLE - : View.VISIBLE); - queueButton.setVisibility(playQueue.getStreams().size() <= 1 || popupPlayerSelected() - ? View.GONE - : View.VISIBLE); + final boolean showPrev = playQueue.getIndex() != 0; + final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size(); + final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected(); + + playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE); + playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f); + playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE); + playNextButton.setAlpha(showNext ? 1.0f : 0.0f); + queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE); + queueButton.setAlpha(showQueue ? 1.0f : 0.0f); } private void showSystemUIPartially() { @@ -2046,6 +2062,7 @@ public class VideoPlayerImpl extends VideoPlayer getControlsRoot().setPadding(0, 0, 0, 0); } queueLayout.setPadding(0, 0, 0, 0); + updateQueue(); updateMetadata(); updatePlayback(); triggerProgressUpdate(); @@ -2217,4 +2234,8 @@ public class VideoPlayerImpl extends VideoPlayer public View getClosingOverlayView() { return closingOverlayView; } + + public boolean isVerticalVideo() { + return isVerticalVideo; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java index 47c0624b8..5405d01c1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java @@ -5,7 +5,6 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.coordinatorlayout.widget.CoordinatorLayout; @@ -21,12 +20,12 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior super(context, attrs); } - boolean visible; Rect globalRect = new Rect(); private boolean skippingInterception = false; private final List skipInterceptionOfElements = Arrays.asList( R.id.detail_content_root_layout, R.id.relatedStreamsLayout, - R.id.playQueuePanel, R.id.viewpager, R.id.bottomControls); + R.id.playQueuePanel, R.id.viewpager, R.id.bottomControls, + R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton); @Override public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent, @@ -48,9 +47,9 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior && event.getAction() == MotionEvent.ACTION_DOWN) { // Without overriding scrolling will not work when user touches these elements for (final Integer element : skipInterceptionOfElements) { - final ViewGroup viewGroup = child.findViewById(element); - if (viewGroup != null) { - visible = viewGroup.getGlobalVisibleRect(globalRect); + final View view = child.findViewById(element); + if (view != null) { + final boolean visible = view.getGlobalVisibleRect(globalRect); if (visible && globalRect.contains((int) event.getRawX(), (int) event.getRawY())) { // Makes bottom part of the player draggable in portrait when diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java new file mode 100644 index 000000000..93952a811 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java @@ -0,0 +1,11 @@ +package org.schabi.newpipe.player.event; + +import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.VideoPlayerImpl; + +public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { + void onServiceConnected(VideoPlayerImpl player, + MainPlayer playerService, + boolean playAfterConnect); + void onServiceDisconnected(); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index fd59e1d99..6efe7510c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -2,13 +2,13 @@ package org.schabi.newpipe.player.helper; import android.content.Context; import android.content.SharedPreferences; -import androidx.preference.PreferenceManager; import android.provider.Settings; import android.view.accessibility.CaptioningManager; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.text.CaptionStyleCompat; @@ -25,6 +25,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.extractor.utils.Utils; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; @@ -47,8 +48,8 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; import static java.lang.annotation.RetentionPolicy.SOURCE; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS; -import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI; 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.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; @@ -171,7 +172,7 @@ public final class PlayerHelper { } final List relatedItems = info.getRelatedStreams(); - if (relatedItems == null) { + if (Utils.isNullOrEmpty(relatedItems)) { return null; } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java new file mode 100644 index 000000000..a5760eddc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -0,0 +1,219 @@ +package org.schabi.newpipe.player.helper; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackParameters; +import org.schabi.newpipe.App; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.VideoPlayerImpl; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; +import org.schabi.newpipe.player.playqueue.PlayQueue; + +public final class PlayerHolder { + private PlayerHolder() { + } + + private static final boolean DEBUG = MainActivity.DEBUG; + private static final String TAG = "PlayerHolder"; + + private static PlayerServiceExtendedEventListener listener; + + private static ServiceConnection serviceConnection; + public static boolean bound; + private static MainPlayer playerService; + private static VideoPlayerImpl player; + + public static void setListener(final PlayerServiceExtendedEventListener newListener) { + listener = newListener; + // Force reload data from service + if (player != null) { + listener.onServiceConnected(player, playerService, false); + startPlayerListener(); + } + } + + public static void removeListener() { + listener = null; + } + + + public static void startService(final Context context, + final boolean playAfterConnect, + final PlayerServiceExtendedEventListener newListener) { + setListener(newListener); + if (bound) { + return; + } + // startService() can be called concurrently and it will give a random crashes + // and NullPointerExceptions inside the service because the service will be + // bound twice. Prevent it with unbinding first + unbind(context); + context.startService(new Intent(context, MainPlayer.class)); + serviceConnection = getServiceConnection(context, playAfterConnect); + bind(context); + } + + public static void stopService(final Context context) { + unbind(context); + context.stopService(new Intent(context, MainPlayer.class)); + } + + private static ServiceConnection getServiceConnection(final Context context, + final boolean playAfterConnect) { + return new ServiceConnection() { + @Override + public void onServiceDisconnected(final ComponentName compName) { + if (DEBUG) { + Log.d(TAG, "Player service is disconnected"); + } + + unbind(context); + } + + @Override + public void onServiceConnected(final ComponentName compName, final IBinder service) { + if (DEBUG) { + Log.d(TAG, "Player service is connected"); + } + final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service; + + playerService = localBinder.getService(); + player = localBinder.getPlayer(); + if (listener != null) { + listener.onServiceConnected(player, playerService, playAfterConnect); + } + startPlayerListener(); + } + }; + } + + private static void bind(final Context context) { + if (DEBUG) { + Log.d(TAG, "bind() called"); + } + + final Intent serviceIntent = new Intent(context, MainPlayer.class); + bound = context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); + if (!bound) { + context.unbindService(serviceConnection); + } + } + + private static void unbind(final Context context) { + if (DEBUG) { + Log.d(TAG, "unbind() called"); + } + + if (bound) { + context.unbindService(serviceConnection); + bound = false; + stopPlayerListener(); + playerService = null; + player = null; + if (listener != null) { + listener.onServiceDisconnected(); + } + } + } + + + private static void startPlayerListener() { + if (player != null) { + player.setFragmentListener(INNER_LISTENER); + } + } + + private static void stopPlayerListener() { + if (player != null) { + player.removeFragmentListener(INNER_LISTENER); + } + } + + + private static final PlayerServiceEventListener INNER_LISTENER = + new PlayerServiceEventListener() { + @Override + public void onFullscreenStateChanged(final boolean fullscreen) { + if (listener != null) { + listener.onFullscreenStateChanged(fullscreen); + } + } + + @Override + public void onScreenRotationButtonClicked() { + if (listener != null) { + listener.onScreenRotationButtonClicked(); + } + } + + @Override + public void onMoreOptionsLongClicked() { + if (listener != null) { + listener.onMoreOptionsLongClicked(); + } + } + + @Override + public void onPlayerError(final ExoPlaybackException error) { + if (listener != null) { + listener.onPlayerError(error); + } + } + + @Override + public void hideSystemUiIfNeeded() { + if (listener != null) { + listener.hideSystemUiIfNeeded(); + } + } + + @Override + public void onQueueUpdate(final PlayQueue queue) { + if (listener != null) { + listener.onQueueUpdate(queue); + } + } + + @Override + public void onPlaybackUpdate(final int state, + final int repeatMode, + final boolean shuffled, + final PlaybackParameters parameters) { + if (listener != null) { + listener.onPlaybackUpdate(state, repeatMode, shuffled, parameters); + } + } + + @Override + public void onProgressUpdate(final int currentProgress, + final int duration, + final int bufferPercent) { + if (listener != null) { + listener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + } + + @Override + public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) { + if (listener != null) { + listener.onMetadataUpdate(info, queue); + } + } + + @Override + public void onServiceStopped() { + if (listener != null) { + listener.onServiceStopped(); + } + unbind(App.getApp()); + } + }; +} diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 767cebcea..838e4e986 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -305,9 +305,7 @@ public final class Localization { } public static String relativeTime(final Calendar calendarTime) { - final String time = getPrettyTime().formatUnrounded(calendarTime); - return time.startsWith("-") ? time.substring(1) : time; - //workaround fix for russian showing -1 day ago, -19hrs ago… + return getPrettyTime().formatUnrounded(calendarTime); } private static void changeAppLanguage(final Locale loc, final Resources res) { diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index fc4a6cacc..eef70c1e5 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -384,8 +384,19 @@ public final class NavigationHelper { } public static void expandMainPlayer(final Context context) { - final Intent intent = new Intent(VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER); - context.sendBroadcast(intent); + context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER)); + } + + public static void sendPlayerStartedEvent(final Context context) { + context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_PLAYER_STARTED)); + } + + public static void showMiniPlayer(final FragmentManager fragmentManager) { + final VideoDetailFragment instance = VideoDetailFragment.getInstanceInCollapsedState(); + defaultTransaction(fragmentManager) + .replace(R.id.fragment_player_holder, instance) + .runOnCommit(() -> sendPlayerStartedEvent(instance.requireActivity())) + .commit(); } public static void openChannelFragment(final FragmentManager fragmentManager, diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index e2d18434d..9c0f03cb3 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -600,16 +600,16 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:alpha="0.9" - android:paddingLeft="@dimen/video_item_search_padding" - android:paddingRight="@dimen/video_item_search_padding" android:descendantFocusability="blocksDescendants" android:background="?attr/windowBackground" > diff --git a/app/src/main/res/layout-large-land/player.xml b/app/src/main/res/layout-large-land/player.xml index 02880d7bd..e0369b086 100644 --- a/app/src/main/res/layout-large-land/player.xml +++ b/app/src/main/res/layout-large-land/player.xml @@ -1,8 +1,8 @@ + android:layout_centerInParent="true" /> + android:layout_alignBottom="@+id/surfaceView" + android:background="@android:color/black" /> + android:layout_gravity="center" /> + android:background="@drawable/player_controls_top_background" + android:visibility="gone" /> + android:background="@drawable/player_controls_background" + android:visibility="gone" /> + tools:visibility="visible" /> @@ -74,270 +74,270 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" - android:orientation="vertical" - android:gravity="top" - android:descendantFocusability="afterDescendants" - android:paddingTop="@dimen/player_main_top_padding" - android:paddingStart="@dimen/player_main_controls_padding" - android:paddingEnd="@dimen/player_main_controls_padding" - android:baselineAligned="false"> - - - - + android:orientation="vertical" + android:paddingStart="@dimen/player_main_controls_padding" + android:paddingTop="@dimen/player_main_top_padding" + android:paddingEnd="@dimen/player_main_controls_padding"> + android:minHeight="45dp" + tools:ignore="RtlHardcoded"> - + + + + + + + + +