diff --git a/app/build.gradle b/app/build.gradle index 80171be62..95d35eeac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,7 +33,7 @@ android { // suffix the app id and the app name with git branch name def workingBranch = getGitWorkingBranch() - def normalizedWorkingBranch = workingBranch.replaceAll("[^A-Za-z]+", "").toLowerCase() + def normalizedWorkingBranch = workingBranch.replaceFirst("^[^A-Za-z]+", "").replaceAll("[^0-9A-Za-z]+", "") if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") { // default values when branch name could not be determined or is master or dev applicationIdSuffix ".debug" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b3b80d88..6fd62aebe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,8 +44,9 @@ + android:name=".player.MainPlayer" + android:exported="false" + android:foregroundServiceType="mediaPlayback"> diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 12fdf8c78..251affaed 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -39,6 +39,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; @@ -268,7 +269,7 @@ public class RouterActivity extends AppCompatActivity { final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext); final LinearLayout rootLayout = (LinearLayout) inflater.inflate( - R.layout.preferred_player_dialog_view, null, false); + R.layout.single_choice_dialog_view, null, false); final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { @@ -278,6 +279,7 @@ public class RouterActivity extends AppCompatActivity { handleChoice(choice.key); + // open future streams always like this one, because "always" button was used by user if (which == DialogInterface.BUTTON_POSITIVE) { preferences.edit() .putString(getString(R.string.preferred_open_action_key), choice.key) @@ -377,23 +379,50 @@ public class RouterActivity extends AppCompatActivity { final boolean isExtAudioEnabled = preferences.getBoolean( getString(R.string.use_external_audio_player_key), false); - returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), - getString(R.string.show_info), - resolveResourceIdFromAttr(context, R.attr.ic_info_outline))); + final AdapterChoiceItem videoPlayer = new AdapterChoiceItem( + getString(R.string.video_player_key), getString(R.string.video_player), + resolveResourceIdFromAttr(context, R.attr.ic_play_arrow)); + final AdapterChoiceItem showInfo = new AdapterChoiceItem( + getString(R.string.show_info_key), getString(R.string.show_info), + resolveResourceIdFromAttr(context, R.attr.ic_info_outline)); + final AdapterChoiceItem popupPlayer = new AdapterChoiceItem( + getString(R.string.popup_player_key), getString(R.string.popup_player), + resolveResourceIdFromAttr(context, R.attr.ic_popup)); + final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem( + getString(R.string.background_player_key), getString(R.string.background_player), + resolveResourceIdFromAttr(context, R.attr.ic_headset)); - if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) { - returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key), - getString(R.string.video_player), - resolveResourceIdFromAttr(context, R.attr.ic_play_arrow))); - returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key), - getString(R.string.popup_player), - resolveResourceIdFromAttr(context, R.attr.ic_popup))); - } + if (linkType == LinkType.STREAM) { + if (isExtVideoEnabled) { + // show both "show info" and "video player", they are two different activities + returnList.add(showInfo); + returnList.add(videoPlayer); + } else if (capabilities.contains(VIDEO) + && PlayerHelper.isAutoplayAllowedByUser(context)) { + // show only "video player" since the details activity will be opened and the video + // will be autoplayed there and "show info" would do the exact same thing + returnList.add(videoPlayer); + } else { + // show only "show info" if video player is not applicable or autoplay is disabled + returnList.add(showInfo); + } - if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) { - returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), - getString(R.string.background_player), - resolveResourceIdFromAttr(context, R.attr.ic_headset))); + if (capabilities.contains(VIDEO)) { + returnList.add(popupPlayer); + } + if (capabilities.contains(AUDIO)) { + returnList.add(backgroundPlayer); + } + + } else { + returnList.add(showInfo); + if (capabilities.contains(VIDEO) && !isExtVideoEnabled) { + returnList.add(videoPlayer); + returnList.add(popupPlayer); + } + if (capabilities.contains(AUDIO) && !isExtAudioEnabled) { + returnList.add(backgroundPlayer); + } } returnList.add(new AdapterChoiceItem(getString(R.string.download_key), 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 b731d0270..222fc4687 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 @@ -11,6 +11,7 @@ import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.database.ContentObserver; +import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; @@ -102,13 +103,12 @@ import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.AnimatedProgressBar; 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; @@ -125,6 +125,7 @@ import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; +import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -337,7 +338,7 @@ public class VideoDetailFragment stopPlayerListener(); playerService = null; player = null; - saveCurrentAndRestoreDefaultBrightness(); + restoreDefaultBrightness(); } } @@ -404,7 +405,7 @@ public class VideoDetailFragment settingsContentObserver = new ContentObserver(new Handler()) { @Override public void onChange(final boolean selfChange) { - if (activity != null && !PlayerHelper.globalScreenOrientationLocked(activity)) { + if (activity != null && !globalScreenOrientationLocked(activity)) { activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } } @@ -426,7 +427,7 @@ public class VideoDetailFragment if (currentWorker != null) { currentWorker.dispose(); } - saveCurrentAndRestoreDefaultBrightness(); + restoreDefaultBrightness(); PreferenceManager.getDefaultSharedPreferences(requireContext()) .edit() .putString(getString(R.string.stream_info_selected_tab_key), @@ -538,31 +539,51 @@ public class VideoDetailFragment super.onSaveInstanceState(outState); if (!isLoading.get() && currentInfo != null && isVisible()) { - outState.putSerializable(INFO_KEY, currentInfo); + final String infoCacheKey = SerializedCache.getInstance() + .put(currentInfo, StreamInfo.class); + if (infoCacheKey != null) { + outState.putString(INFO_KEY, infoCacheKey); + } } if (playQueue != null) { - outState.putSerializable(VideoPlayer.PLAY_QUEUE_KEY, playQueue); + final String queueCacheKey = SerializedCache.getInstance() + .put(playQueue, PlayQueue.class); + if (queueCacheKey != null) { + outState.putString(VideoPlayer.PLAY_QUEUE_KEY, queueCacheKey); + } + } + final String stackCacheKey = SerializedCache.getInstance().put(stack, LinkedList.class); + if (stackCacheKey != null) { + outState.putString(STACK_KEY, stackCacheKey); } - outState.putSerializable(STACK_KEY, stack); } @Override protected void onRestoreInstanceState(@NonNull final Bundle savedState) { super.onRestoreInstanceState(savedState); - Serializable serializable = savedState.getSerializable(INFO_KEY); - if (serializable instanceof StreamInfo) { - currentInfo = (StreamInfo) serializable; - InfoCache.getInstance().putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM); + final String infoCacheKey = savedState.getString(INFO_KEY); + if (infoCacheKey != null) { + currentInfo = SerializedCache.getInstance().take(infoCacheKey, StreamInfo.class); + if (currentInfo != null) { + InfoCache.getInstance() + .putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM); + } } - serializable = savedState.getSerializable(STACK_KEY); - if (serializable instanceof Collection) { - //noinspection unchecked - stack.addAll((Collection) serializable); + final String stackCacheKey = savedState.getString(STACK_KEY); + if (stackCacheKey != null) { + final LinkedList cachedStack = + SerializedCache.getInstance().take(stackCacheKey, LinkedList.class); + if (cachedStack != null) { + stack.addAll(cachedStack); + } + } + final String queueCacheKey = savedState.getString(VideoPlayer.PLAY_QUEUE_KEY); + if (queueCacheKey != null) { + playQueue = SerializedCache.getInstance().take(queueCacheKey, PlayQueue.class); } - playQueue = (PlayQueue) savedState.getSerializable(VideoPlayer.PLAY_QUEUE_KEY); } /*////////////////////////////////////////////////////////////////////////// @@ -958,6 +979,9 @@ public class VideoDetailFragment return; } setInitialData(sid, videoUrl, title, queue); + if (player != null) { + player.disablePreloadingOfCurrentTrack(); + } startLoading(false, true); } @@ -1233,7 +1257,7 @@ public class VideoDetailFragment } private boolean isExternalPlayerEnabled() { - return PreferenceManager.getDefaultSharedPreferences(getContext()) + return PreferenceManager.getDefaultSharedPreferences(requireContext()) .getBoolean(getString(R.string.use_external_video_player_key), false); } @@ -1244,23 +1268,7 @@ public class VideoDetailFragment && !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; - } + && PlayerHelper.isAutoplayAllowedByUser(requireContext()); } private void addVideoPlayerView() { @@ -1821,9 +1829,6 @@ public class VideoDetailFragment setOverlayPlayPauseImage(); switch (state) { - case BasePlayer.STATE_COMPLETED: - restoreDefaultOrientation(); - break; case BasePlayer.STATE_PLAYING: if (positionView.getAlpha() != 1.0f && player.getPlayQueue() != null @@ -1882,10 +1887,11 @@ public class VideoDetailFragment public void onPlayerError(final ExoPlaybackException error) { if (error.type == ExoPlaybackException.TYPE_SOURCE || error.type == ExoPlaybackException.TYPE_UNEXPECTED) { - hideMainPlayer(); + // Properly exit from fullscreen if (playerService != null && player.isFullscreen()) { player.toggleFullscreen(); } + hideMainPlayer(); } } @@ -1924,7 +1930,13 @@ public class VideoDetailFragment } scrollToTop(); - addVideoPlayerView(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + addVideoPlayerView(); + } else { + // KitKat needs a delay before addVideoPlayerView call or it reports wrong height in + // activity.getWindow().getDecorView().getHeight() + new Handler().post(this::addVideoPlayerView); + } } @Override @@ -1932,13 +1944,15 @@ public class VideoDetailFragment // 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)) { + // or portrait & unlocked global orientation + if (DeviceUtils.isTablet(activity) + && (!globalScreenOrientationLocked(activity) || isLandscape())) { player.toggleFullscreen(); return; } final int newOrientation = isLandscape() - ? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; activity.setRequestedOrientation(newOrientation); @@ -1983,7 +1997,11 @@ public class VideoDetailFragment WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; } activity.getWindow().getDecorView().setSystemUiVisibility(0); - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr( + requireContext(), android.R.attr.colorPrimary)); + } } private void hideSystemUi() { @@ -1998,18 +2016,26 @@ public class VideoDetailFragment // Prevent jumping of the player on devices with cutout if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { activity.getWindow().getAttributes().layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; } - final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + 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; + // In multiWindow mode status bar is not transparent for devices with cutout + // if I include this flag. So without it is better in this case + if (!isInMultiWindow()) { + visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN; + } activity.getWindow().getDecorView().setSystemUiVisibility(visibility); - activity.getWindow().setFlags( - WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, - WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && (isInMultiWindow() || (player != null && player.isFullscreen()))) { + activity.getWindow().setStatusBarColor(Color.TRANSPARENT); + activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); + } + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } // Listener implementation @@ -2027,13 +2053,11 @@ public class VideoDetailFragment && player.getPlayer().getPlaybackState() != Player.STATE_IDLE; } - private void saveCurrentAndRestoreDefaultBrightness() { + private void restoreDefaultBrightness() { final WindowManager.LayoutParams lp = activity.getWindow().getAttributes(); if (lp.screenBrightness == -1) { return; } - // Save current brightness level - PlayerHelper.setScreenBrightness(activity, lp.screenBrightness); // Restore the old brightness when fragment.onPause() called or // when a player is in portrait @@ -2052,7 +2076,7 @@ public class VideoDetailFragment || !player.isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) { // Apply system brightness when the player is not in fullscreen - saveCurrentAndRestoreDefaultBrightness(); + restoreDefaultBrightness(); } else { // Restore already saved brightness level final float brightnessLevel = PlayerHelper.getScreenBrightness(activity); @@ -2071,11 +2095,9 @@ public class VideoDetailFragment } 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()) { + if (globalScreenOrientationLocked(activity) && !player.isPlaying()) { player.onPlay(); - player.showControlsThenHide(); } } @@ -2278,6 +2300,7 @@ public class VideoDetailFragment && player.videoPlayerSelected()) { player.toggleFullscreen(); } + setOverlayLook(appBarLayout, behavior, 1); break; case BottomSheetBehavior.STATE_COLLAPSED: moveFocusToMainFragment(true); @@ -2287,6 +2310,7 @@ public class VideoDetailFragment if (player != null) { player.onQueueClosed(); } + setOverlayLook(appBarLayout, behavior, 0); break; case BottomSheetBehavior.STATE_DRAGGING: case BottomSheetBehavior.STATE_SETTLING: 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 6817e4595..64eaf3a3d 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 @@ -52,6 +52,7 @@ import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; @@ -78,7 +79,7 @@ import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovement import static java.util.Arrays.asList; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class SearchFragment extends BaseListFragment +public class SearchFragment extends BaseListFragment> implements BackPressable { /*////////////////////////////////////////////////////////////////////////// // Search @@ -133,7 +134,6 @@ public class SearchFragment extends BaseListFragment menuItemToFilterName; private StreamingService service; private Page nextPage; - private String contentCountry; private boolean isSuggestionsEnabled = true; private Disposable searchDisposable; @@ -154,6 +154,7 @@ public class SearchFragment extends BaseListFragment 0 && !isLoading.get()) { hideSuggestionsPanel(); @@ -742,6 +734,13 @@ public class SearchFragment extends BaseListFragment> network = ExtractorHelper .suggestionsFor(serviceId, query) + .onErrorReturn(throwable -> { + if (!ExceptionUtils.isNetworkRelated(throwable)) { + showSnackBarError(throwable, UserAction.GET_SUGGESTIONS, + NewPipe.getNameOfService(serviceId), searchString, 0); + } + return new ArrayList<>(); + }) .toObservable() .map(strings -> { final List result = new ArrayList<>(); @@ -791,21 +790,23 @@ public class SearchFragment extends BaseListFragment - NavigationHelper.getIntentByLink(activity, streamingService, ss)) + .fromCallable(() -> NavigationHelper.getIntentByLink(activity, + streamingService, theSearchString)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(intent -> { @@ -820,29 +821,27 @@ public class SearchFragment extends BaseListFragment { }, error -> showSnackBarError(error, UserAction.SEARCHED, - NewPipe.getNameOfService(serviceId), ss, 0) - ); - suggestionPublisher.onNext(ss); + NewPipe.getNameOfService(serviceId), theSearchString, 0) + )); + suggestionPublisher.onNext(theSearchString); startLoading(false); } @Override public void startLoading(final boolean forceLoad) { super.startLoading(forceLoad); - if (disposables != null) { - disposables.clear(); - } + disposables.clear(); if (searchDisposable != null) { searchDisposable.dispose(); } @@ -881,8 +880,7 @@ public class SearchFragment extends BaseListFragment cf) { - this.filterItemCheckedId = item.getItemId(); + private void changeContentFilter(final MenuItem item, final List theContentFilter) { + filterItemCheckedId = item.getItemId(); item.setChecked(true); - this.contentFilter = new String[]{cf.get(0)}; + contentFilter = new String[]{theContentFilter.get(0)}; if (!TextUtils.isEmpty(searchString)) { - search(searchString, this.contentFilter, sortFilter); + search(searchString, contentFilter, sortFilter); } } - private void setQuery(final int sid, final String ss, final String[] cf, final String sf) { - this.serviceId = sid; - this.searchString = searchString; - this.contentFilter = cf; - this.sortFilter = sf; + private void setQuery(final int theServiceId, + final String theSearchString, + final String[] theContentFilter, + final String theSortFilter) { + serviceId = theServiceId; + searchString = theSearchString; + contentFilter = theContentFilter; + sortFilter = theSortFilter; } /*////////////////////////////////////////////////////////////////////////// @@ -924,7 +925,7 @@ public class SearchFragment extends BaseListFragment suggestionListAdapter.setItems(suggestions)); - if (errorPanelRoot.getVisibility() == View.VISIBLE) { + if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) { hideLoading(); } } @@ -1027,7 +1028,7 @@ public class SearchFragment extends BaseListFragment result) { showListFooter(false); infoListAdapter.addInfoItemList(result.getItems()); nextPage = result.getNextPage(); @@ -1066,8 +1067,7 @@ public class SearchFragment extends BaseListFragment simpleExoPlayer.getDuration()) { + normalizedPositionMillis = simpleExoPlayer.getDuration(); + } + + simpleExoPlayer.seekTo(normalizedPositionMillis); } } @@ -1593,6 +1604,11 @@ public abstract class BasePlayer implements return currentMetadata; } + @NonNull + public LoadController getLoadController() { + return (LoadController) loadControl; + } + @NonNull public String getVideoUrl() { return currentMetadata == null diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java index 273f37cc8..0aed3469f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -19,35 +19,18 @@ 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 androidx.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 android.view.ViewGroup; +import android.view.WindowManager; -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; @@ -64,7 +47,6 @@ public final class MainPlayer extends Service { private VideoPlayerImpl playerImpl; private WindowManager windowManager; - private SharedPreferences sharedPreferences; private final IBinder mBinder = new MainPlayer.LocalBinder(); @@ -78,30 +60,26 @@ public final class MainPlayer extends Service { // 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"; + 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"; + static final String ACTION_SHUFFLE + = "org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE"; + public static final String ACTION_RECREATE_NOTIFICATION + = "org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION"; /*////////////////////////////////////////////////////////////////////////// // Service's LifeCycle @@ -113,9 +91,7 @@ public final class MainPlayer extends Service { 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(); @@ -143,7 +119,7 @@ public final class MainPlayer extends Service { if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) || intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) { - showNotificationAndStartForeground(); + NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this); } playerImpl.handleIntent(intent); @@ -171,12 +147,13 @@ public final class MainPlayer extends Service { // 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); + playerImpl.onQueueClosed(); // 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); + NotificationUtil.getInstance().cancelNotificationAndStopForeground(this); } } } @@ -219,6 +196,10 @@ public final class MainPlayer extends Service { } if (playerImpl != null) { + // Exit from fullscreen when user closes the player via notification + if (playerImpl.isFullscreen()) { + playerImpl.toggleFullscreen(); + } removeViewFromParent(); playerImpl.setRecovery(); @@ -227,11 +208,8 @@ public final class MainPlayer extends Service { playerImpl.removePopupFromView(); playerImpl.destroy(); } - if (notificationManager != null) { - notificationManager.cancel(NOTIFICATION_ID); - } - stopForeground(true); + NotificationUtil.getInstance().cancelNotificationAndStopForeground(this); stopSelf(); } @@ -270,206 +248,6 @@ public final class MainPlayer extends Service { } } - 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); - } - - 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 { diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java b/app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java new file mode 100644 index 000000000..cf58c8f76 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java @@ -0,0 +1,165 @@ +package org.schabi.newpipe.player; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.DrawableRes; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.Localization; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +public final class NotificationConstants { + + private NotificationConstants() { } + + + public static final int NOTHING = 0; + public static final int PREVIOUS = 1; + public static final int NEXT = 2; + public static final int REWIND = 3; + public static final int FORWARD = 4; + public static final int SMART_REWIND_PREVIOUS = 5; + public static final int SMART_FORWARD_NEXT = 6; + public static final int PLAY_PAUSE = 7; + public static final int PLAY_PAUSE_BUFFERING = 8; + public static final int REPEAT = 9; + public static final int SHUFFLE = 10; + public static final int CLOSE = 11; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, + PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT, SHUFFLE, CLOSE}) + public @interface Action { } + + @DrawableRes + public static final int[] ACTION_ICONS = { + 0, + R.drawable.exo_icon_previous, + R.drawable.exo_icon_next, + R.drawable.exo_icon_rewind, + R.drawable.exo_icon_fastforward, + R.drawable.exo_icon_previous, + R.drawable.exo_icon_next, + R.drawable.ic_pause_white_24dp, + R.drawable.ic_hourglass_top_white_24dp, + R.drawable.exo_icon_repeat_all, + R.drawable.exo_icon_shuffle_on, + R.drawable.ic_close_white_24dp, + }; + + + @Action + public static final int[] SLOT_DEFAULTS = { + SMART_REWIND_PREVIOUS, + PLAY_PAUSE_BUFFERING, + SMART_FORWARD_NEXT, + REPEAT, + CLOSE, + }; + + @Action + public static final int[][] SLOT_ALLOWED_ACTIONS = { + new int[] {PREVIOUS, REWIND, SMART_REWIND_PREVIOUS}, + new int[] {REWIND, PLAY_PAUSE, PLAY_PAUSE_BUFFERING}, + new int[] {NEXT, FORWARD, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING}, + new int[] {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, + SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE}, + new int[] {NOTHING, NEXT, FORWARD, SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE}, + }; + + public static final int[] SLOT_PREF_KEYS = { + R.string.notification_slot_0_key, + R.string.notification_slot_1_key, + R.string.notification_slot_2_key, + R.string.notification_slot_3_key, + R.string.notification_slot_4_key, + }; + + + public static final Integer[] SLOT_COMPACT_DEFAULTS = {0, 1, 2}; + + public static final int[] SLOT_COMPACT_PREF_KEYS = { + R.string.notification_slot_compact_0_key, + R.string.notification_slot_compact_1_key, + R.string.notification_slot_compact_2_key, + }; + + + public static String getActionName(@NonNull final Context context, @Action final int action) { + switch (action) { + case PREVIOUS: + return context.getString(R.string.exo_controls_previous_description); + case NEXT: + return context.getString(R.string.exo_controls_next_description); + case REWIND: + return context.getString(R.string.exo_controls_rewind_description); + case FORWARD: + return context.getString(R.string.exo_controls_fastforward_description); + case SMART_REWIND_PREVIOUS: + return Localization.concatenateStrings( + context.getString(R.string.exo_controls_rewind_description), + context.getString(R.string.exo_controls_previous_description)); + case SMART_FORWARD_NEXT: + return Localization.concatenateStrings( + context.getString(R.string.exo_controls_fastforward_description), + context.getString(R.string.exo_controls_next_description)); + case PLAY_PAUSE: + return Localization.concatenateStrings( + context.getString(R.string.exo_controls_play_description), + context.getString(R.string.exo_controls_pause_description)); + case PLAY_PAUSE_BUFFERING: + return Localization.concatenateStrings( + context.getString(R.string.exo_controls_play_description), + context.getString(R.string.exo_controls_pause_description), + context.getString(R.string.notification_action_buffering)); + case REPEAT: + return context.getString(R.string.notification_action_repeat); + case SHUFFLE: + return context.getString(R.string.notification_action_shuffle); + case CLOSE: + return context.getString(R.string.close); + case NOTHING: default: + return context.getString(R.string.notification_action_nothing); + } + } + + + /** + * @param context the context to use + * @param sharedPreferences the shared preferences to query values from + * @param slotCount remove indices >= than this value (set to {@code 5} to do nothing, or make + * it lower if there are slots with empty actions) + * @return a sorted list of the indices of the slots to use as compact slots + */ + public static List getCompactSlotsFromPreferences( + @NonNull final Context context, + final SharedPreferences sharedPreferences, + final int slotCount) { + final SortedSet compactSlots = new TreeSet<>(); + for (int i = 0; i < 3; i++) { + final int compactSlot = sharedPreferences.getInt( + context.getString(SLOT_COMPACT_PREF_KEYS[i]), Integer.MAX_VALUE); + + if (compactSlot == Integer.MAX_VALUE) { + // settings not yet populated, return default values + return new ArrayList<>(Arrays.asList(SLOT_COMPACT_DEFAULTS)); + } + + // a negative value (-1) is set when the user does not want a particular compact slot + if (compactSlot >= 0 && compactSlot < slotCount) { + compactSlots.add(compactSlot); + } + } + return new ArrayList<>(compactSlots); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java new file mode 100644 index 000000000..370631116 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java @@ -0,0 +1,371 @@ +package org.schabi.newpipe.player; + +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.content.pm.ServiceInfo; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; + +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.NavigationHelper; + +import java.util.List; + +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; +import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; +import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; +import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; +import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; +import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; + +/** + * This is a utility class for player notifications. + * + * @author cool-student + */ +public final class NotificationUtil { + private static final String TAG = NotificationUtil.class.getSimpleName(); + private static final boolean DEBUG = BasePlayer.DEBUG; + private static final int NOTIFICATION_ID = 123789; + + @Nullable private static NotificationUtil instance = null; + + @NotificationConstants.Action + private int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone(); + + private NotificationManagerCompat notificationManager; + private NotificationCompat.Builder notificationBuilder; + + private NotificationUtil() { + } + + public static NotificationUtil getInstance() { + if (instance == null) { + instance = new NotificationUtil(); + } + return instance; + } + + + ///////////////////////////////////////////////////// + // NOTIFICATION + ///////////////////////////////////////////////////// + + /** + * Creates the notification if it does not exist already and recreates it if forceRecreate is + * true. Updates the notification with the data in the player. + * @param player the player currently open, to take data from + * @param forceRecreate whether to force the recreation of the notification even if it already + * exists + */ + synchronized void createNotificationIfNeededAndUpdate(final VideoPlayerImpl player, + final boolean forceRecreate) { + if (forceRecreate || notificationBuilder == null) { + notificationBuilder = createNotification(player); + } + updateNotification(player); + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } + + private synchronized NotificationCompat.Builder createNotification( + final VideoPlayerImpl player) { + if (DEBUG) { + Log.d(TAG, "createNotification()"); + } + notificationManager = NotificationManagerCompat.from(player.context); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(player.context, + player.context.getString(R.string.notification_channel_id)); + + initializeNotificationSlots(player); + + // count the number of real slots, to make sure compact slots indices are not out of bound + int nonNothingSlotCount = 5; + if (notificationSlots[3] == NotificationConstants.NOTHING) { + --nonNothingSlotCount; + } + if (notificationSlots[4] == NotificationConstants.NOTHING) { + --nonNothingSlotCount; + } + + // build the compact slot indices array (need code to convert from Integer... because Java) + final List compactSlotList = NotificationConstants.getCompactSlotsFromPreferences( + player.context, player.sharedPreferences, nonNothingSlotCount); + final int[] compactSlots = new int[compactSlotList.size()]; + for (int i = 0; i < compactSlotList.size(); i++) { + compactSlots[i] = compactSlotList.get(i); + } + + builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle() + .setMediaSession(player.mediaSessionManager.getSessionToken()) + .setShowActionsInCompactView(compactSlots)) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setColor(ContextCompat.getColor(player.context, R.color.gray)) + .setCategory(NotificationCompat.CATEGORY_TRANSPORT) + .setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID, + new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT)); + + return builder; + } + + /** + * Updates the notification builder and the button icons depending on the playback state. + * @param player the player currently open, to take data from + */ + private synchronized void updateNotification(final VideoPlayerImpl player) { + if (DEBUG) { + Log.d(TAG, "updateNotification()"); + } + + // also update content intent, in case the user switched players + notificationBuilder.setContentIntent(PendingIntent.getActivity(player.context, + NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT)); + notificationBuilder.setContentTitle(player.getVideoTitle()); + notificationBuilder.setContentText(player.getUploaderName()); + notificationBuilder.setTicker(player.getVideoTitle()); + updateActions(notificationBuilder, player); + setLargeIcon(notificationBuilder, player); + } + + + @SuppressLint("RestrictedApi") + boolean shouldUpdateBufferingSlot() { + if (notificationBuilder.mActions.size() < 3) { + // this should never happen, but let's make sure notification actions are populated + return true; + } + + // only second and third slot could contain PLAY_PAUSE_BUFFERING, update them only if they + // are not already in the buffering state (the only one with a null action intent) + return (notificationSlots[1] == NotificationConstants.PLAY_PAUSE_BUFFERING + && notificationBuilder.mActions.get(1).actionIntent != null) + || (notificationSlots[2] == NotificationConstants.PLAY_PAUSE_BUFFERING + && notificationBuilder.mActions.get(2).actionIntent != null); + } + + + void createNotificationAndStartForeground(final VideoPlayerImpl player, final Service service) { + if (notificationBuilder == null) { + notificationBuilder = createNotification(player); + } + updateNotification(player); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + service.startForeground(NOTIFICATION_ID, notificationBuilder.build(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK); + } else { + service.startForeground(NOTIFICATION_ID, notificationBuilder.build()); + } + } + + void cancelNotificationAndStopForeground(final Service service) { + service.stopForeground(true); + + if (notificationManager != null) { + notificationManager.cancel(NOTIFICATION_ID); + } + notificationManager = null; + notificationBuilder = null; + } + + + ///////////////////////////////////////////////////// + // ACTIONS + ///////////////////////////////////////////////////// + + private void initializeNotificationSlots(final VideoPlayerImpl player) { + for (int i = 0; i < 5; ++i) { + notificationSlots[i] = player.sharedPreferences.getInt( + player.context.getString(NotificationConstants.SLOT_PREF_KEYS[i]), + NotificationConstants.SLOT_DEFAULTS[i]); + } + } + + @SuppressLint("RestrictedApi") + private void updateActions(final NotificationCompat.Builder builder, + final VideoPlayerImpl player) { + builder.mActions.clear(); + for (int i = 0; i < 5; ++i) { + addAction(builder, player, notificationSlots[i]); + } + } + + private void addAction(final NotificationCompat.Builder builder, + final VideoPlayerImpl player, + @NotificationConstants.Action final int slot) { + final NotificationCompat.Action action = getAction(player, slot); + if (action != null) { + builder.addAction(action); + } + } + + @Nullable + private NotificationCompat.Action getAction( + final VideoPlayerImpl player, + @NotificationConstants.Action final int selectedAction) { + final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction]; + switch (selectedAction) { + case NotificationConstants.PREVIOUS: + return getAction(player, baseActionIcon, + R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); + + case NotificationConstants.NEXT: + return getAction(player, baseActionIcon, + R.string.exo_controls_next_description, ACTION_PLAY_NEXT); + + case NotificationConstants.REWIND: + return getAction(player, baseActionIcon, + R.string.exo_controls_rewind_description, ACTION_FAST_REWIND); + + case NotificationConstants.FORWARD: + return getAction(player, baseActionIcon, + R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); + + case NotificationConstants.SMART_REWIND_PREVIOUS: + if (player.playQueue != null && player.playQueue.size() > 1) { + return getAction(player, R.drawable.exo_notification_previous, + R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); + } else { + return getAction(player, R.drawable.exo_controls_rewind, + R.string.exo_controls_rewind_description, ACTION_FAST_REWIND); + } + + case NotificationConstants.SMART_FORWARD_NEXT: + if (player.playQueue != null && player.playQueue.size() > 1) { + return getAction(player, R.drawable.exo_notification_next, + R.string.exo_controls_next_description, ACTION_PLAY_NEXT); + } else { + return getAction(player, R.drawable.exo_controls_fastforward, + R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); + } + + case NotificationConstants.PLAY_PAUSE_BUFFERING: + if (player.getCurrentState() == BasePlayer.STATE_PREFLIGHT + || player.getCurrentState() == BasePlayer.STATE_BLOCKED + || player.getCurrentState() == BasePlayer.STATE_BUFFERING) { + // null intent -> show hourglass icon that does nothing when clicked + return new NotificationCompat.Action(R.drawable.ic_hourglass_top_white_24dp_png, + player.context.getString(R.string.notification_action_buffering), + null); + } + + case NotificationConstants.PLAY_PAUSE: + if (player.getCurrentState() == BasePlayer.STATE_COMPLETED) { + return getAction(player, R.drawable.ic_replay_white_24dp_png, + R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); + } else if (player.isPlaying() + || player.getCurrentState() == BasePlayer.STATE_PREFLIGHT + || player.getCurrentState() == BasePlayer.STATE_BLOCKED + || player.getCurrentState() == BasePlayer.STATE_BUFFERING) { + return getAction(player, R.drawable.exo_notification_pause, + R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); + } else { + return getAction(player, R.drawable.exo_notification_play, + R.string.exo_controls_play_description, ACTION_PLAY_PAUSE); + } + + case NotificationConstants.REPEAT: + if (player.getRepeatMode() == REPEAT_MODE_ALL) { + return getAction(player, R.drawable.exo_media_action_repeat_all, + R.string.exo_controls_repeat_all_description, ACTION_REPEAT); + } else if (player.getRepeatMode() == REPEAT_MODE_ONE) { + return getAction(player, R.drawable.exo_media_action_repeat_one, + R.string.exo_controls_repeat_one_description, ACTION_REPEAT); + } else /* player.getRepeatMode() == REPEAT_MODE_OFF */ { + return getAction(player, R.drawable.exo_media_action_repeat_off, + R.string.exo_controls_repeat_off_description, ACTION_REPEAT); + } + + case NotificationConstants.SHUFFLE: + if (player.playQueue != null && player.playQueue.isShuffled()) { + return getAction(player, R.drawable.exo_controls_shuffle_on, + R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE); + } else { + return getAction(player, R.drawable.exo_controls_shuffle_off, + R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE); + } + + case NotificationConstants.CLOSE: + return getAction(player, R.drawable.ic_close_white_24dp_png, + R.string.close, ACTION_CLOSE); + + case NotificationConstants.NOTHING: + default: + // do nothing + return null; + } + } + + private NotificationCompat.Action getAction(final VideoPlayerImpl player, + @DrawableRes final int drawable, + @StringRes final int title, + final String intentAction) { + return new NotificationCompat.Action(drawable, player.context.getString(title), + PendingIntent.getBroadcast(player.context, NOTIFICATION_ID, + new Intent(intentAction), FLAG_UPDATE_CURRENT)); + } + + private Intent getIntentForNotification(final VideoPlayerImpl player) { + if (player.audioPlayerSelected() || player.popupPlayerSelected()) { + // Means we play in popup or audio only. Let's show the play queue + return NavigationHelper.getPlayQueueActivityIntent(player.context); + } else { + // We are playing in fragment. Don't open another activity just show fragment. That's it + final Intent intent = NavigationHelper.getPlayerIntent( + player.context, MainActivity.class, null, true); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + return intent; + } + } + + + ///////////////////////////////////////////////////// + // BITMAP + ///////////////////////////////////////////////////// + + private void setLargeIcon(final NotificationCompat.Builder builder, + final VideoPlayerImpl player) { + final boolean scaleImageToSquareAspectRatio = player.sharedPreferences.getBoolean( + player.context.getString(R.string.scale_to_square_image_in_notifications_key), + false); + if (scaleImageToSquareAspectRatio) { + builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail())); + } else { + builder.setLargeIcon(player.getThumbnail()); + } + } + + private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) { + return getResizedBitmap(bitmap, bitmap.getWidth(), bitmap.getWidth()); + } + + private Bitmap getResizedBitmap(final Bitmap bitmap, final int newWidth, final int newHeight) { + final int width = bitmap.getWidth(); + final int height = bitmap.getHeight(); + final float scaleWidth = ((float) newWidth) / width; + final float scaleHeight = ((float) newHeight) / height; + final Matrix matrix = new Matrix(); + matrix.postScale(scaleWidth, scaleHeight); + return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false); + } +} 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 f43814944..78a42875f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -131,6 +131,8 @@ public abstract class VideoPlayer extends BasePlayer private View controlsRoot; private TextView currentDisplaySeek; + private View playerTopShadow; + private View playerBottomShadow; private View bottomControlsRoot; private SeekBar playbackSeekBar; @@ -193,6 +195,8 @@ public abstract class VideoPlayer extends BasePlayer this.controlAnimationView = view.findViewById(R.id.controlAnimationView); this.controlsRoot = view.findViewById(R.id.playbackControlRoot); this.currentDisplaySeek = view.findViewById(R.id.currentDisplaySeek); + this.playerTopShadow = view.findViewById(R.id.playerTopShadow); + this.playerBottomShadow = view.findViewById(R.id.playerBottomShadow); this.playbackSeekBar = view.findViewById(R.id.playbackSeekBar); this.playbackCurrentTime = view.findViewById(R.id.playbackCurrentTime); this.playbackEndTime = view.findViewById(R.id.playbackEndTime); @@ -359,11 +363,11 @@ public abstract class VideoPlayer extends BasePlayer return true; }); // apply caption language from previous user preference - if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage) - || searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage) - || userPreferredLanguage.contains("(") && captionLanguage.startsWith( - userPreferredLanguage - .substring(0, userPreferredLanguage.indexOf('('))))) { + if (userPreferredLanguage != null + && (captionLanguage.equals(userPreferredLanguage) + || (searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage)) + || (userPreferredLanguage.contains("(") && captionLanguage.startsWith( + userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); if (textRendererIndex != RENDERER_UNAVAILABLE) { trackSelector.setPreferredTextLanguage(captionLanguage); @@ -857,7 +861,6 @@ public abstract class VideoPlayer extends BasePlayer } qualityPopupMenu.show(); isSomePopupMenuVisible = true; - showControls(DEFAULT_CONTROLS_DURATION); final VideoStream videoStream = getSelectedVideoStream(); if (videoStream != null) { @@ -875,7 +878,6 @@ public abstract class VideoPlayer extends BasePlayer } playbackSpeedPopupMenu.show(); isSomePopupMenuVisible = true; - showControls(DEFAULT_CONTROLS_DURATION); } private void onCaptionClicked() { @@ -884,7 +886,6 @@ public abstract class VideoPlayer extends BasePlayer } captionPopupMenu.show(); isSomePopupMenuVisible = true; - showControls(DEFAULT_CONTROLS_DURATION); } void onResizeClicked() { @@ -1061,6 +1062,7 @@ public abstract class VideoPlayer extends BasePlayer ? DEFAULT_CONTROLS_HIDE_TIME : DPAD_CONTROLS_HIDE_TIME; + showHideShadow(true, DEFAULT_CONTROLS_DURATION, 0); animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0, () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime)); } @@ -1070,6 +1072,7 @@ public abstract class VideoPlayer extends BasePlayer Log.d(TAG, "showControls() called"); } controlsVisibilityHandler.removeCallbacksAndMessages(null); + showHideShadow(true, duration, 0); animateView(controlsRoot, true, duration); } @@ -1089,8 +1092,10 @@ public abstract class VideoPlayer extends BasePlayer Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); } controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler.postDelayed(() -> - animateView(controlsRoot, false, duration), delay); + controlsVisibilityHandler.postDelayed(() -> { + showHideShadow(false, duration, 0); + animateView(controlsRoot, false, duration); + }, delay); } public void hideControlsAndButton(final long duration, final long delay, final View button) { @@ -1109,6 +1114,11 @@ public abstract class VideoPlayer extends BasePlayer }; } + void showHideShadow(final boolean show, final long duration, final long delay) { + animateView(playerTopShadow, show, duration, delay, null); + animateView(playerBottomShadow, show, duration, delay, null); + } + public abstract void hideSystemUIIfNeeded(); /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java index 580acc6e5..b327d619a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -27,22 +27,21 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.database.ContentObserver; import android.graphics.Bitmap; +import android.graphics.Color; import android.graphics.PixelFormat; -import android.graphics.Point; import android.net.Uri; import android.os.Build; import android.os.Handler; +import android.view.DisplayCutout; import androidx.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; @@ -57,7 +56,6 @@ import android.widget.RelativeLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -67,6 +65,7 @@ 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.Timeline; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; @@ -107,7 +106,6 @@ import java.util.List; import java.util.Set; 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; @@ -115,10 +113,11 @@ 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_RECREATE_NOTIFICATION; import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; -import static org.schabi.newpipe.player.MainPlayer.NOTIFICATION_ID; +import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; 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.player.helper.PlayerHelper.globalScreenOrientationLocked; 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; @@ -141,14 +140,12 @@ public class VideoPlayerImpl extends VideoPlayer 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; @@ -195,7 +192,6 @@ public class VideoPlayerImpl extends VideoPlayer private boolean isVerticalVideo = false; private boolean fragmentIsVisible = false; boolean shouldUpdateOnProgress; - int timesNotificationUpdated; private final MainPlayer service; private PlayerServiceEventListener fragmentListener; @@ -206,9 +202,6 @@ public class VideoPlayerImpl extends VideoPlayer @NonNull private final AudioPlaybackResolver resolver; - private int cachedDuration; - private String cachedDurationString; - // Popup private WindowManager.LayoutParams popupLayoutParams; public WindowManager windowManager; @@ -263,6 +256,7 @@ public class VideoPlayerImpl extends VideoPlayer } else { getRootView().setVisibility(View.VISIBLE); initVideoPlayer(); + onQueueClosed(); // Android TV: without it focus will frame the whole player playPauseButton.requestFocus(); } @@ -320,6 +314,9 @@ public class VideoPlayerImpl extends VideoPlayer titleTextView.setSelected(true); channelTextView.setSelected(true); + + // Prevent hiding of bottom sheet via swipe inside queue + this.itemsList.setNestedScrollingEnabled(false); } @Override @@ -344,7 +341,6 @@ public class VideoPlayerImpl extends VideoPlayer * 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()) { @@ -497,6 +493,17 @@ public class VideoPlayerImpl extends VideoPlayer Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, settingsContentObserver); getRootView().addOnLayoutChangeListener(this); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + queueLayout.setOnApplyWindowInsetsListener((view, windowInsets) -> { + final DisplayCutout cutout = windowInsets.getDisplayCutout(); + if (cutout != null) { + view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), + cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); + } + return windowInsets; + }); + } } public boolean onKeyDown(final int keyCode) { @@ -594,29 +601,32 @@ public class VideoPlayerImpl extends VideoPlayer setupScreenRotationButton(); } - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Video Listener - //////////////////////////////////////////////////////////////////////////*/ + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Video Listener + //////////////////////////////////////////////////////////////////////////*/ + + void onShuffleOrRepeatModeChanged() { + updatePlaybackButtons(); + updatePlayback(); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } @Override public void onRepeatModeChanged(final int i) { super.onRepeatModeChanged(i); - updatePlaybackButtons(); - updatePlayback(); - service.resetNotification(); - service.updateNotification(-1); + onShuffleOrRepeatModeChanged(); } @Override public void onShuffleClicked() { super.onShuffleClicked(); - updatePlaybackButtons(); - updatePlayback(); + onShuffleOrRepeatModeChanged(); + } - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ + /*////////////////////////////////////////////////////////////////////////// + // Playback Listener + //////////////////////////////////////////////////////////////////////////*/ @Override public void onPlayerError(final ExoPlaybackException error) { @@ -627,6 +637,13 @@ public class VideoPlayerImpl extends VideoPlayer } } + @Override + public void onTimelineChanged(final Timeline timeline, final int reason) { + super.onTimelineChanged(timeline, reason); + // force recreate notification to ensure seek bar is shown when preparation finishes + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); + } + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { super.onMetadataChanged(tag); @@ -637,8 +654,7 @@ public class VideoPlayerImpl extends VideoPlayer titleTextView.setText(tag.getMetadata().getName()); channelTextView.setText(tag.getMetadata().getUploaderName()); - service.resetNotification(); - service.updateNotification(-1); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); updateMetadata(); } @@ -680,35 +696,17 @@ public class VideoPlayerImpl extends VideoPlayer 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; - } + // setMetadata only updates the metadata when any of the metadata keys are null + mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(), getThumbnail(), + duration); + } - 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 + public void onPlayQueueEdited() { + updatePlayback(); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); } @Override @@ -746,53 +744,33 @@ public class VideoPlayerImpl extends VideoPlayer } /*////////////////////////////////////////////////////////////////////////// - // Player Overrides - //////////////////////////////////////////////////////////////////////////*/ + // Player Overrides + //////////////////////////////////////////////////////////////////////////*/ @Override public void toggleFullscreen() { if (DEBUG) { Log.d(TAG, "toggleFullscreen() called"); } - if (simpleExoPlayer == null || getCurrentMetadata() == null) { + if (popupPlayerSelected() + || simpleExoPlayer == null + || getCurrentMetadata() == null + || fragmentListener == null) { return; } - if (popupPlayerSelected()) { - setRecovery(); - service.removeViewFromParent(); - final 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; + isFullscreen = !isFullscreen; + if (!isFullscreen) { + // Apply window insets because Android will not do it when orientation changes + // from landscape to portrait (open vertical video to reproduce) + getControlsRoot().setPadding(0, 0, 0, 0); } else { - if (fragmentListener == null) { - return; - } - - isFullscreen = !isFullscreen; - setControlsSize(); - fragmentListener.onFullscreenStateChanged(isFullscreen()); + // Android needs tens milliseconds to send new insets but a user is able to see + // how controls changes it's position from `0` to `nav bar height` padding. + // So just hide the controls to hide this visual inconsistency + hideControls(0, 0); } + fragmentListener.onFullscreenStateChanged(isFullscreen()); if (!isFullscreen()) { titleTextView.setVisibility(View.GONE); @@ -806,6 +784,40 @@ public class VideoPlayerImpl extends VideoPlayer setupScreenRotationButton(); } + public void switchFromPopupToMain() { + if (DEBUG) { + Log.d(TAG, "switchFromPopupToMain() called"); + } + if (!popupPlayerSelected() || simpleExoPlayer == null || getCurrentMetadata() == null) { + return; + } + + setRecovery(); + service.removeViewFromParent(); + final 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); + } + @Override public void onClick(final View v) { super.onClick(v); @@ -833,9 +845,12 @@ public class VideoPlayerImpl extends VideoPlayer } else if (v.getId() == openInBrowser.getId()) { onOpenInBrowserClicked(); } else if (v.getId() == fullscreenButton.getId()) { - toggleFullscreen(); + switchFromPopupToMain(); } else if (v.getId() == screenRotationButton.getId()) { - if (!isVerticalVideo) { + // Only if it's not a vertical video or vertical video but in landscape with locked + // orientation a screen orientation can be changed automatically + if (!isVerticalVideo + || (service.isLandscape() && globalScreenOrientationLocked(service))) { fragmentListener.onScreenRotationButtonClicked(); } else { toggleFullscreen(); @@ -850,9 +865,12 @@ public class VideoPlayerImpl extends VideoPlayer if (getCurrentState() != STATE_COMPLETED) { getControlsVisibilityHandler().removeCallbacksAndMessages(null); + showHideShadow(true, DEFAULT_CONTROLS_DURATION, 0); animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { - if (v.getId() == playPauseButton.getId()) { + if (v.getId() == playPauseButton.getId() + // Hide controls in fullscreen immediately + || (v.getId() == screenRotationButton.getId() && isFullscreen)) { hideControls(0, 0); } else { hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); @@ -918,7 +936,7 @@ public class VideoPlayerImpl extends VideoPlayer buildQueue(); updatePlaybackButtons(); - getControlsRoot().setVisibility(View.INVISIBLE); + hideControls(0, 0); queueLayout.requestFocus(); animateView(queueLayout, SLIDE_AND_ALPHA, true, DEFAULT_CONTROLS_DURATION); @@ -1010,9 +1028,8 @@ public class VideoPlayerImpl extends VideoPlayer private void setupScreenRotationButton() { final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service); - final boolean tabletInLandscape = DeviceUtils.isTablet(service) && service.isLandscape(); final boolean showButton = videoPlayerSelected() - && (orientationLocked || isVerticalVideo || tabletInLandscape); + && (orientationLocked || isVerticalVideo || DeviceUtils.isTablet(service)); screenRotationButton.setVisibility(showButton ? View.VISIBLE : View.GONE); screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(service, isFullscreen() ? R.drawable.ic_fullscreen_exit_white_24dp @@ -1024,6 +1041,8 @@ public class VideoPlayerImpl extends VideoPlayer if (orientationLocked && isFullscreen() && service.isLandscape() == isVerticalVideo + && !DeviceUtils.isTv(service) + && !DeviceUtils.isTablet(service) && fragmentListener != null) { fragmentListener.onScreenRotationButtonClicked(); } @@ -1054,6 +1073,7 @@ public class VideoPlayerImpl extends VideoPlayer super.onDismiss(menu); if (isPlaying()) { hideControls(DEFAULT_CONTROLS_DURATION, 0); + hideSystemUIIfNeeded(); } } @@ -1078,15 +1098,6 @@ public class VideoPlayerImpl extends VideoPlayer setInitialGestureValues(); queueLayout.getLayoutParams().height = height - queueLayout.getTop(); - - if (popupPlayerSelected()) { - final 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); - } } } @@ -1169,8 +1180,7 @@ public class VideoPlayerImpl extends VideoPlayer animatePlayButtons(false, 100); getRootView().setKeepScreenOn(false); - service.resetNotification(); - service.updateNotification(R.drawable.exo_controls_play); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); } @Override @@ -1178,8 +1188,9 @@ public class VideoPlayerImpl extends VideoPlayer super.onBuffering(); getRootView().setKeepScreenOn(true); - service.resetNotification(); - service.updateNotification(R.drawable.exo_controls_play); + if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) { + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } } @Override @@ -1197,10 +1208,7 @@ public class VideoPlayerImpl extends VideoPlayer checkLandscape(); getRootView().setKeepScreenOn(true); - service.resetNotification(); - service.updateNotification(R.drawable.exo_controls_pause); - - service.startForeground(NOTIFICATION_ID, service.getNotBuilder().build()); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); } @Override @@ -1216,13 +1224,12 @@ public class VideoPlayerImpl extends VideoPlayer 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); + NotificationUtil.getInstance().cancelNotificationAndStopForeground(service); + } else { + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); } getRootView().setKeepScreenOn(false); @@ -1234,8 +1241,7 @@ public class VideoPlayerImpl extends VideoPlayer animatePlayButtons(false, 100); getRootView().setKeepScreenOn(true); - service.resetNotification(); - service.updateNotification(R.drawable.exo_controls_play); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); } @@ -1245,20 +1251,20 @@ public class VideoPlayerImpl extends VideoPlayer playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); }); - getRootView().setKeepScreenOn(false); + getRootView().setKeepScreenOn(false); updateWindowFlags(IDLE_WINDOW_FLAGS); - service.resetNotification(); - service.updateNotification(R.drawable.ic_replay_white_24dp); - + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + if (isFullscreen) { + toggleFullscreen(); + } super.onCompleted(); } @Override public void destroy() { super.destroy(); - service.getContentResolver().unregisterContentObserver(settingsContentObserver); } @@ -1282,6 +1288,8 @@ public class VideoPlayerImpl extends VideoPlayer intentFilter.addAction(ACTION_PLAY_NEXT); intentFilter.addAction(ACTION_FAST_REWIND); intentFilter.addAction(ACTION_FAST_FORWARD); + intentFilter.addAction(ACTION_SHUFFLE); + intentFilter.addAction(ACTION_RECREATE_NOTIFICATION); intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED); intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED); @@ -1331,6 +1339,17 @@ public class VideoPlayerImpl extends VideoPlayer case ACTION_REPEAT: onRepeatClicked(); break; + case ACTION_SHUFFLE: + onShuffleClicked(); + break; + case ACTION_RECREATE_NOTIFICATION: + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); + break; + case Intent.ACTION_HEADSET_PLUG: //FIXME + /*notificationManager.cancel(NOTIFICATION_ID); + mediaSessionManager.dispose(); + mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/ + break; case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED: fragmentIsVisible = true; useVideoSource(true); @@ -1349,20 +1368,6 @@ public class VideoPlayerImpl extends VideoPlayer 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(); @@ -1372,23 +1377,18 @@ public class VideoPlayerImpl extends VideoPlayer // 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())) { + if (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())) { + if (popupPlayerSelected() && (isPlaying() || isLoading())) { useVideoSource(false); } break; } - service.resetNotification(); } /*////////////////////////////////////////////////////////////////////////// @@ -1400,10 +1400,7 @@ public class VideoPlayerImpl extends VideoPlayer 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); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); } @Override @@ -1411,20 +1408,18 @@ public class VideoPlayerImpl extends VideoPlayer final View view, final FailReason failReason) { super.onLoadingFailed(imageUri, view, failReason); - service.resetNotification(); - service.updateNotification(-1); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); } @Override public void onLoadingCancelled(final String imageUri, final View view) { super.onLoadingCancelled(imageUri, view); - service.resetNotification(); - service.updateNotification(-1); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); } - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ private void setInitialGestureValues() { if (getAudioReactor() != null) { @@ -1526,9 +1521,10 @@ public class VideoPlayerImpl extends VideoPlayer showOrHideButtons(); getControlsVisibilityHandler().removeCallbacksAndMessages(null); - getControlsVisibilityHandler().postDelayed(() -> - animateView(getControlsRoot(), false, duration, 0, - this::hideSystemUIIfNeeded), delay + getControlsVisibilityHandler().postDelayed(() -> { + showHideShadow(false, duration, 0); + animateView(getControlsRoot(), false, duration, 0, this::hideSystemUIIfNeeded); + }, delay ); } @@ -1556,11 +1552,17 @@ public class VideoPlayerImpl extends VideoPlayer } private void showSystemUIPartially() { - if (isFullscreen() && getParentActivity() != null) { + final AppCompatActivity activity = getParentActivity(); + if (isFullscreen() && activity != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.getWindow().setStatusBarColor(Color.TRANSPARENT); + activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); + } final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; - getParentActivity().getWindow().getDecorView().setSystemUiVisibility(visibility); + activity.getWindow().getDecorView().setSystemUiVisibility(visibility); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } } @@ -1571,90 +1573,8 @@ public class VideoPlayerImpl extends VideoPlayer } } - /** - * 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 boolean isLandscape = service.isLandscape(); - final int width = isFullscreen - ? (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) || !isLandscape; - controlsRoot.getLayoutParams().height = isFullscreen && !isInMultiWindow() - && navBarAtTheBottom ? size.y : ViewGroup.LayoutParams.MATCH_PARENT; - controlsRoot.requestLayout(); - - final DisplayMetrics metrics = getRootView().getResources().getDisplayMetrics(); - int topPadding = isFullscreen && !isInMultiWindow() ? getStatusBarHeight() : 0; - topPadding = !isLandscape && DeviceUtils.hasCutout(topPadding, metrics) ? 0 : topPadding; - getRootView().findViewById(R.id.playbackWindowRoot).setTranslationY(topPadding); - getBottomControlsRoot().setTranslationY(-topPadding); - } - - /** - * @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.isLandscape() - ? service.getResources().getIdentifier( - "status_bar_height_landscape", "dimen", "android") - : service.getResources().getIdentifier( - "status_bar_height", "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; + public void disablePreloadingOfCurrentTrack() { + getLoadController().disablePreloadingOfCurrentTrack(); } protected void setMuteButton(final ImageButton button, final boolean isMuted) { @@ -1724,8 +1644,6 @@ public class VideoPlayerImpl extends VideoPlayer && !DeviceUtils.isTablet(service)) { toggleFullscreen(); } - - setControlsSize(); } private void buildQueue() { @@ -2122,6 +2040,12 @@ public class VideoPlayerImpl extends VideoPlayer public void setFragmentListener(final PlayerServiceEventListener listener) { fragmentListener = listener; fragmentIsVisible = true; + // Apply window insets because Android will not do it when orientation changes + // from landscape to portrait + if (!isFullscreen) { + getControlsRoot().setPadding(0, 0, 0, 0); + } + queueLayout.setPadding(0, 0, 0, 0); updateMetadata(); updatePlayback(); triggerProgressUpdate(); diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java index 19c621221..47c0624b8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java @@ -39,12 +39,13 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior } // Found that user still swiping, continue following - if (skippingInterception) { + if (skippingInterception || getState() == BottomSheetBehavior.STATE_SETTLING) { return false; } // Don't need to do anything if bottomSheet isn't expanded - if (getState() == BottomSheetBehavior.STATE_EXPANDED) { + if (getState() == BottomSheetBehavior.STATE_EXPANDED + && event.getAction() == MotionEvent.ACTION_DOWN) { // Without overriding scrolling will not work when user touches these elements for (final Integer element : skipInterceptionOfElements) { final ViewGroup viewGroup = child.findViewById(element); 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 index 4aa6070eb..a2def2a64 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -9,6 +9,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.Window; import android.view.WindowManager; +import android.widget.ProgressBar; import androidx.appcompat.content.res.AppCompatResources; import org.schabi.newpipe.R; import org.schabi.newpipe.player.BasePlayer; @@ -264,14 +265,19 @@ public class PlayerGestureListener } 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(); + final ProgressBar bar = playerImpl.getBrightnessProgressBar(); + final float oldBrightness = layoutParams.screenBrightness; + bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness)))); + bar.incrementProgressBy((int) distanceY); + + final float currentProgressPercent = (float) bar.getProgress() / bar.getMax(); layoutParams.screenBrightness = currentProgressPercent; window.setAttributes(layoutParams); + // Save current brightness level + PlayerHelper.setScreenBrightness(parent, currentProgressPercent); + if (DEBUG) { Log.d(TAG, "onScroll().brightnessControl, " + "currentBrightness = " + currentProgressPercent); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index e164e0563..d123a263b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -13,6 +13,7 @@ public class LoadController implements LoadControl { private final long initialPlaybackBufferUs; private final LoadControl internalLoadControl; + private boolean preloadingEnabled = true; /*////////////////////////////////////////////////////////////////////////// // Default Load Control @@ -41,6 +42,7 @@ public class LoadController implements LoadControl { @Override public void onPrepared() { + preloadingEnabled = true; internalLoadControl.onPrepared(); } @@ -52,11 +54,13 @@ public class LoadController implements LoadControl { @Override public void onStopped() { + preloadingEnabled = true; internalLoadControl.onStopped(); } @Override public void onReleased() { + preloadingEnabled = true; internalLoadControl.onReleased(); } @@ -78,6 +82,9 @@ public class LoadController implements LoadControl { @Override public boolean shouldContinueLoading(final long bufferedDurationUs, final float playbackSpeed) { + if (!preloadingEnabled) { + return false; + } return internalLoadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); } @@ -90,4 +97,8 @@ public class LoadController implements LoadControl { .shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering); return isInitialPlaybackBufferFilled || isInternalStartingPlayback; } + + public void disablePreloadingOfCurrentTrack() { + preloadingEnabled = false; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java index 849593e89..b0c641433 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -3,44 +3,58 @@ package org.schabi.newpipe.player.helper; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.media.MediaMetadata; -import android.os.Build; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; import android.view.KeyEvent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.core.app.NotificationCompat; -import androidx.media.app.NotificationCompat.MediaStyle; import androidx.media.session.MediaButtonReceiver; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.player.mediasession.MediaSessionCallback; import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController; public class MediaSessionManager { - private static final String TAG = "MediaSessionManager"; + private static final String TAG = MediaSessionManager.class.getSimpleName(); + public static final boolean DEBUG = MainActivity.DEBUG; @NonNull private final MediaSessionCompat mediaSession; @NonNull private final MediaSessionConnector sessionConnector; + private int lastAlbumArtHashCode; + public MediaSessionManager(@NonNull final Context context, @NonNull final Player player, @NonNull final MediaSessionCallback callback) { - this.mediaSession = new MediaSessionCompat(context, TAG); - this.mediaSession.setActive(true); + mediaSession = new MediaSessionCompat(context, TAG); + mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + mediaSession.setActive(true); - this.sessionConnector = new MediaSessionConnector(mediaSession); - this.sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback)); - this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); - this.sessionConnector.setPlayer(player); + mediaSession.setPlaybackState(new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_NONE, -1, 1) + .setActions(PlaybackStateCompat.ACTION_SEEK_TO + | PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_SET_REPEAT_MODE + | PlaybackStateCompat.ACTION_STOP) + .build()); + + sessionConnector = new MediaSessionConnector(mediaSession); + sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback)); + sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); + sessionConnector.setPlayer(player); } @Nullable @@ -49,46 +63,78 @@ public class MediaSessionManager { return MediaButtonReceiver.handleIntent(mediaSession, intent); } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public void setLockScreenArt(final NotificationCompat.Builder builder, - @Nullable final Bitmap thumbnailBitmap) { - if (thumbnailBitmap == null || !mediaSession.isActive()) { + public MediaSessionCompat.Token getSessionToken() { + return mediaSession.getSessionToken(); + } + + public void setMetadata(final String title, + final String artist, + final Bitmap albumArt, + final long duration) { + if (albumArt == null || !mediaSession.isActive()) { return; } - mediaSession.setMetadata( - new MediaMetadataCompat.Builder() - .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thumbnailBitmap) - .build() - ); + if (DEBUG) { + if (getMetadataAlbumArt() == null) { + Log.d(TAG, "N_getMetadataAlbumArt: thumb == null"); + } + if (getMetadataTitle() == null) { + Log.d(TAG, "N_getMetadataTitle: title == null"); + } + if (getMetadataArtist() == null) { + Log.d(TAG, "N_getMetadataArtist: artist == null"); + } + if (getMetadataDuration() <= 1) { + Log.d(TAG, "N_getMetadataDuration: duration <= 1; " + getMetadataDuration()); + } + } - final MediaStyle mediaStyle = new MediaStyle() - .setMediaSession(mediaSession.getSessionToken()); + if (getMetadataAlbumArt() == null || getMetadataTitle() == null + || getMetadataArtist() == null || getMetadataDuration() <= 1 + || albumArt.hashCode() != lastAlbumArtHashCode) { + if (DEBUG) { + Log.d(TAG, "setMetadata: N_Metadata update: t: " + title + " a: " + artist + + " thumb: " + albumArt.hashCode() + " d: " + duration); + } - builder.setStyle(mediaStyle); + mediaSession.setMetadata(new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) + .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt) + .putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, albumArt) + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration).build()); + lastAlbumArtHashCode = albumArt.hashCode(); + } } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public void clearLockScreenArt(final NotificationCompat.Builder builder) { - mediaSession.setMetadata( - new MediaMetadataCompat.Builder() - .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null) - .build() - ); + private Bitmap getMetadataAlbumArt() { + return mediaSession.getController().getMetadata() + .getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART); + } - final MediaStyle mediaStyle = new MediaStyle() - .setMediaSession(mediaSession.getSessionToken()); + private String getMetadataTitle() { + return mediaSession.getController().getMetadata() + .getString(MediaMetadataCompat.METADATA_KEY_TITLE); + } - builder.setStyle(mediaStyle); + private String getMetadataArtist() { + return mediaSession.getController().getMetadata() + .getString(MediaMetadataCompat.METADATA_KEY_ARTIST); + } + + private long getMetadataDuration() { + return mediaSession.getController().getMetadata() + .getLong(MediaMetadataCompat.METADATA_KEY_DURATION); } /** * Should be called on player destruction to prevent leakage. */ public void dispose() { - this.sessionConnector.setPlayer(null); - this.sessionConnector.setQueueNavigator(null); - this.mediaSession.setActive(false); - this.mediaSession.release(); + sessionConnector.setPlayer(null); + sessionConnector.setQueueNavigator(null); + mediaSession.setActive(false); + mediaSession.release(); } } 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 6667d0ecb..fd59e1d99 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 @@ -28,6 +28,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream; 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.util.ListHelper; import java.lang.annotation.Retention; import java.text.DecimalFormat; @@ -248,6 +249,18 @@ public final class PlayerHelper { } } + public static boolean isAutoplayAllowedByUser(@NonNull final Context context) { + switch (PlayerHelper.getAutoplayType(context)) { + case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER: + return false; + case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI: + return !ListHelper.isMeteredNetwork(context); + case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS: + default: + return true; + } + } + @NonNull public static SeekParameters getSeekParameters(@NonNull final Context context) { return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT; diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 23e813c4b..1b8c62e64 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -255,21 +255,21 @@ public class MediaSourceManager { // Loading and Syncing switch (event.type()) { - case INIT: - case REORDER: - case ERROR: - case SELECT: + case INIT: case REORDER: case ERROR: case SELECT: loadImmediate(); // low frequency, critical events break; - case APPEND: - case REMOVE: - case MOVE: - case RECOVERY: + case APPEND: case REMOVE: case MOVE: case RECOVERY: default: loadDebounced(); // high frequency or noncritical events break; } + // update ui and notification + switch (event.type()) { + case APPEND: case REMOVE: case MOVE: case REORDER: + playbackListener.onPlayQueueEdited(); + } + if (!isPlayQueueReady()) { maybeBlock(); playQueue.fetch(); diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index 0755bdd7a..811f82b3b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -69,7 +69,7 @@ public interface PlaybackListener { MediaSource sourceOf(PlayQueueItem item, StreamInfo info); /** - * Called when the play queue can no longer to played or used. + * Called when the play queue can no longer be played or used. * Currently, this means the play queue is empty and complete. * Signals to the listener that it should shutdown. *

@@ -77,4 +77,13 @@ public interface PlaybackListener { *

*/ void onPlaybackShutdown(); + + /** + * Called whenever the play queue was edited (items were added, deleted or moved), + * use this to e.g. update notification buttons or fragment ui. + *

+ * May be called at any time. + *

+ */ + void onPlayQueueEdited(); } diff --git a/app/src/main/java/org/schabi/newpipe/report/UserAction.java b/app/src/main/java/org/schabi/newpipe/report/UserAction.java index faa5e7a44..6fa697f71 100644 --- a/app/src/main/java/org/schabi/newpipe/report/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/report/UserAction.java @@ -20,7 +20,8 @@ public enum UserAction { DELETE_FROM_HISTORY("delete from history"), PLAY_STREAM("Play stream"), DOWNLOAD_POSTPROCESSING("download post-processing"), - DOWNLOAD_FAILED("download failed"); + DOWNLOAD_FAILED("download failed"), + PREFERENCES_MIGRATION("migration of preferences"); private final String message; diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java index c49be8c56..7a42c9bea 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -5,12 +5,12 @@ import android.os.Bundle; import androidx.preference.PreferenceManager; import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceFragmentCompat; import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.util.ThemeHelper; public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); @@ -25,24 +25,16 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { } @Override - public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); + public void onViewCreated(@NonNull final View rootView, + @Nullable final Bundle savedInstanceState) { + super.onViewCreated(rootView, savedInstanceState); setDivider(null); - updateTitle(); + ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle()); } @Override public void onResume() { super.onResume(); - updateTitle(); - } - - private void updateTitle() { - if (getActivity() instanceof AppCompatActivity) { - final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getPreferenceScreen().getTitle()); - } - } + ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle()); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 98b463da2..bc9fb1dd9 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -10,6 +10,7 @@ import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import java.io.File; +import java.util.Set; /* * Created by k3b on 07.01.2016. @@ -38,6 +39,22 @@ public final class NewPipeSettings { private NewPipeSettings() { } public static void initSettings(final Context context) { + // check if there are entries in the prefs to determine whether this is the first app run + Boolean isFirstRun = null; + final Set prefsKeys = PreferenceManager.getDefaultSharedPreferences(context) + .getAll().keySet(); + for (final String key: prefsKeys) { + // ACRA stores some info in the prefs during app initialization + // which happens before this method is called. Therefore ignore ACRA-related keys. + if (!key.toLowerCase().startsWith("acra")) { + isFirstRun = false; + break; + } + } + if (isFirstRun == null) { + isFirstRun = true; + } + PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true); PreferenceManager.setDefaultValues(context, R.xml.content_settings, true); PreferenceManager.setDefaultValues(context, R.xml.download_settings, true); @@ -49,6 +66,8 @@ public final class NewPipeSettings { getVideoDownloadFolder(context); getAudioDownloadFolder(context); + + SettingMigrations.initMigrations(context, isFirstRun); } private static void getVideoDownloadFolder(final Context context) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.java new file mode 100644 index 000000000..ce1e9e5a5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.java @@ -0,0 +1,270 @@ +package org.schabi.newpipe.settings; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.fragment.app.Fragment; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.NotificationConstants; +import org.schabi.newpipe.util.DeviceUtils; +import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.FocusOverlayView; + +import java.util.List; + +public class NotificationSettingsFragment extends Fragment { + + private Switch scaleSwitch; + private NotificationSlot[] notificationSlots; + + private SharedPreferences pref; + private List compactSlots; + private String scaleKey; + + //////////////////////////////////////////////////////////////////////////// + // Lifecycle + //////////////////////////////////////////////////////////////////////////// + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + pref = PreferenceManager.getDefaultSharedPreferences(requireContext()); + scaleKey = getString(R.string.scale_to_square_image_in_notifications_key); + } + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, + final ViewGroup container, + @Nullable final Bundle savedInstanceState) { + return inflater.inflate(R.layout.settings_notification, container, false); + } + + @Override + public void onViewCreated(@NonNull final View rootView, + @Nullable final Bundle savedInstanceState) { + super.onViewCreated(rootView, savedInstanceState); + + setupScaleSwitch(rootView); + setupActions(rootView); + } + + @Override + public void onResume() { + super.onResume(); + ThemeHelper.setTitleToAppCompatActivity(getActivity(), + getString(R.string.settings_category_notification_title)); + } + + @Override + public void onPause() { + super.onPause(); + saveChanges(); + requireContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION)); + } + + + //////////////////////////////////////////////////////////////////////////// + // Setup + //////////////////////////////////////////////////////////////////////////// + + private void setupScaleSwitch(@NonNull final View view) { + scaleSwitch = view.findViewById(R.id.notificationScaleSwitch); + scaleSwitch.setChecked(pref.getBoolean(scaleKey, false)); + + view.findViewById(R.id.notificationScaleSwitchClickableArea) + .setOnClickListener(v -> scaleSwitch.toggle()); + } + + private void setupActions(@NonNull final View view) { + compactSlots = + NotificationConstants.getCompactSlotsFromPreferences(requireContext(), pref, 5); + notificationSlots = new NotificationSlot[5]; + for (int i = 0; i < 5; i++) { + notificationSlots[i] = new NotificationSlot(i, view); + } + } + + + //////////////////////////////////////////////////////////////////////////// + // Saving + //////////////////////////////////////////////////////////////////////////// + + private void saveChanges() { + final SharedPreferences.Editor editor = pref.edit(); + editor.putBoolean(scaleKey, scaleSwitch.isChecked()); + + for (int i = 0; i < 3; i++) { + editor.putInt(getString(NotificationConstants.SLOT_COMPACT_PREF_KEYS[i]), + (i < compactSlots.size() ? compactSlots.get(i) : -1)); + } + + for (int i = 0; i < 5; i++) { + editor.putInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]), + notificationSlots[i].selectedAction); + } + + editor.apply(); + } + + + //////////////////////////////////////////////////////////////////////////// + // Notification action + //////////////////////////////////////////////////////////////////////////// + + private static final int[] SLOT_ITEMS = { + R.id.notificationAction0, + R.id.notificationAction1, + R.id.notificationAction2, + R.id.notificationAction3, + R.id.notificationAction4, + }; + + private static final int[] SLOT_TITLES = { + R.string.notification_action_0_title, + R.string.notification_action_1_title, + R.string.notification_action_2_title, + R.string.notification_action_3_title, + R.string.notification_action_4_title, + }; + + private class NotificationSlot { + + final int i; + @NotificationConstants.Action int selectedAction; + + ImageView icon; + TextView summary; + + NotificationSlot(final int actionIndex, final View parentView) { + this.i = actionIndex; + + final View view = parentView.findViewById(SLOT_ITEMS[i]); + setupSelectedAction(view); + setupTitle(view); + setupCheckbox(view); + } + + void setupTitle(final View view) { + ((TextView) view.findViewById(R.id.notificationActionTitle)) + .setText(SLOT_TITLES[i]); + view.findViewById(R.id.notificationActionClickableArea).setOnClickListener( + v -> openActionChooserDialog()); + } + + void setupCheckbox(final View view) { + final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox); + compactSlotCheckBox.setChecked(compactSlots.contains(i)); + view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener( + v -> { + if (compactSlotCheckBox.isChecked()) { + compactSlots.remove((Integer) i); + } else if (compactSlots.size() < 3) { + compactSlots.add(i); + } else { + Toast.makeText(requireContext(), + R.string.notification_actions_at_most_three, + Toast.LENGTH_SHORT).show(); + return; + } + + compactSlotCheckBox.toggle(); + }); + } + + void setupSelectedAction(final View view) { + icon = view.findViewById(R.id.notificationActionIcon); + summary = view.findViewById(R.id.notificationActionSummary); + selectedAction = pref.getInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]), + NotificationConstants.SLOT_DEFAULTS[i]); + updateInfo(); + } + + void updateInfo() { + if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) { + icon.setImageDrawable(null); + } else { + icon.setImageDrawable(AppCompatResources.getDrawable(requireContext(), + NotificationConstants.ACTION_ICONS[selectedAction])); + } + + summary.setText(NotificationConstants.getActionName(requireContext(), selectedAction)); + } + + void openActionChooserDialog() { + final LayoutInflater inflater = LayoutInflater.from(requireContext()); + final LinearLayout rootLayout = (LinearLayout) inflater.inflate( + R.layout.single_choice_dialog_view, null, false); + final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); + + final AlertDialog alertDialog = new AlertDialog.Builder(requireContext()) + .setTitle(SLOT_TITLES[i]) + .setView(radioGroup) + .setCancelable(true) + .create(); + + final View.OnClickListener radioButtonsClickListener = v -> { + selectedAction = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][v.getId()]; + updateInfo(); + alertDialog.dismiss(); + }; + + for (int id = 0; id < NotificationConstants.SLOT_ALLOWED_ACTIONS[i].length; ++id) { + final int action = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][id]; + final RadioButton radioButton + = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); + + // if present set action icon with correct color + if (NotificationConstants.ACTION_ICONS[action] != 0) { + final Drawable drawable = AppCompatResources.getDrawable(requireContext(), + NotificationConstants.ACTION_ICONS[action]); + if (drawable != null) { + final int color = ThemeHelper.resolveColorFromAttr(requireContext(), + android.R.attr.textColorPrimary); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + drawable.setTint(color); + } else { + drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + radioButton.setCompoundDrawablesWithIntrinsicBounds( + null, null, drawable, null); + } + } + + radioButton.setText(NotificationConstants.getActionName(requireContext(), action)); + radioButton.setChecked(action == selectedAction); + radioButton.setId(id); + radioButton.setLayoutParams(new RadioGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + radioButton.setOnClickListener(radioButtonsClickListener); + radioGroup.addView(radioButton); + } + alertDialog.show(); + + if (DeviceUtils.isTv(requireContext())) { + FocusOverlayView.setupFocusObserver(alertDialog); + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java index 61cee7b20..7559f6ed5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -22,9 +22,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.AppCompatImageView; import androidx.fragment.app.Fragment; @@ -117,7 +115,8 @@ public class PeertubeInstanceListFragment extends Fragment { @Override public void onResume() { super.onResume(); - updateTitle(); + ThemeHelper.setTitleToAppCompatActivity(getActivity(), + getString(R.string.peertube_instance_url_title)); } @Override @@ -176,15 +175,6 @@ public class PeertubeInstanceListFragment extends Fragment { sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); } - private void updateTitle() { - if (getActivity() instanceof AppCompatActivity) { - final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(R.string.peertube_instance_url_title); - } - } - } - private void saveChanges() { final JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances"); for (final PeertubeInstance instance : instanceList) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java new file mode 100644 index 000000000..26e72722e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java @@ -0,0 +1,140 @@ +package org.schabi.newpipe.settings; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import androidx.preference.PreferenceManager; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.ErrorActivity.ErrorInfo; +import org.schabi.newpipe.report.UserAction; + +import static org.schabi.newpipe.MainActivity.DEBUG; + +public final class SettingMigrations { + private static final String TAG = SettingMigrations.class.toString(); + /** + * Version number for preferences. Must be incremented every time a migration is necessary. + */ + public static final int VERSION = 2; + private static SharedPreferences sp; + + public static final Migration MIGRATION_0_1 = new Migration(0, 1) { + @Override + public void migrate(final Context context) { + // We changed the content of the dialog which opens when sharing a link to NewPipe + // by removing the "open detail page" option. + // Therefore, show the dialog once again to ensure users need to choose again and are + // aware of the changed dialog. + final SharedPreferences.Editor editor = sp.edit(); + editor.putString(context.getString(R.string.preferred_open_action_key), + context.getString(R.string.always_ask_open_action_key)); + editor.apply(); + } + }; + + public static final Migration MIGRATION_1_2 = new Migration(1, 2) { + @Override + protected void migrate(final Context context) { + // The new application workflow introduced in #2907 allows minimizing videos + // while playing to do other stuff within the app. + // For an even better workflow, we minimize a stream when switching the app to play in + // background. + // Therefore, set default value to background, if it has not been changed yet. + final String minimizeOnExitKey = context.getString(R.string.minimize_on_exit_key); + if (sp.getString(minimizeOnExitKey, "") + .equals(context.getString(R.string.minimize_on_exit_none_key))) { + final SharedPreferences.Editor editor = sp.edit(); + editor.putString(minimizeOnExitKey, + context.getString(R.string.minimize_on_exit_background_key)); + editor.apply(); + } + } + }; + + /** + * List of all implemented migrations. + *

+ * Append new migrations to the end of the list to keep it sorted ascending. + * If not sorted correctly, migrations which depend on each other, may fail. + */ + private static final Migration[] SETTING_MIGRATIONS = { + MIGRATION_0_1, + MIGRATION_1_2 + }; + + + public static void initMigrations(final Context context, final boolean isFirstRun) { + // setup migrations and check if there is something to do + sp = PreferenceManager.getDefaultSharedPreferences(context); + final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version); + final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0); + + // no migration to run, already up to date + if (isFirstRun) { + sp.edit().putInt(lastPrefVersionKey, VERSION).apply(); + return; + } else if (lastPrefVersion == VERSION) { + return; + } + + // run migrations + int currentVersion = lastPrefVersion; + for (final Migration currentMigration : SETTING_MIGRATIONS) { + try { + if (currentMigration.shouldMigrate(currentVersion)) { + if (DEBUG) { + Log.d(TAG, "Migrating preferences from version " + + currentVersion + " to " + currentMigration.newVersion); + } + currentMigration.migrate(context); + currentVersion = currentMigration.newVersion; + } + } catch (final Exception e) { + // save the version with the last successful migration and report the error + sp.edit().putInt(lastPrefVersionKey, currentVersion).apply(); + final ErrorInfo errorInfo = ErrorInfo.make( + UserAction.PREFERENCES_MIGRATION, + "none", + "Migrating preferences from version " + lastPrefVersion + " to " + + VERSION + ". " + + "Error at " + currentVersion + " => " + ++currentVersion, + 0 + ); + ErrorActivity.reportError(context, e, SettingMigrations.class, null, errorInfo); + return; + } + } + + // store the current preferences version + sp.edit().putInt(lastPrefVersionKey, currentVersion).apply(); + } + + private SettingMigrations() { } + + abstract static class Migration { + public final int oldVersion; + public final int newVersion; + + protected Migration(final int oldVersion, final int newVersion) { + this.oldVersion = oldVersion; + this.newVersion = newVersion; + } + + /** + * @param currentVersion current settings version + * @return Returns whether this migration should be run. + * A migration is necessary if the old version of this migration is lower than or equal to + * the current settings version. + */ + private boolean shouldMigrate(final int currentVersion) { + return oldVersion >= currentVersion; + } + + protected abstract void migrate(Context context); + + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java index 44fe987ee..2554ecc5c 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -16,9 +16,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.AppCompatImageView; import androidx.fragment.app.Fragment; @@ -92,7 +90,8 @@ public class ChooseTabsFragment extends Fragment { @Override public void onResume() { super.onResume(); - updateTitle(); + ThemeHelper.setTitleToAppCompatActivity(getActivity(), + getString(R.string.main_page_content)); } @Override @@ -137,15 +136,6 @@ public class ChooseTabsFragment extends Fragment { tabList.addAll(tabsManager.getTabs()); } - private void updateTitle() { - if (getActivity() instanceof AppCompatActivity) { - final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(R.string.main_page_content); - } - } - } - private void saveChanges() { tabsManager.saveTabs(tabList); } diff --git a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java index 7592d2f35..d852c2296 100644 --- a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java @@ -6,8 +6,6 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.BatteryManager; import android.os.Build; -import android.util.DisplayMetrics; -import android.util.TypedValue; import android.view.KeyEvent; import androidx.annotation.NonNull; @@ -74,17 +72,4 @@ public final class DeviceUtils { return false; } } - - /* - * Compares current status bar height with default status bar height in Android and decides, - * does the device has cutout or not - * */ - public static boolean hasCutout(final float statusBarHeight, final DisplayMetrics metrics) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - final float defaultStatusBarHeight = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 25, metrics); - return statusBarHeight > defaultStatusBarHeight; - } - return false; - } } 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 693265fcd..fc4a6cacc 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -57,7 +57,7 @@ import org.schabi.newpipe.settings.SettingsActivity; import java.util.ArrayList; -@SuppressWarnings({"unused", "WeakerAccess"}) +@SuppressWarnings({"unused"}) public final class NavigationHelper { public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag"; @@ -69,16 +69,18 @@ public final class NavigationHelper { //////////////////////////////////////////////////////////////////////////*/ @NonNull - public static Intent getPlayerIntent(@NonNull final Context context, - @NonNull final Class targetClazz, - @NonNull final PlayQueue playQueue, - @Nullable final String quality, - final boolean resumePlayback) { + public static Intent getPlayerIntent(@NonNull final Context context, + @NonNull final Class targetClazz, + @Nullable final PlayQueue playQueue, + @Nullable final String quality, + final boolean resumePlayback) { final Intent intent = new Intent(context, targetClazz); - final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); - if (cacheKey != null) { - intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); + if (playQueue != null) { + final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); + if (cacheKey != null) { + intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); + } } if (quality != null) { intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); @@ -90,53 +92,51 @@ public final class NavigationHelper { } @NonNull - public static Intent getPlayerIntent(@NonNull final Context context, - @NonNull final Class targetClazz, - @NonNull final PlayQueue playQueue, - final boolean resumePlayback) { + public static Intent getPlayerIntent(@NonNull final Context context, + @NonNull final Class targetClazz, + @Nullable final PlayQueue playQueue, + final boolean resumePlayback) { return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback); } @NonNull - public static Intent getPlayerEnqueueIntent(@NonNull final Context context, - @NonNull final Class targetClazz, - @NonNull final PlayQueue playQueue, - final boolean selectOnAppend, - final boolean resumePlayback) { + public static Intent getPlayerEnqueueIntent(@NonNull final Context context, + @NonNull final Class targetClazz, + @Nullable final PlayQueue playQueue, + final boolean selectOnAppend, + final boolean resumePlayback) { return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) .putExtra(BasePlayer.APPEND_ONLY, true) .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); } @NonNull - public static Intent getPlayerIntent(@NonNull final Context context, - @NonNull final Class targetClazz, - @NonNull final PlayQueue playQueue, - final int repeatMode, - final float playbackSpeed, - final float playbackPitch, - final boolean playbackSkipSilence, - @Nullable final String playbackQuality, - final boolean resumePlayback, - final boolean startPaused, - final boolean isMuted) { + public static Intent getPlayerIntent(@NonNull final Context context, + @NonNull final Class targetClazz, + @Nullable final PlayQueue playQueue, + final int repeatMode, + final float playbackSpeed, + final float playbackPitch, + final boolean playbackSkipSilence, + @Nullable final String playbackQuality, + final boolean resumePlayback, + final boolean startPaused, + final boolean isMuted) { return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) .putExtra(BasePlayer.START_PAUSED, startPaused) .putExtra(BasePlayer.IS_MUTED, isMuted); } - public static void playOnMainPlayer( - final AppCompatActivity activity, - final PlayQueue queue, - final boolean autoPlay) { + 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) { + public static void playOnMainPlayer(final FragmentManager fragmentManager, + final PlayQueue queue, + final boolean autoPlay) { final PlayQueueItem currentStream = queue.getItem(); openVideoDetailFragment( fragmentManager, @@ -148,7 +148,7 @@ public final class NavigationHelper { } public static void playOnMainPlayer(@NonNull final Context context, - @NonNull final PlayQueue queue, + @Nullable final PlayQueue queue, @NonNull final StreamingService.LinkType linkType, @NonNull final String url, @NonNull final String title, @@ -553,18 +553,14 @@ public final class NavigationHelper { return true; } - public static Intent getBackgroundPlayerActivityIntent(final Context context) { - return getServicePlayerActivityIntent(context, BackgroundPlayerActivity.class); - } - - private static Intent getServicePlayerActivityIntent(final Context context, - final Class activityClass) { - final Intent intent = new Intent(context, activityClass); + public static Intent getPlayQueueActivityIntent(final Context context) { + final Intent intent = new Intent(context, BackgroundPlayerActivity.class); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } return intent; } + /*////////////////////////////////////////////////////////////////////////// // Link handling //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index 051fbbcf8..a1af0387a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -19,6 +19,7 @@ package org.schabi.newpipe.util; +import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import androidx.preference.PreferenceManager; @@ -26,7 +27,10 @@ import android.util.TypedValue; import android.view.ContextThemeWrapper; import androidx.annotation.AttrRes; +import androidx.annotation.Nullable; import androidx.annotation.StyleRes; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import org.schabi.newpipe.R; @@ -231,4 +235,20 @@ public final class ThemeHelper { return PreferenceManager.getDefaultSharedPreferences(context) .getString(themeKey, defaultTheme); } + + /** + * Sets the title to the activity, if the activity is an {@link AppCompatActivity} and has an + * action bar. + * @param activity the activity to set the title of + * @param title the title to set to the activity + */ + public static void setTitleToAppCompatActivity(@Nullable final Activity activity, + final CharSequence title) { + if (activity instanceof AppCompatActivity) { + final ActionBar actionBar = ((AppCompatActivity) activity).getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(title); + } + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/views/CustomCollapsingToolbarLayout.java b/app/src/main/java/org/schabi/newpipe/views/CustomCollapsingToolbarLayout.java new file mode 100644 index 000000000..23e16ff58 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/CustomCollapsingToolbarLayout.java @@ -0,0 +1,39 @@ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import com.google.android.material.appbar.CollapsingToolbarLayout; + +public class CustomCollapsingToolbarLayout extends CollapsingToolbarLayout { + public CustomCollapsingToolbarLayout(@NonNull final Context context) { + super(context); + overrideListener(); + } + + public CustomCollapsingToolbarLayout(@NonNull final Context context, + @Nullable final AttributeSet attrs) { + super(context, attrs); + overrideListener(); + } + + public CustomCollapsingToolbarLayout(@NonNull final Context context, + @Nullable final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + overrideListener(); + } + + /** + * CollapsingToolbarLayout sets it's own setOnApplyInsetsListener which consumes + * system insets {@link CollapsingToolbarLayout#onWindowInsetChanged(WindowInsetsCompat)} + * so we will not receive them in subviews with fitsSystemWindows = true. + * Override Google's behavior + * */ + public void overrideListener() { + ViewCompat.setOnApplyWindowInsetsListener(this, (v, insets) -> insets); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java index 798712b6b..a23172bd3 100644 --- a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java +++ b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java @@ -1,14 +1,17 @@ package org.schabi.newpipe.views; import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; 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_FIT; 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 resizeMode = RESIZE_MODE_FIT; private int baseHeight = 0; private int maxHeight = 0; private float videoAspectRatio = 0.0f; @@ -30,7 +33,7 @@ public class ExpandableSurfaceView extends SurfaceView { 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 + && resizeMode != RESIZE_MODE_FIT && verticalVideo ? maxHeight : baseHeight; if (height == 0) { @@ -42,26 +45,22 @@ public class ExpandableSurfaceView extends SurfaceView { 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; + if (resizeMode == RESIZE_MODE_FIT + // KitKat doesn't work well when a view has a scale like needed for ZOOM + || (resizeMode == RESIZE_MODE_ZOOM && VERSION.SDK_INT < VERSION_CODES.LOLLIPOP)) { + if (aspectDeformation > 0) { + height = (int) (width / videoAspectRatio); + } else { + width = (int) (height * videoAspectRatio); + } + } else if (resizeMode == RESIZE_MODE_ZOOM) { + if (aspectDeformation < 0) { + scaleY = viewAspectRatio / videoAspectRatio; + } else { + scaleX = videoAspectRatio / viewAspectRatio; + } } + super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); } diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java index 1ffb7d069..f400b62b1 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java @@ -17,15 +17,19 @@ */ package org.schabi.newpipe.views; +import android.annotation.TargetApi; import android.content.Context; import android.graphics.Rect; +import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import android.view.WindowInsets; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; +import org.schabi.newpipe.R; public final class FocusAwareCoordinator extends CoordinatorLayout { private final Rect childFocus = new Rect(); @@ -63,4 +67,41 @@ public final class FocusAwareCoordinator extends CoordinatorLayout { requestChildRectangleOnScreen(child, childFocus, false); } } + + /** + * Applies window insets to all children, not just for the first who consume the insets. + * Makes possible for multiple fragments to co-exist. Without this code + * the first ViewGroup who consumes will be the last who receive the insets + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public WindowInsets dispatchApplyWindowInsets(final WindowInsets insets) { + boolean consumed = false; + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final WindowInsets res = child.dispatchApplyWindowInsets(insets); + if (res.isConsumed()) { + consumed = true; + } + } + + if (consumed) { + insets.consumeSystemWindowInsets(); + } + return insets; + } + + /** + * Adjusts player's controls manually because fitsSystemWindows doesn't work when multiple + * receivers adjust its bounds. So when two listeners are present (like in profile page) + * the player's controls will not receive insets. This method fixes it + */ + @Override + protected boolean fitSystemWindows(final Rect insets) { + final ViewGroup controls = findViewById(R.id.playbackControlRoot); + if (controls != null) { + controls.setPadding(insets.left, insets.top, insets.right, insets.bottom); + } + return super.fitSystemWindows(insets); + } } diff --git a/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png deleted file mode 100644 index ceb1a1eeb..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_close_white_24dp_png.png b/app/src/main/res/drawable-hdpi/ic_close_white_24dp_png.png new file mode 100644 index 000000000..2f73a04b1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_close_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_hourglass_top_white_24dp_png.png b/app/src/main/res/drawable-hdpi/ic_hourglass_top_white_24dp_png.png new file mode 100644 index 000000000..13050da08 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_hourglass_top_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_replay_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_replay_white_24dp.png deleted file mode 100644 index fcddcf02d..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_replay_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_replay_white_24dp_png.png b/app/src/main/res/drawable-hdpi/ic_replay_white_24dp_png.png new file mode 100644 index 000000000..8c5afb380 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_replay_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png deleted file mode 100644 index af7f8288d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_close_white_24dp_png.png b/app/src/main/res/drawable-mdpi/ic_close_white_24dp_png.png new file mode 100644 index 000000000..d8aa2f7c4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_close_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_hourglass_top_white_24dp_png.png b/app/src/main/res/drawable-mdpi/ic_hourglass_top_white_24dp_png.png new file mode 100644 index 000000000..2343e8cb9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_hourglass_top_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_replay_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_replay_white_24dp.png deleted file mode 100644 index 3b4191325..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_replay_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_replay_white_24dp_png.png b/app/src/main/res/drawable-mdpi/ic_replay_white_24dp_png.png new file mode 100644 index 000000000..038804d11 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_replay_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png deleted file mode 100644 index b7c7ffd0e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_close_white_24dp_png.png b/app/src/main/res/drawable-xhdpi/ic_close_white_24dp_png.png new file mode 100644 index 000000000..40782d057 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_close_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_hourglass_top_white_24dp_png.png b/app/src/main/res/drawable-xhdpi/ic_hourglass_top_white_24dp_png.png new file mode 100644 index 000000000..bdf88fb3b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_hourglass_top_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_replay_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_replay_white_24dp.png deleted file mode 100644 index 1573fb111..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_replay_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_replay_white_24dp_png.png b/app/src/main/res/drawable-xhdpi/ic_replay_white_24dp_png.png new file mode 100644 index 000000000..4456670ba Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_replay_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png deleted file mode 100644 index 6b717e0dd..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp_png.png b/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp_png.png new file mode 100644 index 000000000..2cd1a8865 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_hourglass_top_white_24dp_png.png b/app/src/main/res/drawable-xxhdpi/ic_hourglass_top_white_24dp_png.png new file mode 100644 index 000000000..f9a097f4d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_hourglass_top_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_replay_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_replay_white_24dp.png deleted file mode 100644 index 5105c2251..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_replay_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_replay_white_24dp_png.png b/app/src/main/res/drawable-xxhdpi/ic_replay_white_24dp_png.png new file mode 100644 index 000000000..e456ce595 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_replay_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png deleted file mode 100644 index 396419219..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp_png.png b/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp_png.png new file mode 100644 index 000000000..4d278c5bf Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_hourglass_top_white_24dp_png.png b/app/src/main/res/drawable-xxxhdpi/ic_hourglass_top_white_24dp_png.png new file mode 100644 index 000000000..7a099dabf Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_hourglass_top_white_24dp_png.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_replay_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_replay_white_24dp.png deleted file mode 100644 index 04cbde9af..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_replay_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_replay_white_24dp_png.png b/app/src/main/res/drawable-xxxhdpi/ic_replay_white_24dp_png.png new file mode 100644 index 000000000..c2e9eb27b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_replay_white_24dp_png.png differ diff --git a/app/src/main/res/drawable/ic_close_white_24dp.xml b/app/src/main/res/drawable/ic_close_white_24dp.xml new file mode 100644 index 000000000..d11cc5c9c --- /dev/null +++ b/app/src/main/res/drawable/ic_close_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_hourglass_top_white_24dp.xml b/app/src/main/res/drawable/ic_hourglass_top_white_24dp.xml new file mode 100644 index 000000000..d6156dfa1 --- /dev/null +++ b/app/src/main/res/drawable/ic_hourglass_top_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_replay_white_24dp.xml b/app/src/main/res/drawable/ic_replay_white_24dp.xml new file mode 100644 index 000000000..8e84c195b --- /dev/null +++ b/app/src/main/res/drawable/ic_replay_white_24dp.xml @@ -0,0 +1,9 @@ + + + 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 f69832b81..e2d18434d 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 @@ -20,7 +20,6 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="5" - android:fitsSystemWindows="true" android:isScrollContainer="true"> - @@ -162,7 +160,7 @@ - + + + + + - - - - + android:layout_height="match_parent"> -