diff --git a/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java b/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java index ab20d2ff3..55e747cd5 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java +++ b/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java @@ -2,9 +2,9 @@ package org.schabi.newpipe.report; import android.os.Parcel; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import org.schabi.newpipe.R; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c24c91193..16ed422e0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,8 +43,8 @@ + android:name=".player.MainPlayer" + android:exported="false"> @@ -52,25 +52,9 @@ - - - - - - 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 09f9aea58..90e5edcd3 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 @@ -4,11 +4,14 @@ import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; import android.widget.OverScroller; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; +import org.schabi.newpipe.R; import java.lang.reflect.Field; @@ -20,6 +23,9 @@ public final class FlingBehavior extends AppBarLayout.Behavior { super(context, attrs); } + private boolean allowScroll = true; + private final Rect globalRect = new Rect(); + @Override public boolean onRequestChildRectangleOnScreen( @NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child, @@ -55,6 +61,15 @@ 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; + } + } + allowScroll = true; switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // remove reference to old nested scrolling child @@ -68,6 +83,26 @@ public final class FlingBehavior extends AppBarLayout.Behavior { return super.onInterceptTouchEvent(parent, child, ev); } + @Override + public boolean onStartNestedScroll(@NonNull final CoordinatorLayout parent, + @NonNull final AppBarLayout child, + @NonNull final View directTargetChild, + final View target, + final int nestedScrollAxes, + final int type) { + return allowScroll && super.onStartNestedScroll( + parent, child, directTargetChild, target, nestedScrollAxes, type); + } + + @Override + public boolean onNestedFling(@NonNull final CoordinatorLayout coordinatorLayout, + @NonNull final AppBarLayout child, + @NonNull final View target, final float velocityX, + final float velocityY, final boolean consumed) { + return allowScroll && super.onNestedFling( + coordinatorLayout, child, target, velocityX, velocityY, consumed); + } + @Nullable private OverScroller getScrollerField() { try { diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 37d6d62f5..fb1ca2342 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -29,6 +29,8 @@ import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; import android.util.Log; + +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -37,10 +39,10 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; @@ -51,6 +53,7 @@ import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.navigation.NavigationView; import org.schabi.newpipe.extractor.NewPipe; @@ -61,14 +64,18 @@ import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; +import org.schabi.newpipe.player.VideoPlayer; +import org.schabi.newpipe.player.event.OnKeyDownListener; +import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PeertubeHelper; import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.TLSSocketFactoryCompat; @@ -137,7 +144,7 @@ public class MainActivity extends AppCompatActivity { ErrorActivity.reportUiError(this, e); } - if (AndroidTvUtils.isTv(this)) { + if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(this); } } @@ -518,13 +525,27 @@ public class MainActivity extends AppCompatActivity { handleIntent(intent); } + @Override + public boolean onKeyDown(final int keyCode, final KeyEvent event) { + final Fragment fragment = getSupportFragmentManager() + .findFragmentById(R.id.fragment_player_holder); + if (fragment instanceof OnKeyDownListener + && !bottomSheetHiddenOrCollapsed()) { + // Provide keyDown event to fragment which then sends this event + // to the main player service + return ((OnKeyDownListener) fragment).onKeyDown(keyCode) + || super.onKeyDown(keyCode, event); + } + return super.onKeyDown(keyCode, event); + } + @Override public void onBackPressed() { if (DEBUG) { Log.d(TAG, "onBackPressed() called"); } - if (AndroidTvUtils.isTv(this)) { + if (DeviceUtils.isTv(this)) { View drawerPanel = findViewById(R.id.navigation); if (drawer.isDrawerOpen(drawerPanel)) { drawer.closeDrawers(); @@ -532,11 +553,32 @@ public class MainActivity extends AppCompatActivity { } } - Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); - // If current fragment implements BackPressable (i.e. can/wanna handle back press) - // delegate the back press to it - if (fragment instanceof BackPressable) { - if (((BackPressable) fragment).onBackPressed()) { + // In case bottomSheet is not visible on the screen or collapsed we can assume that the user + // interacts with a fragment inside fragment_holder so all back presses should be + // handled by it + if (bottomSheetHiddenOrCollapsed()) { + final Fragment fragment = getSupportFragmentManager() + .findFragmentById(R.id.fragment_holder); + // If current fragment implements BackPressable (i.e. can/wanna handle back press) + // delegate the back press to it + if (fragment instanceof BackPressable) { + if (((BackPressable) fragment).onBackPressed()) { + return; + } + } + + } else { + final Fragment fragmentPlayer = getSupportFragmentManager() + .findFragmentById(R.id.fragment_player_holder); + // If current fragment implements BackPressable (i.e. can/wanna handle back press) + // delegate the back press to it + if (fragmentPlayer instanceof BackPressable) { + if (!((BackPressable) fragmentPlayer).onBackPressed()) { + final FrameLayout bottomSheetLayout = + findViewById(R.id.fragment_player_holder); + BottomSheetBehavior.from(bottomSheetLayout) + .setState(BottomSheetBehavior.STATE_COLLAPSED); + } return; } } @@ -563,7 +605,7 @@ public class MainActivity extends AppCompatActivity { break; case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE: Fragment fragment = getSupportFragmentManager() - .findFragmentById(R.id.fragment_holder); + .findFragmentById(R.id.fragment_player_holder); if (fragment instanceof VideoDetailFragment) { ((VideoDetailFragment) fragment).openDownloadDialog(); } @@ -615,10 +657,6 @@ public class MainActivity extends AppCompatActivity { super.onCreateOptionsMenu(menu); Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); - if (!(fragment instanceof VideoDetailFragment)) { - findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner).setVisibility(View.GONE); - } - if (!(fragment instanceof SearchFragment)) { findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container) .setVisibility(View.GONE); @@ -660,6 +698,13 @@ public class MainActivity extends AppCompatActivity { } StateSaver.clearStateFiles(); if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) { + // When user watch a video inside popup and then tries to open the video in main player + // while the app is closed he will see a blank fragment on place of kiosk. + // Let's open it first + if (getSupportFragmentManager().getBackStackEntryCount() == 0) { + NavigationHelper.openMainFragment(getSupportFragmentManager()); + } + handleIntent(getIntent()); } else { NavigationHelper.gotoMainFragment(getSupportFragmentManager()); @@ -708,8 +753,14 @@ public class MainActivity extends AppCompatActivity { case STREAM: boolean autoPlay = intent .getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); + final String intentCacheKey = intent + .getStringExtra(VideoPlayer.PLAY_QUEUE_KEY); + final PlayQueue playQueue = intentCacheKey != null + ? SerializedCache.getInstance() + .take(intentCacheKey, PlayQueue.class) + : null; NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), - serviceId, url, title, autoPlay); + serviceId, url, title, autoPlay, playQueue); break; case CHANNEL: NavigationHelper.openChannelFragment(getSupportFragmentManager(), @@ -742,4 +793,17 @@ public class MainActivity extends AppCompatActivity { ErrorActivity.reportUiError(this, e); } } + /* + * Utils + * */ + + private boolean bottomSheetHiddenOrCollapsed() { + final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder); + final BottomSheetBehavior bottomSheetBehavior = + BottomSheetBehavior.from(bottomSheetLayout); + + final int sheetState = bottomSheetBehavior.getState(); + return sheetState == BottomSheetBehavior.STATE_HIDDEN + || sheetState == BottomSheetBehavior.STATE_COLLAPSED; + } } diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 39f6b217d..e9e166c22 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -44,7 +44,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ListHelper; @@ -347,7 +347,7 @@ public class RouterActivity extends AppCompatActivity { alertDialog.show(); - if (AndroidTvUtils.isTv(this)) { + if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(alertDialog); } } @@ -701,7 +701,7 @@ public class RouterActivity extends AppCompatActivity { playQueue = new SinglePlayQueue((StreamInfo) info); if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue, true); + openMainPlayer(playQueue, choice); } else if (playerChoice.equals(backgroundPlayerKey)) { NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); } else if (playerChoice.equals(popupPlayerKey)) { @@ -716,7 +716,7 @@ public class RouterActivity extends AppCompatActivity { : new PlaylistPlayQueue((PlaylistInfo) info); if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue, true); + openMainPlayer(playQueue, choice); } else if (playerChoice.equals(backgroundPlayerKey)) { NavigationHelper.playOnBackgroundPlayer(this, playQueue, true); } else if (playerChoice.equals(popupPlayerKey)) { @@ -726,6 +726,11 @@ public class RouterActivity extends AppCompatActivity { }; } + private void openMainPlayer(final PlayQueue playQueue, final Choice choice) { + NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType, + choice.url, "", true, true); + } + @Override public void onDestroy() { super.onDestroy(); diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index e46ded40d..5415c4ff8 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -13,7 +13,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import org.schabi.newpipe.R; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -57,7 +57,7 @@ public class DownloadActivity extends AppCompatActivity { } }); - if (AndroidTvUtils.isTv(this)) { + if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(this); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java index f966880b1..2fe615764 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java @@ -1,16 +1,29 @@ package org.schabi.newpipe.fragments.detail; +import org.schabi.newpipe.player.playqueue.PlayQueue; + import java.io.Serializable; class StackItem implements Serializable { private final int serviceId; - private final String url; + private String url; private String title; + private PlayQueue playQueue; - StackItem(final int serviceId, final String url, final String title) { + StackItem(final int serviceId, final String url, + final String title, final PlayQueue playQueue) { this.serviceId = serviceId; this.url = url; this.title = title; + this.playQueue = playQueue; + } + + public void setUrl(final String url) { + this.url = url; + } + + public void setPlayQueue(final PlayQueue queue) { + this.playQueue = queue; } public int getServiceId() { @@ -29,6 +42,10 @@ class StackItem implements Serializable { return url; } + public PlayQueue getPlayQueue() { + return playQueue; + } + @Override public String toString() { return getServiceId() + ":" + getUrl() + " > " + getTitle(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 336e3997e..6c459ffe9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1,48 +1,60 @@ 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; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; import android.preference.PreferenceManager; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.tabs.TabLayout; +import androidx.fragment.app.Fragment; +import androidx.core.content.ContextCompat; +import androidx.viewpager.widget.ViewPager; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; -import androidx.viewpager.widget.ViewPager; -import com.google.android.material.appbar.AppBarLayout; -import com.google.android.material.tabs.TabLayout; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; @@ -68,33 +80,39 @@ import org.schabi.newpipe.fragments.list.comments.CommentsFragment; import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.MainVideoPlayer; -import org.schabi.newpipe.player.PopupVideoPlayer; +import org.schabi.newpipe.player.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.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.InfoCache; -import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ShareUtils; -import org.schabi.newpipe.util.StreamItemAdapter; -import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.LargeTextMovementMethod; import java.io.Serializable; import java.util.Collection; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.concurrent.TimeUnit; import icepick.State; @@ -107,31 +125,61 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; +import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; +import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class VideoDetailFragment extends BaseStateFragment - implements BackPressable, SharedPreferences.OnSharedPreferenceChangeListener, - View.OnClickListener, View.OnLongClickListener { +public class VideoDetailFragment + extends BaseStateFragment + implements BackPressable, + SharedPreferences.OnSharedPreferenceChangeListener, + View.OnClickListener, + View.OnLongClickListener, + PlayerEventListener, + PlayerServiceEventListener, + OnKeyDownListener { public static final String AUTO_PLAY = "auto_play"; - private int updateFlags = 0; private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1; - private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2; - private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4; - private static final int COMMENTS_UPDATE_FLAG = 0x8; + private static final int COMMENTS_UPDATE_FLAG = 0x2; + private static final float MAX_OVERLAY_ALPHA = 0.9f; + private static final float MAX_PLAYER_HEIGHT = 0.7f; + + public static final String ACTION_SHOW_MAIN_PLAYER = + "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_VIDEO_FRAGMENT_RESUMED = + "org.schabi.newpipe.VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED"; + public static final String ACTION_VIDEO_FRAGMENT_STOPPED = + "org.schabi.newpipe.VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED"; + + private static final String COMMENTS_TAB_TAG = "COMMENTS"; + private static final String RELATED_TAB_TAG = "NEXT VIDEO"; + private static final String EMPTY_TAB_TAG = "EMPTY TAB"; + + private static final String INFO_KEY = "info_key"; + private static final String STACK_KEY = "stack_key"; - private boolean autoPlayEnabled; private boolean showRelatedStreams; private boolean showComments; private String selectedTabTag; + private int updateFlags = 0; + @State protected int serviceId = Constants.NO_SERVICE_ID; @State protected String name; @State protected String url; + @State + protected PlayQueue playQueue; + @State + int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; + @State + protected boolean autoPlayEnabled = true; private StreamInfo currentInfo; private Disposable currentWorker; @@ -142,15 +190,13 @@ public class VideoDetailFragment extends BaseStateFragment private List sortedVideoStreams; private int selectedVideoStreamIndex = -1; + private BottomSheetBehavior bottomSheetBehavior; + private BroadcastReceiver broadcastReceiver; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ - private Menu menu; - - private Spinner spinnerToolbar; - private LinearLayout contentRootLayoutHiding; private View thumbnailBackgroundButton; @@ -187,31 +233,147 @@ public class VideoDetailFragment extends BaseStateFragment private ImageView thumbsDownImageView; private TextView thumbsDisabledTextView; + private RelativeLayout overlay; + private LinearLayout overlayMetadata; + private ImageView overlayThumbnailImageView; + private TextView overlayTitleTextView; + private TextView overlayChannelTextView; + private LinearLayout overlayButtons; + private ImageButton overlayPlayPauseButton; + private ImageButton overlayCloseButton; + private AppBarLayout appBarLayout; private ViewPager viewPager; private TabAdaptor pageAdapter; private TabLayout tabLayout; private FrameLayout relatedStreamsLayout; + private ContentObserver settingsContentObserver; + private ServiceConnection serviceConnection; + private boolean bound; + private MainPlayer playerService; + private VideoPlayerImpl player; + + + /*////////////////////////////////////////////////////////////////////////// + // Service management + //////////////////////////////////////////////////////////////////////////*/ + + 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"); + } + + 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"); + } + + final Intent serviceIntent = new Intent(context, MainPlayer.class); + bound = context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); + if (!bound) { + context.unbindService(serviceConnection); + } + } + + 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; + } + } + + 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)); + } + + /*////////////////////////////////////////////////////////////////////////*/ - private static final String COMMENTS_TAB_TAG = "COMMENTS"; - private static final String RELATED_TAB_TAG = "NEXT VIDEO"; - private static final String EMPTY_TAB_TAG = "EMPTY TAB"; - - private static final String INFO_KEY = "info_key"; - private static final String STACK_KEY = "stack_key"; - - /** - * Stack that contains the "navigation history".
- * The peek is the current video. - */ - private final LinkedList stack = new LinkedList<>(); - public static VideoDetailFragment getInstance(final int serviceId, final String videoUrl, - final String name) { + final String name, final PlayQueue playQueue) { VideoDetailFragment instance = new VideoDetailFragment(); - instance.setInitialData(serviceId, videoUrl, name); + instance.setInitialData(serviceId, videoUrl, name, playQueue); return instance; } @@ -223,7 +385,6 @@ public class VideoDetailFragment extends BaseStateFragment @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setHasOptionsMenu(true); showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(getString(R.string.show_next_video_key), true); @@ -236,6 +397,20 @@ public class VideoDetailFragment extends BaseStateFragment PreferenceManager.getDefaultSharedPreferences(activity) .registerOnSharedPreferenceChangeListener(this); + + setupBroadcastReceiver(); + + settingsContentObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(final boolean selfChange) { + if (activity != null && !PlayerHelper.globalScreenOrientationLocked(activity)) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + } + }; + activity.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, + settingsContentObserver); } @Override @@ -250,6 +425,7 @@ public class VideoDetailFragment extends BaseStateFragment if (currentWorker != null) { currentWorker.dispose(); } + setupBrightness(true); PreferenceManager.getDefaultSharedPreferences(getContext()) .edit() .putString(getString(R.string.stream_info_selected_tab_key), @@ -261,40 +437,54 @@ public class VideoDetailFragment extends BaseStateFragment public void onResume() { super.onResume(); + activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED)); + + setupBrightness(false); + if (updateFlags != 0) { if (!isLoading.get() && currentInfo != null) { if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) { startLoading(false); } - if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) { - setupActionBar(currentInfo); - } if ((updateFlags & COMMENTS_UPDATE_FLAG) != 0) { startLoading(false); } } - if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0 - && menu != null) { - updateMenuItemVisibility(); - } - updateFlags = 0; } - // Check if it was loading when the fragment was stopped/paused, - if (wasLoading.getAndSet(false)) { - selectAndLoadVideo(serviceId, url, name); - } else if (currentInfo != null) { - updateProgressInfo(currentInfo); + // Check if it was loading when the fragment was stopped/paused + if (wasLoading.getAndSet(false) && !wasCleared()) { + startLoading(false); + } + } + + @Override + public void onStop() { + super.onStop(); + + if (!activity.isChangingConfigurations()) { + activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_STOPPED)); } } @Override public void onDestroy() { super.onDestroy(); + + // 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()); + } else { + unbind(requireContext()); + } + PreferenceManager.getDefaultSharedPreferences(activity) .unregisterOnSharedPreferenceChangeListener(this); + activity.unregisterReceiver(broadcastReceiver); + activity.getContentResolver().unregisterContentObserver(settingsContentObserver); if (positionSubscriber != null) { positionSubscriber.dispose(); @@ -302,22 +492,10 @@ public class VideoDetailFragment extends BaseStateFragment if (currentWorker != null) { currentWorker.dispose(); } - if (disposables != null) { - disposables.clear(); - } + disposables.clear(); positionSubscriber = null; currentWorker = null; - disposables = null; - } - - @Override - public void onDestroyView() { - if (DEBUG) { - Log.d(TAG, "onDestroyView() called"); - } - spinnerToolbar.setOnItemSelectedListener(null); - spinnerToolbar.setAdapter(null); - super.onDestroyView(); + bottomSheetBehavior.setBottomSheetCallback(null); } @Override @@ -344,13 +522,6 @@ public class VideoDetailFragment extends BaseStateFragment if (key.equals(getString(R.string.show_next_video_key))) { showRelatedStreams = sharedPreferences.getBoolean(key, true); updateFlags |= RELATED_STREAMS_UPDATE_FLAG; - } else if (key.equals(getString(R.string.default_video_format_key)) - || key.equals(getString(R.string.default_resolution_key)) - || key.equals(getString(R.string.show_higher_resolutions_key)) - || key.equals(getString(R.string.use_external_video_player_key))) { - updateFlags |= RESOLUTIONS_MENU_UPDATE_FLAG; - } else if (key.equals(getString(R.string.show_play_with_kodi_key))) { - updateFlags |= TOOLBAR_ITEMS_UPDATE_FLAG; } else if (key.equals(getString(R.string.show_comments_key))) { showComments = sharedPreferences.getBoolean(key, true); updateFlags |= COMMENTS_UPDATE_FLAG; @@ -369,6 +540,9 @@ public class VideoDetailFragment extends BaseStateFragment outState.putSerializable(INFO_KEY, currentInfo); } + if (playQueue != null) { + outState.putSerializable(VideoPlayer.PLAY_QUEUE_KEY, playQueue); + } outState.putSerializable(STACK_KEY, stack); } @@ -388,6 +562,7 @@ public class VideoDetailFragment extends BaseStateFragment //noinspection unchecked stack.addAll((Collection) serializable); } + playQueue = (PlayQueue) savedState.getSerializable(VideoPlayer.PLAY_QUEUE_KEY); } /*////////////////////////////////////////////////////////////////////////// @@ -396,10 +571,6 @@ public class VideoDetailFragment extends BaseStateFragment @Override public void onClick(final View v) { - if (isLoading.get() || currentInfo == null) { - return; - } - switch (v.getId()) { case R.id.detail_controls_background: openBackgroundPlayer(false); @@ -434,16 +605,30 @@ public class VideoDetailFragment extends BaseStateFragment } break; case R.id.detail_thumbnail_root_layout: - if (currentInfo.getVideoStreams().isEmpty() - && currentInfo.getVideoOnlyStreams().isEmpty()) { - openBackgroundPlayer(false); - } else { - openVideoPlayer(); - } + openVideoPlayer(); break; case R.id.detail_title_root_layout: toggleTitleAndDescription(); break; + case R.id.overlay_thumbnail: + case R.id.overlay_metadata_layout: + case R.id.overlay_buttons_layout: + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + break; + case R.id.overlay_play_pause_button: + if (playerIsNotStopped()) { + player.onPlayPause(); + player.hideControls(0, 0); + showSystemUi(); + } else { + openVideoPlayer(); + } + + setOverlayPlayPauseImage(); + break; + case R.id.overlay_close_button: + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + break; } } @@ -473,7 +658,13 @@ public class VideoDetailFragment extends BaseStateFragment openPopupPlayer(true); break; case R.id.detail_controls_download: - NavigationHelper.openDownloads(getActivity()); + NavigationHelper.openDownloads(activity); + break; + case R.id.overlay_thumbnail: + case R.id.overlay_metadata_layout: + if (currentInfo != null) { + openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName()); + } break; case R.id.detail_uploader_root_layout: if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { @@ -515,8 +706,6 @@ public class VideoDetailFragment extends BaseStateFragment @Override protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner); - thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout); thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view); thumbnailPlayButton = rootView.findViewById(R.id.detail_thumbnail_play_button); @@ -553,6 +742,15 @@ public class VideoDetailFragment extends BaseStateFragment subChannelTextView = rootView.findViewById(R.id.detail_sub_channel_text_view); subChannelThumb = rootView.findViewById(R.id.detail_sub_channel_thumbnail_view); + overlay = rootView.findViewById(R.id.overlay_layout); + overlayMetadata = rootView.findViewById(R.id.overlay_metadata_layout); + overlayThumbnailImageView = rootView.findViewById(R.id.overlay_thumbnail); + overlayTitleTextView = rootView.findViewById(R.id.overlay_title_text_view); + overlayChannelTextView = rootView.findViewById(R.id.overlay_channel_text_view); + overlayButtons = rootView.findViewById(R.id.overlay_buttons_layout); + overlayPlayPauseButton = rootView.findViewById(R.id.overlay_play_pause_button); + overlayCloseButton = rootView.findViewById(R.id.overlay_close_button); + appBarLayout = rootView.findViewById(R.id.appbarlayout); viewPager = rootView.findViewById(R.id.viewpager); pageAdapter = new TabAdaptor(getChildFragmentManager()); @@ -566,7 +764,7 @@ public class VideoDetailFragment extends BaseStateFragment thumbnailBackgroundButton.requestFocus(); - if (AndroidTvUtils.isTv(getContext())) { + if (DeviceUtils.isTv(getContext())) { // remove ripple effects from detail controls final int transparent = getResources().getColor(R.color.transparent_background_color); detailControlsAddToPlaylist.setBackgroundColor(transparent); @@ -596,8 +794,20 @@ public class VideoDetailFragment extends BaseStateFragment detailControlsPopup.setLongClickable(true); detailControlsBackground.setOnLongClickListener(this); detailControlsPopup.setOnLongClickListener(this); + + overlayThumbnailImageView.setOnClickListener(this); + overlayThumbnailImageView.setOnLongClickListener(this); + overlayMetadata.setOnClickListener(this); + overlayMetadata.setOnLongClickListener(this); + overlayButtons.setOnClickListener(this); + overlayCloseButton.setOnClickListener(this); + overlayPlayPauseButton.setOnClickListener(this); + detailControlsBackground.setOnTouchListener(getOnControlsTouchListener()); detailControlsPopup.setOnTouchListener(getOnControlsTouchListener()); + + setupBottomPlayer(); + startService(requireContext(), false); } private View.OnTouchListener getOnControlsTouchListener() { @@ -617,6 +827,7 @@ public class VideoDetailFragment extends BaseStateFragment private void initThumbnailViews(@NonNull final StreamInfo info) { thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); + if (!TextUtils.isEmpty(info.getThumbnailUrl())) { final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { @@ -643,144 +854,19 @@ public class VideoDetailFragment extends BaseStateFragment } } - /*////////////////////////////////////////////////////////////////////////// - // Menu - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreateOptionsMenu(final Menu m, final MenuInflater inflater) { - this.menu = m; - - // CAUTION set item properties programmatically otherwise it would not be accepted by - // appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu); - - inflater.inflate(R.menu.video_detail_menu, m); - - updateMenuItemVisibility(); - - ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setDisplayHomeAsUpEnabled(true); - supportActionBar.setDisplayShowTitleEnabled(false); - } - } - - private void updateMenuItemVisibility() { - // show kodi button if it supports the current service and it is enabled in settings - menu.findItem(R.id.action_play_with_kodi).setVisible( - KoreUtil.isServiceSupportedByKore(serviceId) - && PreferenceManager.getDefaultSharedPreferences(activity).getBoolean( - activity.getString(R.string.show_play_with_kodi_key), false)); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - int id = item.getItemId(); - if (id == R.id.action_settings) { - NavigationHelper.openSettings(requireContext()); - return true; - } - - if (isLoading.get()) { - // if still loading, block menu buttons related to video info - return true; - } - - switch (id) { - case R.id.menu_item_share: - if (currentInfo != null) { - ShareUtils.shareUrl(requireContext(), currentInfo.getName(), - currentInfo.getOriginalUrl()); - } - return true; - case R.id.menu_item_openInBrowser: - if (currentInfo != null) { - ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl()); - } - return true; - case R.id.action_play_with_kodi: - try { - NavigationHelper.playWithKore(activity, Uri.parse(currentInfo.getUrl())); - } catch (Exception e) { - if (DEBUG) { - Log.i(TAG, "Failed to start kore", e); - } - KoreUtil.showInstallKoreDialog(activity); - } - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void setupActionBarOnError(final String u) { - if (DEBUG) { - Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + u + "]"); - } - Log.e("-----", "missing code"); - } - - private void setupActionBar(final StreamInfo info) { - if (DEBUG) { - Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]"); - } - boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity) - .getBoolean(activity.getString(R.string.use_external_video_player_key), false); - - sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), - info.getVideoOnlyStreams(), false); - selectedVideoStreamIndex = ListHelper - .getDefaultResolutionIndex(activity, sortedVideoStreams); - - final StreamItemAdapter streamsAdapter = new StreamItemAdapter<>( - activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), - isExternalPlayerEnabled); - spinnerToolbar.setAdapter(streamsAdapter); - spinnerToolbar.setSelection(selectedVideoStreamIndex); - spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(final AdapterView parent, final View view, - final int position, final long id) { - selectedVideoStreamIndex = position; - } - - @Override - public void onNothingSelected(final AdapterView parent) { } - }); - } - /*////////////////////////////////////////////////////////////////////////// // OwnStack //////////////////////////////////////////////////////////////////////////*/ - private void pushToStack(final int sid, final String videoUrl, final String title) { - if (DEBUG) { - Log.d(TAG, "pushToStack() called with: serviceId = [" - + sid + "], videoUrl = [" + videoUrl + "], title = [" + title + "]"); - } + /** + * Stack that contains the "navigation history".
+ * The peek is the current video. + */ + protected final LinkedList stack = new LinkedList<>(); - if (stack.size() > 0 - && stack.peek().getServiceId() == sid - && stack.peek().getUrl().equals(videoUrl)) { - Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" - + sid + "], videoUrl == peek.getUrl = [" + videoUrl + "]"); - return; - } else { - Log.d(TAG, "pushToStack() wasn't equal"); - } - - stack.push(new StackItem(sid, videoUrl, title)); - } - - private void setTitleToUrl(final int sid, final String videoUrl, final String title) { - if (title != null && !title.isEmpty()) { - for (StackItem stackItem : stack) { - if (stack.peek().getServiceId() == sid - && stackItem.getUrl().equals(videoUrl)) { - stackItem.setTitle(title); - } - } - } + @Override + public boolean onKeyDown(final int keyCode) { + return player != null && player.onKeyDown(keyCode); } @Override @@ -788,27 +874,75 @@ public class VideoDetailFragment extends BaseStateFragment if (DEBUG) { Log.d(TAG, "onBackPressed() called"); } + + // If we are in fullscreen mode just exit from it via first back press + if (player != null && player.isFullscreen()) { + if (!DeviceUtils.isTablet(activity)) { + player.onPause(); + } + restoreDefaultOrientation(); + setAutoplay(false); + return true; + } + + // If we have something in history of played items we replay it here + if (player != null + && player.getPlayQueue() != null + && player.videoPlayerSelected() + && player.getPlayQueue().previous()) { + return true; + } // That means that we are on the start of the stack, // return false to let the MainActivity handle the onBack if (stack.size() <= 1) { + restoreDefaultOrientation(); + return false; } // Remove top stack.pop(); // Get stack item from the new top - StackItem peek = stack.peek(); + assert stack.peek() != null; + setupFromHistoryItem(stack.peek()); - selectAndLoadVideo(peek.getServiceId(), peek.getUrl(), - !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : ""); return true; } + private void setupFromHistoryItem(final StackItem item) { + setAutoplay(false); + hideMainPlayer(); + + setInitialData( + item.getServiceId(), + item.getUrl(), + !TextUtils.isEmpty(item.getTitle()) ? item.getTitle() : "", + item.getPlayQueue()); + startLoading(false); + + // Maybe an item was deleted in background activity + if (item.getPlayQueue().getItem() == null) { + return; + } + + final PlayQueueItem playQueueItem = item.getPlayQueue().getItem(); + // Update title, url, uploader from the last item in the stack (it's current now) + final boolean isPlayerStopped = player == null || player.isPlayerStopped(); + if (playQueueItem != null && isPlayerStopped) { + updateOverlayData(playQueueItem.getTitle(), + playQueueItem.getUploader(), playQueueItem.getThumbnailUrl()); + } + } + /*////////////////////////////////////////////////////////////////////////// // Info loading and handling //////////////////////////////////////////////////////////////////////////*/ @Override protected void doInitialLoadLogic() { + if (wasCleared()) { + return; + } + if (currentInfo == null) { prepareAndLoadInfo(); } else { @@ -816,9 +950,16 @@ public class VideoDetailFragment extends BaseStateFragment } } - public void selectAndLoadVideo(final int sid, final String videoUrl, final String title) { - setInitialData(sid, videoUrl, title); - prepareAndLoadInfo(); + public void selectAndLoadVideo(final int sid, final String videoUrl, final String title, + final PlayQueue queue) { + // Situation when user switches from players to main player. + // All needed data is here, we can start watching + if (this.playQueue != null && this.playQueue.equals(queue)) { + openVideoPlayer(); + return; + } + setInitialData(sid, videoUrl, title, queue); + startLoading(false, true); } private void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) { @@ -827,22 +968,19 @@ public class VideoDetailFragment extends BaseStateFragment + "info = [" + info + "], scrollToTop = [" + scrollToTop + "]"); } - setInitialData(info.getServiceId(), info.getUrl(), info.getName()); - pushToStack(serviceId, url, name); showLoading(); initTabs(); if (scrollToTop) { - appBarLayout.setExpanded(true, true); + scrollToTop(); } handleResult(info); showContent(); } - private void prepareAndLoadInfo() { - appBarLayout.setExpanded(true, true); - pushToStack(serviceId, url, name); + protected void prepareAndLoadInfo() { + scrollToTop(); startLoading(false); } @@ -856,20 +994,46 @@ public class VideoDetailFragment extends BaseStateFragment currentWorker.dispose(); } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + runWorker(forceLoad, stack.isEmpty()); + } + private void startLoading(final boolean forceLoad, final boolean addToBackStack) { + super.startLoading(forceLoad); + + initTabs(); + currentInfo = null; + if (currentWorker != null) { + currentWorker.dispose(); + } + + runWorker(forceLoad, addToBackStack); + } + + private void runWorker(final boolean forceLoad, final boolean addToBackStack) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe((@NonNull final StreamInfo result) -> { isLoading.set(false); + hideMainPlayer(); if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean( getString(R.string.show_age_restricted_content), false)) { hideAgeRestrictedContent(); } else { - currentInfo = result; handleResult(result); showContent(); + if (addToBackStack) { + if (playQueue == null) { + playQueue = new SinglePlayQueue(result); + } + if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(playQueue)) { + stack.push(new StackItem(serviceId, url, name, playQueue)); + } + } + if (isAutoplayEnabled()) { + openVideoPlayer(); + } } }, (@NonNull final Throwable throwable) -> { isLoading.set(false); @@ -884,8 +1048,8 @@ public class VideoDetailFragment extends BaseStateFragment pageAdapter.clearAllItems(); if (shouldShowComments()) { - pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url, name), - COMMENTS_TAB_TAG); + pageAdapter.addFragment( + CommentsFragment.getInstance(serviceId, url, name), COMMENTS_TAB_TAG); } if (showRelatedStreams && null == relatedStreamsLayout) { @@ -921,17 +1085,28 @@ public class VideoDetailFragment extends BaseStateFragment } } + public void scrollToTop() { + appBarLayout.setExpanded(true, true); + } + /*////////////////////////////////////////////////////////////////////////// // Play Utils //////////////////////////////////////////////////////////////////////////*/ private void openBackgroundPlayer(final boolean append) { - AudioStream audioStream = currentInfo.getAudioStreams() + final AudioStream audioStream = currentInfo.getAudioStreams() .get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); - boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) + final boolean useExternalAudioPlayer = PreferenceManager + .getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); + // If a user watched video inside fullscreen mode and than chose another player + // return to non-fullscreen mode + if (player != null && player.isFullscreen()) { + player.toggleFullscreen(); + } + if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { openNormalBackgroundPlayer(append); } else { @@ -945,45 +1120,95 @@ public class VideoDetailFragment extends BaseStateFragment return; } - final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); + // See UI changes while remote playQueue changes + if (!bound) { + startService(requireContext(), false); + } + + // If a user watched video inside fullscreen mode and than chose another player + // return to non-fullscreen mode + if (player != null && player.isFullscreen()) { + player.toggleFullscreen(); + } + + final PlayQueue queue = setupPlayQueueForIntent(append); if (append) { - NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue, false); + NavigationHelper.enqueueOnPopupPlayer(activity, queue, false); } else { - Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - final Intent intent = NavigationHelper.getPlayerIntent(activity, - PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true); - activity.startService(intent); + replaceQueueIfUserConfirms(() -> NavigationHelper + .playOnPopupPlayer(activity, queue, true)); } } private void openVideoPlayer() { - VideoStream selectedVideoStream = getSelectedVideoStream(); - if (PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(this.getString(R.string.use_external_video_player_key), false)) { - startOnExternalPlayer(activity, currentInfo, selectedVideoStream); + showExternalPlaybackDialog(); } else { - openNormalPlayer(); + replaceQueueIfUserConfirms(this::openMainPlayer); } } private void openNormalBackgroundPlayer(final boolean append) { - final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); + // See UI changes while remote playQueue changes + if (!bound) { + startService(requireContext(), false); + } + + final PlayQueue queue = setupPlayQueueForIntent(append); if (append) { - NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue, false); + NavigationHelper.enqueueOnBackgroundPlayer(activity, queue, false); } else { - NavigationHelper.playOnBackgroundPlayer(activity, itemQueue, true); + replaceQueueIfUserConfirms(() -> NavigationHelper + .playOnBackgroundPlayer(activity, queue, true)); } } - private void openNormalPlayer() { - Intent mIntent; - final PlayQueue playQueue = new SinglePlayQueue(currentInfo); - mIntent = NavigationHelper.getPlayerIntent(activity, - MainVideoPlayer.class, - playQueue, - getSelectedVideoStream().getResolution(), true); - startActivity(mIntent); + private void openMainPlayer() { + if (playerService == null) { + startService(requireContext(), true); + return; + } + if (currentInfo == null) { + return; + } + + 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() + Objects.requireNonNull(playerService.getView()).setVisibility(View.GONE); + addVideoPlayerView(); + + final Intent playerIntent = NavigationHelper + .getPlayerIntent(requireContext(), MainPlayer.class, queue, null, true); + activity.startService(playerIntent); + } + + private void hideMainPlayer() { + if (playerService == null + || playerService.getView() == null + || !player.videoPlayerSelected()) { + return; + } + + removeVideoPlayerView(); + playerService.stop(isAutoplayEnabled()); + playerService.getView().setVisibility(View.GONE); + } + + private PlayQueue setupPlayQueueForIntent(final boolean append) { + if (append) { + return new SinglePlayQueue(currentInfo); + } + + PlayQueue queue = playQueue; + // Size can be 0 because queue removes bad stream automatically when error occurs + if (queue == null || queue.size() == 0) { + queue = new SinglePlayQueue(currentInfo); + } + + return queue; } /*////////////////////////////////////////////////////////////////////////// @@ -1008,9 +1233,70 @@ public class VideoDetailFragment extends BaseStateFragment )); } - @Nullable - private VideoStream getSelectedVideoStream() { - return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; + private boolean isExternalPlayerEnabled() { + return PreferenceManager.getDefaultSharedPreferences(getContext()) + .getBoolean(getString(R.string.use_external_video_player_key), false); + } + + // This method overrides default behaviour when setAutoplay() is called. + // Don't auto play if the user selected an external player or disabled it in settings + private boolean isAutoplayEnabled() { + return autoPlayEnabled + && !isExternalPlayerEnabled() + && (player == null || player.videoPlayerSelected()) + && bottomSheetState != BottomSheetBehavior.STATE_HIDDEN + && isAutoplayAllowedByUser(); + } + + private boolean isAutoplayAllowedByUser() { + if (activity == null) { + return false; + } + + switch (PlayerHelper.getAutoplayType(activity)) { + case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER: + return false; + case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI: + return !ListHelper.isMeteredNetwork(activity); + case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS: + default: + return true; + } + } + + private void addVideoPlayerView() { + if (player == null || getView() == null) { + return; + } + + final FrameLayout viewHolder = getView().findViewById(R.id.player_placeholder); + + // Check if viewHolder already contains a child + if (player.getRootView().getParent() != viewHolder) { + removeVideoPlayerView(); + } + setHeightThumbnail(); + + // Prevent from re-adding a view multiple times + if (player.getRootView().getParent() == null) { + viewHolder.addView(player.getRootView()); + } + } + + private void removeVideoPlayerView() { + makeDefaultHeightForVideoPlaceholder(); + + playerService.removeViewFromParent(); + } + + private void makeDefaultHeightForVideoPlaceholder() { + if (getView() == null) { + return; + } + + final FrameLayout viewHolder = getView().findViewById(R.id.player_placeholder); + viewHolder.getLayoutParams().height = FrameLayout.LayoutParams.MATCH_PARENT; + viewHolder.requestLayout(); } private void prepareDescription(final Description description) { @@ -1051,25 +1337,47 @@ public class VideoDetailFragment extends BaseStateFragment } } + /** + * 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 + * screen orientations. It knows about multiWindow feature + * and about videos with aspectRatio ZOOM (the height for them will be a bit higher, + * {@link #MAX_PLAYER_HEIGHT}) + */ private void setHeightThumbnail() { final DisplayMetrics metrics = getResources().getDisplayMetrics(); - boolean isPortrait = metrics.heightPixels > metrics.widthPixels; - int height = isPortrait - ? (int) (metrics.widthPixels / (16.0f / 9.0f)) - : (int) (metrics.heightPixels / 2f); + final boolean isPortrait = metrics.heightPixels > metrics.widthPixels; + + final int height; + if (player != null && player.isFullscreen()) { + height = isInMultiWindow() + ? Objects.requireNonNull(getView()).getHeight() + : activity.getWindow().getDecorView().getHeight(); + } else { + height = isPortrait + ? (int) (metrics.widthPixels / (16.0f / 9.0f)) + : (int) (metrics.heightPixels / 2.0f); + } + thumbnailImageView.setLayoutParams( new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); thumbnailImageView.setMinimumHeight(height); + if (player != null) { + final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT); + player.getSurfaceView().setHeights(height, player.isFullscreen() ? height : maxHeight); + } } private void showContent() { contentRootLayoutHiding.setVisibility(View.VISIBLE); } - protected void setInitialData(final int sid, final String u, final String title) { + protected void setInitialData(final int sid, final String u, final String title, + final PlayQueue queue) { this.serviceId = sid; this.url = u; this.name = !TextUtils.isEmpty(title) ? title : ""; + this.playQueue = queue; } private void setErrorImage(final int imageResource) { @@ -1094,6 +1402,45 @@ public class VideoDetailFragment extends BaseStateFragment setErrorImage(imageError); } + private void setupBroadcastReceiver() { + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + if (intent.getAction().equals(ACTION_SHOW_MAIN_PLAYER)) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } else if (intent.getAction().equals(ACTION_HIDE_MAIN_PLAYER)) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + } + }; + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_SHOW_MAIN_PLAYER); + intentFilter.addAction(ACTION_HIDE_MAIN_PLAYER); + activity.registerReceiver(broadcastReceiver, intentFilter); + } + + + /*////////////////////////////////////////////////////////////////////////// + // Orientation listener + //////////////////////////////////////////////////////////////////////////*/ + + private void restoreDefaultOrientation() { + if (player == null || !player.videoPlayerSelected() || activity == null) { + return; + } + + if (player != null && player.isFullscreen()) { + player.toggleFullscreen(); + } + // This will show systemUI and pause the player. + // User can tap on Play button and video will be in fullscreen mode again + // Note for tablet: trying to avoid orientation changes since it's not easy + // to physically rotate the tablet every time + if (!DeviceUtils.isTablet(activity)) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + } + /*////////////////////////////////////////////////////////////////////////// // Contract //////////////////////////////////////////////////////////////////////////*/ @@ -1108,7 +1455,6 @@ public class VideoDetailFragment extends BaseStateFragment contentRootLayoutHiding.setVisibility(View.INVISIBLE); } - animateView(spinnerToolbar, false, 200); animateView(thumbnailPlayButton, false, 50); animateView(detailDurationView, false, 100); animateView(detailPositionView, false, 100); @@ -1124,7 +1470,8 @@ public class VideoDetailFragment extends BaseStateFragment if (relatedStreamsLayout != null) { if (showRelatedStreams) { - relatedStreamsLayout.setVisibility(View.INVISIBLE); + relatedStreamsLayout.setVisibility( + player != null && player.isFullscreen() ? View.GONE : View.INVISIBLE); } else { relatedStreamsLayout.setVisibility(View.GONE); } @@ -1140,24 +1487,23 @@ public class VideoDetailFragment extends BaseStateFragment public void handleResult(@NonNull final StreamInfo info) { super.handleResult(info); - setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); + currentInfo = info; + setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName(), playQueue); if (showRelatedStreams) { if (null == relatedStreamsLayout) { //phone pageAdapter.updateItem(RELATED_TAB_TAG, - RelatedVideosFragment.getInstance(currentInfo)); + RelatedVideosFragment.getInstance(info)); pageAdapter.notifyDataSetUpdate(); } else { //tablet getChildFragmentManager().beginTransaction() .replace(R.id.relatedStreamsLayout, - RelatedVideosFragment.getInstance(currentInfo)) - .commitNow(); - relatedStreamsLayout.setVisibility(View.VISIBLE); + RelatedVideosFragment.getInstance(info)) + .commitAllowingStateLoss(); + relatedStreamsLayout.setVisibility( + player != null && player.isFullscreen() ? View.GONE : View.VISIBLE); } } - - //pushToStack(serviceId, url, name); - animateView(thumbnailPlayButton, true, 200); videoTitleTextView.setText(name); @@ -1248,15 +1594,20 @@ public class VideoDetailFragment extends BaseStateFragment videoUploadDateView.setVisibility(View.GONE); } + sortedVideoStreams = ListHelper.getSortedStreamVideosList( + activity, + info.getVideoStreams(), + info.getVideoOnlyStreams(), + false); + selectedVideoStreamIndex = ListHelper + .getDefaultResolutionIndex(activity, sortedVideoStreams); prepareDescription(info.getDescription()); updateProgressInfo(info); - - animateView(spinnerToolbar, true, 500); - setupActionBar(info); initThumbnailViews(info); - setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName()); - setTitleToUrl(info.getServiceId(), info.getOriginalUrl(), info.getName()); + if (player == null || player.isPlayerStopped()) { + updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); + } if (!info.getErrors().isEmpty()) { showSnackBarError(info.getErrors(), @@ -1270,7 +1621,6 @@ public class VideoDetailFragment extends BaseStateFragment case LIVE_STREAM: case AUDIO_LIVE_STREAM: detailControlsDownload.setVisibility(View.GONE); - spinnerToolbar.setVisibility(View.GONE); break; default: if (info.getAudioStreams().isEmpty()) { @@ -1279,18 +1629,10 @@ public class VideoDetailFragment extends BaseStateFragment if (!info.getVideoStreams().isEmpty() || !info.getVideoOnlyStreams().isEmpty()) { break; } - detailControlsPopup.setVisibility(View.GONE); - spinnerToolbar.setVisibility(View.GONE); thumbnailPlayButton.setImageResource(R.drawable.ic_headset_shadow); break; } - - if (autoPlayEnabled) { - openVideoPlayer(); - // Only auto play in the first open - autoPlayEnabled = false; - } } private void hideAgeRestrictedContent() { @@ -1331,15 +1673,15 @@ public class VideoDetailFragment extends BaseStateFragment public void openDownloadDialog() { try { - DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); + final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); downloadDialog.setVideoStreams(sortedVideoStreams); downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); - downloadDialog.show(getActivity().getSupportFragmentManager(), "downloadDialog"); + downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); } catch (Exception e) { - ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, + final ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, ServiceList.all() .get(currentInfo .getServiceId()) @@ -1347,10 +1689,10 @@ public class VideoDetailFragment extends BaseStateFragment .getName(), "", R.string.could_not_setup_download_menu); - ErrorActivity.reportError(getActivity(), + ErrorActivity.reportError(activity, e, - getActivity().getClass(), - getActivity().findViewById(android.R.id.content), info); + activity.getClass(), + activity.findViewById(android.R.id.content), info); } } @@ -1367,8 +1709,8 @@ public class VideoDetailFragment extends BaseStateFragment int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error : exception instanceof ExtractionException - ? R.string.parsing_error - : R.string.general_error; + ? R.string.parsing_error + : R.string.general_error; onUnrecoverableError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, errorId); @@ -1384,17 +1726,28 @@ public class VideoDetailFragment extends BaseStateFragment final boolean playbackResumeEnabled = prefs .getBoolean(activity.getString(R.string.enable_watch_history_key), true) && prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true); - - if (!playbackResumeEnabled || info.getDuration() <= 0) { - positionView.setVisibility(View.INVISIBLE); - detailPositionView.setVisibility(View.GONE); - - // TODO: Remove this check when separation of concerns is done. - // (live streams weren't getting updated because they are mixed) - if (!info.getStreamType().equals(StreamType.LIVE_STREAM) - && !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { - return; + final boolean showPlaybackPosition = prefs.getBoolean( + activity.getString(R.string.enable_playback_state_lists_key), true); + if (!playbackResumeEnabled) { + if (playQueue == null || playQueue.getStreams().isEmpty() + || playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET + || !showPlaybackPosition) { + positionView.setVisibility(View.INVISIBLE); + detailPositionView.setVisibility(View.GONE); + // TODO: Remove this check when separation of concerns is done. + // (live streams weren't getting updated because they are mixed) + if (!info.getStreamType().equals(StreamType.LIVE_STREAM) + && !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { + return; + } + } else { + // Show saved position from backStack if user allows it + showPlaybackProgress(playQueue.getItem().getRecoveryPosition(), + playQueue.getItem().getDuration() * 1000); + animateView(positionView, true, 500); + animateView(detailPositionView, true, 500); } + return; } final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); @@ -1405,11 +1758,7 @@ public class VideoDetailFragment extends BaseStateFragment .onErrorComplete() .observeOn(AndroidSchedulers.mainThread()) .subscribe(state -> { - final int seconds - = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()); - positionView.setMax((int) info.getDuration()); - positionView.setProgressAnimated(seconds); - detailPositionView.setText(Localization.getDurationString(seconds)); + showPlaybackProgress(state.getProgressTime(), info.getDuration() * 1000); animateView(positionView, true, 500); animateView(detailPositionView, true, 500); }, e -> { @@ -1417,8 +1766,546 @@ public class VideoDetailFragment extends BaseStateFragment e.printStackTrace(); } }, () -> { - animateView(positionView, false, 500); - animateView(detailPositionView, false, 500); + positionView.setVisibility(View.GONE); + detailPositionView.setVisibility(View.GONE); }); } + + private void showPlaybackProgress(final long progress, final long duration) { + final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress); + final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration); + positionView.setMax(durationSeconds); + positionView.setProgressAnimated(progressSeconds); + detailPositionView.setText(Localization.getDurationString(progressSeconds)); + if (positionView.getVisibility() != View.VISIBLE) { + animateView(positionView, true, 100); + animateView(detailPositionView, true, 100); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Player event listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onQueueUpdate(final PlayQueue queue) { + playQueue = queue; + // This should be the only place where we push data to stack. + // 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)); + } else { + final StackItem stackWithQueue = findQueueInStack(queue); + if (stackWithQueue != null) { + // On every MainPlayer service's destroy() playQueue gets disposed and + // no longer able to track progress. That's why we update our cached disposed + // queue with the new one that is active and have the same history. + // Without that the cached playQueue will have an old recovery position + stackWithQueue.setPlayQueue(queue); + } + } + + if (DEBUG) { + Log.d(TAG, "onQueueUpdate() called with: serviceId = [" + + serviceId + "], videoUrl = [" + url + "], name = [" + + name + "], playQueue = [" + playQueue + "]"); + } + } + + @Override + public void onPlaybackUpdate(final int state, + final int repeatMode, + final boolean shuffled, + final PlaybackParameters parameters) { + setOverlayPlayPauseImage(); + + switch (state) { + case BasePlayer.STATE_COMPLETED: + restoreDefaultOrientation(); + break; + case BasePlayer.STATE_PLAYING: + if (positionView.getAlpha() != 1.0f + && player.getPlayQueue() != null + && player.getPlayQueue().getItem() != null + && player.getPlayQueue().getItem().getUrl().equals(url)) { + animateView(positionView, true, 100); + animateView(detailPositionView, true, 100); + } + break; + } + } + + @Override + public void onProgressUpdate(final int currentProgress, + final int duration, + final int bufferPercent) { + // Progress updates every second even if media is paused. It's useless until playing + if (!player.getPlayer().isPlaying() || playQueue == null) { + return; + } + + if (player.getPlayQueue().getItem().getUrl().equals(url)) { + showPlaybackProgress(currentProgress, duration); + } + } + + @Override + public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) { + final StackItem item = findQueueInStack(queue); + if (item != null) { + // When PlayQueue can have multiple streams (PlaylistPlayQueue or ChannelPlayQueue) + // every new played stream gives new title and url. + // StackItem contains information about first played stream. Let's update it here + item.setTitle(info.getName()); + item.setUrl(info.getUrl()); + } + // They are not equal when user watches something in popup while browsing in fragment and + // then changes screen orientation. In that case the fragment will set itself as + // a service listener and will receive initial call to onMetadataUpdate() + if (!queue.equals(playQueue)) { + return; + } + + updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); + if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) { + return; + } + + currentInfo = info; + setInitialData(info.getServiceId(), info.getUrl(), info.getName(), queue); + setAutoplay(false); + prepareAndHandleInfo(info, true); + } + + @Override + public void onPlayerError(final ExoPlaybackException error) { + if (error.type == ExoPlaybackException.TYPE_SOURCE + || error.type == ExoPlaybackException.TYPE_UNEXPECTED) { + hideMainPlayer(); + if (playerService != null && player.isFullscreen()) { + player.toggleFullscreen(); + } + } + } + + @Override + public void onServiceStopped() { + unbind(requireContext()); + setOverlayPlayPauseImage(); + if (currentInfo != null) { + updateOverlayData(currentInfo.getName(), + currentInfo.getUploaderName(), + currentInfo.getThumbnailUrl()); + } + } + + @Override + public void onFullscreenStateChanged(final boolean fullscreen) { + if (playerService.getView() == null || player.getParentActivity() == null) { + return; + } + + final View view = playerService.getView(); + final ViewGroup parent = (ViewGroup) view.getParent(); + if (parent == null) { + return; + } + + if (fullscreen) { + hideSystemUiIfNeeded(); + } else { + showSystemUi(); + } + + if (relatedStreamsLayout != null) { + relatedStreamsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE); + } + scrollToTop(); + + addVideoPlayerView(); + } + + @Override + public void onScreenRotationButtonClicked() { + // In tablet user experience will be better if screen will not be rotated + // from landscape to portrait every time. + // Just turn on fullscreen mode in landscape orientation + if (isLandscape() && DeviceUtils.isTablet(activity)) { + player.toggleFullscreen(); + return; + } + + final int newOrientation = isLandscape() + ? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + + activity.setRequestedOrientation(newOrientation); + } + + /* + * Will scroll down to description view after long click on moreOptionsButton + * */ + @Override + public void onMoreOptionsLongClicked() { + final CoordinatorLayout.LayoutParams params = + (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams(); + final AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior(); + final ValueAnimator valueAnimator = ValueAnimator + .ofInt(0, -getView().findViewById(R.id.player_placeholder).getHeight()); + valueAnimator.setInterpolator(new DecelerateInterpolator()); + valueAnimator.addUpdateListener(animation -> { + behavior.setTopAndBottomOffset((int) animation.getAnimatedValue()); + appBarLayout.requestLayout(); + }); + valueAnimator.setInterpolator(new DecelerateInterpolator()); + valueAnimator.setDuration(500); + valueAnimator.start(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Player related utils + //////////////////////////////////////////////////////////////////////////*/ + + private void showSystemUi() { + if (DEBUG) { + Log.d(TAG, "showSystemUi() called"); + } + + if (activity == null) { + return; + } + + activity.getWindow().getDecorView().setSystemUiVisibility(0); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + + private void hideSystemUi() { + if (DEBUG) { + Log.d(TAG, "hideSystemUi() called"); + } + + if (activity == null) { + return; + } + + final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + activity.getWindow().getDecorView().setSystemUiVisibility(visibility); + activity.getWindow().setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + + // Listener implementation + public void hideSystemUiIfNeeded() { + if (player != null + && player.isFullscreen() + && bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { + hideSystemUi(); + } + } + + private boolean playerIsNotStopped() { + return player != null + && player.getPlayer() != null + && player.getPlayer().getPlaybackState() != Player.STATE_IDLE; + } + + private void setupBrightness(final boolean save) { + if (activity == null) { + return; + } + + final WindowManager.LayoutParams lp = activity.getWindow().getAttributes(); + if (save) { + // Save current brightness level + PlayerHelper.setScreenBrightness(activity, lp.screenBrightness); + + // Restore the old brightness when fragment.onPause() called. + // It means when user leaves this fragment brightness will be set to system brightness + lp.screenBrightness = -1; + } else { + // Restore already saved brightness level + final float brightnessLevel = PlayerHelper.getScreenBrightness(activity); + if (brightnessLevel <= 0.0f && brightnessLevel > 1.0f) { + return; + } + + lp.screenBrightness = brightnessLevel; + } + activity.getWindow().setAttributes(lp); + } + + private void checkLandscape() { + if ((!player.isPlaying() && player.getPlayQueue() != playQueue) + || player.getPlayQueue() == null) { + setAutoplay(true); + } + + player.checkLandscape(); + final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(activity); + // Let's give a user time to look at video information page if video is not playing + if (orientationLocked && !player.isPlaying()) { + player.onPlay(); + player.showControlsThenHide(); + } + } + + private boolean isLandscape() { + return getResources().getDisplayMetrics().heightPixels < getResources() + .getDisplayMetrics().widthPixels; + } + + private boolean isInMultiWindow() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode(); + } + + /* + * Means that the player fragment was swiped away via BottomSheetLayout + * and is empty but ready for any new actions. See cleanUp() + * */ + private boolean wasCleared() { + return url == null; + } + + private StackItem findQueueInStack(final PlayQueue queue) { + StackItem item = null; + final Iterator iterator = stack.descendingIterator(); + while (iterator.hasNext()) { + final StackItem next = iterator.next(); + if (next.getPlayQueue().equals(queue)) { + item = next; + break; + } + } + return item; + } + + private void replaceQueueIfUserConfirms(final Runnable onAllow) { + @Nullable final PlayQueue activeQueue = player == null ? null : player.getPlayQueue(); + + // Player will have STATE_IDLE when a user pressed back button + if (isClearingQueueConfirmationRequired(activity) + && playerIsNotStopped() + && activeQueue != null + && !activeQueue.equals(playQueue) + && activeQueue.getStreams().size() > 1) { + showClearingQueueConfirmation(onAllow); + } else { + onAllow.run(); + } + } + + private void showClearingQueueConfirmation(final Runnable onAllow) { + new AlertDialog.Builder(activity) + .setTitle(R.string.clear_queue_confirmation_description) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + onAllow.run(); + dialog.dismiss(); + }).show(); + } + + private void showExternalPlaybackDialog() { + if (sortedVideoStreams == null) { + return; + } + CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()]; + for (int i = 0; i < sortedVideoStreams.size(); i++) { + resolutions[i] = sortedVideoStreams.get(i).getResolution(); + } + AlertDialog.Builder builder = new AlertDialog.Builder(activity) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(R.string.open_in_browser, (dialog, i) -> + ShareUtils.openUrlInBrowser(requireActivity(), url) + ); + // Maybe there are no video streams available, show just `open in browser` button + if (resolutions.length > 0) { + builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> { + dialog.dismiss(); + startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i)); + } + ); + } + builder.show(); + } + + /* + * Remove unneeded information while waiting for a next task + * */ + private void cleanUp() { + // New beginning + stack.clear(); + if (currentWorker != null) { + currentWorker.dispose(); + } + stopService(requireContext()); + setInitialData(0, null, "", null); + currentInfo = null; + updateOverlayData(null, null, null); + } + + /*////////////////////////////////////////////////////////////////////////// + // Bottom mini player + //////////////////////////////////////////////////////////////////////////*/ + + /** + * That's for Android TV support. Move focus from main fragment to the player or back + * based on what is currently selected + * + * @param toMain if true than the main fragment will be focused or the player otherwise + */ + private void moveFocusToMainFragment(final boolean toMain) { + final ViewGroup mainFragment = requireActivity().findViewById(R.id.fragment_holder); + // Hamburger button steels a focus even under bottomSheet + final Toolbar toolbar = requireActivity().findViewById(R.id.toolbar); + final int afterDescendants = ViewGroup.FOCUS_AFTER_DESCENDANTS; + final int blockDescendants = ViewGroup.FOCUS_BLOCK_DESCENDANTS; + if (toMain) { + mainFragment.setDescendantFocusability(afterDescendants); + toolbar.setDescendantFocusability(afterDescendants); + ((ViewGroup) requireView()).setDescendantFocusability(blockDescendants); + mainFragment.requestFocus(); + } else { + mainFragment.setDescendantFocusability(blockDescendants); + toolbar.setDescendantFocusability(blockDescendants); + ((ViewGroup) requireView()).setDescendantFocusability(afterDescendants); + thumbnailBackgroundButton.requestFocus(); + } + } + + private void setupBottomPlayer() { + final CoordinatorLayout.LayoutParams params = + (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams(); + final AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior(); + + final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder); + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout); + bottomSheetBehavior.setState(bottomSheetState); + final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height); + if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) { + bottomSheetBehavior.setPeekHeight(peekHeight); + if (bottomSheetState == BottomSheetBehavior.STATE_COLLAPSED) { + overlay.setAlpha(MAX_OVERLAY_ALPHA); + } else if (bottomSheetState == BottomSheetBehavior.STATE_EXPANDED) { + overlay.setAlpha(0); + setOverlayElementsClickable(false); + } + } + + bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull final View bottomSheet, final int newState) { + bottomSheetState = newState; + + switch (newState) { + case BottomSheetBehavior.STATE_HIDDEN: + moveFocusToMainFragment(true); + + bottomSheetBehavior.setPeekHeight(0); + cleanUp(); + break; + case BottomSheetBehavior.STATE_EXPANDED: + moveFocusToMainFragment(false); + + bottomSheetBehavior.setPeekHeight(peekHeight); + // Disable click because overlay buttons located on top of buttons + // from the player + setOverlayElementsClickable(false); + hideSystemUiIfNeeded(); + // Conditions when the player should be expanded to fullscreen + if (isLandscape() + && player != null + && player.isPlaying() + && !player.isFullscreen() + && !DeviceUtils.isTablet(activity) + && player.videoPlayerSelected()) { + player.toggleFullscreen(); + } + break; + case BottomSheetBehavior.STATE_COLLAPSED: + moveFocusToMainFragment(true); + + // Re-enable clicks + setOverlayElementsClickable(true); + if (player != null) { + player.onQueueClosed(); + } + break; + case BottomSheetBehavior.STATE_DRAGGING: + case BottomSheetBehavior.STATE_SETTLING: + if (player != null && player.isFullscreen()) { + showSystemUi(); + } + if (player != null && player.isControlsVisible()) { + player.hideControls(0, 0); + } + break; + } + } + + @Override + public void onSlide(@NonNull final View bottomSheet, final float slideOffset) { + setOverlayLook(appBarLayout, behavior, slideOffset); + } + }); + + // User opened a new page and the player will hide itself + activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> { + if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + } + + private void updateOverlayData(@Nullable final String title, + @Nullable final String uploader, + @Nullable final String thumbnailUrl) { + overlayTitleTextView.setText(TextUtils.isEmpty(title) ? "" : title); + overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader); + overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); + if (!TextUtils.isEmpty(thumbnailUrl)) { + IMAGE_LOADER.displayImage(thumbnailUrl, overlayThumbnailImageView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null); + } + } + + private void setOverlayPlayPauseImage() { + final int attr = player != null && player.isPlaying() + ? R.attr.ic_pause + : R.attr.ic_play_arrow; + overlayPlayPauseButton.setImageResource( + ThemeHelper.resolveResourceIdFromAttr(activity, attr)); + } + + private void setOverlayLook(final AppBarLayout appBar, + final AppBarLayout.Behavior behavior, + final float slideOffset) { + // SlideOffset < 0 when mini player is about to close via swipe. + // Stop animation in this case + if (behavior == null || slideOffset < 0) { + return; + } + overlay.setAlpha(Math.min(MAX_OVERLAY_ALPHA, 1 - slideOffset)); + // These numbers are not special. They just do a cool transition + behavior.setTopAndBottomOffset( + (int) (-thumbnailImageView.getHeight() * 2 * (1 - slideOffset) / 3)); + appBar.requestLayout(); + } + + private void setOverlayElementsClickable(final boolean enable) { + overlayThumbnailImageView.setClickable(enable); + overlayThumbnailImageView.setLongClickable(enable); + overlayMetadata.setClickable(enable); + overlayMetadata.setLongClickable(enable); + overlayButtons.setClickable(enable); + overlayPlayPauseButton.setClickable(enable); + overlayCloseButton.setClickable(enable); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 330aa7b42..14911e593 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -519,7 +519,7 @@ public class ChannelFragment extends BaseListInfoFragment monitorSubscription(result); headerPlayAllButton.setOnClickListener(view -> NavigationHelper - .playOnMainPlayer(activity, getPlayQueue(), false)); + .playOnMainPlayer(activity, getPlayQueue(), true)); headerPopupButton.setOnClickListener(view -> NavigationHelper .playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> NavigationHelper diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index e2ec9c1f3..48ec7b505 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -318,7 +318,7 @@ public class PlaylistFragment extends BaseListInfoFragment { .subscribe(getPlaylistBookmarkSubscriber()); headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true)); headerPopupButton.setOnClickListener(view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 4f21565f4..12abc29ae 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -48,7 +48,7 @@ import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; @@ -482,16 +482,16 @@ public class SearchFragment extends BaseListFragment { - if (AndroidTvUtils.isTv(itemBuilder.getContext())) { + if (DeviceUtils.isTv(itemBuilder.getContext())) { openCommentAuthor(item); } else { ShareUtils.copyToClipboard(itemBuilder.getContext(), commentText); diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 582be00d9..4b5ad31a3 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -321,7 +321,7 @@ public class StatisticsPlaylistFragment } headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true)); headerPopupButton.setOnClickListener(view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 485d3f391..96056bd39 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -492,7 +492,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment - NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true)); headerPopupButton.setOnClickListener(view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt index 66387d298..80036cd4a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt @@ -40,7 +40,7 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.Dia import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem import org.schabi.newpipe.local.subscription.item.PickerIconItem import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem -import org.schabi.newpipe.util.AndroidTvUtils +import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.ThemeHelper class FeedGroupDialog : DialogFragment(), BackPressable { @@ -237,7 +237,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable { } toolbar_search_edit_text.setOnClickListener { - if (AndroidTvUtils.isTv(context)) { + if (DeviceUtils.isTv(context)) { showKeyboardSearch() } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java deleted file mode 100644 index 943d685b1..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ /dev/null @@ -1,684 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * BackgroundPlayer.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.os.Build; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.util.Log; -import android.view.View; -import android.widget.RemoteViews; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.core.app.NotificationCompat; - -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.source.MediaSource; -import com.nostra13.universalimageloader.core.assist.FailReason; - -import org.schabi.newpipe.BuildConfig; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.event.PlayerEventListener; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.util.BitmapUtils; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ThemeHelper; - -import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - -/** - * Service Background Player implementing {@link VideoPlayer}. - * - * @author mauriciocolli - */ -public final class BackgroundPlayer extends Service { - public static final String ACTION_CLOSE - = "org.schabi.newpipe.player.BackgroundPlayer.CLOSE"; - public static final String ACTION_PLAY_PAUSE - = "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE"; - public static final String ACTION_REPEAT - = "org.schabi.newpipe.player.BackgroundPlayer.REPEAT"; - public static final String ACTION_PLAY_NEXT - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_NEXT"; - public static final String ACTION_PLAY_PREVIOUS - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_PREVIOUS"; - public static final String ACTION_FAST_REWIND - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND"; - public static final String ACTION_FAST_FORWARD - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD"; - - public static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource"; - private static final String TAG = "BackgroundPlayer"; - private static final boolean DEBUG = BasePlayer.DEBUG; - private static final int NOTIFICATION_ID = 123789; - private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60; - private BasePlayerImpl basePlayerImpl; - - /*////////////////////////////////////////////////////////////////////////// - // Service-Activity Binder - //////////////////////////////////////////////////////////////////////////*/ - private SharedPreferences sharedPreferences; - - /*////////////////////////////////////////////////////////////////////////// - // Notification - //////////////////////////////////////////////////////////////////////////*/ - private PlayerEventListener activityListener; - private IBinder mBinder; - private NotificationManager notificationManager; - private NotificationCompat.Builder notBuilder; - private RemoteViews notRemoteView; - private RemoteViews bigNotRemoteView; - private boolean shouldUpdateOnProgress; - private int timesNotificationUpdated; - - /*////////////////////////////////////////////////////////////////////////// - // Service's LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate() { - if (DEBUG) { - Log.d(TAG, "onCreate() called"); - } - notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - assureCorrectAppLanguage(this); - ThemeHelper.setTheme(this); - basePlayerImpl = new BasePlayerImpl(this); - basePlayerImpl.setup(); - - mBinder = new PlayerServiceBinder(basePlayerImpl); - shouldUpdateOnProgress = true; - } - - @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 + "]"); - } - basePlayerImpl.handleIntent(intent); - if (basePlayerImpl.mediaSessionManager != null) { - basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent); - } - return START_NOT_STICKY; - } - - @Override - public void onDestroy() { - if (DEBUG) { - Log.d(TAG, "destroy() called"); - } - onClose(); - } - - @Override - protected void attachBaseContext(final Context base) { - super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); - } - - @Override - public IBinder onBind(final Intent intent) { - return mBinder; - } - - /*////////////////////////////////////////////////////////////////////////// - // Actions - //////////////////////////////////////////////////////////////////////////*/ - private void onClose() { - if (DEBUG) { - Log.d(TAG, "onClose() called"); - } - - if (basePlayerImpl != null) { - basePlayerImpl.savePlaybackState(); - basePlayerImpl.stopActivityBinding(); - basePlayerImpl.destroy(); - } - if (notificationManager != null) { - notificationManager.cancel(NOTIFICATION_ID); - } - mBinder = null; - basePlayerImpl = null; - - stopForeground(true); - stopSelf(); - } - - private void onScreenOnOff(final boolean on) { - if (DEBUG) { - Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); - } - shouldUpdateOnProgress = on; - basePlayerImpl.triggerProgressUpdate(); - if (on) { - basePlayerImpl.startProgressLoop(); - } else { - basePlayerImpl.stopProgressLoop(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Notification - //////////////////////////////////////////////////////////////////////////*/ - - private void resetNotification() { - notBuilder = createNotification(); - timesNotificationUpdated = 0; - } - - private NotificationCompat.Builder createNotification() { - notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, - R.layout.player_background_notification); - bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, - R.layout.player_background_notification_expanded); - - setupNotification(notRemoteView); - setupNotification(bigNotRemoteView); - - NotificationCompat.Builder builder = new NotificationCompat - .Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setCustomContentView(notRemoteView) - .setCustomBigContentView(bigNotRemoteView); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - setLockScreenThumbnail(builder); - } - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - builder.setPriority(NotificationCompat.PRIORITY_MAX); - } - return builder; - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private void setLockScreenThumbnail(final NotificationCompat.Builder builder) { - boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean( - getString(R.string.enable_lock_screen_video_thumbnail_key), true); - - if (isLockScreenThumbnailEnabled) { - basePlayerImpl.mediaSessionManager.setLockScreenArt( - builder, - getCenteredThumbnailBitmap() - ); - } else { - basePlayerImpl.mediaSessionManager.clearLockScreenArt(builder); - } - } - - @Nullable - private Bitmap getCenteredThumbnailBitmap() { - final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; - final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; - - return BitmapUtils.centerCrop(basePlayerImpl.getThumbnail(), screenWidth, screenHeight); - } - - private void setupNotification(final RemoteViews remoteViews) { - if (basePlayerImpl == null) { - return; - } - - remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle()); - remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName()); - - remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); - remoteViews.setOnClickPendingIntent(R.id.notificationStop, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); - remoteViews.setOnClickPendingIntent(R.id.notificationRepeat, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); - - // Starts background player activity -- attempts to unlock lockscreen - final Intent intent = NavigationHelper.getBackgroundPlayerActivityIntent(this); - remoteViews.setOnClickPendingIntent(R.id.notificationContent, - PendingIntent.getActivity(this, NOTIFICATION_ID, intent, - PendingIntent.FLAG_UPDATE_CURRENT)); - - if (basePlayerImpl.playQueue != null && basePlayerImpl.playQueue.size() > 1) { - remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_previous); - remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_next); - remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); - remoteViews.setOnClickPendingIntent(R.id.notificationFForward, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT)); - } else { - remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_rewind); - remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_fastforward); - remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT)); - remoteViews.setOnClickPendingIntent(R.id.notificationFForward, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT)); - } - - setRepeatModeIcon(remoteViews, basePlayerImpl.getRepeatMode()); - } - - /** - * Updates the notification, and the play/pause button in it. - * Used for changes on the remoteView - * - * @param drawableId if != -1, sets the drawable with that id on the play/pause button - */ - private synchronized void updateNotification(final int drawableId) { -// if (DEBUG) { -// Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); -// } - if (notBuilder == null) { - return; - } - if (drawableId != -1) { - if (notRemoteView != null) { - notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); - } - if (bigNotRemoteView != null) { - bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); - } - } - notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); - timesNotificationUpdated++; - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) { - switch (repeatMode) { - case Player.REPEAT_MODE_OFF: - remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_repeat_off); - break; - case Player.REPEAT_MODE_ONE: - remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_repeat_one); - break; - case Player.REPEAT_MODE_ALL: - remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_repeat_all); - break; - } - } - ////////////////////////////////////////////////////////////////////////// - - protected class BasePlayerImpl extends BasePlayer { - @NonNull - private final AudioPlaybackResolver resolver; - private int cachedDuration; - private String cachedDurationString; - - BasePlayerImpl(final Context context) { - super(context); - this.resolver = new AudioPlaybackResolver(context, dataSource); - } - - @Override - public void initPlayer(final boolean playOnReady) { - super.initPlayer(playOnReady); - } - - @Override - public void handleIntent(final Intent intent) { - super.handleIntent(intent); - - resetNotification(); - if (bigNotRemoteView != null) { - bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); - } - if (notRemoteView != null) { - notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); - } - startForeground(NOTIFICATION_ID, notBuilder.build()); - } - - /*////////////////////////////////////////////////////////////////////////// - // Thumbnail Loading - //////////////////////////////////////////////////////////////////////////*/ - - private void updateNotificationThumbnail() { - if (basePlayerImpl == null) { - return; - } - if (notRemoteView != null) { - notRemoteView.setImageViewBitmap(R.id.notificationCover, - basePlayerImpl.getThumbnail()); - } - if (bigNotRemoteView != null) { - bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, - basePlayerImpl.getThumbnail()); - } - } - - @Override - public void onLoadingComplete(final String imageUri, final View view, - final Bitmap loadedImage) { - super.onLoadingComplete(imageUri, view, loadedImage); - resetNotification(); - updateNotificationThumbnail(); - updateNotification(-1); - } - - @Override - public void onLoadingFailed(final String imageUri, final View view, - final FailReason failReason) { - super.onLoadingFailed(imageUri, view, failReason); - resetNotification(); - updateNotificationThumbnail(); - updateNotification(-1); - } - - /*////////////////////////////////////////////////////////////////////////// - // States Implementation - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onPrepared(final boolean playWhenReady) { - super.onPrepared(playWhenReady); - } - - @Override - public void onShuffleClicked() { - super.onShuffleClicked(); - updatePlayback(); - } - - @Override - public void onMuteUnmuteButtonClicked() { - super.onMuteUnmuteButtonClicked(); - updatePlayback(); - } - - @Override - public void onUpdateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - updateProgress(currentProgress, duration, bufferPercent); - - if (!shouldUpdateOnProgress) { - return; - } - if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) { - resetNotification(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) { - updateNotificationThumbnail(); - } - } - if (bigNotRemoteView != null) { - if (cachedDuration != duration) { - cachedDuration = duration; - cachedDurationString = getTimeString(duration); - } - bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, - currentProgress, false); - bigNotRemoteView.setTextViewText(R.id.notificationTime, - getTimeString(currentProgress) + " / " + cachedDurationString); - } - if (notRemoteView != null) { - notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, - currentProgress, false); - } - updateNotification(-1); - } - - @Override - public void onPlayPrevious() { - super.onPlayPrevious(); - triggerProgressUpdate(); - } - - @Override - public void onPlayNext() { - super.onPlayNext(); - triggerProgressUpdate(); - } - - @Override - public void destroy() { - super.destroy(); - if (notRemoteView != null) { - notRemoteView.setImageViewBitmap(R.id.notificationCover, null); - } - if (bigNotRemoteView != null) { - bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { - super.onPlaybackParametersChanged(playbackParameters); - updatePlayback(); - } - - @Override - public void onLoadingChanged(final boolean isLoading) { - // Disable default behavior - } - - @Override - public void onRepeatModeChanged(final int i) { - resetNotification(); - updateNotification(-1); - updatePlayback(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - super.onMetadataChanged(tag); - resetNotification(); - updateNotificationThumbnail(); - updateNotification(-1); - updateMetadata(); - } - - @Override - @Nullable - public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - return resolver.resolve(info); - } - - @Override - public void onPlaybackShutdown() { - super.onPlaybackShutdown(); - onClose(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Activity Event Listener - //////////////////////////////////////////////////////////////////////////*/ - - /*package-private*/ void setActivityListener(final PlayerEventListener listener) { - activityListener = listener; - updateMetadata(); - updatePlayback(); - triggerProgressUpdate(); - } - - /*package-private*/ void removeActivityListener(final PlayerEventListener listener) { - if (activityListener == listener) { - activityListener = null; - } - } - - private void updateMetadata() { - if (activityListener != null && getCurrentMetadata() != null) { - activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); - } - } - - private void updatePlayback() { - if (activityListener != null && simpleExoPlayer != null && playQueue != null) { - activityListener.onPlaybackUpdate(currentState, getRepeatMode(), - playQueue.isShuffled(), getPlaybackParameters()); - } - } - - private void updateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - if (activityListener != null) { - activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); - } - } - - private void stopActivityBinding() { - if (activityListener != null) { - activityListener.onServiceStopped(); - activityListener = null; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Broadcast Receiver - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void setupBroadcastReceiver(final IntentFilter intentFltr) { - super.setupBroadcastReceiver(intentFltr); - intentFltr.addAction(ACTION_CLOSE); - intentFltr.addAction(ACTION_PLAY_PAUSE); - intentFltr.addAction(ACTION_REPEAT); - intentFltr.addAction(ACTION_PLAY_PREVIOUS); - intentFltr.addAction(ACTION_PLAY_NEXT); - intentFltr.addAction(ACTION_FAST_REWIND); - intentFltr.addAction(ACTION_FAST_FORWARD); - - intentFltr.addAction(Intent.ACTION_SCREEN_ON); - intentFltr.addAction(Intent.ACTION_SCREEN_OFF); - - intentFltr.addAction(Intent.ACTION_HEADSET_PLUG); - } - - @Override - public void onBroadcastReceived(final Intent intent) { - super.onBroadcastReceived(intent); - if (intent == null || intent.getAction() == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); - } - switch (intent.getAction()) { - case ACTION_CLOSE: - onClose(); - break; - case ACTION_PLAY_PAUSE: - onPlayPause(); - break; - case ACTION_REPEAT: - onRepeatClicked(); - break; - case ACTION_PLAY_NEXT: - onPlayNext(); - break; - case ACTION_PLAY_PREVIOUS: - onPlayPrevious(); - break; - case ACTION_FAST_FORWARD: - onFastForward(); - break; - case ACTION_FAST_REWIND: - onFastRewind(); - break; - case Intent.ACTION_SCREEN_ON: - onScreenOnOff(true); - break; - case Intent.ACTION_SCREEN_OFF: - onScreenOnOff(false); - break; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // States - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void changeState(final int state) { - super.changeState(state); - updatePlayback(); - } - - @Override - public void onPlaying() { - super.onPlaying(); - resetNotification(); - updateNotificationThumbnail(); - updateNotification(R.drawable.exo_controls_pause); - } - - @Override - public void onPaused() { - super.onPaused(); - resetNotification(); - updateNotificationThumbnail(); - updateNotification(R.drawable.exo_controls_play); - } - - @Override - public void onCompleted() { - super.onCompleted(); - resetNotification(); - if (bigNotRemoteView != null) { - bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); - } - if (notRemoteView != null) { - notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); - } - updateNotificationThumbnail(); - updateNotification(R.drawable.ic_replay_white_24dp); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java index 9da3c3c86..0e5222f5e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java @@ -1,13 +1,13 @@ package org.schabi.newpipe.player; import android.content.Intent; +import android.view.Menu; import android.view.MenuItem; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; -import static org.schabi.newpipe.player.BackgroundPlayer.ACTION_CLOSE; - public final class BackgroundPlayerActivity extends ServicePlayerActivity { private static final String TAG = "BackgroundPlayerActivity"; @@ -19,25 +19,25 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { @Override public String getSupportActionTitle() { - return getResources().getString(R.string.title_activity_background_player); + return getResources().getString(R.string.title_activity_play_queue); } @Override public Intent getBindIntent() { - return new Intent(this, BackgroundPlayer.class); + return new Intent(this, MainPlayer.class); } @Override public void startPlayerListener() { - if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) { - ((BackgroundPlayer.BasePlayerImpl) player).setActivityListener(this); + if (player instanceof VideoPlayerImpl) { + ((VideoPlayerImpl) player).setActivityListener(this); } } @Override public void stopPlayerListener() { - if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) { - ((BackgroundPlayer.BasePlayerImpl) player).removeActivityListener(this); + if (player instanceof VideoPlayerImpl) { + ((VideoPlayerImpl) player).removeActivityListener(this); } } @@ -56,18 +56,30 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { } this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startService( - getSwitchIntent(PopupVideoPlayer.class) - .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()) - ); + NavigationHelper.playOnPopupPlayer( + getApplicationContext(), player.playQueue, this.player.isPlaying()); return true; } + + if (item.getItemId() == R.id.action_switch_background) { + this.player.setRecovery(); + NavigationHelper.playOnBackgroundPlayer( + getApplicationContext(), player.playQueue, this.player.isPlaying()); + return true; + } + return false; } @Override - public Intent getPlayerShutdownIntent() { - return new Intent(ACTION_CLOSE); + public void setupMenu(final Menu menu) { + if (player == null) { + return; + } + + menu.findItem(R.id.action_switch_popup) + .setVisible(!((VideoPlayerImpl) player).popupPlayerSelected()); + menu.findItem(R.id.action_switch_background) + .setVisible(!((VideoPlayerImpl) player).audioPlayerSelected()); } } 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 61c5d9e68..1a8c98fd2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -54,6 +54,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; +import io.reactivex.android.schedulers.AndroidSchedulers; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.R; @@ -128,13 +129,15 @@ public abstract class BasePlayer implements @NonNull public static final String SELECT_ON_APPEND = "select_on_append"; @NonNull + public static final String PLAYER_TYPE = "player_type"; + @NonNull public static final String IS_MUTED = "is_muted"; /*////////////////////////////////////////////////////////////////////////// // Playback //////////////////////////////////////////////////////////////////////////*/ - protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; + protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f}; protected PlayQueue playQueue; protected PlayQueueAdapter playQueueAdapter; @@ -159,6 +162,10 @@ public abstract class BasePlayer implements protected static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; + public static final int PLAYER_TYPE_VIDEO = 0; + public static final int PLAYER_TYPE_AUDIO = 1; + public static final int PLAYER_TYPE_POPUP = 2; + protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; protected MediaSessionManager mediaSessionManager; @@ -223,7 +230,7 @@ public abstract class BasePlayer implements public void setup() { if (simpleExoPlayer == null) { - initPlayer(/*playOnInit=*/true); + initPlayer(true); } initListeners(); } @@ -250,7 +257,8 @@ public abstract class BasePlayer implements registerBroadcastReceiver(); } - public void initListeners() { } + public void initListeners() { + } public void handleIntent(final Intent intent) { if (DEBUG) { @@ -288,34 +296,72 @@ public abstract class BasePlayer implements final float playbackPitch = savedParameters.pitch; final boolean playbackSkipSilence = savedParameters.skipSilence; + final boolean samePlayQueue = playQueue != null && playQueue.equals(queue); + final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); final boolean isMuted = intent .getBooleanExtra(IS_MUTED, simpleExoPlayer != null && isMuted()); + /* + * There are 3 situations when playback shouldn't be started from scratch (zero timestamp): + * 1. User pressed on a timestamp link and the same video should be rewound to the timestamp + * 2. User changed a player from, for example. main to popup, or from audio to main, etc + * 3. User chose to resume a video based on a saved timestamp from history of played videos + * In those cases time will be saved because re-init of the play queue is a not an instant + * task and requires network calls + * */ // seek to timestamp if stream is already playing if (simpleExoPlayer != null && queue.size() == 1 && playQueue != null + && playQueue.size() == 1 && playQueue.getItem() != null && queue.getItem().getUrl().equals(playQueue.getItem().getUrl()) - && queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET - ) { + && queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + // Player can have state = IDLE when playback is stopped or failed + // and we should retry() in this case + if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) { + simpleExoPlayer.retry(); + } simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition()); return; - } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) && isPlaybackResumeEnabled()) { + + } else if (samePlayQueue && !playQueue.isDisposed() && simpleExoPlayer != null) { + // Do not re-init the same PlayQueue. Save time + // Player can have state = IDLE when playback is stopped or failed + // and we should retry() in this case + if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) { + simpleExoPlayer.retry(); + } + return; + } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) + && isPlaybackResumeEnabled() + && !samePlayQueue) { final PlayQueueItem item = queue.getItem(); if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { stateLoader = recordManager.loadStreamState(item) - .observeOn(mainThread()) - .doFinally(() -> initPlayback(queue, repeatMode, playbackSpeed, - playbackPitch, playbackSkipSilence, true, isMuted)) + .observeOn(AndroidSchedulers.mainThread()) + // Do not place initPlayback() in doFinally() because + // it restarts playback after destroy() + //.doFinally() .subscribe( - state -> queue - .setRecovery(queue.getIndex(), state.getProgressTime()), + state -> { + queue.setRecovery(queue.getIndex(), state.getProgressTime()); + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, true, isMuted); + }, error -> { if (DEBUG) { error.printStackTrace(); } + // In case any error we can start playback without history + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, true, isMuted); + }, + () -> { + // Completed but not found in history + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, true, isMuted); } ); databaseUpdateReactor.add(stateLoader); @@ -323,8 +369,11 @@ public abstract class BasePlayer implements } } // Good to go... - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, - /*playOnInit=*/!intent.getBooleanExtra(START_PAUSED, false), isMuted); + // In a case of equal PlayQueues we can re-init old one but only when it is disposed + initPlayback(samePlayQueue ? playQueue : queue, repeatMode, + playbackSpeed, playbackPitch, playbackSkipSilence, + !intent.getBooleanExtra(START_PAUSED, false), + isMuted); } private PlaybackParameters retrievePlaybackParametersFromPreferences() { @@ -410,6 +459,7 @@ public abstract class BasePlayer implements databaseUpdateReactor.clear(); progressUpdateReactor.set(null); + ImageLoader.getInstance().stop(); } /*////////////////////////////////////////////////////////////////////////// @@ -561,7 +611,8 @@ public abstract class BasePlayer implements } } - public void onPausedSeek() { } + public void onPausedSeek() { + } public void onCompleted() { if (DEBUG) { @@ -1089,6 +1140,7 @@ public abstract class BasePlayer implements } simpleExoPlayer.setPlayWhenReady(true); + savePlaybackState(); } public void onPause() { @@ -1101,6 +1153,7 @@ public abstract class BasePlayer implements audioReactor.abandonAudioFocus(); simpleExoPlayer.setPlayWhenReady(false); + savePlaybackState(); } public void onPlayPause() { @@ -1433,6 +1486,10 @@ public abstract class BasePlayer implements return simpleExoPlayer != null && simpleExoPlayer.isPlaying(); } + public boolean isLoading() { + return simpleExoPlayer != null && simpleExoPlayer.isLoading(); + } + @Player.RepeatMode public int getRepeatMode() { return simpleExoPlayer == null @@ -1473,8 +1530,9 @@ public abstract class BasePlayer implements /** * Sets the playback parameters of the player, and also saves them to shared preferences. * Speed and pitch are rounded up to 2 decimal places before being used or saved. - * @param speed the playback speed, will be rounded to up to 2 decimal places - * @param pitch the playback pitch, will be rounded to up to 2 decimal places + * + * @param speed the playback speed, will be rounded to up to 2 decimal places + * @param pitch the playback pitch, will be rounded to up to 2 decimal places * @param skipSilence skip silence during playback */ public void setPlaybackParameters(final float speed, final float pitch, @@ -1490,11 +1548,11 @@ public abstract class BasePlayer implements private void savePlaybackParametersToPreferences(final float speed, final float pitch, final boolean skipSilence) { PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putFloat(context.getString(R.string.playback_speed_key), speed) - .putFloat(context.getString(R.string.playback_pitch_key), pitch) - .putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence) - .apply(); + .edit() + .putFloat(context.getString(R.string.playback_speed_key), speed) + .putFloat(context.getString(R.string.playback_pitch_key), pitch) + .putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence) + .apply(); } public PlayQueue getPlayQueue() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java new file mode 100644 index 000000000..f62cf27b4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -0,0 +1,483 @@ +/* + * Copyright 2017 Mauricio Colli + * Part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.player; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.util.DisplayMetrics; +import android.view.ViewGroup; +import android.view.WindowManager; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; + +import com.google.android.exoplayer2.Player; + +import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.BitmapUtils; +import org.schabi.newpipe.util.NavigationHelper; +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 = BasePlayer.DEBUG; + + private VideoPlayerImpl playerImpl; + private WindowManager windowManager; + private SharedPreferences sharedPreferences; + + private final IBinder mBinder = new MainPlayer.LocalBinder(); + + public enum PlayerType { + VIDEO, + AUDIO, + POPUP + } + + /*////////////////////////////////////////////////////////////////////////// + // Notification + //////////////////////////////////////////////////////////////////////////*/ + + static final int NOTIFICATION_ID = 123789; + private NotificationManager notificationManager; + private NotificationCompat.Builder notBuilder; + private RemoteViews notRemoteView; + private RemoteViews bigNotRemoteView; + + static final String ACTION_CLOSE = + "org.schabi.newpipe.player.MainPlayer.CLOSE"; + static final String ACTION_PLAY_PAUSE = + "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE"; + static final String ACTION_OPEN_CONTROLS = + "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS"; + static final String ACTION_REPEAT = + "org.schabi.newpipe.player.MainPlayer.REPEAT"; + static final String ACTION_PLAY_NEXT = + "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT"; + static final String ACTION_PLAY_PREVIOUS = + "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS"; + static final String ACTION_FAST_REWIND = + "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND"; + static final String ACTION_FAST_FORWARD = + "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD"; + + private static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource"; + + /*////////////////////////////////////////////////////////////////////////// + // Service's LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate() { + if (DEBUG) { + Log.d(TAG, "onCreate() called"); + } + assureCorrectAppLanguage(this); + notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); + windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + + ThemeHelper.setTheme(this); + createView(); + } + + private void createView() { + final View layout = View.inflate(this, R.layout.player, null); + + playerImpl = new VideoPlayerImpl(this); + playerImpl.setup(layout); + playerImpl.shouldUpdateOnProgress = true; + } + + @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()) + && playerImpl.playQueue == 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(VideoPlayer.PLAY_QUEUE_KEY) != null) { + showNotificationAndStartForeground(); + } + + playerImpl.handleIntent(intent); + if (playerImpl.mediaSessionManager != null) { + playerImpl.mediaSessionManager.handleMediaButtonIntent(intent); + } + return START_NOT_STICKY; + } + + public void stop(final boolean autoplayEnabled) { + if (DEBUG) { + Log.d(TAG, "stop() called"); + } + + if (playerImpl.getPlayer() != null) { + playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); + // Releases wifi & cpu, disables keepScreenOn, etc. + if (!autoplayEnabled) { + playerImpl.onPause(); + } + // We can't just pause the player here because it will make transition + // from one stream to a new stream not smooth + playerImpl.getPlayer().stop(false); + playerImpl.setRecovery(); + // Android TV will handle back button in case controls will be visible + // (one more additional unneeded click while the player is hidden) + playerImpl.hideControls(0, 0); + // 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 + if (!autoplayEnabled) { + stopForeground(true); + } + } + } + + @Override + public void onTaskRemoved(final Intent rootIntent) { + super.onTaskRemoved(rootIntent); + onDestroy(); + // Unload from memory completely + Runtime.getRuntime().halt(0); + } + + @Override + public void onDestroy() { + if (DEBUG) { + Log.d(TAG, "destroy() called"); + } + onClose(); + } + + @Override + protected void attachBaseContext(final Context base) { + super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); + } + + @Override + public IBinder onBind(final Intent intent) { + return mBinder; + } + + /*////////////////////////////////////////////////////////////////////////// + // Actions + //////////////////////////////////////////////////////////////////////////*/ + private void onClose() { + if (DEBUG) { + Log.d(TAG, "onClose() called"); + } + + if (playerImpl != null) { + removeViewFromParent(); + + playerImpl.setRecovery(); + playerImpl.savePlaybackState(); + playerImpl.stopActivityBinding(); + playerImpl.removePopupFromView(); + playerImpl.destroy(); + } + if (notificationManager != null) { + notificationManager.cancel(NOTIFICATION_ID); + } + + stopForeground(true); + stopSelf(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + boolean isLandscape() { + // DisplayMetrics from activity context knows about MultiWindow feature + // while DisplayMetrics from app context doesn't + final DisplayMetrics metrics = (playerImpl != null + && playerImpl.getParentActivity() != null) + ? playerImpl.getParentActivity().getResources().getDisplayMetrics() + : getResources().getDisplayMetrics(); + return metrics.heightPixels < metrics.widthPixels; + } + + public View getView() { + if (playerImpl == null) { + return null; + } + + return playerImpl.getRootView(); + } + + public void removeViewFromParent() { + if (getView().getParent() != null) { + if (playerImpl.getParentActivity() != null) { + // This means view was added to fragment + final ViewGroup parent = (ViewGroup) getView().getParent(); + parent.removeView(getView()); + } else { + // This means view was added by windowManager for popup player + windowManager.removeViewImmediate(getView()); + } + } + } + + private void showNotificationAndStartForeground() { + resetNotification(); + if (getBigNotRemoteView() != null) { + getBigNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false); + } + if (getNotRemoteView() != null) { + getNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false); + } + startForeground(NOTIFICATION_ID, getNotBuilder().build()); + } + + /*////////////////////////////////////////////////////////////////////////// + // Notification + //////////////////////////////////////////////////////////////////////////*/ + + void resetNotification() { + notBuilder = createNotification(); + playerImpl.timesNotificationUpdated = 0; + } + + private NotificationCompat.Builder createNotification() { + notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, + R.layout.player_notification); + bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, + R.layout.player_notification_expanded); + + setupNotification(notRemoteView); + setupNotification(bigNotRemoteView); + + final NotificationCompat.Builder builder = new NotificationCompat + .Builder(this, getString(R.string.notification_channel_id)) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCustomContentView(notRemoteView) + .setCustomBigContentView(bigNotRemoteView); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setLockScreenThumbnail(builder); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + builder.setPriority(NotificationCompat.PRIORITY_MAX); + } + return builder; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void setLockScreenThumbnail(final NotificationCompat.Builder builder) { + final boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean( + getString(R.string.enable_lock_screen_video_thumbnail_key), true); + + if (isLockScreenThumbnailEnabled) { + playerImpl.mediaSessionManager.setLockScreenArt( + builder, + getCenteredThumbnailBitmap() + ); + } else { + playerImpl.mediaSessionManager.clearLockScreenArt(builder); + } + } + + @Nullable + private Bitmap getCenteredThumbnailBitmap() { + final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; + final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; + + return BitmapUtils.centerCrop(playerImpl.getThumbnail(), screenWidth, screenHeight); + } + + private void setupNotification(final RemoteViews remoteViews) { + // Don't show anything until player is playing + if (playerImpl == null) { + return; + } + + remoteViews.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); + remoteViews.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); + remoteViews.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail()); + + remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationStop, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); + // Starts VideoDetailFragment or opens BackgroundPlayerActivity. + remoteViews.setOnClickPendingIntent(R.id.notificationContent, + PendingIntent.getActivity(this, NOTIFICATION_ID, + getIntentForNotification(), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationRepeat, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); + + + if (playerImpl.playQueue != null && playerImpl.playQueue.size() > 1) { + remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_previous); + remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_next); + remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationFForward, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT)); + } else { + remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_rewind); + remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_fastforward); + remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationFForward, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT)); + } + + setRepeatModeIcon(remoteViews, playerImpl.getRepeatMode()); + } + + /** + * Updates the notification, and the play/pause button in it. + * Used for changes on the remoteView + * + * @param drawableId if != -1, sets the drawable with that id on the play/pause button + */ + synchronized void updateNotification(final int drawableId) { + /*if (DEBUG) { + Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); + }*/ + if (notBuilder == null) { + return; + } + if (drawableId != -1) { + if (notRemoteView != null) { + notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + } + if (bigNotRemoteView != null) { + bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + } + } + notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); + playerImpl.timesNotificationUpdated++; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) { + if (remoteViews == null) { + return; + } + + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + remoteViews.setInt(R.id.notificationRepeat, + SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_off); + break; + case Player.REPEAT_MODE_ONE: + remoteViews.setInt(R.id.notificationRepeat, + SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_one); + break; + case Player.REPEAT_MODE_ALL: + remoteViews.setInt(R.id.notificationRepeat, + SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_all); + break; + } + } + + private Intent getIntentForNotification() { + final Intent intent; + if (playerImpl.audioPlayerSelected() || playerImpl.popupPlayerSelected()) { + // Means we play in popup or audio only. Let's show BackgroundPlayerActivity + intent = NavigationHelper.getBackgroundPlayerActivityIntent(getApplicationContext()); + } else { + // We are playing in fragment. Don't open another activity just show fragment. That's it + intent = NavigationHelper.getPlayerIntent(this, MainActivity.class, null, true); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + } + return intent; + } + + /*////////////////////////////////////////////////////////////////////////// + // Getters + //////////////////////////////////////////////////////////////////////////*/ + + NotificationCompat.Builder getNotBuilder() { + return notBuilder; + } + + RemoteViews getBigNotRemoteView() { + return bigNotRemoteView; + } + + RemoteViews getNotRemoteView() { + return notRemoteView; + } + + + public class LocalBinder extends Binder { + + public MainPlayer getService() { + return MainPlayer.this; + } + + public VideoPlayerImpl getPlayer() { + return MainPlayer.this.playerImpl; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java deleted file mode 100644 index 56744d858..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ /dev/null @@ -1,1472 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * Copyright 2019 Eltex ltd - * MainVideoPlayer.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.database.ContentObserver; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.media.AudioManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.provider.Settings; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; -import android.view.DisplayCutout; -import android.view.GestureDetector; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.PopupMenu; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.SeekBar; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.app.ActivityCompat; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.ui.SubtitleView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.player.helper.PlaybackParameterDialog; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; -import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; -import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; -import org.schabi.newpipe.util.AndroidTvUtils; -import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.util.KoreUtil; -import org.schabi.newpipe.util.ListHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.ShareUtils; -import org.schabi.newpipe.util.StateSaver; -import org.schabi.newpipe.util.ThemeHelper; -import org.schabi.newpipe.views.FocusOverlayView; - -import java.util.List; -import java.util.Queue; -import java.util.UUID; - -import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; -import static org.schabi.newpipe.player.VideoPlayer.DPAD_CONTROLS_HIDE_TIME; -import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; -import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; -import static org.schabi.newpipe.util.AnimationUtils.animateRotation; -import static org.schabi.newpipe.util.AnimationUtils.animateView; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; -import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; - -/** - * Activity Player implementing {@link VideoPlayer}. - * - * @author mauriciocolli - */ -public final class MainVideoPlayer extends AppCompatActivity - implements StateSaver.WriteRead, PlaybackParameterDialog.Callback { - private static final String TAG = ".MainVideoPlayer"; - private static final boolean DEBUG = BasePlayer.DEBUG; - - private GestureDetector gestureDetector; - - private VideoPlayerImpl playerImpl; - - private SharedPreferences defaultPreferences; - - @Nullable - private PlayerState playerState; - private boolean isInMultiWindow; - private boolean isBackPressed; - - private ContentObserver rotationObserver; - - /*////////////////////////////////////////////////////////////////////////// - // Activity LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void onCreate(@Nullable final Bundle savedInstanceState) { - assureCorrectAppLanguage(this); - super.onCreate(savedInstanceState); - if (DEBUG) { - Log.d(TAG, "onCreate() called with: " - + "savedInstanceState = [" + savedInstanceState + "]"); - } - defaultPreferences = PreferenceManager.getDefaultSharedPreferences(this); - ThemeHelper.setTheme(this); - getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - getWindow().setStatusBarColor(Color.BLACK); - } - setVolumeControlStream(AudioManager.STREAM_MUSIC); - - WindowManager.LayoutParams lp = getWindow().getAttributes(); - lp.screenBrightness = PlayerHelper.getScreenBrightness(getApplicationContext()); - getWindow().setAttributes(lp); - - hideSystemUi(); - setContentView(R.layout.activity_main_player); - - playerImpl = new VideoPlayerImpl(this); - playerImpl.setup(findViewById(android.R.id.content)); - - if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) { - return; // We have saved states, stop here to restore it - } - - final Intent intent = getIntent(); - if (intent != null) { - playerImpl.handleIntent(intent); - } else { - Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show(); - finish(); - } - - rotationObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(final boolean selfChange) { - super.onChange(selfChange); - if (globalScreenOrientationLocked()) { - final String orientKey = getString(R.string.last_orientation_landscape_key); - - final boolean lastOrientationWasLandscape = defaultPreferences - .getBoolean(orientKey, AndroidTvUtils.isTv(getApplicationContext())); - setLandscape(lastOrientationWasLandscape); - } else { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); - } - } - }; - - getContentResolver().registerContentObserver( - Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), - false, rotationObserver); - - if (AndroidTvUtils.isTv(this)) { - FocusOverlayView.setupFocusObserver(this); - } - } - - @Override - protected void onRestoreInstanceState(@NonNull final Bundle bundle) { - if (DEBUG) { - Log.d(TAG, "onRestoreInstanceState() called"); - } - super.onRestoreInstanceState(bundle); - StateSaver.tryToRestore(bundle, this); - } - - @Override - protected void onNewIntent(final Intent intent) { - if (DEBUG) { - Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); - } - super.onNewIntent(intent); - if (intent != null) { - playerState = null; - playerImpl.handleIntent(intent); - } - } - - @Override - public boolean onKeyDown(final int keyCode, final KeyEvent event) { - switch (event.getKeyCode()) { - default: - break; - case KeyEvent.KEYCODE_BACK: - if (AndroidTvUtils.isTv(getApplicationContext()) - && playerImpl.isControlsVisible()) { - playerImpl.hideControls(0, 0); - hideSystemUi(); - return true; - } - break; - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_CENTER: - View playerRoot = playerImpl.getRootView(); - View controls = playerImpl.getControlsRoot(); - if (playerRoot.hasFocus() && !controls.hasFocus()) { - // do not interfere with focus in playlist etc. - return super.onKeyDown(keyCode, event); - } - - if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { - return true; - } - - if (!playerImpl.isControlsVisible()) { - playerImpl.playPauseButton.requestFocus(); - playerImpl.showControlsThenHide(); - showSystemUi(); - return true; - } else { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); - } - break; - } - - return super.onKeyDown(keyCode, event); - } - - @Override - protected void onResume() { - if (DEBUG) { - Log.d(TAG, "onResume() called"); - } - assureCorrectAppLanguage(this); - super.onResume(); - - if (globalScreenOrientationLocked()) { - final String orientKey = getString(R.string.last_orientation_landscape_key); - - boolean lastOrientationWasLandscape = defaultPreferences - .getBoolean(orientKey, AndroidTvUtils.isTv(getApplicationContext())); - setLandscape(lastOrientationWasLandscape); - } - - final int lastResizeMode = defaultPreferences.getInt( - getString(R.string.last_resize_mode), AspectRatioFrameLayout.RESIZE_MODE_FIT); - playerImpl.setResizeMode(lastResizeMode); - - // Upon going in or out of multiwindow mode, isInMultiWindow will always be false, - // since the first onResume needs to restore the player. - // Subsequent onResume calls while multiwindow mode remains the same and the player is - // prepared should be ignored. - if (isInMultiWindow) { - return; - } - isInMultiWindow = isInMultiWindow(); - - if (playerState != null) { - playerImpl.setPlaybackQuality(playerState.getPlaybackQuality()); - playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(), - playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(), - playerState.isPlaybackSkipSilence(), playerState.wasPlaying(), - playerImpl.isMuted()); - } - } - - @Override - public void onConfigurationChanged(final Configuration newConfig) { - super.onConfigurationChanged(newConfig); - assureCorrectAppLanguage(this); - - if (playerImpl.isSomePopupMenuVisible()) { - playerImpl.getQualityPopupMenu().dismiss(); - playerImpl.getPlaybackSpeedPopupMenu().dismiss(); - } - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - isBackPressed = true; - } - - @Override - protected void onSaveInstanceState(final Bundle outState) { - if (DEBUG) { - Log.d(TAG, "onSaveInstanceState() called"); - } - super.onSaveInstanceState(outState); - if (playerImpl == null) { - return; - } - - playerImpl.setRecovery(); - if (!playerImpl.gotDestroyed()) { - playerState = createPlayerState(); - } - StateSaver.tryToSave(isChangingConfigurations(), null, outState, this); - } - - @Override - protected void onStop() { - if (DEBUG) { - Log.d(TAG, "onStop() called"); - } - super.onStop(); - PlayerHelper.setScreenBrightness(getApplicationContext(), - getWindow().getAttributes().screenBrightness); - - if (playerImpl == null) { - return; - } - if (!isBackPressed) { - playerImpl.minimize(); - } - playerState = createPlayerState(); - playerImpl.destroy(); - - if (rotationObserver != null) { - getContentResolver().unregisterContentObserver(rotationObserver); - } - - isInMultiWindow = false; - isBackPressed = false; - } - - @Override - protected void attachBaseContext(final Context newBase) { - super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase)); - } - - @Override - protected void onPause() { - playerImpl.savePlaybackState(); - super.onPause(); - } - - /*////////////////////////////////////////////////////////////////////////// - // State Saving - //////////////////////////////////////////////////////////////////////////*/ - - private PlayerState createPlayerState() { - return new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(), - playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(), - playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(), - playerImpl.isPlaying()); - } - - @Override - public String generateSuffix() { - return "." + UUID.randomUUID().toString() + ".player"; - } - - @Override - public void writeTo(final Queue objectsToSave) { - if (objectsToSave == null) { - return; - } - objectsToSave.add(playerState); - } - - @Override - @SuppressWarnings("unchecked") - public void readFrom(@NonNull final Queue savedObjects) { - playerState = (PlayerState) savedObjects.poll(); - } - - /*////////////////////////////////////////////////////////////////////////// - // View - //////////////////////////////////////////////////////////////////////////*/ - - private void showSystemUi() { - if (DEBUG) { - Log.d(TAG, "showSystemUi() called"); - } - if (playerImpl != null && playerImpl.queueVisible) { - return; - } - - final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - @ColorInt final int systenUiColor = - ActivityCompat.getColor(getApplicationContext(), R.color.video_overlay_color); - getWindow().setStatusBarColor(systenUiColor); - getWindow().setNavigationBarColor(systenUiColor); - } - - getWindow().getDecorView().setSystemUiVisibility(visibility); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - - private void hideSystemUi() { - if (DEBUG) { - Log.d(TAG, "hideSystemUi() called"); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - } - getWindow().getDecorView().setSystemUiVisibility(visibility); - } - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - - private void toggleOrientation() { - setLandscape(!isLandscape()); - defaultPreferences.edit() - .putBoolean(getString(R.string.last_orientation_landscape_key), !isLandscape()) - .apply(); - } - - private boolean isLandscape() { - return getResources().getDisplayMetrics().heightPixels - < getResources().getDisplayMetrics().widthPixels; - } - - private void setLandscape(final boolean v) { - setRequestedOrientation(v - ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); - } - - private boolean globalScreenOrientationLocked() { - // 1: Screen orientation changes using accelerometer - // 0: Screen orientation is locked - return !(android.provider.Settings.System - .getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1); - } - - protected void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { - switch (repeatMode) { - case Player.REPEAT_MODE_OFF: - imageButton.setImageResource(R.drawable.exo_controls_repeat_off); - break; - case Player.REPEAT_MODE_ONE: - imageButton.setImageResource(R.drawable.exo_controls_repeat_one); - break; - case Player.REPEAT_MODE_ALL: - imageButton.setImageResource(R.drawable.exo_controls_repeat_all); - break; - } - } - - protected void setShuffleButton(final ImageButton shuffleButton, final boolean shuffled) { - final int shuffleAlpha = shuffled ? 255 : 77; - shuffleButton.setImageAlpha(shuffleAlpha); - } - - protected void setMuteButton(final ImageButton muteButton, final boolean isMuted) { - muteButton.setImageDrawable(AppCompatResources.getDrawable(getApplicationContext(), isMuted - ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); - } - - - private boolean isInMultiWindow() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode(); - } - - //////////////////////////////////////////////////////////////////////////// - // Playback Parameters Listener - //////////////////////////////////////////////////////////////////////////// - - @Override - public void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, - final boolean playbackSkipSilence) { - if (playerImpl != null) { - playerImpl.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); - } - } - - /////////////////////////////////////////////////////////////////////////// - - @SuppressWarnings({"unused", "WeakerAccess"}) - private class VideoPlayerImpl extends VideoPlayer { - private static final float MAX_GESTURE_LENGTH = 0.75f; - - private TextView titleTextView; - private TextView channelTextView; - private RelativeLayout volumeRelativeLayout; - private ProgressBar volumeProgressBar; - private ImageView volumeImageView; - private RelativeLayout brightnessRelativeLayout; - private ProgressBar brightnessProgressBar; - private ImageView brightnessImageView; - private ImageButton queueButton; - private ImageButton repeatButton; - private ImageButton shuffleButton; - - private ImageButton playPauseButton; - private ImageButton playPreviousButton; - private ImageButton playNextButton; - private Button closeButton; - - private RelativeLayout queueLayout; - private ImageButton itemsListCloseButton; - private RecyclerView itemsList; - private ItemTouchHelper itemTouchHelper; - - private boolean queueVisible; - - private ImageButton moreOptionsButton; - private ImageButton kodiButton; - private ImageButton shareButton; - private ImageButton toggleOrientationButton; - private ImageButton switchPopupButton; - private ImageButton switchBackgroundButton; - private ImageButton muteButton; - - private RelativeLayout windowRootLayout; - private View secondaryControls; - - private int maxGestureLength; - - VideoPlayerImpl(final Context context) { - super("VideoPlayerImpl" + MainVideoPlayer.TAG, context); - } - - @Override - public void initViews(final View view) { - super.initViews(view); - this.titleTextView = view.findViewById(R.id.titleTextView); - this.channelTextView = view.findViewById(R.id.channelTextView); - this.volumeRelativeLayout = view.findViewById(R.id.volumeRelativeLayout); - this.volumeProgressBar = view.findViewById(R.id.volumeProgressBar); - this.volumeImageView = view.findViewById(R.id.volumeImageView); - this.brightnessRelativeLayout = view.findViewById(R.id.brightnessRelativeLayout); - this.brightnessProgressBar = view.findViewById(R.id.brightnessProgressBar); - this.brightnessImageView = view.findViewById(R.id.brightnessImageView); - this.queueButton = view.findViewById(R.id.queueButton); - this.repeatButton = view.findViewById(R.id.repeatButton); - this.shuffleButton = view.findViewById(R.id.shuffleButton); - - this.playPauseButton = view.findViewById(R.id.playPauseButton); - this.playPreviousButton = view.findViewById(R.id.playPreviousButton); - this.playNextButton = view.findViewById(R.id.playNextButton); - this.closeButton = view.findViewById(R.id.closeButton); - - this.moreOptionsButton = view.findViewById(R.id.moreOptionsButton); - this.secondaryControls = view.findViewById(R.id.secondaryControls); - this.kodiButton = view.findViewById(R.id.kodi); - this.shareButton = view.findViewById(R.id.share); - this.toggleOrientationButton = view.findViewById(R.id.toggleOrientation); - this.switchBackgroundButton = view.findViewById(R.id.switchBackground); - this.muteButton = view.findViewById(R.id.switchMute); - this.switchPopupButton = view.findViewById(R.id.switchPopup); - - this.queueLayout = findViewById(R.id.playQueuePanel); - this.itemsListCloseButton = findViewById(R.id.playQueueClose); - this.itemsList = findViewById(R.id.playQueue); - - titleTextView.setSelected(true); - channelTextView.setSelected(true); - - getRootView().setKeepScreenOn(true); - } - - @Override - protected void setupSubtitleView(@NonNull final SubtitleView view, - final float captionScale, - @NonNull final CaptionStyleCompat captionStyle) { - final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); - final float captionRatioInverse = 20f + 4f * (1f - captionScale); - view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, - (float) minimumLength / captionRatioInverse); - view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); - view.setStyle(captionStyle); - } - - @Override - public void initListeners() { - super.initListeners(); - - PlayerGestureListener listener = new PlayerGestureListener(); - gestureDetector = new GestureDetector(context, listener); - gestureDetector.setIsLongpressEnabled(false); - getRootView().setOnTouchListener(listener); - - queueButton.setOnClickListener(this); - repeatButton.setOnClickListener(this); - shuffleButton.setOnClickListener(this); - - playPauseButton.setOnClickListener(this); - playPreviousButton.setOnClickListener(this); - playNextButton.setOnClickListener(this); - closeButton.setOnClickListener(this); - - moreOptionsButton.setOnClickListener(this); - kodiButton.setOnClickListener(this); - shareButton.setOnClickListener(this); - toggleOrientationButton.setOnClickListener(this); - switchBackgroundButton.setOnClickListener(this); - muteButton.setOnClickListener(this); - switchPopupButton.setOnClickListener(this); - - getRootView().addOnLayoutChangeListener((view, l, t, r, b, ol, ot, or, ob) -> { - if (l != ol || t != ot || r != or || b != ob) { - // Use smaller value to be consistent between screen orientations - // (and to make usage easier) - int width = r - l; - int height = b - t; - maxGestureLength = (int) (Math.min(width, height) * MAX_GESTURE_LENGTH); - - if (DEBUG) { - Log.d(TAG, "maxGestureLength = " + maxGestureLength); - } - - volumeProgressBar.setMax(maxGestureLength); - brightnessProgressBar.setMax(maxGestureLength); - - setInitialGestureValues(); - } - }); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - queueLayout.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { - @Override - public WindowInsets onApplyWindowInsets(final View view, - final WindowInsets windowInsets) { - final DisplayCutout cutout = windowInsets.getDisplayCutout(); - if (cutout != null) { - view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), - cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); - } - return windowInsets; - } - }); - } - } - - public void minimize() { - switch (PlayerHelper.getMinimizeOnExitAction(context)) { - case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND: - onPlayBackgroundButtonClicked(); - break; - case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP: - onFullScreenButtonClicked(); - break; - case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE: - default: - // No action - break; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Video Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onRepeatModeChanged(final int i) { - super.onRepeatModeChanged(i); - updatePlaybackButtons(); - } - - @Override - public void onShuffleClicked() { - super.onShuffleClicked(); - updatePlaybackButtons(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - super.onMetadataChanged(tag); - - // show kodi button if it supports the current service and it is enabled in settings - final boolean showKodiButton = - KoreUtil.isServiceSupportedByKore(tag.getMetadata().getServiceId()) - && PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.show_play_with_kodi_key), false); - kodiButton.setVisibility(showKodiButton ? View.VISIBLE : View.GONE); - - titleTextView.setText(tag.getMetadata().getName()); - channelTextView.setText(tag.getMetadata().getUploaderName()); - } - - @Override - public void onPlaybackShutdown() { - super.onPlaybackShutdown(); - finish(); - } - - public void onKodiShare() { - onPause(); - try { - NavigationHelper.playWithKore(context, Uri.parse(playerImpl.getVideoUrl())); - } catch (Exception e) { - if (DEBUG) { - Log.i(TAG, "Failed to start kore", e); - } - KoreUtil.showInstallKoreDialog(context); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Player Overrides - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onFullScreenButtonClicked() { - super.onFullScreenButtonClicked(); - - if (DEBUG) { - Log.d(TAG, "onFullScreenButtonClicked() called"); - } - if (simpleExoPlayer == null) { - return; - } - - if (!PermissionHelper.isPopupEnabled(context)) { - PermissionHelper.showPopupEnablementToast(context); - return; - } - - setRecovery(); - final Intent intent = NavigationHelper.getPlayerIntent( - context, - PopupVideoPlayer.class, - this.getPlayQueue(), - this.getRepeatMode(), - this.getPlaybackSpeed(), - this.getPlaybackPitch(), - this.getPlaybackSkipSilence(), - this.getPlaybackQuality(), - false, - !isPlaying(), - isMuted() - ); - context.startService(intent); - - ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); - destroy(); - finish(); - } - - public void onPlayBackgroundButtonClicked() { - if (DEBUG) { - Log.d(TAG, "onPlayBackgroundButtonClicked() called"); - } - if (playerImpl.getPlayer() == null) { - return; - } - - setRecovery(); - final Intent intent = NavigationHelper.getPlayerIntent( - context, - BackgroundPlayer.class, - this.getPlayQueue(), - this.getRepeatMode(), - this.getPlaybackSpeed(), - this.getPlaybackPitch(), - this.getPlaybackSkipSilence(), - this.getPlaybackQuality(), - false, - !isPlaying(), - isMuted() - ); - context.startService(intent); - - ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); - destroy(); - finish(); - } - - @Override - public void onMuteUnmuteButtonClicked() { - super.onMuteUnmuteButtonClicked(); - setMuteButton(muteButton, playerImpl.isMuted()); - } - - - @Override - public void onClick(final View v) { - super.onClick(v); - if (v.getId() == playPauseButton.getId()) { - onPlayPause(); - } else if (v.getId() == playPreviousButton.getId()) { - onPlayPrevious(); - } else if (v.getId() == playNextButton.getId()) { - onPlayNext(); - } else if (v.getId() == queueButton.getId()) { - onQueueClicked(); - return; - } else if (v.getId() == repeatButton.getId()) { - onRepeatClicked(); - return; - } else if (v.getId() == shuffleButton.getId()) { - onShuffleClicked(); - return; - } else if (v.getId() == moreOptionsButton.getId()) { - onMoreOptionsClicked(); - } else if (v.getId() == shareButton.getId()) { - onShareClicked(); - } else if (v.getId() == toggleOrientationButton.getId()) { - onScreenRotationClicked(); - } else if (v.getId() == switchPopupButton.getId()) { - onFullScreenButtonClicked(); - } else if (v.getId() == switchBackgroundButton.getId()) { - onPlayBackgroundButtonClicked(); - } else if (v.getId() == muteButton.getId()) { - onMuteUnmuteButtonClicked(); - } else if (v.getId() == closeButton.getId()) { - onPlaybackShutdown(); - return; - } else if (v.getId() == kodiButton.getId()) { - onKodiShare(); - } - - if (getCurrentState() != STATE_COMPLETED) { - getControlsVisibilityHandler().removeCallbacksAndMessages(null); - animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { - if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { - safeHideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - }); - } - } - - private void onQueueClicked() { - queueVisible = true; - hideSystemUi(); - - buildQueue(); - updatePlaybackButtons(); - - getControlsRoot().setVisibility(View.INVISIBLE); - animateView(queueLayout, SLIDE_AND_ALPHA, true, DEFAULT_CONTROLS_DURATION); - - itemsList.scrollToPosition(playQueue.getIndex()); - } - - private void onQueueClosed() { - animateView(queueLayout, SLIDE_AND_ALPHA, false, DEFAULT_CONTROLS_DURATION); - queueVisible = false; - } - - private void onMoreOptionsClicked() { - if (DEBUG) { - Log.d(TAG, "onMoreOptionsClicked() called"); - } - - final boolean isMoreControlsVisible - = secondaryControls.getVisibility() == View.VISIBLE; - - animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, - isMoreControlsVisible ? 0 : 180); - animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, - DEFAULT_CONTROLS_DURATION); - showControls(DEFAULT_CONTROLS_DURATION); - setMuteButton(muteButton, playerImpl.isMuted()); - } - - private void onShareClicked() { - // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) - ShareUtils.shareUrl(MainVideoPlayer.this, playerImpl.getVideoTitle(), - playerImpl.getVideoUrl() - + "&t=" + playerImpl.getPlaybackSeekBar().getProgress() / 1000); - } - - private void onScreenRotationClicked() { - if (DEBUG) { - Log.d(TAG, "onScreenRotationClicked() called"); - } - toggleOrientation(); - showControlsThenHide(); - } - - @Override - public void onPlaybackSpeedClicked() { - PlaybackParameterDialog - .newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence()) - .show(getSupportFragmentManager(), TAG); - } - - @Override - public void onStopTrackingTouch(final SeekBar seekBar) { - super.onStopTrackingTouch(seekBar); - if (wasPlaying()) { - showControlsThenHide(); - } - } - - @Override - public void onDismiss(final PopupMenu menu) { - super.onDismiss(menu); - if (isPlaying()) { - hideControls(DEFAULT_CONTROLS_DURATION, 0); - } - hideSystemUi(); - } - - @Override - protected int nextResizeMode(final int currentResizeMode) { - final int newResizeMode; - switch (currentResizeMode) { - case AspectRatioFrameLayout.RESIZE_MODE_FIT: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; - break; - case AspectRatioFrameLayout.RESIZE_MODE_FILL: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; - break; - default: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - break; - } - - storeResizeMode(newResizeMode); - return newResizeMode; - } - - private void storeResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { - defaultPreferences.edit() - .putInt(getString(R.string.last_resize_mode), resizeMode) - .apply(); - } - - @Override - protected VideoPlaybackResolver.QualityResolver getQualityResolver() { - return new VideoPlaybackResolver.QualityResolver() { - @Override - public int getDefaultResolutionIndex(final List sortedVideos) { - return ListHelper.getDefaultResolutionIndex(context, sortedVideos); - } - - @Override - public int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); - } - }; - } - - /*////////////////////////////////////////////////////////////////////////// - // States - //////////////////////////////////////////////////////////////////////////*/ - - private void animatePlayButtons(final boolean show, final int duration) { - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); - animateView(playPreviousButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); - animateView(playNextButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); - } - - @Override - public void onBlocked() { - super.onBlocked(); - playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); - animatePlayButtons(false, 100); - animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); - getRootView().setKeepScreenOn(true); - } - - @Override - public void onBuffering() { - super.onBuffering(); - getRootView().setKeepScreenOn(true); - } - - @Override - public void onPlaying() { - super.onPlaying(); - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); - animatePlayButtons(true, 200); - playPauseButton.requestFocus(); - animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); - }); - - getRootView().setKeepScreenOn(true); - } - - @Override - public void onPaused() { - super.onPaused(); - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); - animatePlayButtons(true, 200); - playPauseButton.requestFocus(); - animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); - }); - - showSystemUi(); - getRootView().setKeepScreenOn(false); - } - - @Override - public void onPausedSeek() { - super.onPausedSeek(); - animatePlayButtons(false, 100); - getRootView().setKeepScreenOn(true); - } - - - @Override - public void onCompleted() { - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); - animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); - animateView(closeButton, true, DEFAULT_CONTROLS_DURATION); - }); - getRootView().setKeepScreenOn(false); - super.onCompleted(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void setInitialGestureValues() { - if (getAudioReactor() != null) { - final float currentVolumeNormalized - = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume(); - volumeProgressBar.setProgress( - (int) (volumeProgressBar.getMax() * currentVolumeNormalized)); - } - - float screenBrightness = getWindow().getAttributes().screenBrightness; - if (screenBrightness < 0) { - screenBrightness = Settings.System.getInt(getContentResolver(), - Settings.System.SCREEN_BRIGHTNESS, 0) / 255.0f; - } - - brightnessProgressBar.setProgress( - (int) (brightnessProgressBar.getMax() * screenBrightness)); - - if (DEBUG) { - Log.d(TAG, "setInitialGestureValues: volumeProgressBar.getProgress() [" - + volumeProgressBar.getProgress() + "] " - + "brightnessProgressBar.getProgress() [" - + brightnessProgressBar.getProgress() + "]"); - } - } - - @Override - public void showControlsThenHide() { - if (queueVisible) { - return; - } - - super.showControlsThenHide(); - } - - @Override - public void showControls(final long duration) { - if (queueVisible) { - return; - } - - super.showControls(duration); - } - - @Override - public void safeHideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]"); - } - - View controlsRoot = getControlsRoot(); - if (controlsRoot.isInTouchMode()) { - getControlsVisibilityHandler().removeCallbacksAndMessages(null); - getControlsVisibilityHandler().postDelayed(() -> - animateView(controlsRoot, false, duration, 0, - MainVideoPlayer.this::hideSystemUi), delay); - } - } - - @Override - public void hideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); - } - getControlsVisibilityHandler().removeCallbacksAndMessages(null); - getControlsVisibilityHandler().postDelayed(() -> - animateView(getControlsRoot(), false, duration, 0, - MainVideoPlayer.this::hideSystemUi), - /*delayMillis=*/delay - ); - } - - private void updatePlaybackButtons() { - if (repeatButton == null || shuffleButton == null - || simpleExoPlayer == null || playQueue == null) { - return; - } - - setRepeatModeButton(repeatButton, getRepeatMode()); - setShuffleButton(shuffleButton, playQueue.isShuffled()); - } - - private void buildQueue() { - itemsList.setAdapter(playQueueAdapter); - itemsList.setClickable(true); - itemsList.setLongClickable(true); - - itemsList.clearOnScrollListeners(); - itemsList.addOnScrollListener(getQueueScrollListener()); - - itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); - itemTouchHelper.attachToRecyclerView(itemsList); - - playQueueAdapter.setSelectedListener(getOnSelectedListener()); - - itemsListCloseButton.setOnClickListener(view -> onQueueClosed()); - } - - private OnScrollBelowItemsListener getQueueScrollListener() { - return new OnScrollBelowItemsListener() { - @Override - public void onScrolledDown(final RecyclerView recyclerView) { - if (playQueue != null && !playQueue.isComplete()) { - playQueue.fetch(); - } else if (itemsList != null) { - itemsList.clearOnScrollListeners(); - } - } - }; - } - - private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new PlayQueueItemTouchCallback() { - @Override - public void onMove(final int sourceIndex, final int targetIndex) { - if (playQueue != null) { - playQueue.move(sourceIndex, targetIndex); - } - } - - @Override - public void onSwiped(final int index) { - if (index != -1) { - playQueue.remove(index); - } - } - }; - } - - private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { - return new PlayQueueItemBuilder.OnSelectedListener() { - @Override - public void selected(final PlayQueueItem item, final View view) { - onSelected(item); - } - - @Override - public void held(final PlayQueueItem item, final View view) { - final int index = playQueue.indexOf(item); - if (index != -1) { - playQueue.remove(index); - } - } - - @Override - public void onStartDrag(final PlayQueueItemHolder viewHolder) { - if (itemTouchHelper != null) { - itemTouchHelper.startDrag(viewHolder); - } - } - }; - } - - /////////////////////////////////////////////////////////////////////////// - // Getters - /////////////////////////////////////////////////////////////////////////// - - public TextView getTitleTextView() { - return titleTextView; - } - - public TextView getChannelTextView() { - return channelTextView; - } - - public RelativeLayout getVolumeRelativeLayout() { - return volumeRelativeLayout; - } - - public ProgressBar getVolumeProgressBar() { - return volumeProgressBar; - } - - public ImageView getVolumeImageView() { - return volumeImageView; - } - - public RelativeLayout getBrightnessRelativeLayout() { - return brightnessRelativeLayout; - } - - public ProgressBar getBrightnessProgressBar() { - return brightnessProgressBar; - } - - public ImageView getBrightnessImageView() { - return brightnessImageView; - } - - public ImageButton getRepeatButton() { - return repeatButton; - } - - public ImageButton getMuteButton() { - return muteButton; - } - - public ImageButton getPlayPauseButton() { - return playPauseButton; - } - - public int getMaxGestureLength() { - return maxGestureLength; - } - } - - private class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener - implements View.OnTouchListener { - private static final int MOVEMENT_THRESHOLD = 40; - - private final boolean isVolumeGestureEnabled = PlayerHelper - .isVolumeGestureEnabled(getApplicationContext()); - private final boolean isBrightnessGestureEnabled = PlayerHelper - .isBrightnessGestureEnabled(getApplicationContext()); - - private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume(); - - private boolean isMoving; - - @Override - public boolean onDoubleTap(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onDoubleTap() called with: " - + "e = [" + e + "], " - + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", " - + "xy = " + e.getX() + ", " + e.getY()); - } - - if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) { - playerImpl.onFastForward(); - } else if (e.getX() < playerImpl.getRootView().getWidth() / 3) { - playerImpl.onFastRewind(); - } else { - playerImpl.getPlayPauseButton().performClick(); - } - - return true; - } - - @Override - public boolean onSingleTapConfirmed(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); - } - if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { - return true; - } - - if (playerImpl.isControlsVisible()) { - playerImpl.hideControls(150, 0); - } else { - playerImpl.playPauseButton.requestFocus(); - playerImpl.showControlsThenHide(); - showSystemUi(); - } - - return true; - } - - @Override - public boolean onDown(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onDown() called with: e = [" + e + "]"); - } - - return super.onDown(e); - } - - @Override - public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent, - final float distanceX, final float distanceY) { - if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) { - return false; - } - - final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(); - final boolean isTouchingNavigationBar = initialEvent.getY() - > playerImpl.getRootView().getHeight() - getNavigationBarHeight(); - if (isTouchingStatusBar || isTouchingNavigationBar) { - return false; - } - -// if (DEBUG) { -// Log.d(TAG, "MainVideoPlayer.onScroll = " + -// "e1.getRaw = [" + initialEvent.getRawX() + ", " -// + initialEvent.getRawY() + "], " + -// "e2.getRaw = [" + movingEvent.getRawX() + ", " -// + movingEvent.getRawY() + "], " + -// "distanceXy = [" + distanceX + ", " + distanceY + "]"); -// } - - final boolean insideThreshold - = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; - if (!isMoving && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) - || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { - return false; - } - - isMoving = true; - - boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled; - boolean acceptVolumeArea = acceptAnyArea - || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2; - boolean acceptBrightnessArea = acceptAnyArea || !acceptVolumeArea; - - if (isVolumeGestureEnabled && acceptVolumeArea) { - playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); - float currentProgressPercent = - (float) playerImpl.getVolumeProgressBar().getProgress() - / playerImpl.getMaxGestureLength(); - int currentVolume = (int) (maxVolume * currentProgressPercent); - playerImpl.getAudioReactor().setVolume(currentVolume); - - if (DEBUG) { - Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); - } - - final int resId = currentProgressPercent <= 0 - ? R.drawable.ic_volume_off_white_24dp - : currentProgressPercent < 0.25 - ? R.drawable.ic_volume_mute_white_24dp - : currentProgressPercent < 0.75 - ? R.drawable.ic_volume_down_white_24dp - : R.drawable.ic_volume_up_white_24dp; - - playerImpl.getVolumeImageView().setImageDrawable( - AppCompatResources.getDrawable(getApplicationContext(), resId) - ); - - if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { - animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); - } - if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); - } - } else if (isBrightnessGestureEnabled && acceptBrightnessArea) { - playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY); - float currentProgressPercent - = (float) playerImpl.getBrightnessProgressBar().getProgress() - / playerImpl.getMaxGestureLength(); - WindowManager.LayoutParams layoutParams = getWindow().getAttributes(); - layoutParams.screenBrightness = currentProgressPercent; - getWindow().setAttributes(layoutParams); - - if (DEBUG) { - Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " - + currentProgressPercent); - } - - final int resId = currentProgressPercent < 0.25 - ? R.drawable.ic_brightness_low_white_24dp - : currentProgressPercent < 0.75 - ? R.drawable.ic_brightness_medium_white_24dp - : R.drawable.ic_brightness_high_white_24dp; - - playerImpl.getBrightnessImageView().setImageDrawable( - AppCompatResources.getDrawable(getApplicationContext(), resId) - ); - - if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, - 200); - } - if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); - } - } - return true; - } - - private int getNavigationBarHeight() { - int resId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); - if (resId > 0) { - return getResources().getDimensionPixelSize(resId); - } - return 0; - } - - private int getStatusBarHeight() { - int resId = getResources().getIdentifier("status_bar_height", "dimen", "android"); - if (resId > 0) { - return getResources().getDimensionPixelSize(resId); - } - return 0; - } - - private void onScrollEnd() { - if (DEBUG) { - Log.d(TAG, "onScrollEnd() called"); - } - - if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, - 200, 200); - } - if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, - 200, 200); - } - - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - } - - @Override - public boolean onTouch(final View v, final MotionEvent event) { -// if (DEBUG) { -// Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]"); -// } - gestureDetector.onTouchEvent(event); - if (event.getAction() == MotionEvent.ACTION_UP && isMoving) { - isMoving = false; - onScrollEnd(); - } - return true; - } - - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java deleted file mode 100644 index 0ccec3067..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ /dev/null @@ -1,1311 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * PopupVideoPlayer.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.annotation.SuppressLint; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.PixelFormat; -import android.os.Build; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.GestureDetector; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.animation.AnticipateInterpolator; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.PopupMenu; -import android.widget.RemoteViews; -import android.widget.SeekBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.ui.SubtitleView; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.nostra13.universalimageloader.core.assist.FailReason; - -import org.schabi.newpipe.BuildConfig; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.player.event.PlayerEventListener; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; -import org.schabi.newpipe.util.ListHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ThemeHelper; - -import java.util.List; - -import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; -import static org.schabi.newpipe.util.AnimationUtils.animateView; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - -/** - * Service Popup Player implementing {@link VideoPlayer}. - * - * @author mauriciocolli - */ -public final class PopupVideoPlayer extends Service { - public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE"; - public static final String ACTION_PLAY_PAUSE - = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE"; - public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT"; - private static final String TAG = ".PopupVideoPlayer"; - private static final boolean DEBUG = BasePlayer.DEBUG; - private static final int NOTIFICATION_ID = 40028922; - private static final String POPUP_SAVED_WIDTH = "popup_saved_width"; - private static final String POPUP_SAVED_X = "popup_saved_x"; - private static final String POPUP_SAVED_Y = "popup_saved_y"; - - private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; - - private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; - - private WindowManager windowManager; - private WindowManager.LayoutParams popupLayoutParams; - private GestureDetector popupGestureDetector; - - private View closeOverlayView; - private FloatingActionButton closeOverlayButton; - - private int tossFlingVelocity; - - private float screenWidth; - private float screenHeight; - private float popupWidth; - private float popupHeight; - - private float minimumWidth; - private float minimumHeight; - private float maximumWidth; - private float maximumHeight; - - private NotificationManager notificationManager; - private NotificationCompat.Builder notBuilder; - private RemoteViews notRemoteView; - - private VideoPlayerImpl playerImpl; - private boolean isPopupClosing = false; - - /*////////////////////////////////////////////////////////////////////////// - // Service-Activity Binder - //////////////////////////////////////////////////////////////////////////*/ - - private PlayerEventListener activityListener; - private IBinder mBinder; - - /*////////////////////////////////////////////////////////////////////////// - // Service LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate() { - assureCorrectAppLanguage(this); - windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); - notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); - - playerImpl = new VideoPlayerImpl(this); - ThemeHelper.setTheme(this); - - mBinder = new PlayerServiceBinder(playerImpl); - } - - @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 (playerImpl.getPlayer() == null) { - initPopup(); - initPopupCloseOverlay(); - } - - playerImpl.handleIntent(intent); - - return START_NOT_STICKY; - } - - @Override - public void onConfigurationChanged(final Configuration newConfig) { - assureCorrectAppLanguage(this); - if (DEBUG) { - Log.d(TAG, "onConfigurationChanged() called with: " - + "newConfig = [" + newConfig + "]"); - } - updateScreenSize(); - updatePopupSize(popupLayoutParams.width, -1); - checkPopupPositionBounds(); - } - - @Override - public void onDestroy() { - if (DEBUG) { - Log.d(TAG, "onDestroy() called"); - } - closePopup(); - } - - @Override - protected void attachBaseContext(final Context base) { - super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); - } - - @Override - public IBinder onBind(final Intent intent) { - return mBinder; - } - - /*////////////////////////////////////////////////////////////////////////// - // Init - //////////////////////////////////////////////////////////////////////////*/ - - @SuppressLint("RtlHardcoded") - private void initPopup() { - if (DEBUG) { - Log.d(TAG, "initPopup() called"); - } - View rootView = View.inflate(this, R.layout.player_popup, null); - playerImpl.setup(rootView); - - tossFlingVelocity = PlayerHelper.getTossFlingVelocity(this); - - updateScreenSize(); - - final boolean popupRememberSizeAndPos = PlayerHelper.isRememberingPopupDimensions(this); - final float defaultSize = getResources().getDimension(R.dimen.popup_default_width); - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - popupWidth = popupRememberSizeAndPos - ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize; - - final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O - ? WindowManager.LayoutParams.TYPE_PHONE - : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - - popupLayoutParams = new WindowManager.LayoutParams( - (int) popupWidth, (int) getMinimumVideoHeight(popupWidth), - layoutParamType, - IDLE_WINDOW_FLAGS, - PixelFormat.TRANSLUCENT); - popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; - - int centerX = (int) (screenWidth / 2f - popupWidth / 2f); - int centerY = (int) (screenHeight / 2f - popupHeight / 2f); - popupLayoutParams.x = popupRememberSizeAndPos - ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; - popupLayoutParams.y = popupRememberSizeAndPos - ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; - - checkPopupPositionBounds(); - - PopupWindowGestureListener listener = new PopupWindowGestureListener(); - popupGestureDetector = new GestureDetector(this, listener); - rootView.setOnTouchListener(listener); - - playerImpl.getLoadingPanel().setMinimumWidth(popupLayoutParams.width); - playerImpl.getLoadingPanel().setMinimumHeight(popupLayoutParams.height); - windowManager.addView(rootView, popupLayoutParams); - } - - @SuppressLint("RtlHardcoded") - private void initPopupCloseOverlay() { - if (DEBUG) { - Log.d(TAG, "initPopupCloseOverlay() called"); - } - closeOverlayView = View.inflate(this, R.layout.player_popup_close_overlay, null); - closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton); - - final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O - ? WindowManager.LayoutParams.TYPE_PHONE - : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - - WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, - layoutParamType, - flags, - PixelFormat.TRANSLUCENT); - closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - closeOverlayLayoutParams.softInputMode = WindowManager - .LayoutParams.SOFT_INPUT_ADJUST_RESIZE; - - closeOverlayButton.setVisibility(View.GONE); - windowManager.addView(closeOverlayView, closeOverlayLayoutParams); - } - - /*////////////////////////////////////////////////////////////////////////// - // Notification - //////////////////////////////////////////////////////////////////////////*/ - - private void resetNotification() { - notBuilder = createNotification(); - } - - private NotificationCompat.Builder createNotification() { - notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, - R.layout.player_popup_notification); - - notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); - notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); - notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail()); - - notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), - PendingIntent.FLAG_UPDATE_CURRENT)); - notRemoteView.setOnClickPendingIntent(R.id.notificationStop, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), - PendingIntent.FLAG_UPDATE_CURRENT)); - notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), - PendingIntent.FLAG_UPDATE_CURRENT)); - - // Starts popup player activity -- attempts to unlock lockscreen - final Intent intent = NavigationHelper.getPopupPlayerActivityIntent(this); - notRemoteView.setOnClickPendingIntent(R.id.notificationContent, - PendingIntent.getActivity(this, NOTIFICATION_ID, intent, - PendingIntent.FLAG_UPDATE_CURRENT)); - - setRepeatModeRemote(notRemoteView, playerImpl.getRepeatMode()); - - NotificationCompat.Builder builder = new NotificationCompat - .Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContent(notRemoteView); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - builder.setPriority(NotificationCompat.PRIORITY_MAX); - } - return builder; - } - - /** - * Updates the notification, and the play/pause button in it. - * Used for changes on the remoteView - * - * @param drawableId if != -1, sets the drawable with that id on the play/pause button - */ - private void updateNotification(final int drawableId) { - if (DEBUG) { - Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); - } - if (notBuilder == null || notRemoteView == null) { - return; - } - if (drawableId != -1) { - notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); - } - notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); - } - - /*////////////////////////////////////////////////////////////////////////// - // Misc - //////////////////////////////////////////////////////////////////////////*/ - - public void closePopup() { - if (DEBUG) { - Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); - } - if (isPopupClosing) { - return; - } - isPopupClosing = true; - - if (playerImpl != null) { - playerImpl.savePlaybackState(); - if (playerImpl.getRootView() != null) { - windowManager.removeView(playerImpl.getRootView()); - } - playerImpl.setRootView(null); - playerImpl.stopActivityBinding(); - playerImpl.destroy(); - playerImpl = null; - } - - mBinder = null; - if (notificationManager != null) { - notificationManager.cancel(NOTIFICATION_ID); - } - - animateOverlayAndFinishService(); - } - - private void animateOverlayAndFinishService() { - final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - - closeOverlayButton.getY()); - - closeOverlayButton.animate().setListener(null).cancel(); - closeOverlayButton.animate() - .setInterpolator(new AnticipateInterpolator()) - .translationY(targetTranslationY) - .setDuration(400) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(final Animator animation) { - end(); - } - - @Override - public void onAnimationEnd(final Animator animation) { - end(); - } - - private void end() { - windowManager.removeView(closeOverlayView); - - stopForeground(true); - stopSelf(); - } - }).start(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - /** - * @see #checkPopupPositionBounds(float, float) - * @return if the popup was out of bounds and have been moved back to it - */ - @SuppressWarnings("UnusedReturnValue") - private boolean checkPopupPositionBounds() { - return checkPopupPositionBounds(screenWidth, screenHeight); - } - - /** - * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary - * that goes from (0, 0) to (boundaryWidth, boundaryHeight). - *

- * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed - * and {@code true} is returned to represent this change. - *

- * - * @param boundaryWidth width of the boundary - * @param boundaryHeight height of the boundary - * @return if the popup was out of bounds and have been moved back to it - */ - private boolean checkPopupPositionBounds(final float boundaryWidth, - final float boundaryHeight) { - if (DEBUG) { - Log.d(TAG, "checkPopupPositionBounds() called with: " - + "boundaryWidth = [" + boundaryWidth + "], " - + "boundaryHeight = [" + boundaryHeight + "]"); - } - - if (popupLayoutParams.x < 0) { - popupLayoutParams.x = 0; - return true; - } else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) { - popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width); - return true; - } - - if (popupLayoutParams.y < 0) { - popupLayoutParams.y = 0; - return true; - } else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) { - popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height); - return true; - } - - return false; - } - - private void savePositionAndSize() { - SharedPreferences sharedPreferences = PreferenceManager - .getDefaultSharedPreferences(PopupVideoPlayer.this); - sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply(); - sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply(); - sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply(); - } - - private float getMinimumVideoHeight(final float width) { - final float height = width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have -// if (DEBUG) { -// Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], " -// + "returned: " + height); -// } - return height; - } - - private void updateScreenSize() { - DisplayMetrics metrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getMetrics(metrics); - - screenWidth = metrics.widthPixels; - screenHeight = metrics.heightPixels; - if (DEBUG) { - Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", " - + "screenHeight = " + screenHeight); - } - - popupWidth = getResources().getDimension(R.dimen.popup_default_width); - popupHeight = getMinimumVideoHeight(popupWidth); - - minimumWidth = getResources().getDimension(R.dimen.popup_minimum_width); - minimumHeight = getMinimumVideoHeight(minimumWidth); - - maximumWidth = screenWidth; - maximumHeight = screenHeight; - } - - private void updatePopupSize(final int width, final int height) { - if (playerImpl == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "updatePopupSize() called with: " - + "width = [" + width + "], height = [" + height + "]"); - } - - final int actualWidth = (int) (width > maximumWidth ? maximumWidth - : width < minimumWidth ? minimumWidth : width); - - final int actualHeight; - if (height == -1) { - actualHeight = (int) getMinimumVideoHeight(width); - } else { - actualHeight = (int) (height > maximumHeight ? maximumHeight - : height < minimumHeight ? minimumHeight : height); - } - - popupLayoutParams.width = actualWidth; - popupLayoutParams.height = actualHeight; - popupWidth = actualWidth; - popupHeight = actualHeight; - - if (DEBUG) { - Log.d(TAG, "updatePopupSize() updated values: " - + "width = [" + actualWidth + "], height = [" + actualHeight + "]"); - } - windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); - } - - protected void setRepeatModeRemote(final RemoteViews remoteViews, final int repeatMode) { - final String methodName = "setImageResource"; - - if (remoteViews == null) { - return; - } - - switch (repeatMode) { - case Player.REPEAT_MODE_OFF: - remoteViews.setInt(R.id.notificationRepeat, methodName, - R.drawable.exo_controls_repeat_off); - break; - case Player.REPEAT_MODE_ONE: - remoteViews.setInt(R.id.notificationRepeat, methodName, - R.drawable.exo_controls_repeat_one); - break; - case Player.REPEAT_MODE_ALL: - remoteViews.setInt(R.id.notificationRepeat, methodName, - R.drawable.exo_controls_repeat_all); - break; - } - } - - private void updateWindowFlags(final int flags) { - if (popupLayoutParams == null || windowManager == null || playerImpl == null) { - return; - } - - popupLayoutParams.flags = flags; - windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); - } - /////////////////////////////////////////////////////////////////////////// - - protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener { - private TextView resizingIndicator; - private ImageButton fullScreenButton; - private ImageView videoPlayPause; - - private View extraOptionsView; - private View closingOverlayView; - - VideoPlayerImpl(final Context context) { - super("VideoPlayerImpl" + PopupVideoPlayer.TAG, context); - } - - @Override - public void handleIntent(final Intent intent) { - super.handleIntent(intent); - - resetNotification(); - startForeground(NOTIFICATION_ID, notBuilder.build()); - } - - @Override - public void initViews(final View view) { - super.initViews(view); - resizingIndicator = view.findViewById(R.id.resizing_indicator); - fullScreenButton = view.findViewById(R.id.fullScreenButton); - fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked()); - videoPlayPause = view.findViewById(R.id.videoPlayPause); - - extraOptionsView = view.findViewById(R.id.extraOptionsView); - closingOverlayView = view.findViewById(R.id.closingOverlay); - view.addOnLayoutChangeListener(this); - } - - @Override - public void initListeners() { - super.initListeners(); - videoPlayPause.setOnClickListener(v -> onPlayPause()); - } - - @Override - protected void setupSubtitleView(@NonNull final SubtitleView view, final float captionScale, - @NonNull final CaptionStyleCompat captionStyle) { - float captionRatio = (captionScale - 1f) / 5f + 1f; - view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); - view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); - view.setStyle(captionStyle); - } - - @Override - public void onLayoutChange(final View view, final int left, final int top, final int right, - final int bottom, final int oldLeft, final int oldTop, - final int oldRight, final int oldBottom) { - float widthDp = Math.abs(right - left) / getResources().getDisplayMetrics().density; - final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP ? View.VISIBLE : View.GONE; - extraOptionsView.setVisibility(visibility); - } - - @Override - public void destroy() { - if (notRemoteView != null) { - notRemoteView.setImageViewBitmap(R.id.notificationCover, null); - } - super.destroy(); - } - - @Override - public void onFullScreenButtonClicked() { - super.onFullScreenButtonClicked(); - - if (DEBUG) { - Log.d(TAG, "onFullScreenButtonClicked() called"); - } - - setRecovery(); - final Intent intent = NavigationHelper.getPlayerIntent( - context, - MainVideoPlayer.class, - this.getPlayQueue(), - this.getRepeatMode(), - this.getPlaybackSpeed(), - this.getPlaybackPitch(), - this.getPlaybackSkipSilence(), - this.getPlaybackQuality(), - false, - !isPlaying(), - isMuted() - ); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - closePopup(); - } - - @Override - public void onDismiss(final PopupMenu menu) { - super.onDismiss(menu); - if (isPlaying()) { - hideControls(500, 0); - } - } - - @Override - protected int nextResizeMode(final int resizeMode) { - if (resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { - return AspectRatioFrameLayout.RESIZE_MODE_FIT; - } else { - return AspectRatioFrameLayout.RESIZE_MODE_FILL; - } - } - - @Override - public void onStopTrackingTouch(final SeekBar seekBar) { - super.onStopTrackingTouch(seekBar); - if (wasPlaying()) { - hideControls(100, 0); - } - } - - @Override - public void onShuffleClicked() { - super.onShuffleClicked(); - updatePlayback(); - } - - @Override - public void onMuteUnmuteButtonClicked() { - super.onMuteUnmuteButtonClicked(); - updatePlayback(); - } - - @Override - public void onUpdateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - updateProgress(currentProgress, duration, bufferPercent); - super.onUpdateProgress(currentProgress, duration, bufferPercent); - } - - @Override - protected VideoPlaybackResolver.QualityResolver getQualityResolver() { - return new VideoPlaybackResolver.QualityResolver() { - @Override - public int getDefaultResolutionIndex(final List sortedVideos) { - return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); - } - - @Override - public int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return ListHelper.getPopupResolutionIndex(context, sortedVideos, - playbackQuality); - } - }; - } - - /*////////////////////////////////////////////////////////////////////////// - // Thumbnail Loading - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onLoadingComplete(final String imageUri, final View view, - final Bitmap loadedImage) { - super.onLoadingComplete(imageUri, view, loadedImage); - if (playerImpl == null) { - return; - } - // rebuild notification here since remote view does not release bitmaps, - // causing memory leaks - resetNotification(); - updateNotification(-1); - } - - @Override - public void onLoadingFailed(final String imageUri, final View view, - final FailReason failReason) { - super.onLoadingFailed(imageUri, view, failReason); - resetNotification(); - updateNotification(-1); - } - - @Override - public void onLoadingCancelled(final String imageUri, final View view) { - super.onLoadingCancelled(imageUri, view); - resetNotification(); - updateNotification(-1); - } - - /*////////////////////////////////////////////////////////////////////////// - // Activity Event Listener - //////////////////////////////////////////////////////////////////////////*/ - - /*package-private*/ void setActivityListener(final PlayerEventListener listener) { - activityListener = listener; - updateMetadata(); - updatePlayback(); - triggerProgressUpdate(); - } - - /*package-private*/ void removeActivityListener(final PlayerEventListener listener) { - if (activityListener == listener) { - activityListener = null; - } - } - - private void updateMetadata() { - if (activityListener != null && getCurrentMetadata() != null) { - activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); - } - } - - private void updatePlayback() { - if (activityListener != null && simpleExoPlayer != null && playQueue != null) { - activityListener.onPlaybackUpdate(currentState, getRepeatMode(), - playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); - } - } - - private void updateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - if (activityListener != null) { - activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); - } - } - - private void stopActivityBinding() { - if (activityListener != null) { - activityListener.onServiceStopped(); - activityListener = null; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Video Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onRepeatModeChanged(final int i) { - super.onRepeatModeChanged(i); - setRepeatModeRemote(notRemoteView, i); - updatePlayback(); - resetNotification(); - updateNotification(-1); - } - - @Override - public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { - super.onPlaybackParametersChanged(playbackParameters); - updatePlayback(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - super.onMetadataChanged(tag); - resetNotification(); - updateNotification(-1); - updateMetadata(); - } - - @Override - public void onPlaybackShutdown() { - super.onPlaybackShutdown(); - closePopup(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Broadcast Receiver - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void setupBroadcastReceiver(final IntentFilter intentFltr) { - super.setupBroadcastReceiver(intentFltr); - if (DEBUG) { - Log.d(TAG, "setupBroadcastReceiver() called with: " - + "intentFilter = [" + intentFltr + "]"); - } - intentFltr.addAction(ACTION_CLOSE); - intentFltr.addAction(ACTION_PLAY_PAUSE); - intentFltr.addAction(ACTION_REPEAT); - - intentFltr.addAction(Intent.ACTION_SCREEN_ON); - intentFltr.addAction(Intent.ACTION_SCREEN_OFF); - } - - @Override - public void onBroadcastReceived(final Intent intent) { - super.onBroadcastReceived(intent); - if (intent == null || intent.getAction() == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); - } - switch (intent.getAction()) { - case ACTION_CLOSE: - closePopup(); - break; - case ACTION_PLAY_PAUSE: - onPlayPause(); - break; - case ACTION_REPEAT: - onRepeatClicked(); - break; - case Intent.ACTION_SCREEN_ON: - enableVideoRenderer(true); - break; - case Intent.ACTION_SCREEN_OFF: - enableVideoRenderer(false); - break; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // States - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void changeState(final int state) { - super.changeState(state); - updatePlayback(); - } - - @Override - public void onBlocked() { - super.onBlocked(); - resetNotification(); - updateNotification(R.drawable.exo_controls_play); - } - - @Override - public void onPlaying() { - super.onPlaying(); - - updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); - - resetNotification(); - updateNotification(R.drawable.exo_controls_pause); - - videoPlayPause.setBackgroundResource(R.drawable.exo_controls_pause); - hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - - startForeground(NOTIFICATION_ID, notBuilder.build()); - } - - @Override - public void onBuffering() { - super.onBuffering(); - resetNotification(); - updateNotification(R.drawable.exo_controls_play); - } - - @Override - public void onPaused() { - super.onPaused(); - - updateWindowFlags(IDLE_WINDOW_FLAGS); - - resetNotification(); - updateNotification(R.drawable.exo_controls_play); - videoPlayPause.setBackgroundResource(R.drawable.exo_controls_play); - - stopForeground(false); - } - - @Override - public void onPausedSeek() { - super.onPausedSeek(); - resetNotification(); - updateNotification(R.drawable.exo_controls_play); - - videoPlayPause.setBackgroundResource(R.drawable.exo_controls_play); - } - - @Override - public void onCompleted() { - super.onCompleted(); - - updateWindowFlags(IDLE_WINDOW_FLAGS); - - resetNotification(); - updateNotification(R.drawable.ic_replay_white_24dp); - videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white_24dp); - - stopForeground(false); - } - - @Override - public void showControlsThenHide() { - videoPlayPause.setVisibility(View.VISIBLE); - super.showControlsThenHide(); - } - - public void showControls(final long duration) { - videoPlayPause.setVisibility(View.VISIBLE); - super.showControls(duration); - } - - public void hideControls(final long duration, final long delay) { - super.hideControlsAndButton(duration, delay, videoPlayPause); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - /*package-private*/ void enableVideoRenderer(final boolean enable) { - final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); - if (videoRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setParameters(trackSelector.buildUponParameters() - .setRendererDisabled(videoRendererIndex, !enable)); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Getters - //////////////////////////////////////////////////////////////////////////*/ - - @SuppressWarnings("WeakerAccess") - public TextView getResizingIndicator() { - return resizingIndicator; - } - - public View getClosingOverlayView() { - return closingOverlayView; - } - } - - private class PopupWindowGestureListener extends GestureDetector.SimpleOnGestureListener - implements View.OnTouchListener { - private int initialPopupX; - private int initialPopupY; - private boolean isMoving; - private boolean isResizing; - - //initial co-ordinates and distance between fingers - private double initPointerDistance = -1; - private float initFirstPointerX = -1; - private float initFirstPointerY = -1; - private float initSecPointerX = -1; - private float initSecPointerY = -1; - - - @Override - public boolean onDoubleTap(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onDoubleTap() called with: e = [" + e + "], " - + "rawXy = " + e.getRawX() + ", " + e.getRawY() - + ", xy = " + e.getX() + ", " + e.getY()); - } - if (playerImpl == null || !playerImpl.isPlaying()) { - return false; - } - - playerImpl.hideControls(0, 0); - - if (e.getX() > popupWidth / 2) { - playerImpl.onFastForward(); - } else { - playerImpl.onFastRewind(); - } - - return true; - } - - @Override - public boolean onSingleTapConfirmed(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); - } - if (playerImpl == null || playerImpl.getPlayer() == null) { - return false; - } - if (playerImpl.isControlsVisible()) { - playerImpl.hideControls(100, 100); - } else { - playerImpl.showControlsThenHide(); - - } - return true; - } - - @Override - public boolean onDown(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onDown() called with: e = [" + e + "]"); - } - - // Fix popup position when the user touch it, it may have the wrong one - // because the soft input is visible (the draggable area is currently resized). - checkPopupPositionBounds(closeOverlayView.getWidth(), closeOverlayView.getHeight()); - - initialPopupX = popupLayoutParams.x; - initialPopupY = popupLayoutParams.y; - popupWidth = popupLayoutParams.width; - popupHeight = popupLayoutParams.height; - return super.onDown(e); - } - - @Override - public void onLongPress(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); - } - updateScreenSize(); - checkPopupPositionBounds(); - updatePopupSize((int) screenWidth, -1); - } - - @Override - public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent, - final float distanceX, final float distanceY) { - if (isResizing || playerImpl == null) { - return super.onScroll(initialEvent, movingEvent, distanceX, distanceY); - } - - if (!isMoving) { - animateView(closeOverlayButton, true, 200); - } - - isMoving = true; - - float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()); - float posX = (int) (initialPopupX + diffX); - float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()); - float posY = (int) (initialPopupY + diffY); - - if (posX > (screenWidth - popupWidth)) { - posX = (int) (screenWidth - popupWidth); - } else if (posX < 0) { - posX = 0; - } - - if (posY > (screenHeight - popupHeight)) { - posY = (int) (screenHeight - popupHeight); - } else if (posY < 0) { - posY = 0; - } - - popupLayoutParams.x = (int) posX; - popupLayoutParams.y = (int) posY; - - final View closingOverlayView = playerImpl.getClosingOverlayView(); - if (isInsideClosingRadius(movingEvent)) { - if (closingOverlayView.getVisibility() == View.GONE) { - animateView(closingOverlayView, true, 250); - } - } else { - if (closingOverlayView.getVisibility() == View.VISIBLE) { - animateView(closingOverlayView, false, 0); - } - } - -// if (DEBUG) { -// Log.d(TAG, "PopupVideoPlayer.onScroll = " -// + "e1.getRaw = [" + initialEvent.getRawX() + ", " -// + initialEvent.getRawY() + "], " -// + "e1.getX,Y = [" + initialEvent.getX() + ", " -// + initialEvent.getY() + "], " -// + "e2.getRaw = [" + movingEvent.getRawX() + ", " -// + movingEvent.getRawY() + "], " -// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], " -// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], " -// + "posX,Y = [" + posX + ", " + posY + "], " -// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]"); -// } - windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); - return true; - } - - private void onScrollEnd(final MotionEvent event) { - if (DEBUG) { - Log.d(TAG, "onScrollEnd() called"); - } - if (playerImpl == null) { - return; - } - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - - if (isInsideClosingRadius(event)) { - closePopup(); - } else { - animateView(playerImpl.getClosingOverlayView(), false, 0); - - if (!isPopupClosing) { - animateView(closeOverlayButton, false, 200); - } - } - } - - @Override - public boolean onFling(final MotionEvent e1, final MotionEvent e2, - final float velocityX, final float velocityY) { - if (DEBUG) { - Log.d(TAG, "Fling velocity: dX=[" + velocityX + "], dY=[" + velocityY + "]"); - } - if (playerImpl == null) { - return false; - } - - final float absVelocityX = Math.abs(velocityX); - final float absVelocityY = Math.abs(velocityY); - if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { - if (absVelocityX > tossFlingVelocity) { - popupLayoutParams.x = (int) velocityX; - } - if (absVelocityY > tossFlingVelocity) { - popupLayoutParams.y = (int) velocityY; - } - checkPopupPositionBounds(); - windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); - return true; - } - return false; - } - - @Override - public boolean onTouch(final View v, final MotionEvent event) { - popupGestureDetector.onTouchEvent(event); - if (playerImpl == null) { - return false; - } - if (event.getPointerCount() == 2 && !isMoving && !isResizing) { - if (DEBUG) { - Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); - } - playerImpl.showAndAnimateControl(-1, true); - playerImpl.getLoadingPanel().setVisibility(View.GONE); - - playerImpl.hideControls(0, 0); - animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); - animateView(playerImpl.getResizingIndicator(), true, 200, 0); - - //record co-ordinates of fingers - initFirstPointerX = event.getX(0); - initFirstPointerY = event.getY(0); - initSecPointerX = event.getX(1); - initSecPointerY = event.getY(1); - //record distance between fingers - initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX, - initFirstPointerY - initSecPointerY); - - isResizing = true; - } - - if (event.getAction() == MotionEvent.ACTION_MOVE && !isMoving && isResizing) { - if (DEBUG) { - Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], " - + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); - } - return handleMultiDrag(event); - } - - if (event.getAction() == MotionEvent.ACTION_UP) { - if (DEBUG) { - Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], " - + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); - } - if (isMoving) { - isMoving = false; - onScrollEnd(event); - } - - if (isResizing) { - isResizing = false; - - initPointerDistance = -1; - initFirstPointerX = -1; - initFirstPointerY = -1; - initSecPointerX = -1; - initSecPointerY = -1; - - animateView(playerImpl.getResizingIndicator(), false, 100, 0); - playerImpl.changeState(playerImpl.getCurrentState()); - } - - if (!isPopupClosing) { - savePositionAndSize(); - } - } - - v.performClick(); - return true; - } - - private boolean handleMultiDrag(final MotionEvent event) { - if (initPointerDistance != -1 && event.getPointerCount() == 2) { - // get the movements of the fingers - double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX, - event.getY(0) - initFirstPointerY); - double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX, - event.getY(1) - initSecPointerY); - - // minimum threshold beyond which pinch gesture will work - int minimumMove = ViewConfiguration.get(PopupVideoPlayer.this).getScaledTouchSlop(); - - if (Math.max(firstPointerMove, secPointerMove) > minimumMove) { - // calculate current distance between the pointers - double currentPointerDistance = - Math.hypot(event.getX(0) - event.getX(1), - event.getY(0) - event.getY(1)); - - // change co-ordinates of popup so the center stays at the same position - double newWidth = (popupWidth * currentPointerDistance / initPointerDistance); - initPointerDistance = currentPointerDistance; - popupLayoutParams.x += (popupWidth - newWidth) / 2; - - checkPopupPositionBounds(); - updateScreenSize(); - - updatePopupSize((int) Math.min(screenWidth, newWidth), -1); - return true; - } - } - return false; - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private int distanceFromCloseButton(final MotionEvent popupMotionEvent) { - final int closeOverlayButtonX = closeOverlayButton.getLeft() - + closeOverlayButton.getWidth() / 2; - final int closeOverlayButtonY = closeOverlayButton.getTop() - + closeOverlayButton.getHeight() / 2; - - float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); - float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); - - return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) - + Math.pow(closeOverlayButtonY - fingerY, 2)); - } - - private float getClosingRadius() { - final int buttonRadius = closeOverlayButton.getWidth() / 2; - // 20% wider than the button itself - return buttonRadius * 1.2f; - } - - private boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) { - return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java deleted file mode 100644 index efb4176a6..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.schabi.newpipe.player; - -import android.content.Intent; -import android.view.MenuItem; - -import org.schabi.newpipe.R; - -import static org.schabi.newpipe.player.PopupVideoPlayer.ACTION_CLOSE; - -public final class PopupVideoPlayerActivity extends ServicePlayerActivity { - - private static final String TAG = "PopupVideoPlayerActivity"; - - @Override - public String getTag() { - return TAG; - } - - @Override - public String getSupportActionTitle() { - return getResources().getString(R.string.title_activity_popup_player); - } - - @Override - public Intent getBindIntent() { - return new Intent(this, PopupVideoPlayer.class); - } - - @Override - public void startPlayerListener() { - if (player != null && player instanceof PopupVideoPlayer.VideoPlayerImpl) { - ((PopupVideoPlayer.VideoPlayerImpl) player).setActivityListener(this); - } - } - - @Override - public void stopPlayerListener() { - if (player != null && player instanceof PopupVideoPlayer.VideoPlayerImpl) { - ((PopupVideoPlayer.VideoPlayerImpl) player).removeActivityListener(this); - } - } - - @Override - public int getPlayerOptionMenuResource() { - return R.menu.menu_play_queue_popup; - } - - @Override - public boolean onPlayerOptionSelected(final MenuItem item) { - if (item.getItemId() == R.id.action_switch_background) { - this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startService( - getSwitchIntent(BackgroundPlayer.class) - .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()) - ); - return true; - } - return false; - } - - @Override - public Intent getPlayerShutdownIntent() { - return new Intent(ACTION_CLOSE); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 72becef8f..3043a7fc3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -27,17 +27,21 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; +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.Constants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -110,7 +114,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity public abstract boolean onPlayerOptionSelected(MenuItem item); - public abstract Intent getPlayerShutdownIntent(); + public abstract void setupMenu(Menu m); //////////////////////////////////////////////////////////////////////////// // Activity Lifecycle //////////////////////////////////////////////////////////////////////////// @@ -152,6 +156,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return true; } + // Allow to setup visibility of menuItems + @Override + public boolean onPrepareOptionsMenu(final Menu m) { + setupMenu(m); + return super.onPrepareOptionsMenu(m); + } + @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { @@ -175,11 +186,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return true; case R.id.action_switch_main: this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); getApplicationContext().startActivity( - getSwitchIntent(MainVideoPlayer.class) - .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()) - ); + getSwitchIntent(MainActivity.class, MainPlayer.PlayerType.VIDEO) + .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())); return true; } return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item); @@ -191,13 +200,22 @@ public abstract class ServicePlayerActivity extends AppCompatActivity unbind(); } - protected Intent getSwitchIntent(final Class clazz) { + protected Intent getSwitchIntent(final Class clazz, final MainPlayer.PlayerType playerType) { return NavigationHelper.getPlayerIntent(getApplicationContext(), clazz, this.player.getPlayQueue(), this.player.getRepeatMode(), this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(), - this.player.getPlaybackSkipSilence(), null, false, false, this.player.isMuted()) + this.player.getPlaybackSkipSilence(), + null, + true, + !this.player.isPlaying(), + this.player.isMuted()) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()); + .putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM) + .putExtra(Constants.KEY_URL, this.player.getVideoUrl()) + .putExtra(Constants.KEY_TITLE, this.player.getVideoTitle()) + .putExtra(Constants.KEY_SERVICE_ID, + this.player.getCurrentMetadata().getMetadata().getServiceId()) + .putExtra(VideoPlayer.PLAYER_TYPE, playerType); } //////////////////////////////////////////////////////////////////////////// @@ -247,6 +265,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (service instanceof PlayerServiceBinder) { player = ((PlayerServiceBinder) service).getPlayerInstance(); + } else if (service instanceof MainPlayer.LocalBinder) { + player = ((MainPlayer.LocalBinder) service).getPlayer(); } if (player == null || player.getPlayQueue() == null @@ -500,7 +520,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return; } PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), - player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag()); + player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag()); } @Override @@ -571,6 +591,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity // Binding Service Listener //////////////////////////////////////////////////////////////////////////// + @Override + public void onQueueUpdate(final PlayQueue queue) { + } + @Override public void onPlaybackUpdate(final int state, final int repeatMode, final boolean shuffled, final PlaybackParameters parameters) { @@ -610,7 +634,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } @Override - public void onMetadataUpdate(final StreamInfo info) { + public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) { if (info != null) { metadataTitle.setText(info.getName()); metadataArtist.setText(info.getUploaderName()); diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 576d42a00..e621f9f33 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -34,16 +34,16 @@ import android.os.Build; import android.os.Handler; import android.preference.PreferenceManager; import android.util.Log; + import android.view.Menu; import android.view.MenuItem; -import android.view.SurfaceView; import android.view.View; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; @@ -69,6 +69,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.views.ExpandableSurfaceView; import java.util.ArrayList; import java.util.List; @@ -117,8 +118,7 @@ public abstract class VideoPlayer extends BasePlayer private View rootView; - private AspectRatioFrameLayout aspectRatioFrameLayout; - private SurfaceView surfaceView; + private ExpandableSurfaceView surfaceView; private View surfaceForeground; private View loadingPanel; @@ -135,7 +135,7 @@ public abstract class VideoPlayer extends BasePlayer private TextView playbackLiveSync; private TextView playbackSpeedTextView; - private View topControlsRoot; + private LinearLayout topControlsRoot; private TextView qualityTextView; private SubtitleView subtitleView; @@ -182,7 +182,6 @@ public abstract class VideoPlayer extends BasePlayer public void initViews(final View view) { this.rootView = view; - this.aspectRatioFrameLayout = view.findViewById(R.id.aspectRatioLayout); this.surfaceView = view.findViewById(R.id.surfaceView); this.surfaceForeground = view.findViewById(R.id.surfaceForeground); this.loadingPanel = view.findViewById(R.id.loading_panel); @@ -207,12 +206,10 @@ public abstract class VideoPlayer extends BasePlayer this.resizeView = view.findViewById(R.id.resizeTextView); resizeView.setText(PlayerHelper - .resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode())); + .resizeTypeOf(context, getSurfaceView().getResizeMode())); this.captionTextView = view.findViewById(R.id.captionTextView); - //this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); } @@ -520,7 +517,6 @@ public abstract class VideoPlayer extends BasePlayer super.onCompleted(); showControls(500); - animateView(endScreen, true, 800); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); loadingPanel.setVisibility(View.GONE); @@ -555,7 +551,7 @@ public abstract class VideoPlayer extends BasePlayer + "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], " + "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); } - aspectRatioFrameLayout.setAspectRatio(((float) width) / height); + getSurfaceView().setAspectRatio(((float) width) / height); } @Override @@ -620,12 +616,6 @@ public abstract class VideoPlayer extends BasePlayer playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed())); super.onPrepared(playWhenReady); - - if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) { - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler - .postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION); - } } @Override @@ -675,7 +665,7 @@ public abstract class VideoPlayer extends BasePlayer } } - protected void onFullScreenButtonClicked() { + protected void toggleFullscreen() { changeState(STATE_BLOCKED); } @@ -799,16 +789,16 @@ public abstract class VideoPlayer extends BasePlayer showControls(DEFAULT_CONTROLS_DURATION); } - private void onResizeClicked() { - if (getAspectRatioFrameLayout() != null) { - final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); + void onResizeClicked() { + if (getSurfaceView() != null) { + final int currentResizeMode = getSurfaceView().getResizeMode(); final int newResizeMode = nextResizeMode(currentResizeMode); setResizeMode(newResizeMode); } } protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { - getAspectRatioFrameLayout().setResizeMode(resizeMode); + getSurfaceView().setResizeMode(resizeMode); getResizeView().setText(PlayerHelper.resizeTypeOf(context, resizeMode)); } @@ -916,9 +906,9 @@ public abstract class VideoPlayer extends BasePlayer if (drawableId == -1) { if (controlAnimationView.getVisibility() == View.VISIBLE) { controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, - PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), - PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f), - PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f) + PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f), + PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f), + PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f) ).setDuration(DEFAULT_CONTROLS_DURATION); controlViewAnimator.addListener(new AnimatorListenerAdapter() { @Override @@ -1020,6 +1010,9 @@ public abstract class VideoPlayer extends BasePlayer animateView(controlsRoot, false, duration); }; } + + public abstract void hideSystemUIIfNeeded(); + /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ @@ -1033,11 +1026,7 @@ public abstract class VideoPlayer extends BasePlayer this.resolver.setPlaybackQuality(quality); } - public AspectRatioFrameLayout getAspectRatioFrameLayout() { - return aspectRatioFrameLayout; - } - - public SurfaceView getSurfaceView() { + public ExpandableSurfaceView getSurfaceView() { return surfaceView; } @@ -1096,7 +1085,7 @@ public abstract class VideoPlayer extends BasePlayer return playbackEndTime; } - public View getTopControlsRoot() { + public LinearLayout getTopControlsRoot() { return topControlsRoot; } @@ -1108,6 +1097,10 @@ public abstract class VideoPlayer extends BasePlayer return qualityPopupMenu; } + public TextView getPlaybackSpeedTextView() { + return playbackSpeedTextView; + } + public PopupMenu getPlaybackSpeedPopupMenu() { return playbackSpeedPopupMenu; } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java new file mode 100644 index 000000000..0f98b2296 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -0,0 +1,2177 @@ +/* + * Copyright 2017 Mauricio Colli + * Part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.player; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Display; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.AnticipateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.text.CaptionStyleCompat; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.nostra13.universalimageloader.core.assist.FailReason; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; +import org.schabi.newpipe.player.event.PlayerEventListener; +import org.schabi.newpipe.player.event.PlayerGestureListener; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.helper.PlaybackParameterDialog; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; +import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; +import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; +import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; +import org.schabi.newpipe.util.DeviceUtils; +import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.KoreUtil; +import org.schabi.newpipe.util.ListHelper; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ShareUtils; + +import java.util.List; + +import static android.content.Context.WINDOW_SERVICE; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; +import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; +import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; +import static org.schabi.newpipe.player.MainPlayer.ACTION_OPEN_CONTROLS; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; +import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; +import static org.schabi.newpipe.player.MainPlayer.NOTIFICATION_ID; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; +import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; +import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; +import static org.schabi.newpipe.util.AnimationUtils.animateRotation; +import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; +import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + +/** + * Unified UI for all players. + * + * @author mauriciocolli + */ + +public class VideoPlayerImpl extends VideoPlayer + implements View.OnLayoutChangeListener, + PlaybackParameterDialog.Callback, + View.OnLongClickListener { + private static final String TAG = ".VideoPlayerImpl"; + + static final String POPUP_SAVED_WIDTH = "popup_saved_width"; + static final String POPUP_SAVED_X = "popup_saved_x"; + static final String POPUP_SAVED_Y = "popup_saved_y"; + private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; + private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + + private static final float MAX_GESTURE_LENGTH = 0.75f; + private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60; + + private TextView titleTextView; + private TextView channelTextView; + private RelativeLayout volumeRelativeLayout; + private ProgressBar volumeProgressBar; + private ImageView volumeImageView; + private RelativeLayout brightnessRelativeLayout; + private ProgressBar brightnessProgressBar; + private ImageView brightnessImageView; + private TextView resizingIndicator; + private ImageButton queueButton; + private ImageButton repeatButton; + private ImageButton shuffleButton; + private ImageButton playWithKodi; + private ImageButton openInBrowser; + private ImageButton fullscreenButton; + private ImageButton playerCloseButton; + private ImageButton screenRotationButton; + private ImageButton muteButton; + + private ImageButton playPauseButton; + private ImageButton playPreviousButton; + private ImageButton playNextButton; + + private RelativeLayout queueLayout; + private ImageButton itemsListCloseButton; + private RecyclerView itemsList; + private ItemTouchHelper itemTouchHelper; + + private boolean queueVisible; + private MainPlayer.PlayerType playerType = MainPlayer.PlayerType.VIDEO; + + private ImageButton moreOptionsButton; + private ImageButton shareButton; + + private View primaryControls; + private View secondaryControls; + + private int maxGestureLength; + + private boolean audioOnly = false; + private boolean isFullscreen = false; + private boolean isVerticalVideo = false; + private boolean fragmentIsVisible = false; + boolean shouldUpdateOnProgress; + int timesNotificationUpdated; + + private final MainPlayer service; + private PlayerServiceEventListener fragmentListener; + private PlayerEventListener activityListener; + private GestureDetector gestureDetector; + private final SharedPreferences defaultPreferences; + private ContentObserver settingsContentObserver; + @NonNull + private final AudioPlaybackResolver resolver; + + private int cachedDuration; + private String cachedDurationString; + + // Popup + private WindowManager.LayoutParams popupLayoutParams; + public WindowManager windowManager; + + private View closingOverlayView; + private View closeOverlayView; + private FloatingActionButton closeOverlayButton; + + public boolean isPopupClosing = false; + + private float screenWidth; + private float screenHeight; + private float popupWidth; + private float popupHeight; + private float minimumWidth; + private float minimumHeight; + private float maximumWidth; + private float maximumHeight; + // Popup end + + + @Override + public void handleIntent(final Intent intent) { + if (intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) == null) { + return; + } + + final MainPlayer.PlayerType oldPlayerType = playerType; + choosePlayerTypeFromIntent(intent); + audioOnly = audioPlayerSelected(); + + // We need to setup audioOnly before super(), see "sourceOf" + super.handleIntent(intent); + + if (oldPlayerType != playerType && playQueue != null) { + // If playerType changes from one to another we should reload the player + // (to disable/enable video stream or to set quality) + setRecovery(); + reload(); + } + + setupElementsVisibility(); + setupElementsSize(); + + if (audioPlayerSelected()) { + service.removeViewFromParent(); + } else if (popupPlayerSelected()) { + getRootView().setVisibility(View.VISIBLE); + initPopup(); + initPopupCloseOverlay(); + playPauseButton.requestFocus(); + } else { + getRootView().setVisibility(View.VISIBLE); + initVideoPlayer(); + // Android TV: without it focus will frame the whole player + playPauseButton.requestFocus(); + } + + onPlay(); + } + + VideoPlayerImpl(final MainPlayer service) { + super("MainPlayer" + TAG, service); + this.service = service; + this.shouldUpdateOnProgress = true; + this.windowManager = (WindowManager) service.getSystemService(WINDOW_SERVICE); + this.defaultPreferences = PreferenceManager.getDefaultSharedPreferences(service); + this.resolver = new AudioPlaybackResolver(context, dataSource); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public void initViews(final View view) { + super.initViews(view); + this.titleTextView = view.findViewById(R.id.titleTextView); + this.channelTextView = view.findViewById(R.id.channelTextView); + this.volumeRelativeLayout = view.findViewById(R.id.volumeRelativeLayout); + this.volumeProgressBar = view.findViewById(R.id.volumeProgressBar); + this.volumeImageView = view.findViewById(R.id.volumeImageView); + this.brightnessRelativeLayout = view.findViewById(R.id.brightnessRelativeLayout); + this.brightnessProgressBar = view.findViewById(R.id.brightnessProgressBar); + this.brightnessImageView = view.findViewById(R.id.brightnessImageView); + this.resizingIndicator = view.findViewById(R.id.resizing_indicator); + this.queueButton = view.findViewById(R.id.queueButton); + this.repeatButton = view.findViewById(R.id.repeatButton); + this.shuffleButton = view.findViewById(R.id.shuffleButton); + this.playWithKodi = view.findViewById(R.id.playWithKodi); + this.openInBrowser = view.findViewById(R.id.openInBrowser); + this.fullscreenButton = view.findViewById(R.id.fullScreenButton); + this.screenRotationButton = view.findViewById(R.id.screenRotationButton); + this.playerCloseButton = view.findViewById(R.id.playerCloseButton); + this.muteButton = view.findViewById(R.id.switchMute); + + this.playPauseButton = view.findViewById(R.id.playPauseButton); + this.playPreviousButton = view.findViewById(R.id.playPreviousButton); + this.playNextButton = view.findViewById(R.id.playNextButton); + + this.moreOptionsButton = view.findViewById(R.id.moreOptionsButton); + this.primaryControls = view.findViewById(R.id.primaryControls); + this.secondaryControls = view.findViewById(R.id.secondaryControls); + this.shareButton = view.findViewById(R.id.share); + + this.queueLayout = view.findViewById(R.id.playQueuePanel); + this.itemsListCloseButton = view.findViewById(R.id.playQueueClose); + this.itemsList = view.findViewById(R.id.playQueue); + + closingOverlayView = view.findViewById(R.id.closingOverlay); + + titleTextView.setSelected(true); + channelTextView.setSelected(true); + } + + @Override + protected void setupSubtitleView(final @NonNull SubtitleView view, + final float captionScale, + @NonNull final CaptionStyleCompat captionStyle) { + if (popupPlayerSelected()) { + float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f; + view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); + view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); + view.setStyle(captionStyle); + } else { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + final float captionRatioInverse = 20f + 4f * (1.0f - captionScale); + view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, + (float) minimumLength / captionRatioInverse); + view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); + view.setStyle(captionStyle); + } + } + + /** + * This method ensures that popup and main players have different look. + * We use one layout for both players and need to decide what to show and what to hide. + * Additional measuring should be done inside {@link #setupElementsSize}. + * {@link #setControlsSize} is used to adapt the UI to fullscreen mode, multiWindow, navBar, etc + */ + private void setupElementsVisibility() { + if (popupPlayerSelected()) { + fullscreenButton.setVisibility(View.VISIBLE); + screenRotationButton.setVisibility(View.GONE); + getResizeView().setVisibility(View.GONE); + getRootView().findViewById(R.id.metadataView).setVisibility(View.GONE); + queueButton.setVisibility(View.GONE); + moreOptionsButton.setVisibility(View.GONE); + getTopControlsRoot().setOrientation(LinearLayout.HORIZONTAL); + primaryControls.getLayoutParams().width = LinearLayout.LayoutParams.WRAP_CONTENT; + secondaryControls.setAlpha(1.0f); + secondaryControls.setVisibility(View.VISIBLE); + secondaryControls.setTranslationY(0); + shareButton.setVisibility(View.GONE); + playWithKodi.setVisibility(View.GONE); + openInBrowser.setVisibility(View.GONE); + muteButton.setVisibility(View.GONE); + playerCloseButton.setVisibility(View.GONE); + getTopControlsRoot().bringToFront(); + getTopControlsRoot().setClickable(false); + getTopControlsRoot().setFocusable(false); + getBottomControlsRoot().bringToFront(); + onQueueClosed(); + } else { + fullscreenButton.setVisibility(View.GONE); + setupScreenRotationButton(); + getResizeView().setVisibility(View.VISIBLE); + getRootView().findViewById(R.id.metadataView).setVisibility(View.VISIBLE); + moreOptionsButton.setVisibility(View.VISIBLE); + getTopControlsRoot().setOrientation(LinearLayout.VERTICAL); + primaryControls.getLayoutParams().width = LinearLayout.LayoutParams.MATCH_PARENT; + secondaryControls.setVisibility(View.INVISIBLE); + moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(service, + R.drawable.ic_expand_more_white_24dp)); + shareButton.setVisibility(View.VISIBLE); + showHideKodiButton(); + openInBrowser.setVisibility(View.VISIBLE); + muteButton.setVisibility(View.VISIBLE); + 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) + getTopControlsRoot().setClickable(true); + getTopControlsRoot().setFocusable(true); + } + if (!isFullscreen()) { + titleTextView.setVisibility(View.GONE); + channelTextView.setVisibility(View.GONE); + } else { + titleTextView.setVisibility(View.VISIBLE); + channelTextView.setVisibility(View.VISIBLE); + } + setMuteButton(muteButton, isMuted()); + + animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0); + } + + /** + * Changes padding, size of elements based on player selected right now. + * Popup player has small padding in comparison with the main player + */ + private void setupElementsSize() { + if (popupPlayerSelected()) { + final int controlsPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_popup_controls_padding); + final int buttonsPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_popup_buttons_padding); + getTopControlsRoot().setPaddingRelative(controlsPadding, 0, controlsPadding, 0); + getBottomControlsRoot().setPaddingRelative(controlsPadding, 0, controlsPadding, 0); + getQualityTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + getPlaybackSpeedTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + getCaptionTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + getPlaybackSpeedTextView().setMinimumWidth(0); + } else if (videoPlayerSelected()) { + final int buttonsMinWidth = service.getResources() + .getDimensionPixelSize(R.dimen.player_main_buttons_min_width); + final int playerTopPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_main_top_padding); + final int controlsPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_main_controls_padding); + final int buttonsPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_main_buttons_padding); + getTopControlsRoot().setPaddingRelative( + controlsPadding, playerTopPadding, controlsPadding, 0); + getBottomControlsRoot().setPaddingRelative(controlsPadding, 0, controlsPadding, 0); + getQualityTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + getPlaybackSpeedTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + getPlaybackSpeedTextView().setMinimumWidth(buttonsMinWidth); + getCaptionTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + } + } + + @Override + public void initListeners() { + super.initListeners(); + + final PlayerGestureListener listener = new PlayerGestureListener(this, service); + gestureDetector = new GestureDetector(context, listener); + getRootView().setOnTouchListener(listener); + + queueButton.setOnClickListener(this); + repeatButton.setOnClickListener(this); + shuffleButton.setOnClickListener(this); + + playPauseButton.setOnClickListener(this); + playPreviousButton.setOnClickListener(this); + playNextButton.setOnClickListener(this); + + moreOptionsButton.setOnClickListener(this); + moreOptionsButton.setOnLongClickListener(this); + shareButton.setOnClickListener(this); + fullscreenButton.setOnClickListener(this); + screenRotationButton.setOnClickListener(this); + playWithKodi.setOnClickListener(this); + openInBrowser.setOnClickListener(this); + playerCloseButton.setOnClickListener(this); + muteButton.setOnClickListener(this); + + settingsContentObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(final boolean selfChange) { + setupScreenRotationButton(); + } + }; + service.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, + settingsContentObserver); + getRootView().addOnLayoutChangeListener(this); + } + + public boolean onKeyDown(final int keyCode) { + switch (keyCode) { + default: + break; + case KeyEvent.KEYCODE_BACK: + if (DeviceUtils.isTv(service) && isControlsVisible()) { + hideControls(0, 0); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (getRootView().hasFocus() && !getControlsRoot().hasFocus()) { + // do not interfere with focus in playlist etc. + return false; + } + + if (getCurrentState() == BasePlayer.STATE_BLOCKED) { + return true; + } + + if (!isControlsVisible()) { + if (!queueVisible) { + playPauseButton.requestFocus(); + } + showControlsThenHide(); + showSystemUIPartially(); + return true; + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); + } + break; + } + + return false; + } + + public AppCompatActivity getParentActivity() { + // ! instanceof ViewGroup means that view was added via windowManager for Popup + if (getRootView() == null + || getRootView().getParent() == null + || !(getRootView().getParent() instanceof ViewGroup)) { + return null; + } + + final ViewGroup parent = (ViewGroup) getRootView().getParent(); + return (AppCompatActivity) parent.getContext(); + } + + /*////////////////////////////////////////////////////////////////////////// + // View + //////////////////////////////////////////////////////////////////////////*/ + + private void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + imageButton.setImageResource(R.drawable.exo_controls_repeat_off); + break; + case Player.REPEAT_MODE_ONE: + imageButton.setImageResource(R.drawable.exo_controls_repeat_one); + break; + case Player.REPEAT_MODE_ALL: + imageButton.setImageResource(R.drawable.exo_controls_repeat_all); + break; + } + } + + private void setShuffleButton(final ImageButton button, final boolean shuffled) { + final int shuffleAlpha = shuffled ? 255 : 77; + button.setImageAlpha(shuffleAlpha); + } + + //////////////////////////////////////////////////////////////////////////// + // Playback Parameters Listener + //////////////////////////////////////////////////////////////////////////// + + @Override + public void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, + final boolean playbackSkipSilence) { + setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); + } + + @Override + public void onVideoSizeChanged(final int width, final int height, + final int unappliedRotationDegrees, + final float pixelWidthHeightRatio) { + super.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + isVerticalVideo = width < height; + prepareOrientation(); + setupScreenRotationButton(); + } + + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Video Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onRepeatModeChanged(final int i) { + super.onRepeatModeChanged(i); + updatePlaybackButtons(); + updatePlayback(); + service.resetNotification(); + service.updateNotification(-1); + } + + @Override + public void onShuffleClicked() { + super.onShuffleClicked(); + updatePlaybackButtons(); + updatePlayback(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Playback Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onPlayerError(final ExoPlaybackException error) { + super.onPlayerError(error); + + if (fragmentListener != null) { + fragmentListener.onPlayerError(error); + } + } + + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); + + showHideKodiButton(); + + titleTextView.setText(tag.getMetadata().getName()); + channelTextView.setText(tag.getMetadata().getUploaderName()); + + service.resetNotification(); + service.updateNotification(-1); + updateMetadata(); + } + + @Override + public void onPlaybackShutdown() { + if (DEBUG) { + Log.d(TAG, "onPlaybackShutdown() called"); + } + // Override it because we don't want playerImpl destroyed + } + + @Override + public void onMuteUnmuteButtonClicked() { + super.onMuteUnmuteButtonClicked(); + updatePlayback(); + setMuteButton(muteButton, isMuted()); + } + + @Override + public void onUpdateProgress(final int currentProgress, + final int duration, final int bufferPercent) { + super.onUpdateProgress(currentProgress, duration, bufferPercent); + + updateProgress(currentProgress, duration, bufferPercent); + + if (!shouldUpdateOnProgress || getCurrentState() == BasePlayer.STATE_COMPLETED + || getCurrentState() == BasePlayer.STATE_PAUSED || getPlayQueue() == null) { + return; + } + + if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) { + service.resetNotification(); + } + + if (service.getBigNotRemoteView() != null) { + if (cachedDuration != duration) { + cachedDuration = duration; + cachedDurationString = getTimeString(duration); + } + service.getBigNotRemoteView() + .setProgressBar(R.id.notificationProgressBar, + duration, currentProgress, false); + service.getBigNotRemoteView() + .setTextViewText(R.id.notificationTime, + getTimeString(currentProgress) + " / " + cachedDurationString); + } + if (service.getNotRemoteView() != null) { + service.getNotRemoteView() + .setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); + } + service.updateNotification(-1); + } + + @Override + @Nullable + public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { + // For LiveStream or video/popup players we can use super() method + // but not for audio player + if (!audioOnly) { + return super.sourceOf(item, info); + } else { + return resolver.resolve(info); + } + } + + @Override + public void onPlayPrevious() { + super.onPlayPrevious(); + triggerProgressUpdate(); + } + + @Override + public void onPlayNext() { + super.onPlayNext(); + triggerProgressUpdate(); + } + + @Override + protected void initPlayback(@NonNull final PlayQueue queue, final int repeatMode, + final float playbackSpeed, final float playbackPitch, + final boolean playbackSkipSilence, + final boolean playOnReady, final boolean isMuted) { + super.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, playOnReady, isMuted); + updateQueue(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Player Overrides + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void toggleFullscreen() { + if (DEBUG) { + Log.d(TAG, "toggleFullscreen() called"); + } + if (simpleExoPlayer == null || getCurrentMetadata() == null) { + return; + } + + if (popupPlayerSelected()) { + setRecovery(); + service.removeViewFromParent(); + Intent intent = NavigationHelper.getPlayerIntent( + service, + MainActivity.class, + this.getPlayQueue(), + this.getRepeatMode(), + this.getPlaybackSpeed(), + this.getPlaybackPitch(), + this.getPlaybackSkipSilence(), + null, + true, + !isPlaying(), + isMuted() + ); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Constants.KEY_SERVICE_ID, + getCurrentMetadata().getMetadata().getServiceId()); + intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); + intent.putExtra(Constants.KEY_URL, getVideoUrl()); + intent.putExtra(Constants.KEY_TITLE, getVideoTitle()); + intent.putExtra(VideoDetailFragment.AUTO_PLAY, true); + service.onDestroy(); + context.startActivity(intent); + return; + } else { + if (fragmentListener == null) { + return; + } + + isFullscreen = !isFullscreen; + setControlsSize(); + fragmentListener.onFullscreenStateChanged(isFullscreen()); + } + + if (!isFullscreen()) { + titleTextView.setVisibility(View.GONE); + channelTextView.setVisibility(View.GONE); + playerCloseButton.setVisibility(videoPlayerSelected() ? View.VISIBLE : View.GONE); + } else { + titleTextView.setVisibility(View.VISIBLE); + channelTextView.setVisibility(View.VISIBLE); + playerCloseButton.setVisibility(View.GONE); + } + setupScreenRotationButton(); + } + + @Override + public void onClick(final View v) { + super.onClick(v); + if (v.getId() == playPauseButton.getId()) { + onPlayPause(); + } else if (v.getId() == playPreviousButton.getId()) { + onPlayPrevious(); + } else if (v.getId() == playNextButton.getId()) { + onPlayNext(); + } else if (v.getId() == queueButton.getId()) { + onQueueClicked(); + return; + } else if (v.getId() == repeatButton.getId()) { + onRepeatClicked(); + return; + } else if (v.getId() == shuffleButton.getId()) { + onShuffleClicked(); + return; + } else if (v.getId() == moreOptionsButton.getId()) { + onMoreOptionsClicked(); + } else if (v.getId() == shareButton.getId()) { + onShareClicked(); + } else if (v.getId() == playWithKodi.getId()) { + onPlayWithKodiClicked(); + } else if (v.getId() == openInBrowser.getId()) { + onOpenInBrowserClicked(); + } else if (v.getId() == fullscreenButton.getId()) { + toggleFullscreen(); + } else if (v.getId() == screenRotationButton.getId()) { + if (!isVerticalVideo) { + fragmentListener.onScreenRotationButtonClicked(); + } else { + toggleFullscreen(); + } + } else if (v.getId() == muteButton.getId()) { + onMuteUnmuteButtonClicked(); + } else if (v.getId() == playerCloseButton.getId()) { + service.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER)); + } + + if (getCurrentState() != STATE_COMPLETED) { + getControlsVisibilityHandler().removeCallbacksAndMessages(null); + animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { + if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { + if (v.getId() == playPauseButton.getId()) { + hideControls(0, 0); + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + } + }); + } + } + + @Override + public boolean onLongClick(final View v) { + if (v.getId() == moreOptionsButton.getId() && isFullscreen()) { + fragmentListener.onMoreOptionsLongClicked(); + hideControls(0, 0); + hideSystemUIIfNeeded(); + } + return true; + } + + private void onQueueClicked() { + queueVisible = true; + + hideSystemUIIfNeeded(); + buildQueue(); + updatePlaybackButtons(); + + getControlsRoot().setVisibility(View.INVISIBLE); + queueLayout.requestFocus(); + animateView(queueLayout, SLIDE_AND_ALPHA, true, + DEFAULT_CONTROLS_DURATION); + + itemsList.scrollToPosition(playQueue.getIndex()); + } + + public void onQueueClosed() { + if (!queueVisible) { + return; + } + + animateView(queueLayout, SLIDE_AND_ALPHA, false, + DEFAULT_CONTROLS_DURATION, 0, () -> { + // Even when queueLayout is GONE it receives touch events + // and ruins normal behavior of the app. This line fixes it + queueLayout.setTranslationY(-queueLayout.getHeight() * 5); + }); + queueVisible = false; + playPauseButton.requestFocus(); + } + + private void onMoreOptionsClicked() { + if (DEBUG) { + Log.d(TAG, "onMoreOptionsClicked() called"); + } + + final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE; + + animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, + isMoreControlsVisible ? 0 : 180); + animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, + DEFAULT_CONTROLS_DURATION, 0, + () -> { + // Fix for a ripple effect on background drawable. + // When view returns from GONE state it takes more milliseconds than returning + // from INVISIBLE state. And the delay makes ripple background end to fast + if (isMoreControlsVisible) { + secondaryControls.setVisibility(View.INVISIBLE); + } + }); + showControls(DEFAULT_CONTROLS_DURATION); + } + + private void onShareClicked() { + // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) + ShareUtils.shareUrl(service, + getVideoTitle(), + getVideoUrl() + "&t=" + getPlaybackSeekBar().getProgress() / 1000); + } + + private void onPlayWithKodiClicked() { + if (getCurrentMetadata() == null) { + return; + } + onPause(); + try { + NavigationHelper.playWithKore(getParentActivity(), Uri.parse(getVideoUrl())); + } catch (Exception e) { + if (DEBUG) { + Log.i(TAG, "Failed to start kore", e); + } + KoreUtil.showInstallKoreDialog(getParentActivity()); + } + } + + private void onOpenInBrowserClicked() { + if (getCurrentMetadata() == null) { + return; + } + + ShareUtils.openUrlInBrowser(getParentActivity(), + getCurrentMetadata().getMetadata().getOriginalUrl()); + } + + private void showHideKodiButton() { + final boolean kodiEnabled = defaultPreferences.getBoolean( + service.getString(R.string.show_play_with_kodi_key), false); + // show kodi button if it supports the current service and it is enabled in settings + final boolean showKodiButton = playQueue != null && playQueue.getItem() != null + && KoreUtil.isServiceSupportedByKore(playQueue.getItem().getServiceId()) + && PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.show_play_with_kodi_key), false); + playWithKodi.setVisibility(videoPlayerSelected() && kodiEnabled && showKodiButton + ? View.VISIBLE : View.GONE); + } + + private void setupScreenRotationButton() { + final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service); + final boolean tabletInLandscape = DeviceUtils.isTablet(service) && service.isLandscape(); + final boolean showButton = videoPlayerSelected() + && (orientationLocked || isVerticalVideo || tabletInLandscape); + screenRotationButton.setVisibility(showButton ? View.VISIBLE : View.GONE); + screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(service, isFullscreen() + ? R.drawable.ic_fullscreen_exit_white_24dp + : R.drawable.ic_fullscreen_white_24dp)); + } + + private void prepareOrientation() { + final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service); + if (orientationLocked + && isFullscreen() + && service.isLandscape() == isVerticalVideo + && fragmentListener != null) { + fragmentListener.onScreenRotationButtonClicked(); + } + } + + @Override + public void onPlaybackSpeedClicked() { + if (videoPlayerSelected()) { + PlaybackParameterDialog + .newInstance( + getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence(), this) + .show(getParentActivity().getSupportFragmentManager(), null); + } else { + super.onPlaybackSpeedClicked(); + } + } + + @Override + public void onStopTrackingTouch(final SeekBar seekBar) { + super.onStopTrackingTouch(seekBar); + if (wasPlaying()) { + showControlsThenHide(); + } + } + + @Override + public void onDismiss(final PopupMenu menu) { + super.onDismiss(menu); + if (isPlaying()) { + hideControls(DEFAULT_CONTROLS_DURATION, 0); + } + } + + @Override + @SuppressWarnings("checkstyle:ParameterNumber") + 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 smaller value to be consistent between screen orientations + // (and to make usage easier) + int width = r - l; + int height = b - t; + int min = Math.min(width, height); + maxGestureLength = (int) (min * MAX_GESTURE_LENGTH); + + if (DEBUG) { + Log.d(TAG, "maxGestureLength = " + maxGestureLength); + } + + volumeProgressBar.setMax(maxGestureLength); + brightnessProgressBar.setMax(maxGestureLength); + + setInitialGestureValues(); + queueLayout.getLayoutParams().height = height - queueLayout.getTop(); + + if (popupPlayerSelected()) { + float widthDp = Math.abs(r - l) / service.getResources() + .getDisplayMetrics().density; + final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP + ? View.VISIBLE + : View.GONE; + secondaryControls.setVisibility(visibility); + } + } + } + + @Override + protected int nextResizeMode(final int currentResizeMode) { + final int newResizeMode; + switch (currentResizeMode) { + case AspectRatioFrameLayout.RESIZE_MODE_FIT: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; + break; + case AspectRatioFrameLayout.RESIZE_MODE_FILL: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + break; + default: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + break; + } + + storeResizeMode(newResizeMode); + return newResizeMode; + } + + private void storeResizeMode(final @AspectRatioFrameLayout.ResizeMode int resizeMode) { + defaultPreferences.edit() + .putInt(service.getString(R.string.last_resize_mode), resizeMode) + .apply(); + } + + private void restoreResizeMode() { + setResizeMode(defaultPreferences.getInt( + service.getString(R.string.last_resize_mode), + AspectRatioFrameLayout.RESIZE_MODE_FIT)); + } + + @Override + protected VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(final List sortedVideos) { + return videoPlayerSelected() + ? ListHelper.getDefaultResolutionIndex(context, sortedVideos) + : ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); + } + + @Override + public int getOverrideResolutionIndex(final List sortedVideos, + final String playbackQuality) { + return videoPlayerSelected() + ? getResolutionIndex(context, sortedVideos, playbackQuality) + : getPopupResolutionIndex(context, sortedVideos, playbackQuality); + } + }; + } + + /*////////////////////////////////////////////////////////////////////////// + // States + //////////////////////////////////////////////////////////////////////////*/ + + private void animatePlayButtons(final boolean show, final int duration) { + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + if (playQueue.getIndex() > 0 || !show) { + animateView(playPreviousButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + } + if (playQueue.getIndex() + 1 < playQueue.getStreams().size() || !show) { + animateView(playNextButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + } + + } + + @Override + public void changeState(final int state) { + super.changeState(state); + updatePlayback(); + } + + @Override + public void onBlocked() { + super.onBlocked(); + playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); + animatePlayButtons(false, 100); + getRootView().setKeepScreenOn(false); + + service.resetNotification(); + service.updateNotification(R.drawable.exo_controls_play); + } + + @Override + public void onBuffering() { + super.onBuffering(); + getRootView().setKeepScreenOn(true); + + service.resetNotification(); + service.updateNotification(R.drawable.exo_controls_play); + } + + @Override + public void onPlaying() { + super.onPlaying(); + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { + playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); + animatePlayButtons(true, 200); + if (!queueVisible) { + playPauseButton.requestFocus(); + } + }); + + updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); + checkLandscape(); + getRootView().setKeepScreenOn(true); + + service.resetNotification(); + service.updateNotification(R.drawable.exo_controls_pause); + + service.startForeground(NOTIFICATION_ID, service.getNotBuilder().build()); + } + + @Override + public void onPaused() { + super.onPaused(); + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { + playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); + animatePlayButtons(true, 200); + if (!queueVisible) { + playPauseButton.requestFocus(); + } + }); + + updateWindowFlags(IDLE_WINDOW_FLAGS); + + service.resetNotification(); + service.updateNotification(R.drawable.exo_controls_play); + + // Remove running notification when user don't want music (or video in popup) + // to be played in background + if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected()) { + service.stopForeground(true); + } + + getRootView().setKeepScreenOn(false); + } + + @Override + public void onPausedSeek() { + super.onPausedSeek(); + animatePlayButtons(false, 100); + getRootView().setKeepScreenOn(true); + + service.resetNotification(); + service.updateNotification(R.drawable.exo_controls_play); + } + + + @Override + public void onCompleted() { + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { + playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); + animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); + }); + getRootView().setKeepScreenOn(false); + + updateWindowFlags(IDLE_WINDOW_FLAGS); + + service.resetNotification(); + service.updateNotification(R.drawable.ic_replay_white_24dp); + + super.onCompleted(); + } + + @Override + public void destroy() { + super.destroy(); + + service.getContentResolver().unregisterContentObserver(settingsContentObserver); + } + + /*////////////////////////////////////////////////////////////////////////// + // Broadcast Receiver + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void setupBroadcastReceiver(final IntentFilter intentFilter) { + super.setupBroadcastReceiver(intentFilter); + if (DEBUG) { + Log.d(TAG, "setupBroadcastReceiver() called with: " + + "intentFilter = [" + intentFilter + "]"); + } + + intentFilter.addAction(ACTION_CLOSE); + intentFilter.addAction(ACTION_PLAY_PAUSE); + intentFilter.addAction(ACTION_OPEN_CONTROLS); + intentFilter.addAction(ACTION_REPEAT); + intentFilter.addAction(ACTION_PLAY_PREVIOUS); + intentFilter.addAction(ACTION_PLAY_NEXT); + intentFilter.addAction(ACTION_FAST_REWIND); + intentFilter.addAction(ACTION_FAST_FORWARD); + + intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED); + intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED); + + intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + intentFilter.addAction(Intent.ACTION_SCREEN_ON); + intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + + intentFilter.addAction(Intent.ACTION_HEADSET_PLUG); + } + + @Override + public void onBroadcastReceived(final Intent intent) { + super.onBroadcastReceived(intent); + if (intent == null || intent.getAction() == null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + } + + switch (intent.getAction()) { + case ACTION_CLOSE: + service.onDestroy(); + break; + case ACTION_PLAY_NEXT: + onPlayNext(); + break; + case ACTION_PLAY_PREVIOUS: + onPlayPrevious(); + break; + case ACTION_FAST_FORWARD: + onFastForward(); + break; + case ACTION_FAST_REWIND: + onFastRewind(); + break; + case ACTION_PLAY_PAUSE: + onPlayPause(); + if (!fragmentIsVisible) { + // Ensure that we have audio-only stream playing when a user + // started to play from notification's play button from outside of the app + onFragmentStopped(); + } + break; + case ACTION_REPEAT: + onRepeatClicked(); + break; + case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED: + fragmentIsVisible = true; + useVideoSource(true); + break; + case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED: + fragmentIsVisible = false; + onFragmentStopped(); + break; + case Intent.ACTION_CONFIGURATION_CHANGED: + assureCorrectAppLanguage(service); + if (DEBUG) { + Log.d(TAG, "onConfigurationChanged() called"); + } + if (popupPlayerSelected()) { + updateScreenSize(); + updatePopupSize(getPopupLayoutParams().width, -1); + checkPopupPositionBounds(); + } + + // The only situation I need to re-calculate elements sizes is + // when a user rotates a device from landscape to landscape + // because in that case the controls should be aligned to another side of a screen. + // The problem is when user leaves the app and returns back + // (while the app in landscape) Android reports via DisplayMetrics that orientation + // is portrait and it gives wrong sizes calculations. + // Let's skip re-calculation in every case but landscape + final boolean reportedOrientationIsLandscape = service.isLandscape(); + final boolean actualOrientationIsLandscape = context.getResources() + .getConfiguration().orientation == ORIENTATION_LANDSCAPE; + if (reportedOrientationIsLandscape && actualOrientationIsLandscape) { + setControlsSize(); + } + // Close it because when changing orientation from portrait + // (in fullscreen mode) the size of queue layout can be larger than the screen size + onQueueClosed(); + break; + case Intent.ACTION_SCREEN_ON: + shouldUpdateOnProgress = true; + // Interrupt playback only when screen turns on + // and user is watching video in popup player. + // Same actions for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED + if (backgroundPlaybackEnabled() + && popupPlayerSelected() + && (isPlaying() || isLoading())) { + useVideoSource(true); + } + break; + case Intent.ACTION_SCREEN_OFF: + shouldUpdateOnProgress = false; + // Interrupt playback only when screen turns off with popup player working + if (backgroundPlaybackEnabled() + && popupPlayerSelected() + && (isPlaying() || isLoading())) { + useVideoSource(false); + } + break; + } + service.resetNotification(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Thumbnail Loading + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onLoadingComplete(final String imageUri, + final View view, + final Bitmap loadedImage) { + super.onLoadingComplete(imageUri, view, loadedImage); + // rebuild notification here since remote view does not release bitmaps, + // causing memory leaks + service.resetNotification(); + service.updateNotification(-1); + } + + @Override + public void onLoadingFailed(final String imageUri, + final View view, + final FailReason failReason) { + super.onLoadingFailed(imageUri, view, failReason); + service.resetNotification(); + service.updateNotification(-1); + } + + @Override + public void onLoadingCancelled(final String imageUri, final View view) { + super.onLoadingCancelled(imageUri, view); + service.resetNotification(); + service.updateNotification(-1); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void setInitialGestureValues() { + if (getAudioReactor() != null) { + final float currentVolumeNormalized = (float) getAudioReactor() + .getVolume() / getAudioReactor().getMaxVolume(); + volumeProgressBar.setProgress( + (int) (volumeProgressBar.getMax() * currentVolumeNormalized)); + } + } + + private void choosePlayerTypeFromIntent(final Intent intent) { + // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra + if (intent.getIntExtra(PLAYER_TYPE, PLAYER_TYPE_VIDEO) == PLAYER_TYPE_AUDIO) { + playerType = MainPlayer.PlayerType.AUDIO; + } else if (intent.getIntExtra(PLAYER_TYPE, PLAYER_TYPE_VIDEO) == PLAYER_TYPE_POPUP) { + playerType = MainPlayer.PlayerType.POPUP; + } else { + playerType = MainPlayer.PlayerType.VIDEO; + } + } + + public boolean backgroundPlaybackEnabled() { + return PlayerHelper.getMinimizeOnExitAction(service) == MINIMIZE_ON_EXIT_MODE_BACKGROUND; + } + + public boolean minimizeOnPopupEnabled() { + return PlayerHelper.getMinimizeOnExitAction(service) + == PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; + } + + public boolean audioPlayerSelected() { + return playerType == MainPlayer.PlayerType.AUDIO; + } + + public boolean videoPlayerSelected() { + return playerType == MainPlayer.PlayerType.VIDEO; + } + + public boolean popupPlayerSelected() { + return playerType == MainPlayer.PlayerType.POPUP; + } + + public boolean isPlayerStopped() { + return getPlayer() == null || getPlayer().getPlaybackState() == SimpleExoPlayer.STATE_IDLE; + } + + private int distanceFromCloseButton(final MotionEvent popupMotionEvent) { + final int closeOverlayButtonX = closeOverlayButton.getLeft() + + closeOverlayButton.getWidth() / 2; + final int closeOverlayButtonY = closeOverlayButton.getTop() + + closeOverlayButton.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 = closeOverlayButton.getWidth() / 2; + // 20% wider than the button itself + return buttonRadius * 1.2f; + } + + public boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) { + return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); + } + + public boolean isFullscreen() { + return isFullscreen; + } + + public void showControlsThenHide() { + if (DEBUG) { + Log.d(TAG, "showControlsThenHide() called"); + } + showOrHideButtons(); + showSystemUIPartially(); + super.showControlsThenHide(); + } + + @Override + public void showControls(final long duration) { + if (DEBUG) { + Log.d(TAG, "showControls() called with: duration = [" + duration + "]"); + } + showOrHideButtons(); + showSystemUIPartially(); + super.showControls(duration); + } + + @Override + public void hideControls(final long duration, final long delay) { + if (DEBUG) { + Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); + } + + showOrHideButtons(); + + getControlsVisibilityHandler().removeCallbacksAndMessages(null); + getControlsVisibilityHandler().postDelayed(() -> + animateView(getControlsRoot(), false, duration, 0, + this::hideSystemUIIfNeeded), delay + ); + } + + @Override + public void safeHideControls(final long duration, final long delay) { + if (getControlsRoot().isInTouchMode()) { + hideControls(duration, delay); + } + } + + private void showOrHideButtons() { + if (playQueue == null) { + 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); + } + + private void showSystemUIPartially() { + if (isFullscreen() && getParentActivity() != null) { + final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + getParentActivity().getWindow().getDecorView().setSystemUiVisibility(visibility); + } + } + + @Override + public void hideSystemUIIfNeeded() { + if (fragmentListener != null) { + fragmentListener.hideSystemUiIfNeeded(); + } + } + + /** + * Measures width and height of controls visible on screen. + * It ensures that controls will be side-by-side with NavigationBar and notches + * but not under them. Tablets have only bottom NavigationBar + */ + public void setControlsSize() { + final Point size = new Point(); + final Display display = getRootView().getDisplay(); + if (display == null || !videoPlayerSelected()) { + return; + } + // This method will give a correct size of a usable area of a window. + // It doesn't include NavigationBar, notches, etc. + display.getSize(size); + + final int width = isFullscreen + ? (service.isLandscape() + ? size.x : size.y) : ViewGroup.LayoutParams.MATCH_PARENT; + final int gravity = isFullscreen + ? (display.getRotation() == Surface.ROTATION_90 + ? Gravity.START : Gravity.END) + : Gravity.TOP; + + getTopControlsRoot().getLayoutParams().width = width; + final RelativeLayout.LayoutParams topParams = + ((RelativeLayout.LayoutParams) getTopControlsRoot().getLayoutParams()); + topParams.removeRule(RelativeLayout.ALIGN_PARENT_START); + topParams.removeRule(RelativeLayout.ALIGN_PARENT_END); + topParams.addRule(gravity == Gravity.END + ? RelativeLayout.ALIGN_PARENT_END + : RelativeLayout.ALIGN_PARENT_START); + getTopControlsRoot().requestLayout(); + + getBottomControlsRoot().getLayoutParams().width = width; + final RelativeLayout.LayoutParams bottomParams = + ((RelativeLayout.LayoutParams) getBottomControlsRoot().getLayoutParams()); + bottomParams.removeRule(RelativeLayout.ALIGN_PARENT_START); + bottomParams.removeRule(RelativeLayout.ALIGN_PARENT_END); + bottomParams.addRule(gravity == Gravity.END + ? RelativeLayout.ALIGN_PARENT_END + : RelativeLayout.ALIGN_PARENT_START); + getBottomControlsRoot().requestLayout(); + + final ViewGroup controlsRoot = getRootView().findViewById(R.id.playbackWindowRoot); + // In tablet navigationBar located at the bottom of the screen. + // And the situations when we need to set custom height is + // in fullscreen mode in tablet in non-multiWindow mode or with vertical video. + // Other than that MATCH_PARENT is good + final boolean navBarAtTheBottom = DeviceUtils.isTablet(service) || !service.isLandscape(); + controlsRoot.getLayoutParams().height = isFullscreen && !isInMultiWindow() + && navBarAtTheBottom ? size.y : ViewGroup.LayoutParams.MATCH_PARENT; + controlsRoot.requestLayout(); + + final int topPadding = isFullscreen && !isInMultiWindow() ? getStatusBarHeight() : 0; + getRootView().findViewById(R.id.playbackWindowRoot).setPadding(0, topPadding, 0, 0); + getRootView().findViewById(R.id.playbackWindowRoot).requestLayout(); + } + + /** + * @return statusBar height that was found inside system resources + * or default value if no value was provided inside resources + */ + private int getStatusBarHeight() { + int statusBarHeight = 0; + final int resourceId = service.getResources().getIdentifier( + "status_bar_height_landscape", "dimen", "android"); + if (resourceId > 0) { + statusBarHeight = service.getResources().getDimensionPixelSize(resourceId); + } + if (statusBarHeight == 0) { + // Some devices provide wrong value for status bar height in landscape mode, + // this is workaround + final DisplayMetrics metrics = getRootView().getResources().getDisplayMetrics(); + statusBarHeight = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 24, metrics); + } + return statusBarHeight; + } + + protected void setMuteButton(final ImageButton button, final boolean isMuted) { + button.setImageDrawable(AppCompatResources.getDrawable(service, isMuted + ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); + } + + /** + * @return true if main player is attached to activity and activity inside multiWindow mode + */ + private boolean isInMultiWindow() { + final AppCompatActivity parent = getParentActivity(); + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && parent != null + && parent.isInMultiWindowMode(); + } + + private void updatePlaybackButtons() { + if (repeatButton == null + || shuffleButton == null + || simpleExoPlayer == null + || playQueue == null) { + return; + } + + setRepeatModeButton(repeatButton, getRepeatMode()); + setShuffleButton(shuffleButton, playQueue.isShuffled()); + } + + public void checkLandscape() { + final AppCompatActivity parent = getParentActivity(); + final boolean videoInLandscapeButNotInFullscreen = service.isLandscape() + && !isFullscreen() + && videoPlayerSelected() + && !audioOnly; + + final boolean playingState = getCurrentState() != STATE_COMPLETED + && getCurrentState() != STATE_PAUSED; + if (parent != null + && videoInLandscapeButNotInFullscreen + && playingState + && !DeviceUtils.isTablet(service)) { + toggleFullscreen(); + } + + setControlsSize(); + } + + private void buildQueue() { + itemsList.setAdapter(playQueueAdapter); + itemsList.setClickable(true); + itemsList.setLongClickable(true); + + itemsList.clearOnScrollListeners(); + itemsList.addOnScrollListener(getQueueScrollListener()); + + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(itemsList); + + playQueueAdapter.setSelectedListener(getOnSelectedListener()); + + itemsListCloseButton.setOnClickListener(view -> onQueueClosed()); + } + + public void useVideoSource(final boolean video) { + if (playQueue == null || audioOnly == !video || audioPlayerSelected()) { + return; + } + + audioOnly = !video; + // When a user returns from background controls could be hidden + // but systemUI will be shown 100%. Hide it + if (!audioOnly && !isControlsVisible()) { + hideSystemUIIfNeeded(); + } + setRecovery(); + reload(); + } + + private OnScrollBelowItemsListener getQueueScrollListener() { + return new OnScrollBelowItemsListener() { + @Override + public void onScrolledDown(final RecyclerView recyclerView) { + if (playQueue != null && !playQueue.isComplete()) { + playQueue.fetch(); + } else if (itemsList != null) { + itemsList.clearOnScrollListeners(); + } + } + }; + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new PlayQueueItemTouchCallback() { + @Override + public void onMove(final int sourceIndex, final int targetIndex) { + if (playQueue != null) { + playQueue.move(sourceIndex, targetIndex); + } + } + + @Override + public void onSwiped(final int index) { + if (index != -1) { + playQueue.remove(index); + } + } + }; + } + + private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { + return new PlayQueueItemBuilder.OnSelectedListener() { + @Override + public void selected(final PlayQueueItem item, final View view) { + onSelected(item); + } + + @Override + public void held(final PlayQueueItem item, final View view) { + final int index = playQueue.indexOf(item); + if (index != -1) { + playQueue.remove(index); + } + } + + @Override + public void onStartDrag(final PlayQueueItemHolder viewHolder) { + if (itemTouchHelper != null) { + itemTouchHelper.startDrag(viewHolder); + } + } + }; + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + @SuppressLint("RtlHardcoded") + private void initPopup() { + if (DEBUG) { + Log.d(TAG, "initPopup() called"); + } + + // Popup is already added to windowManager + if (popupHasParent()) { + return; + } + + updateScreenSize(); + + final boolean popupRememberSizeAndPos = PlayerHelper.isRememberingPopupDimensions(service); + final float defaultSize = service.getResources().getDimension(R.dimen.popup_default_width); + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(service); + popupWidth = popupRememberSizeAndPos + ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) + : defaultSize; + popupHeight = getMinimumVideoHeight(popupWidth); + + 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; + getSurfaceView().setHeights((int) popupHeight, (int) popupHeight); + + final int centerX = (int) (screenWidth / 2f - popupWidth / 2f); + final int centerY = (int) (screenHeight / 2f - popupHeight / 2f); + popupLayoutParams.x = popupRememberSizeAndPos + ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; + popupLayoutParams.y = popupRememberSizeAndPos + ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; + + checkPopupPositionBounds(); + + getLoadingPanel().setMinimumWidth(popupLayoutParams.width); + getLoadingPanel().setMinimumHeight(popupLayoutParams.height); + + service.removeViewFromParent(); + windowManager.addView(getRootView(), popupLayoutParams); + + // Popup doesn't have aspectRatio selector, using FIT automatically + setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); + } + + @SuppressLint("RtlHardcoded") + private void initPopupCloseOverlay() { + if (DEBUG) { + Log.d(TAG, "initPopupCloseOverlay() called"); + } + + // closeOverlayView is already added to windowManager + if (closeOverlayView != null) { + return; + } + + closeOverlayView = View.inflate(service, R.layout.player_popup_close_overlay, null); + closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton); + + 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); + closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; + closeOverlayLayoutParams.softInputMode = + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + + closeOverlayButton.setVisibility(View.GONE); + windowManager.addView(closeOverlayView, closeOverlayLayoutParams); + } + + private void initVideoPlayer() { + restoreResizeMode(); + getRootView().setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + } + + /*////////////////////////////////////////////////////////////////////////// + // Popup utils + //////////////////////////////////////////////////////////////////////////*/ + + /** + * @return if the popup was out of bounds and have been moved back to it + * @see #checkPopupPositionBounds(float, float) + */ + @SuppressWarnings("UnusedReturnValue") + public boolean checkPopupPositionBounds() { + return checkPopupPositionBounds(screenWidth, screenHeight); + } + + /** + * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary + * that goes from (0, 0) to (boundaryWidth, boundaryHeight). + *

+ * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed + * and {@code true} is returned to represent this change. + *

+ * + * @param boundaryWidth width of the boundary + * @param boundaryHeight height of the boundary + * @return if the popup was out of bounds and have been moved back to it + */ + public boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) { + if (DEBUG) { + Log.d(TAG, "checkPopupPositionBounds() called with: " + + "boundaryWidth = [" + boundaryWidth + "], " + + "boundaryHeight = [" + boundaryHeight + "]"); + } + + if (popupLayoutParams.x < 0) { + popupLayoutParams.x = 0; + return true; + } else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) { + popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width); + return true; + } + + if (popupLayoutParams.y < 0) { + popupLayoutParams.y = 0; + return true; + } else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) { + popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height); + return true; + } + + return false; + } + + public void savePositionAndSize() { + final SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(service); + sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply(); + sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply(); + sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply(); + } + + private float getMinimumVideoHeight(final float width) { + final float height = width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have + /*if (DEBUG) { + Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + + width + "], returned: " + height); + }*/ + return 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); + } + + popupWidth = service.getResources().getDimension(R.dimen.popup_default_width); + popupHeight = getMinimumVideoHeight(popupWidth); + + minimumWidth = service.getResources().getDimension(R.dimen.popup_minimum_width); + minimumHeight = getMinimumVideoHeight(minimumWidth); + + maximumWidth = screenWidth; + maximumHeight = screenHeight; + } + + public void updatePopupSize(final int width, final int height) { + if (DEBUG) { + Log.d(TAG, "updatePopupSize() called with: width = [" + + width + "], height = [" + height + "]"); + } + + if (popupLayoutParams == null + || windowManager == null + || getParentActivity() != null + || getRootView().getParent() == null) { + return; + } + + final int actualWidth = (int) (width > maximumWidth + ? maximumWidth : width < minimumWidth ? minimumWidth : width); + final int actualHeight; + if (height == -1) { + actualHeight = (int) getMinimumVideoHeight(width); + } else { + actualHeight = (int) (height > maximumHeight + ? maximumHeight : height < minimumHeight + ? minimumHeight : height); + } + + popupLayoutParams.width = actualWidth; + popupLayoutParams.height = actualHeight; + popupWidth = actualWidth; + popupHeight = actualHeight; + getSurfaceView().setHeights((int) popupHeight, (int) popupHeight); + + if (DEBUG) { + Log.d(TAG, "updatePopupSize() updated values:" + + " width = [" + actualWidth + "], height = [" + actualHeight + "]"); + } + windowManager.updateViewLayout(getRootView(), popupLayoutParams); + } + + private void updateWindowFlags(final int flags) { + if (popupLayoutParams == null + || windowManager == null + || getParentActivity() != null + || getRootView().getParent() == null) { + return; + } + + popupLayoutParams.flags = flags; + windowManager.updateViewLayout(getRootView(), popupLayoutParams); + } + + private int popupLayoutParamType() { + return Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O + ? WindowManager.LayoutParams.TYPE_PHONE + : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } + + /*////////////////////////////////////////////////////////////////////////// + // Misc + //////////////////////////////////////////////////////////////////////////*/ + + public void closePopup() { + if (DEBUG) { + Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); + } + if (isPopupClosing) { + return; + } + isPopupClosing = true; + + savePlaybackState(); + windowManager.removeView(getRootView()); + + animateOverlayAndFinishService(); + } + + public void removePopupFromView() { + final boolean isCloseOverlayHasParent = closeOverlayView != null + && closeOverlayView.getParent() != null; + if (popupHasParent()) { + windowManager.removeView(getRootView()); + } + if (isCloseOverlayHasParent) { + windowManager.removeView(closeOverlayView); + } + } + + private void animateOverlayAndFinishService() { + final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() + - closeOverlayButton.getY()); + + closeOverlayButton.animate().setListener(null).cancel(); + closeOverlayButton.animate() + .setInterpolator(new AnticipateInterpolator()) + .translationY(targetTranslationY) + .setDuration(400) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(final Animator animation) { + end(); + } + + @Override + public void onAnimationEnd(final Animator animation) { + end(); + } + + private void end() { + windowManager.removeView(closeOverlayView); + closeOverlayView = null; + + service.onDestroy(); + } + }).start(); + } + + private boolean popupHasParent() { + final View root = getRootView(); + return root != null + && root.getLayoutParams() instanceof WindowManager.LayoutParams + && root.getParent() != null; + } + + /////////////////////////////////////////////////////////////////////////// + // Manipulations with listener + /////////////////////////////////////////////////////////////////////////// + + public void setFragmentListener(final PlayerServiceEventListener listener) { + fragmentListener = listener; + fragmentIsVisible = true; + updateMetadata(); + updatePlayback(); + triggerProgressUpdate(); + } + + public void removeFragmentListener(final PlayerServiceEventListener listener) { + if (fragmentListener == listener) { + fragmentListener = null; + } + } + + void setActivityListener(final PlayerEventListener listener) { + activityListener = listener; + updateMetadata(); + updatePlayback(); + triggerProgressUpdate(); + } + + void removeActivityListener(final PlayerEventListener listener) { + if (activityListener == listener) { + activityListener = null; + } + } + + private void updateQueue() { + if (fragmentListener != null && playQueue != null) { + fragmentListener.onQueueUpdate(playQueue); + } + if (activityListener != null && playQueue != null) { + activityListener.onQueueUpdate(playQueue); + } + } + + private void updateMetadata() { + if (fragmentListener != null && getCurrentMetadata() != null) { + fragmentListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); + } + if (activityListener != null && getCurrentMetadata() != null) { + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); + } + } + + private void updatePlayback() { + if (fragmentListener != null && simpleExoPlayer != null && playQueue != null) { + fragmentListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); + } + if (activityListener != null && simpleExoPlayer != null && playQueue != null) { + activityListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), getPlaybackParameters()); + } + } + + private void updateProgress(final int currentProgress, final int duration, + final int bufferPercent) { + if (fragmentListener != null) { + fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + if (activityListener != null) { + activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + } + + void stopActivityBinding() { + if (fragmentListener != null) { + fragmentListener.onServiceStopped(); + fragmentListener = null; + } + if (activityListener != null) { + activityListener.onServiceStopped(); + activityListener = null; + } + } + + /** + * 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 (videoPlayerSelected() && (isPlaying() || isLoading())) { + if (backgroundPlaybackEnabled()) { + useVideoSource(false); + } else if (minimizeOnPopupEnabled()) { + setRecovery(); + NavigationHelper.playOnPopupPlayer(getParentActivity(), playQueue, true); + } else { + onPause(); + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////// + + public RelativeLayout getVolumeRelativeLayout() { + return volumeRelativeLayout; + } + + public ProgressBar getVolumeProgressBar() { + return volumeProgressBar; + } + + public ImageView getVolumeImageView() { + return volumeImageView; + } + + public RelativeLayout getBrightnessRelativeLayout() { + return brightnessRelativeLayout; + } + + public ProgressBar getBrightnessProgressBar() { + return brightnessProgressBar; + } + + public ImageView getBrightnessImageView() { + return brightnessImageView; + } + + public ImageButton getPlayPauseButton() { + return playPauseButton; + } + + public int getMaxGestureLength() { + return maxGestureLength; + } + + public TextView getResizingIndicator() { + return resizingIndicator; + } + + public GestureDetector getGestureDetector() { + return gestureDetector; + } + + public WindowManager.LayoutParams getPopupLayoutParams() { + return popupLayoutParams; + } + + public float getScreenWidth() { + return screenWidth; + } + + public float getScreenHeight() { + return screenHeight; + } + + public float getPopupWidth() { + return popupWidth; + } + + public float getPopupHeight() { + return popupHeight; + } + + public void setPopupWidth(final float width) { + popupWidth = width; + } + + public void setPopupHeight(final float height) { + popupHeight = height; + } + + public View getCloseOverlayButton() { + return closeOverlayButton; + } + + public View getClosingOverlayView() { + return closingOverlayView; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java new file mode 100644 index 000000000..1d0b3ae26 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java @@ -0,0 +1,64 @@ +package org.schabi.newpipe.player.event; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import org.schabi.newpipe.R; + +import java.util.Arrays; +import java.util.List; + +public class CustomBottomSheetBehavior extends BottomSheetBehavior { + + public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs) { + 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); + + @Override + public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent, + @NonNull final FrameLayout child, + final MotionEvent event) { + // Drop following when action ends + if (event.getAction() == MotionEvent.ACTION_CANCEL + || event.getAction() == MotionEvent.ACTION_UP) { + skippingInterception = false; + } + + // Found that user still swiping, continue following + if (skippingInterception) { + return false; + } + + // Don't need to do anything if bottomSheet isn't expanded + if (getState() == BottomSheetBehavior.STATE_EXPANDED) { + // 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); + if (visible + && globalRect.contains((int) event.getRawX(), (int) event.getRawY())) { + skippingInterception = true; + return false; + } + } + } + } + + return super.onInterceptTouchEvent(parent, child, event); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/OnKeyDownListener.java b/app/src/main/java/org/schabi/newpipe/player/event/OnKeyDownListener.java new file mode 100644 index 000000000..fc1f9d80d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/OnKeyDownListener.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.player.event; + +public interface OnKeyDownListener { + boolean onKeyDown(int keyCode); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java index 0809fa0f5..b5520e8be 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java @@ -4,14 +4,13 @@ package org.schabi.newpipe.player.event; import com.google.android.exoplayer2.PlaybackParameters; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.playqueue.PlayQueue; public interface PlayerEventListener { + void onQueueUpdate(PlayQueue queue); void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters); - void onProgressUpdate(int currentProgress, int duration, int bufferPercent); - - void onMetadataUpdate(StreamInfo info); - + void onMetadataUpdate(StreamInfo info, PlayQueue queue); void onServiceStopped(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java new file mode 100644 index 000000000..e37a3a930 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -0,0 +1,622 @@ +package org.schabi.newpipe.player.event; + +import android.app.Activity; +import android.content.Context; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.Window; +import android.view.WindowManager; +import androidx.appcompat.content.res.AppCompatResources; +import org.schabi.newpipe.R; +import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.VideoPlayerImpl; +import org.schabi.newpipe.player.helper.PlayerHelper; + +import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; +import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; +import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; +import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class PlayerGestureListener + extends GestureDetector.SimpleOnGestureListener + implements View.OnTouchListener { + private static final String TAG = ".PlayerGestureListener"; + private static final boolean DEBUG = BasePlayer.DEBUG; + + private final VideoPlayerImpl playerImpl; + private final MainPlayer service; + + private int initialPopupX; + private int initialPopupY; + + private boolean isMovingInMain; + private boolean isMovingInPopup; + + private boolean isResizing; + + private final int tossFlingVelocity; + + private final boolean isVolumeGestureEnabled; + private final boolean isBrightnessGestureEnabled; + private final int maxVolume; + private static final int MOVEMENT_THRESHOLD = 40; + + // [popup] initial coordinates and distance between fingers + private double initPointerDistance = -1; + private float initFirstPointerX = -1; + private float initFirstPointerY = -1; + private float initSecPointerX = -1; + private float initSecPointerY = -1; + + + public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) { + this.playerImpl = playerImpl; + this.service = service; + this.tossFlingVelocity = PlayerHelper.getTossFlingVelocity(service); + + isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service); + isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(service); + maxVolume = playerImpl.getAudioReactor().getMaxVolume(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Helpers + //////////////////////////////////////////////////////////////////////////*/ + + /* + * Main and popup players' gesture listeners is too different. + * So it will be better to have different implementations of them + * */ + @Override + public boolean onDoubleTap(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); + } + + if (playerImpl.popupPlayerSelected()) { + return onDoubleTapInPopup(e); + } else { + return onDoubleTapInMain(e); + } + } + + @Override + public boolean onSingleTapConfirmed(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + } + + if (playerImpl.popupPlayerSelected()) { + return onSingleTapConfirmedInPopup(e); + } else { + return onSingleTapConfirmedInMain(e); + } + } + + @Override + public boolean onDown(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onDown() called with: e = [" + e + "]"); + } + + if (playerImpl.popupPlayerSelected()) { + return onDownInPopup(e); + } else { + return true; + } + } + + @Override + public void onLongPress(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); + } + + if (playerImpl.popupPlayerSelected()) { + onLongPressInPopup(e); + } + } + + @Override + public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent, + final float distanceX, final float distanceY) { + if (playerImpl.popupPlayerSelected()) { + return onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY); + } else { + return onScrollInMain(initialEvent, movingEvent, distanceX, distanceY); + } + } + + @Override + public boolean onFling(final MotionEvent e1, final MotionEvent e2, + final float velocityX, final float velocityY) { + if (DEBUG) { + Log.d(TAG, "onFling() called with velocity: dX=[" + + velocityX + "], dY=[" + velocityY + "]"); + } + + if (playerImpl.popupPlayerSelected()) { + return onFlingInPopup(e1, e2, velocityX, velocityY); + } else { + return true; + } + } + + @Override + public boolean onTouch(final View v, final MotionEvent event) { + /*if (DEBUG && false) { + Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]"); + }*/ + + if (playerImpl.popupPlayerSelected()) { + return onTouchInPopup(v, event); + } else { + return onTouchInMain(v, event); + } + } + + + /*////////////////////////////////////////////////////////////////////////// + // Main player listener + //////////////////////////////////////////////////////////////////////////*/ + + private boolean onDoubleTapInMain(final MotionEvent e) { + if (e.getX() > playerImpl.getRootView().getWidth() * 2.0 / 3.0) { + playerImpl.onFastForward(); + } else if (e.getX() < playerImpl.getRootView().getWidth() / 3.0) { + playerImpl.onFastRewind(); + } else { + playerImpl.getPlayPauseButton().performClick(); + } + + return true; + } + + + private boolean onSingleTapConfirmedInMain(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + } + + if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { + return true; + } + + if (playerImpl.isControlsVisible()) { + playerImpl.hideControls(150, 0); + } else { + if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { + playerImpl.showControls(0); + } else { + playerImpl.showControlsThenHide(); + } + } + return true; + } + + private boolean onScrollInMain(final MotionEvent initialEvent, final MotionEvent movingEvent, + final float distanceX, final float distanceY) { + if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) { + return false; + } + + final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(service); + final boolean isTouchingNavigationBar = initialEvent.getY() + > playerImpl.getRootView().getHeight() - getNavigationBarHeight(service); + if (isTouchingStatusBar || isTouchingNavigationBar) { + return false; + } + + /*if (DEBUG && false) Log.d(TAG, "onScrollInMain = " + + ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + + ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + + ", distanceXy = [" + distanceX + ", " + distanceY + "]");*/ + + final boolean insideThreshold = + Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; + if (!isMovingInMain && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) + || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { + return false; + } + + isMovingInMain = true; + + boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled; + boolean acceptVolumeArea = acceptAnyArea + || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2.0; + + if (isVolumeGestureEnabled && acceptVolumeArea) { + playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); + final float currentProgressPercent = (float) playerImpl + .getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); + final int currentVolume = (int) (maxVolume * currentProgressPercent); + playerImpl.getAudioReactor().setVolume(currentVolume); + + if (DEBUG) { + Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); + } + + playerImpl.getVolumeImageView().setImageDrawable( + AppCompatResources.getDrawable(service, currentProgressPercent <= 0 + ? R.drawable.ic_volume_off_white_24dp + : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp + : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_24dp + : R.drawable.ic_volume_up_white_24dp) + ); + + if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { + animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); + } + if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); + } + } else { + final Activity parent = playerImpl.getParentActivity(); + if (parent == null) { + return true; + } + + final Window window = parent.getWindow(); + + playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY); + final float currentProgressPercent = (float) playerImpl.getBrightnessProgressBar() + .getProgress() / playerImpl.getMaxGestureLength(); + final WindowManager.LayoutParams layoutParams = window.getAttributes(); + layoutParams.screenBrightness = currentProgressPercent; + window.setAttributes(layoutParams); + + if (DEBUG) { + Log.d(TAG, "onScroll().brightnessControl, " + + "currentBrightness = " + currentProgressPercent); + } + + playerImpl.getBrightnessImageView().setImageDrawable( + AppCompatResources.getDrawable(service, + currentProgressPercent < 0.25 + ? R.drawable.ic_brightness_low_white_24dp + : currentProgressPercent < 0.75 + ? R.drawable.ic_brightness_medium_white_24dp + : R.drawable.ic_brightness_high_white_24dp) + ); + + if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { + animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); + } + if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); + } + } + return true; + } + + private void onScrollEndInMain() { + if (DEBUG) { + Log.d(TAG, "onScrollEnd() called"); + } + + if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); + } + if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); + } + + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + } + + private boolean onTouchInMain(final View v, final MotionEvent event) { + playerImpl.getGestureDetector().onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP && isMovingInMain) { + isMovingInMain = false; + onScrollEndInMain(); + } + // This hack allows to stop receiving touch events on appbar + // while touching video player's view + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + v.getParent().requestDisallowInterceptTouchEvent(playerImpl.isFullscreen()); + return true; + case MotionEvent.ACTION_UP: + v.getParent().requestDisallowInterceptTouchEvent(false); + return false; + default: + return true; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Popup player listener + //////////////////////////////////////////////////////////////////////////*/ + + private boolean onDoubleTapInPopup(final MotionEvent e) { + if (playerImpl == null || !playerImpl.isPlaying()) { + return false; + } + + playerImpl.hideControls(0, 0); + + if (e.getX() > playerImpl.getPopupWidth() / 2) { + playerImpl.onFastForward(); + } else { + playerImpl.onFastRewind(); + } + + return true; + } + + private boolean onSingleTapConfirmedInPopup(final MotionEvent e) { + if (playerImpl == null || playerImpl.getPlayer() == null) { + return false; + } + if (playerImpl.isControlsVisible()) { + playerImpl.hideControls(100, 100); + } else { + playerImpl.getPlayPauseButton().requestFocus(); + playerImpl.showControlsThenHide(); + } + return true; + } + + private boolean onDownInPopup(final MotionEvent e) { + // Fix popup position when the user touch it, it may have the wrong one + // because the soft input is visible (the draggable area is currently resized). + playerImpl.updateScreenSize(); + playerImpl.checkPopupPositionBounds(); + + initialPopupX = playerImpl.getPopupLayoutParams().x; + initialPopupY = playerImpl.getPopupLayoutParams().y; + playerImpl.setPopupWidth(playerImpl.getPopupLayoutParams().width); + playerImpl.setPopupHeight(playerImpl.getPopupLayoutParams().height); + return super.onDown(e); + } + + private void onLongPressInPopup(final MotionEvent e) { + playerImpl.updateScreenSize(); + playerImpl.checkPopupPositionBounds(); + playerImpl.updatePopupSize((int) playerImpl.getScreenWidth(), -1); + } + + private boolean onScrollInPopup(final MotionEvent initialEvent, + final MotionEvent movingEvent, + final float distanceX, + final float distanceY) { + if (isResizing || playerImpl == null) { + return super.onScroll(initialEvent, movingEvent, distanceX, distanceY); + } + + if (!isMovingInPopup) { + animateView(playerImpl.getCloseOverlayButton(), true, 200); + } + + isMovingInPopup = true; + + final float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()); + float posX = (int) (initialPopupX + diffX); + final float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()); + float posY = (int) (initialPopupY + diffY); + + if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) { + posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth()); + } else if (posX < 0) { + posX = 0; + } + + if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) { + posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight()); + } else if (posY < 0) { + posY = 0; + } + + playerImpl.getPopupLayoutParams().x = (int) posX; + playerImpl.getPopupLayoutParams().y = (int) posY; + + final View closingOverlayView = playerImpl.getClosingOverlayView(); + if (playerImpl.isInsideClosingRadius(movingEvent)) { + if (closingOverlayView.getVisibility() == View.GONE) { + animateView(closingOverlayView, true, 250); + } + } else { + if (closingOverlayView.getVisibility() == View.VISIBLE) { + animateView(closingOverlayView, false, 0); + } + } + +// if (DEBUG) { +// Log.d(TAG, "onScrollInPopup = " +// + "e1.getRaw = [" + initialEvent.getRawX() + ", " +// + initialEvent.getRawY() + "], " +// + "e1.getX,Y = [" + initialEvent.getX() + ", " +// + initialEvent.getY() + "], " +// + "e2.getRaw = [" + movingEvent.getRawX() + ", " +// + movingEvent.getRawY() + "], " +// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], " +// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], " +// + "posX,Y = [" + posX + ", " + posY + "], " +// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]"); +// } + playerImpl.windowManager + .updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams()); + return true; + } + + private void onScrollEndInPopup(final MotionEvent event) { + if (playerImpl == null) { + return; + } + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + + if (playerImpl.isInsideClosingRadius(event)) { + playerImpl.closePopup(); + } else { + animateView(playerImpl.getClosingOverlayView(), false, 0); + + if (!playerImpl.isPopupClosing) { + animateView(playerImpl.getCloseOverlayButton(), false, 200); + } + } + } + + private boolean onFlingInPopup(final MotionEvent e1, + final MotionEvent e2, + final float velocityX, + final float velocityY) { + if (playerImpl == null) { + return false; + } + + final float absVelocityX = Math.abs(velocityX); + final float absVelocityY = Math.abs(velocityY); + if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { + if (absVelocityX > tossFlingVelocity) { + playerImpl.getPopupLayoutParams().x = (int) velocityX; + } + if (absVelocityY > tossFlingVelocity) { + playerImpl.getPopupLayoutParams().y = (int) velocityY; + } + playerImpl.checkPopupPositionBounds(); + playerImpl.windowManager + .updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams()); + return true; + } + return false; + } + + private boolean onTouchInPopup(final View v, final MotionEvent event) { + if (playerImpl == null) { + return false; + } + playerImpl.getGestureDetector().onTouchEvent(event); + + if (event.getPointerCount() == 2 && !isMovingInPopup && !isResizing) { + if (DEBUG) { + Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); + } + playerImpl.showAndAnimateControl(-1, true); + playerImpl.getLoadingPanel().setVisibility(View.GONE); + + playerImpl.hideControls(0, 0); + animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); + animateView(playerImpl.getResizingIndicator(), true, 200, 0); + //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 = Math.hypot(initFirstPointerX - initSecPointerX, + initFirstPointerY - initSecPointerY); + + isResizing = true; + } + + if (event.getAction() == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) { + if (DEBUG) { + Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], " + + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + } + return handleMultiDrag(event); + } + + if (event.getAction() == MotionEvent.ACTION_UP) { + if (DEBUG) { + Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], " + + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + } + if (isMovingInPopup) { + isMovingInPopup = false; + onScrollEndInPopup(event); + } + + if (isResizing) { + isResizing = false; + + initPointerDistance = -1; + initFirstPointerX = -1; + initFirstPointerY = -1; + initSecPointerX = -1; + initSecPointerY = -1; + + animateView(playerImpl.getResizingIndicator(), false, 100, 0); + playerImpl.changeState(playerImpl.getCurrentState()); + } + + if (!playerImpl.isPopupClosing) { + playerImpl.savePositionAndSize(); + } + } + + v.performClick(); + return true; + } + + private boolean handleMultiDrag(final MotionEvent event) { + if (initPointerDistance != -1 && event.getPointerCount() == 2) { + // get the movements of the fingers + double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX, + event.getY(0) - initFirstPointerY); + double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX, + event.getY(1) - initSecPointerY); + + // minimum threshold beyond which pinch gesture will work + int minimumMove = ViewConfiguration.get(service).getScaledTouchSlop(); + + if (Math.max(firstPointerMove, secPointerMove) > minimumMove) { + // calculate current distance between the pointers + final double currentPointerDistance = + Math.hypot(event.getX(0) - event.getX(1), + event.getY(0) - event.getY(1)); + + double popupWidth = playerImpl.getPopupWidth(); + // change co-ordinates of popup so the center stays at the same position + double newWidth = (popupWidth * currentPointerDistance / initPointerDistance); + initPointerDistance = currentPointerDistance; + playerImpl.getPopupLayoutParams().x += (popupWidth - newWidth) / 2; + + playerImpl.checkPopupPositionBounds(); + playerImpl.updateScreenSize(); + + playerImpl.updatePopupSize( + (int) Math.min(playerImpl.getScreenWidth(), newWidth), + -1); + return true; + } + } + return false; + } + + + /* + * Utils + * */ + + private int getNavigationBarHeight(final Context context) { + int resId = context.getResources() + .getIdentifier("navigation_bar_height", "dimen", "android"); + if (resId > 0) { + return context.getResources().getDimensionPixelSize(resId); + } + return 0; + } + + private int getStatusBarHeight(final Context context) { + int resId = context.getResources() + .getIdentifier("status_bar_height", "dimen", "android"); + if (resId > 0) { + return context.getResources().getDimensionPixelSize(resId); + } + return 0; + } +} + + diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java new file mode 100644 index 000000000..f8d03087e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java @@ -0,0 +1,15 @@ +package org.schabi.newpipe.player.event; + +import com.google.android.exoplayer2.ExoPlaybackException; + +public interface PlayerServiceEventListener extends PlayerEventListener { + void onFullscreenStateChanged(boolean fullscreen); + + void onScreenRotationButtonClicked(); + + void onMoreOptionsLongClicked(); + + void onPlayerError(ExoPlaybackException error); + + void hideSystemUiIfNeeded(); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 369e3236e..4b326ca7d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -114,7 +114,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An private void onAudioFocusGain() { Log.d(TAG, "onAudioFocusGain() called"); player.setVolume(DUCK_AUDIO_TO); - animateAudio(DUCK_AUDIO_TO, 1f); + animateAudio(DUCK_AUDIO_TO, 1.0f); if (PlayerHelper.isResumeAfterAudioFocusGain(context)) { player.setPlayWhenReady(true); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 0d511d565..ae547de9f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -92,8 +92,10 @@ public class PlaybackParameterDialog extends DialogFragment { public static PlaybackParameterDialog newInstance(final double playbackTempo, final double playbackPitch, - final boolean playbackSkipSilence) { + final boolean playbackSkipSilence, + final Callback callback) { PlaybackParameterDialog dialog = new PlaybackParameterDialog(); + dialog.callback = callback; dialog.initialTempo = playbackTempo; dialog.initialPitch = playbackPitch; @@ -111,9 +113,9 @@ public class PlaybackParameterDialog extends DialogFragment { @Override public void onAttach(final Context context) { super.onAttach(context); - if (context != null && context instanceof Callback) { + if (context instanceof Callback) { callback = (Callback) context; - } else { + } else if (callback == null) { dismiss(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index e63e56bf9..0a4dd83ac 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; +import android.provider.Settings; import android.view.accessibility.CaptioningManager; import androidx.annotation.IntDef; @@ -45,6 +46,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; import static java.lang.annotation.RetentionPolicy.SOURCE; +import static org.schabi.newpipe.player.helper.PlayerHelper.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.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; @@ -56,6 +60,15 @@ public final class PlayerHelper { private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x"); private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%"); + @Retention(SOURCE) + @IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI, + AUTOPLAY_TYPE_NEVER}) + public @interface AutoplayType { + int AUTOPLAY_TYPE_ALWAYS = 0; + int AUTOPLAY_TYPE_WIFI = 1; + int AUTOPLAY_TYPE_NEVER = 2; + } + private PlayerHelper() { } //////////////////////////////////////////////////////////////////////////// @@ -203,6 +216,11 @@ public final class PlayerHelper { return isAutoQueueEnabled(context, false); } + public static boolean isClearingQueueConfirmationRequired(@NonNull final Context context) { + return getPreferences(context) + .getBoolean(context.getString(R.string.clear_queue_confirmation_key), false); + } + @MinimizeMode public static int getMinimizeOnExitAction(@NonNull final Context context) { final String defaultAction = context.getString(R.string.minimize_on_exit_none_key); @@ -219,6 +237,18 @@ public final class PlayerHelper { } } + @AutoplayType + public static int getAutoplayType(@NonNull final Context context) { + final String type = getAutoplayType(context, context.getString(R.string.autoplay_wifi_key)); + if (type.equals(context.getString(R.string.autoplay_always_key))) { + return AUTOPLAY_TYPE_ALWAYS; + } else if (type.equals(context.getString(R.string.autoplay_never_key))) { + return AUTOPLAY_TYPE_NEVER; + } else { + return AUTOPLAY_TYPE_WIFI; + } + } + @NonNull public static SeekParameters getSeekParameters(@NonNull final Context context) { return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT; @@ -308,7 +338,7 @@ public final class PlayerHelper { final CaptioningManager captioningManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); if (captioningManager == null || !captioningManager.isEnabled()) { - return 1f; + return 1.0f; } return captioningManager.getFontScale(); @@ -324,6 +354,13 @@ public final class PlayerHelper { setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis()); } + public static boolean globalScreenOrientationLocked(final Context context) { + // 1: Screen orientation changes using accelerometer + // 0: Screen orientation is locked + return android.provider.Settings.System.getInt( + context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 0; + } + //////////////////////////////////////////////////////////////////////////// // Private helpers //////////////////////////////////////////////////////////////////////////// @@ -396,6 +433,12 @@ public final class PlayerHelper { .getString(context.getString(R.string.minimize_on_exit_key), key); } + private static String getAutoplayType(@NonNull final Context context, + final String key) { + return getPreferences(context).getString(context.getString(R.string.autoplay_key), + key); + } + private static SinglePlayQueue getAutoQueuedSinglePlayQueue( final StreamInfoItem streamInfoItem) { SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem); diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index 7391294ba..dc5803462 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -51,16 +51,24 @@ public abstract class PlayQueue implements Serializable { @NonNull private final AtomicInteger queueIndex; + private final ArrayList history; private transient BehaviorSubject eventBroadcast; private transient Flowable broadcastReceiver; private transient Subscription reportingReactor; + private transient boolean disposed; + PlayQueue(final int index, final List startWith) { streams = new ArrayList<>(); streams.addAll(startWith); + history = new ArrayList<>(); + if (streams.size() > index) { + history.add(streams.get(index)); + } queueIndex = new AtomicInteger(index); + disposed = false; } /*////////////////////////////////////////////////////////////////////////// @@ -99,6 +107,7 @@ public abstract class PlayQueue implements Serializable { eventBroadcast = null; broadcastReceiver = null; reportingReactor = null; + disposed = true; } /** @@ -149,6 +158,9 @@ public abstract class PlayQueue implements Serializable { if (index >= streams.size()) { newIndex = isComplete() ? index % streams.size() : streams.size() - 1; } + if (oldIndex != newIndex) { + history.add(streams.get(newIndex)); + } queueIndex.set(newIndex); broadcast(new SelectEvent(oldIndex, newIndex)); @@ -269,7 +281,7 @@ public abstract class PlayQueue implements Serializable { * @param items {@link PlayQueueItem}s to append */ public synchronized void append(@NonNull final List items) { - List itemList = new ArrayList<>(items); + final List itemList = new ArrayList<>(items); if (isShuffled()) { backup.addAll(itemList); @@ -314,6 +326,9 @@ public abstract class PlayQueue implements Serializable { public synchronized void error() { final int oldIndex = getIndex(); queueIndex.incrementAndGet(); + if (streams.size() > queueIndex.get()) { + history.add(streams.get(queueIndex.get())); + } broadcast(new ErrorEvent(oldIndex, getIndex())); } @@ -334,7 +349,11 @@ public abstract class PlayQueue implements Serializable { if (backup != null) { backup.remove(getItem(removeIndex)); } - streams.remove(removeIndex); + + history.remove(streams.remove(removeIndex)); + if (streams.size() > queueIndex.get()) { + history.add(streams.get(queueIndex.get())); + } } /** @@ -367,7 +386,7 @@ public abstract class PlayQueue implements Serializable { queueIndex.incrementAndGet(); } - PlayQueueItem playQueueItem = streams.remove(source); + final PlayQueueItem playQueueItem = streams.remove(source); playQueueItem.setAutoQueued(false); streams.add(target, playQueueItem); broadcast(new MoveEvent(source, target)); @@ -427,6 +446,9 @@ public abstract class PlayQueue implements Serializable { streams.add(0, streams.remove(newIndex)); } queueIndex.set(0); + if (streams.size() > 0) { + history.add(streams.get(0)); + } broadcast(new ReorderEvent(originIndex, queueIndex.get())); } @@ -458,10 +480,60 @@ public abstract class PlayQueue implements Serializable { } else { queueIndex.set(0); } + if (streams.size() > queueIndex.get()) { + history.add(streams.get(queueIndex.get())); + } broadcast(new ReorderEvent(originIndex, queueIndex.get())); } + /** + * Selects previous played item. + * + * This method removes currently playing item from history and + * starts playing the last item from history if it exists + * + * @return true if history is not empty and the item can be played + * */ + public synchronized boolean previous() { + if (history.size() <= 1) { + return false; + } + + history.remove(history.size() - 1); + + final PlayQueueItem last = history.remove(history.size() - 1); + setIndex(indexOf(last)); + + return true; + } + + /* + * Compares two PlayQueues. Useful when a user switches players but queue is the same so + * we don't have to do anything with new queue. + * This method also gives a chance to track history of items in a queue in + * VideoDetailFragment without duplicating items from two identical queues + * */ + @Override + public boolean equals(@Nullable final Object obj) { + if (!(obj instanceof PlayQueue) + || getStreams().size() != ((PlayQueue) obj).getStreams().size()) { + return false; + } + + final PlayQueue other = (PlayQueue) obj; + for (int i = 0; i < getStreams().size(); i++) { + if (!getItem(i).getUrl().equals(other.getItem(i).getUrl())) { + return false; + } + } + + return true; + } + + public boolean isDisposed() { + return disposed; + } /*////////////////////////////////////////////////////////////////////////// // Rx Broadcast //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 18cbece6f..26a33917f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -13,7 +13,7 @@ import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import org.schabi.newpipe.R; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -62,7 +62,7 @@ public class SettingsActivity extends AppCompatActivity .commit(); } - if (AndroidTvUtils.isTv(this)) { + if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(this); } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java index 00a29c7ab..9542dbc05 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -1,416 +1,416 @@ -package org.schabi.newpipe.streams; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.schabi.newpipe.streams.WebMReader.Cluster; -import org.schabi.newpipe.streams.WebMReader.Segment; -import org.schabi.newpipe.streams.WebMReader.SimpleBlock; -import org.schabi.newpipe.streams.WebMReader.WebMTrack; -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * @author kapodamy - */ -public class OggFromWebMWriter implements Closeable { - private static final byte FLAG_UNSET = 0x00; - //private static final byte FLAG_CONTINUED = 0x01; - private static final byte FLAG_FIRST = 0x02; - private static final byte FLAG_LAST = 0x04; - - private static final byte HEADER_CHECKSUM_OFFSET = 22; - private static final byte HEADER_SIZE = 27; - - private static final int TIME_SCALE_NS = 1000000000; - - private boolean done = false; - private boolean parsed = false; - - private SharpStream source; - private SharpStream output; - - private int sequenceCount = 0; - private final int streamId; - private byte packetFlag = FLAG_FIRST; - - private WebMReader webm = null; - private WebMTrack webmTrack = null; - private Segment webmSegment = null; - private Cluster webmCluster = null; - private SimpleBlock webmBlock = null; - - private long webmBlockLastTimecode = 0; - private long webmBlockNearDuration = 0; - - private short segmentTableSize = 0; - private final byte[] segmentTable = new byte[255]; - private long segmentTableNextTimestamp = TIME_SCALE_NS; - - private final int[] crc32Table = new int[256]; - - public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { - if (!source.canRead() || !source.canRewind()) { - throw new IllegalArgumentException("source stream must be readable and allows seeking"); - } - if (!target.canWrite() || !target.canRewind()) { - throw new IllegalArgumentException("output stream must be writable and allows seeking"); - } - - this.source = source; - this.output = target; - - this.streamId = (int) System.currentTimeMillis(); - - populateCrc32Table(); - } - - public boolean isDone() { - return done; - } - - public boolean isParsed() { - return parsed; - } - - public WebMTrack[] getTracksFromSource() throws IllegalStateException { - if (!parsed) { - throw new IllegalStateException("source must be parsed first"); - } - - return webm.getAvailableTracks(); - } - - public void parseSource() throws IOException, IllegalStateException { - if (done) { - throw new IllegalStateException("already done"); - } - if (parsed) { - throw new IllegalStateException("already parsed"); - } - - try { - webm = new WebMReader(source); - webm.parse(); - webmSegment = webm.getNextSegment(); - } finally { - parsed = true; - } - } - - public void selectTrack(final int trackIndex) throws IOException { - if (!parsed) { - throw new IllegalStateException("source must be parsed first"); - } - if (done) { - throw new IOException("already done"); - } - if (webmTrack != null) { - throw new IOException("tracks already selected"); - } - - switch (webm.getAvailableTracks()[trackIndex].kind) { - case Audio: - case Video: - break; - default: - throw new UnsupportedOperationException("the track must an audio or video stream"); - } - - try { - webmTrack = webm.selectTrack(trackIndex); - } finally { - parsed = true; - } - } - - @Override - public void close() throws IOException { - done = true; - parsed = true; - - webmTrack = null; - webm = null; - - if (!output.isClosed()) { - output.flush(); - } - - source.close(); - output.close(); - } - - public void build() throws IOException { - float resolution; - SimpleBlock bloq; - ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255)); - ByteBuffer page = ByteBuffer.allocate(64 * 1024); - - header.order(ByteOrder.LITTLE_ENDIAN); - - /* step 1: get the amount of frames per seconds */ - switch (webmTrack.kind) { - case Audio: - resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata); - if (resolution == 0f) { - throw new RuntimeException("cannot get the audio sample rate"); - } - break; - case Video: - // WARNING: untested - if (webmTrack.defaultDuration == 0) { - throw new RuntimeException("missing default frame time"); - } - resolution = 1000f / ((float) webmTrack.defaultDuration - / webmSegment.info.timecodeScale); - break; - default: - throw new RuntimeException("not implemented"); - } - - /* step 2: create packet with code init data */ - if (webmTrack.codecPrivate != null) { - addPacketSegment(webmTrack.codecPrivate.length); - makePacketheader(0x00, header, webmTrack.codecPrivate); - write(header); - output.write(webmTrack.codecPrivate); - } - - /* step 3: create packet with metadata */ - byte[] buffer = makeMetadata(); - if (buffer != null) { - addPacketSegment(buffer.length); - makePacketheader(0x00, header, buffer); - write(header); - output.write(buffer); - } - - /* step 4: calculate amount of packets */ - while (webmSegment != null) { - bloq = getNextBlock(); - - if (bloq != null && addPacketSegment(bloq)) { - int pos = page.position(); - //noinspection ResultOfMethodCallIgnored - bloq.data.read(page.array(), pos, bloq.dataSize); - page.position(pos + bloq.dataSize); - continue; - } - - // calculate the current packet duration using the next block - double elapsedNs = webmTrack.codecDelay; - - if (bloq == null) { - packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed - elapsedNs += webmBlockLastTimecode; - - if (webmTrack.defaultDuration > 0) { - elapsedNs += webmTrack.defaultDuration; - } else { - // hardcoded way, guess the sample duration - elapsedNs += webmBlockNearDuration; - } - } else { - elapsedNs += bloq.absoluteTimeCodeNs; - } - - // get the sample count in the page - elapsedNs = elapsedNs / TIME_SCALE_NS; - elapsedNs = Math.ceil(elapsedNs * resolution); - - // create header and calculate page checksum - int checksum = makePacketheader((long) elapsedNs, header, null); - checksum = calcCrc32(checksum, page.array(), page.position()); - - header.putInt(HEADER_CHECKSUM_OFFSET, checksum); - - // dump data - write(header); - write(page); - - webmBlock = bloq; - } - } - - private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer, - final byte[] immediatePage) { - short length = HEADER_SIZE; - - buffer.putInt(0x5367674f); // "OggS" binary string in little-endian - buffer.put((byte) 0x00); // version - buffer.put(packetFlag); // type - - buffer.putLong(granPos); // granulate position - - buffer.putInt(streamId); // bitstream serial number - buffer.putInt(sequenceCount++); // page sequence number - - buffer.putInt(0x00); // page checksum - - buffer.put((byte) segmentTableSize); // segment table - buffer.put(segmentTable, 0, segmentTableSize); // segment size - - length += segmentTableSize; - - clearSegmentTable(); // clear segment table for next header - - int checksumCrc32 = calcCrc32(0x00, buffer.array(), length); - - if (immediatePage != null) { - checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length); - buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32); - segmentTableNextTimestamp -= TIME_SCALE_NS; - } - - return checksumCrc32; - } - - @Nullable - private byte[] makeMetadata() { - if ("A_OPUS".equals(webmTrack.codecId)) { - return new byte[]{ - 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string - 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) - 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) - }; - } else if ("A_VORBIS".equals(webmTrack.codecId)) { - return new byte[]{ - 0x03, // ¿¿¿??? - 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string - 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) - 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) - }; - } - - // not implemented for the desired codec - return null; - } - - private void write(final ByteBuffer buffer) throws IOException { - output.write(buffer.array(), 0, buffer.position()); - buffer.position(0); - } - - @Nullable - private SimpleBlock getNextBlock() throws IOException { - SimpleBlock res; - - if (webmBlock != null) { - res = webmBlock; - webmBlock = null; - return res; - } - - if (webmSegment == null) { - webmSegment = webm.getNextSegment(); - if (webmSegment == null) { - return null; // no more blocks in the selected track - } - } - - if (webmCluster == null) { - webmCluster = webmSegment.getNextCluster(); - if (webmCluster == null) { - webmSegment = null; - return getNextBlock(); - } - } - - res = webmCluster.getNextSimpleBlock(); - if (res == null) { - webmCluster = null; - return getNextBlock(); - } - - webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode; - webmBlockLastTimecode = res.absoluteTimeCodeNs; - - return res; - } - - private float getSampleFrequencyFromTrack(final byte[] bMetadata) { - // hardcoded way - ByteBuffer buffer = ByteBuffer.wrap(bMetadata); - - while (buffer.remaining() >= 6) { - int id = buffer.getShort() & 0xFFFF; - if (id == 0x0000B584) { - return buffer.getFloat(); - } - } - - return 0f; - } - - private void clearSegmentTable() { - segmentTableNextTimestamp += TIME_SCALE_NS; - packetFlag = FLAG_UNSET; - segmentTableSize = 0; - } - - private boolean addPacketSegment(final SimpleBlock block) { - long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay; - - if (timestamp >= segmentTableNextTimestamp) { - return false; - } - - return addPacketSegment(block.dataSize); - } - - private boolean addPacketSegment(final int size) { - if (size > 65025) { - throw new UnsupportedOperationException("page size cannot be larger than 65025"); - } - - int available = (segmentTable.length - segmentTableSize) * 255; - boolean extra = (size % 255) == 0; - - if (extra) { - // add a zero byte entry in the table - // required to indicate the sample size is multiple of 255 - available -= 255; - } - - // check if possible add the segment, without overflow the table - if (available < size) { - return false; // not enough space on the page - } - - for (int seg = size; seg > 0; seg -= 255) { - segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255); - } - - if (extra) { - segmentTable[segmentTableSize++] = 0x00; - } - - return true; - } - - private void populateCrc32Table() { - for (int i = 0; i < 0x100; i++) { - int crc = i << 24; - for (int j = 0; j < 8; j++) { - long b = crc >>> 31; - crc <<= 1; - crc ^= (int) (0x100000000L - b) & 0x04c11db7; - } - crc32Table[i] = crc; - } - } - - private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) { - int crc = initialCrc; - for (int i = 0; i < size; i++) { - int reg = (crc >>> 24) & 0xff; - crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; - } - - return crc; - } -} +package org.schabi.newpipe.streams; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.schabi.newpipe.streams.WebMReader.Cluster; +import org.schabi.newpipe.streams.WebMReader.Segment; +import org.schabi.newpipe.streams.WebMReader.SimpleBlock; +import org.schabi.newpipe.streams.WebMReader.WebMTrack; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * @author kapodamy + */ +public class OggFromWebMWriter implements Closeable { + private static final byte FLAG_UNSET = 0x00; + //private static final byte FLAG_CONTINUED = 0x01; + private static final byte FLAG_FIRST = 0x02; + private static final byte FLAG_LAST = 0x04; + + private static final byte HEADER_CHECKSUM_OFFSET = 22; + private static final byte HEADER_SIZE = 27; + + private static final int TIME_SCALE_NS = 1000000000; + + private boolean done = false; + private boolean parsed = false; + + private SharpStream source; + private SharpStream output; + + private int sequenceCount = 0; + private final int streamId; + private byte packetFlag = FLAG_FIRST; + + private WebMReader webm = null; + private WebMTrack webmTrack = null; + private Segment webmSegment = null; + private Cluster webmCluster = null; + private SimpleBlock webmBlock = null; + + private long webmBlockLastTimecode = 0; + private long webmBlockNearDuration = 0; + + private short segmentTableSize = 0; + private final byte[] segmentTable = new byte[255]; + private long segmentTableNextTimestamp = TIME_SCALE_NS; + + private final int[] crc32Table = new int[256]; + + public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { + if (!source.canRead() || !source.canRewind()) { + throw new IllegalArgumentException("source stream must be readable and allows seeking"); + } + if (!target.canWrite() || !target.canRewind()) { + throw new IllegalArgumentException("output stream must be writable and allows seeking"); + } + + this.source = source; + this.output = target; + + this.streamId = (int) System.currentTimeMillis(); + + populateCrc32Table(); + } + + public boolean isDone() { + return done; + } + + public boolean isParsed() { + return parsed; + } + + public WebMTrack[] getTracksFromSource() throws IllegalStateException { + if (!parsed) { + throw new IllegalStateException("source must be parsed first"); + } + + return webm.getAvailableTracks(); + } + + public void parseSource() throws IOException, IllegalStateException { + if (done) { + throw new IllegalStateException("already done"); + } + if (parsed) { + throw new IllegalStateException("already parsed"); + } + + try { + webm = new WebMReader(source); + webm.parse(); + webmSegment = webm.getNextSegment(); + } finally { + parsed = true; + } + } + + public void selectTrack(final int trackIndex) throws IOException { + if (!parsed) { + throw new IllegalStateException("source must be parsed first"); + } + if (done) { + throw new IOException("already done"); + } + if (webmTrack != null) { + throw new IOException("tracks already selected"); + } + + switch (webm.getAvailableTracks()[trackIndex].kind) { + case Audio: + case Video: + break; + default: + throw new UnsupportedOperationException("the track must an audio or video stream"); + } + + try { + webmTrack = webm.selectTrack(trackIndex); + } finally { + parsed = true; + } + } + + @Override + public void close() throws IOException { + done = true; + parsed = true; + + webmTrack = null; + webm = null; + + if (!output.isClosed()) { + output.flush(); + } + + source.close(); + output.close(); + } + + public void build() throws IOException { + float resolution; + SimpleBlock bloq; + ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255)); + ByteBuffer page = ByteBuffer.allocate(64 * 1024); + + header.order(ByteOrder.LITTLE_ENDIAN); + + /* step 1: get the amount of frames per seconds */ + switch (webmTrack.kind) { + case Audio: + resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata); + if (resolution == 0f) { + throw new RuntimeException("cannot get the audio sample rate"); + } + break; + case Video: + // WARNING: untested + if (webmTrack.defaultDuration == 0) { + throw new RuntimeException("missing default frame time"); + } + resolution = 1000f / ((float) webmTrack.defaultDuration + / webmSegment.info.timecodeScale); + break; + default: + throw new RuntimeException("not implemented"); + } + + /* step 2: create packet with code init data */ + if (webmTrack.codecPrivate != null) { + addPacketSegment(webmTrack.codecPrivate.length); + makePacketheader(0x00, header, webmTrack.codecPrivate); + write(header); + output.write(webmTrack.codecPrivate); + } + + /* step 3: create packet with metadata */ + byte[] buffer = makeMetadata(); + if (buffer != null) { + addPacketSegment(buffer.length); + makePacketheader(0x00, header, buffer); + write(header); + output.write(buffer); + } + + /* step 4: calculate amount of packets */ + while (webmSegment != null) { + bloq = getNextBlock(); + + if (bloq != null && addPacketSegment(bloq)) { + int pos = page.position(); + //noinspection ResultOfMethodCallIgnored + bloq.data.read(page.array(), pos, bloq.dataSize); + page.position(pos + bloq.dataSize); + continue; + } + + // calculate the current packet duration using the next block + double elapsedNs = webmTrack.codecDelay; + + if (bloq == null) { + packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed + elapsedNs += webmBlockLastTimecode; + + if (webmTrack.defaultDuration > 0) { + elapsedNs += webmTrack.defaultDuration; + } else { + // hardcoded way, guess the sample duration + elapsedNs += webmBlockNearDuration; + } + } else { + elapsedNs += bloq.absoluteTimeCodeNs; + } + + // get the sample count in the page + elapsedNs = elapsedNs / TIME_SCALE_NS; + elapsedNs = Math.ceil(elapsedNs * resolution); + + // create header and calculate page checksum + int checksum = makePacketheader((long) elapsedNs, header, null); + checksum = calcCrc32(checksum, page.array(), page.position()); + + header.putInt(HEADER_CHECKSUM_OFFSET, checksum); + + // dump data + write(header); + write(page); + + webmBlock = bloq; + } + } + + private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer, + final byte[] immediatePage) { + short length = HEADER_SIZE; + + buffer.putInt(0x5367674f); // "OggS" binary string in little-endian + buffer.put((byte) 0x00); // version + buffer.put(packetFlag); // type + + buffer.putLong(granPos); // granulate position + + buffer.putInt(streamId); // bitstream serial number + buffer.putInt(sequenceCount++); // page sequence number + + buffer.putInt(0x00); // page checksum + + buffer.put((byte) segmentTableSize); // segment table + buffer.put(segmentTable, 0, segmentTableSize); // segment size + + length += segmentTableSize; + + clearSegmentTable(); // clear segment table for next header + + int checksumCrc32 = calcCrc32(0x00, buffer.array(), length); + + if (immediatePage != null) { + checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length); + buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32); + segmentTableNextTimestamp -= TIME_SCALE_NS; + } + + return checksumCrc32; + } + + @Nullable + private byte[] makeMetadata() { + if ("A_OPUS".equals(webmTrack.codecId)) { + return new byte[]{ + 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string + 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) + 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) + }; + } else if ("A_VORBIS".equals(webmTrack.codecId)) { + return new byte[]{ + 0x03, // ¿¿¿??? + 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string + 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) + 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) + }; + } + + // not implemented for the desired codec + return null; + } + + private void write(final ByteBuffer buffer) throws IOException { + output.write(buffer.array(), 0, buffer.position()); + buffer.position(0); + } + + @Nullable + private SimpleBlock getNextBlock() throws IOException { + SimpleBlock res; + + if (webmBlock != null) { + res = webmBlock; + webmBlock = null; + return res; + } + + if (webmSegment == null) { + webmSegment = webm.getNextSegment(); + if (webmSegment == null) { + return null; // no more blocks in the selected track + } + } + + if (webmCluster == null) { + webmCluster = webmSegment.getNextCluster(); + if (webmCluster == null) { + webmSegment = null; + return getNextBlock(); + } + } + + res = webmCluster.getNextSimpleBlock(); + if (res == null) { + webmCluster = null; + return getNextBlock(); + } + + webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode; + webmBlockLastTimecode = res.absoluteTimeCodeNs; + + return res; + } + + private float getSampleFrequencyFromTrack(final byte[] bMetadata) { + // hardcoded way + ByteBuffer buffer = ByteBuffer.wrap(bMetadata); + + while (buffer.remaining() >= 6) { + int id = buffer.getShort() & 0xFFFF; + if (id == 0x0000B584) { + return buffer.getFloat(); + } + } + + return 0.0f; + } + + private void clearSegmentTable() { + segmentTableNextTimestamp += TIME_SCALE_NS; + packetFlag = FLAG_UNSET; + segmentTableSize = 0; + } + + private boolean addPacketSegment(final SimpleBlock block) { + long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay; + + if (timestamp >= segmentTableNextTimestamp) { + return false; + } + + return addPacketSegment(block.dataSize); + } + + private boolean addPacketSegment(final int size) { + if (size > 65025) { + throw new UnsupportedOperationException("page size cannot be larger than 65025"); + } + + int available = (segmentTable.length - segmentTableSize) * 255; + boolean extra = (size % 255) == 0; + + if (extra) { + // add a zero byte entry in the table + // required to indicate the sample size is multiple of 255 + available -= 255; + } + + // check if possible add the segment, without overflow the table + if (available < size) { + return false; // not enough space on the page + } + + for (int seg = size; seg > 0; seg -= 255) { + segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255); + } + + if (extra) { + segmentTable[segmentTableSize++] = 0x00; + } + + return true; + } + + private void populateCrc32Table() { + for (int i = 0; i < 0x100; i++) { + int crc = i << 24; + for (int j = 0; j < 8; j++) { + long b = crc >>> 31; + crc <<= 1; + crc ^= (int) (0x100000000L - b) & 0x04c11db7; + } + crc32Table[i] = crc; + } + } + + private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) { + int crc = initialCrc; + for (int i = 0; i < size; i++) { + int reg = (crc >>> 24) & 0xff; + crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; + } + + return crc; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java similarity index 81% rename from app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java rename to app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java index db2ab4aa7..0afa0663c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java @@ -8,22 +8,23 @@ import android.os.BatteryManager; import android.os.Build; import android.view.KeyEvent; +import androidx.annotation.NonNull; import org.schabi.newpipe.App; import static android.content.Context.BATTERY_SERVICE; import static android.content.Context.UI_MODE_SERVICE; -public final class AndroidTvUtils { +public final class DeviceUtils { private static final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv"; private static Boolean isTV = null; - private AndroidTvUtils() { + private DeviceUtils() { } public static boolean isTv(final Context context) { - if (AndroidTvUtils.isTV != null) { - return AndroidTvUtils.isTV; + if (isTV != null) { + return isTV; } PackageManager pm = App.getApp().getPackageManager(); @@ -48,8 +49,15 @@ public final class AndroidTvUtils { isTv = isTv || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); } - AndroidTvUtils.isTV = isTv; - return AndroidTvUtils.isTV; + DeviceUtils.isTV = isTv; + return DeviceUtils.isTV; + } + + public static boolean isTablet(@NonNull final Context context) { + return (context + .getResources() + .getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) + >= Configuration.SCREENLAYOUT_SIZE_LARGE; } public static boolean isConfirmKey(final int keyCode) { diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index 189b6823e..47486ae49 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -536,7 +536,7 @@ public final class ListHelper { * @param context App context * @return {@code true} if connected to a metered network */ - private static boolean isMeteredNetwork(final Context context) { + public static boolean isMeteredNetwork(final Context context) { ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (manager == null || manager.getActiveNetworkInfo() == null) { 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 ccaa79f98..0c8f83474 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -13,6 +13,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -46,14 +47,12 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; -import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; import org.schabi.newpipe.player.BasePlayer; -import org.schabi.newpipe.player.MainVideoPlayer; -import org.schabi.newpipe.player.PopupVideoPlayer; -import org.schabi.newpipe.player.PopupVideoPlayerActivity; +import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.VideoPlayer; import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.settings.SettingsActivity; import java.util.ArrayList; @@ -85,6 +84,7 @@ public final class NavigationHelper { intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); } intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO); return intent; } @@ -112,11 +112,13 @@ public final class NavigationHelper { public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, - final int repeatMode, final float playbackSpeed, + final int repeatMode, + final float playbackSpeed, final float playbackPitch, final boolean playbackSkipSilence, @Nullable final String playbackQuality, - final boolean resumePlayback, final boolean startPaused, + final boolean resumePlayback, + final boolean startPaused, final boolean isMuted) { return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) @@ -124,12 +126,42 @@ public final class NavigationHelper { .putExtra(BasePlayer.IS_MUTED, isMuted); } - public static void playOnMainPlayer(final Context context, final PlayQueue queue, + public static void playOnMainPlayer( + final AppCompatActivity activity, + final PlayQueue queue, + final boolean autoPlay) { + playOnMainPlayer(activity.getSupportFragmentManager(), queue, autoPlay); + } + + public static void playOnMainPlayer( + final FragmentManager fragmentManager, + final PlayQueue queue, + final boolean autoPlay) { + final PlayQueueItem currentStream = queue.getItem(); + openVideoDetailFragment( + fragmentManager, + currentStream.getServiceId(), + currentStream.getUrl(), + currentStream.getTitle(), + autoPlay, + queue); + } + + public static void playOnMainPlayer(@NonNull final Context context, + @NonNull final PlayQueue queue, + @NonNull final StreamingService.LinkType linkType, + @NonNull final String url, + @NonNull final String title, + final boolean autoPlay, final boolean resumePlayback) { - final Intent playerIntent - = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback); - playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(playerIntent); + + final Intent intent = getPlayerIntent(context, MainActivity.class, queue, resumePlayback); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Constants.KEY_LINK_TYPE, linkType); + intent.putExtra(Constants.KEY_URL, url); + intent.putExtra(Constants.KEY_TITLE, title); + intent.putExtra(VideoDetailFragment.AUTO_PLAY, autoPlay); + context.startActivity(intent); } public static void playOnPopupPlayer(final Context context, final PlayQueue queue, @@ -140,16 +172,19 @@ public final class NavigationHelper { } Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - startService(context, - getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback)); + final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP); + startService(context, intent); } - public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue, + public static void playOnBackgroundPlayer(final Context context, + final PlayQueue queue, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) .show(); - startService(context, - getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback)); + final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); + startService(context, intent); } public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, @@ -166,8 +201,10 @@ public final class NavigationHelper { } Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); - startService(context, getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, - selectOnAppend, resumePlayback)); + final Intent intent = getPlayerEnqueueIntent( + context, MainPlayer.class, queue, selectOnAppend, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP); + startService(context, intent); } public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, @@ -175,12 +212,15 @@ public final class NavigationHelper { enqueueOnBackgroundPlayer(context, queue, false, resumePlayback); } - public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, + public static void enqueueOnBackgroundPlayer(final Context context, + final PlayQueue queue, final boolean selectOnAppend, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); - startService(context, getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, - selectOnAppend, resumePlayback)); + final Intent intent = getPlayerEnqueueIntent( + context, MainPlayer.class, queue, selectOnAppend, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); + startService(context, intent); } public static void startService(@NonNull final Context context, @NonNull final Intent intent) { @@ -311,31 +351,43 @@ public final class NavigationHelper { public static void openVideoDetailFragment(final FragmentManager fragmentManager, final int serviceId, final String url, final String title) { - openVideoDetailFragment(fragmentManager, serviceId, url, title, false); + openVideoDetailFragment(fragmentManager, serviceId, url, title, true, null); } - public static void openVideoDetailFragment(final FragmentManager fragmentManager, - final int serviceId, final String url, - final String name, final boolean autoPlay) { - Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_holder); + public static void openVideoDetailFragment( + final FragmentManager fragmentManager, + final int serviceId, + final String url, + final String title, + final boolean autoPlay, + final PlayQueue playQueue) { + final Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_player_holder); if (fragment instanceof VideoDetailFragment && fragment.isVisible()) { - VideoDetailFragment detailFragment = (VideoDetailFragment) fragment; + expandMainPlayer(fragment.requireActivity()); + final VideoDetailFragment detailFragment = (VideoDetailFragment) fragment; detailFragment.setAutoplay(autoPlay); - detailFragment.selectAndLoadVideo(serviceId, url, name == null ? "" : name); + detailFragment + .selectAndLoadVideo(serviceId, url, title == null ? "" : title, playQueue); + detailFragment.scrollToTop(); return; } - VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, - name == null ? "" : name); + final VideoDetailFragment instance = VideoDetailFragment + .getInstance(serviceId, url, title == null ? "" : title, playQueue); instance.setAutoplay(autoPlay); defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, instance) - .addToBackStack(null) + .replace(R.id.fragment_player_holder, instance) + .runOnCommit(() -> expandMainPlayer(instance.requireActivity())) .commit(); } + public static void expandMainPlayer(final Context context) { + final Intent intent = new Intent(VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER); + context.sendBroadcast(intent); + } + public static void openChannelFragment(final FragmentManager fragmentManager, final int serviceId, final String url, final String name) { @@ -505,10 +557,6 @@ public final class NavigationHelper { return getServicePlayerActivityIntent(context, BackgroundPlayerActivity.class); } - public static Intent getPopupPlayerActivityIntent(final Context context) { - return getServicePlayerActivityIntent(context, PopupVideoPlayerActivity.class); - } - private static Intent getServicePlayerActivityIntent(final Context context, final Class activityClass) { Intent intent = new Intent(context, activityClass); diff --git a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java index 0ec2d571d..a3571b96f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java @@ -31,8 +31,9 @@ public final class ShareUtils { // no browser set as default openInDefaultApp(context, url); } else { - final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - intent.setPackage(defaultBrowserPackageName); + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)) + .setPackage(defaultBrowserPackageName) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } } @@ -48,7 +49,8 @@ public final class ShareUtils { private static void openInDefaultApp(final Context context, final String url) { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); context.startActivity(Intent.createChooser( - intent, context.getString(R.string.share_dialog_title))); + intent, context.getString(R.string.share_dialog_title)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } /** @@ -60,7 +62,8 @@ public final class ShareUtils { * @return the package name of the default browser, or "android" if there's no default */ private static String getDefaultBrowserPackageName(final Context context) { - final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://")); + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://")) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity( intent, PackageManager.MATCH_DEFAULT_ONLY); return resolveInfo.activityInfo.packageName; @@ -79,7 +82,8 @@ public final class ShareUtils { intent.putExtra(Intent.EXTRA_SUBJECT, subject); intent.putExtra(Intent.EXTRA_TEXT, url); context.startActivity(Intent.createChooser( - intent, context.getString(R.string.share_dialog_title))); + intent, context.getString(R.string.share_dialog_title)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } /** diff --git a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java new file mode 100644 index 000000000..798712b6b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java @@ -0,0 +1,114 @@ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.SurfaceView; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; + +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + +public class ExpandableSurfaceView extends SurfaceView { + private int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + private int baseHeight = 0; + private int maxHeight = 0; + private float videoAspectRatio = 0.0f; + private float scaleX = 1.0f; + private float scaleY = 1.0f; + + public ExpandableSurfaceView(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (videoAspectRatio == 0.0f) { + return; + } + + int width = MeasureSpec.getSize(widthMeasureSpec); + final boolean verticalVideo = videoAspectRatio < 1; + // Use maxHeight only on non-fit resize mode and in vertical videos + int height = maxHeight != 0 + && resizeMode != AspectRatioFrameLayout.RESIZE_MODE_FIT + && verticalVideo ? maxHeight : baseHeight; + + if (height == 0) { + return; + } + + final float viewAspectRatio = width / ((float) height); + final float aspectDeformation = videoAspectRatio / viewAspectRatio - 1; + scaleX = 1.0f; + scaleY = 1.0f; + + switch (resizeMode) { + case AspectRatioFrameLayout.RESIZE_MODE_FIT: + if (aspectDeformation > 0) { + height = (int) (width / videoAspectRatio); + } else { + width = (int) (height * videoAspectRatio); + } + + break; + case RESIZE_MODE_ZOOM: + if (aspectDeformation < 0) { + scaleY = viewAspectRatio / videoAspectRatio; + } else { + scaleX = videoAspectRatio / viewAspectRatio; + } + + break; + default: + break; + } + super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + } + + /** + * Scale view only in {@link #onLayout} to make transition for ZOOM mode as smooth as possible. + */ + @Override + protected void onLayout(final boolean changed, + final int left, final int top, final int right, final int bottom) { + setScaleX(scaleX); + setScaleY(scaleY); + } + + /** + * @param base The height that will be used in every resize mode as a minimum height + * @param max The max height for vertical videos in non-FIT resize modes + */ + public void setHeights(final int base, final int max) { + if (baseHeight == base && maxHeight == max) { + return; + } + baseHeight = base; + maxHeight = max; + requestLayout(); + } + + public void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int newResizeMode) { + if (resizeMode == newResizeMode) { + return; + } + + resizeMode = newResizeMode; + requestLayout(); + } + + @AspectRatioFrameLayout.ResizeMode + public int getResizeMode() { + return resizeMode; + } + + public void setAspectRatio(final float aspectRatio) { + if (videoAspectRatio == aspectRatio) { + return; + } + + videoAspectRatio = aspectRatio; + requestLayout(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java index 6dbcded48..a50d5a64c 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java @@ -26,7 +26,7 @@ import android.widget.SeekBar; import androidx.appcompat.widget.AppCompatSeekBar; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; /** * SeekBar, adapted for directional navigation. It emulates touch-related callbacks @@ -60,7 +60,7 @@ public final class FocusAwareSeekBar extends AppCompatSeekBar { @Override public boolean onKeyDown(final int keyCode, final KeyEvent event) { - if (!isInTouchMode() && AndroidTvUtils.isConfirmKey(keyCode)) { + if (!isInTouchMode() && DeviceUtils.isConfirmKey(keyCode)) { releaseTrack(); } diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java index bd5ae10e8..64817a154 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java @@ -38,6 +38,7 @@ import android.view.ViewTreeObserver; import android.view.Window; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.view.WindowCallbackWrapper; @@ -95,7 +96,9 @@ public final class FocusOverlayView extends Drawable implements if (focusedView != null && isShown(focusedView)) { focusedView.getGlobalVisibleRect(focusRect); - } else { + } + + if (shouldClearFocusRect(focusedView, focusRect)) { focusRect.setEmpty(); } @@ -170,6 +173,16 @@ public final class FocusOverlayView extends Drawable implements public void setColorFilter(final ColorFilter colorFilter) { } + /* + * When any view in the player looses it's focus (after setVisibility(GONE)) the focus gets + * added to the whole fragment which has a width and height equal to the window frame. + * The easiest way to avoid the unneeded frame is to skip highlighting of rect that is + * equal to the overlayView bounds + * */ + private boolean shouldClearFocusRect(@Nullable final View focusedView, final Rect focusedRect) { + return focusedView == null || focusedRect.equals(getBounds()); + } + public static void setupFocusObserver(final Dialog dialog) { Rect displayRect = new Rect(); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index e93e83a87..680f484e6 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -89,7 +89,7 @@ public abstract class Postprocessing implements Serializable { } public void setTemporalDir(@NonNull File directory) { - long rnd = (int) (Math.random() * 100000f); + long rnd = (int) (Math.random() * 100000.0f); tempFile = new File(directory, rnd + "_" + System.nanoTime() + ".tmp"); } diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index c2d3a9b9e..d490bcb0f 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -210,7 +210,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb } else { h.progress.setMarquee(false); h.status.setText("100%"); - h.progress.setProgress(1f); + h.progress.setProgress(1.0f); h.size.setText(Utility.formatBytes(item.mission.length)); } } @@ -243,7 +243,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb double progress; if (mission.unknownLength) { progress = Double.NaN; - h.progress.setProgress(0f); + h.progress.setProgress(0.0f); } else { progress = done / length; } @@ -310,7 +310,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb for (int i = 0; i < h.lastSpeed.length; i++) { averageSpeed += h.lastSpeed[i]; } - averageSpeed /= h.lastSpeed.length + 1f; + averageSpeed /= h.lastSpeed.length + 1.0f; } String speedStr = Utility.formatSpeed(averageSpeed); diff --git a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java index 3f638d418..bec947540 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java +++ b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java @@ -26,7 +26,7 @@ public class ProgressDrawable extends Drawable { public ProgressDrawable() { mMarqueeLine = null;// marquee disabled - mMarqueeProgress = 0f; + mMarqueeProgress = 0.0f; mMarqueeSize = 0; mMarqueeNext = 0; } @@ -122,7 +122,7 @@ public class ProgressDrawable extends Drawable { } private void setupMarquee(int width, int height) { - mMarqueeSize = (int) ((width * 10f) / 100f);// the size is 10% of the width + mMarqueeSize = (int) ((width * 10.0f) / 100.0f);// the size is 10% of the width mMarqueeLine.rewind(); mMarqueeLine.moveTo(-mMarqueeSize, -mMarqueeSize); diff --git a/app/src/main/res/drawable/ic_next_white_24dp.xml b/app/src/main/res/drawable/ic_next_white_24dp.xml new file mode 100644 index 000000000..603880c2b --- /dev/null +++ b/app/src/main/res/drawable/ic_next_white_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_previous_white_24dp.xml b/app/src/main/res/drawable/ic_previous_white_24dp.xml new file mode 100644 index 000000000..14279ecb2 --- /dev/null +++ b/app/src/main/res/drawable/ic_previous_white_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index 84a29e0c8..a7872a83a 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -185,7 +185,7 @@ android:orientation="horizontal" tools:ignore="RtlHardcoded"> - @@ -238,7 +238,7 @@ app:srcCompat="@drawable/ic_shuffle_white_24dp" tools:ignore="ContentDescription"/> - 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 cb2b9ccfe..f69832b81 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 @@ -1,7 +1,12 @@ - + + + + + @@ -155,7 +169,6 @@ android:id="@+id/detail_content_root_layout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?android:windowBackground" app:layout_scrollFlags="scroll"> @@ -555,25 +568,22 @@ - + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + - - - + @@ -586,3 +596,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/player.xml similarity index 67% rename from app/src/main/res/layout-large-land/activity_main_player.xml rename to app/src/main/res/layout-large-land/player.xml index 16dcff639..46edda8b7 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/player.xml @@ -2,32 +2,24 @@ - + android:layout_centerInParent="true"/> - - - - - - + - - - - - - - - - - - - - - - - + + + + + - + + + + + android:layout_marginTop="6dp" + android:layout_marginRight="8dp" + tools:ignore="RtlHardcoded" + android:layout_weight="1"> @@ -192,94 +139,86 @@ android:singleLine="true" android:textColor="@android:color/white" android:textSize="12sp" - android:clickable="true" tools:text="The Video Artist LONG very LONG very Long"/>