Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use picture-in-picture (PIP) for the popup player on Android >= 8.0 #8750

Closed
wants to merge 9 commits into from
6 changes: 4 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@
tools:ignore="AllowBackup">
<activity
android:name=".MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask">
android:launchMode="singleTask"
android:supportsPictureInPicture="true"
tools:targetApi="n">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/org/schabi/newpipe/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.PictureInPictureHelper;
import org.schabi.newpipe.views.FocusOverlayView;

import java.util.ArrayList;
Expand Down Expand Up @@ -499,6 +500,18 @@ protected void onResume() {
.setVisible(isHistoryEnabled);
}

@Override
protected void onUserLeaveHint() {
// Enter picture-in-picture mode when the home button is pressed, if it is enabled.
final var currentFragment = getSupportFragmentManager()
.findFragmentById(R.id.fragment_player_holder);
if (PictureInPictureHelper.isAndroidPictureInPictureEnabled(this)
&& currentFragment instanceof VideoDetailFragment
&& ((VideoDetailFragment) currentFragment).canEnterAndroidPipMode()) {
PictureInPictureHelper.enterPictureInPictureMode(this);
}
}

@Override
protected void onNewIntent(final Intent intent) {
if (DEBUG) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.player.ui.MainPlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.PictureInPictureHelper;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
Expand Down Expand Up @@ -441,6 +442,30 @@ public void onActivityResult(final int requestCode, final int resultCode, final
}
}

@Override
public void onPictureInPictureModeChanged(final boolean isInPictureInPictureMode) {
final int visibility = isInPictureInPictureMode ? View.GONE : View.VISIBLE;

// Hide thumbnail view controls to avoid black bars in PiP
for (int i = 0; i < binding.detailThumbnailRootLayout.getChildCount(); i++) {
final var child = binding.detailThumbnailRootLayout.getChildAt(i);
if (child != binding.playerPlaceholder) {
child.setVisibility(visibility);
}
}

// Hide other controls
binding.overlayLayout.setVisibility(visibility);
binding.tabLayout.setVisibility(visibility);
binding.detailContentRootLayout.setVisibility(visibility);
binding.viewPager.setVisibility(visibility);

// Hide related videos sidebar on tablets
if (binding.relatedItemsLayout != null) {
binding.relatedItemsLayout.setVisibility(visibility);
}
}

/*//////////////////////////////////////////////////////////////////////////
// OnClick
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -1052,7 +1077,20 @@ private void openBackgroundPlayer(final boolean append) {
}

private void openPopupPlayer(final boolean append) {
if (!PermissionHelper.isPopupEnabledElseAsk(activity)) {
final var activity = requireActivity();
if (PictureInPictureHelper.isAndroidPictureInPictureEnabled(activity)) {
if (canEnterAndroidPipMode()) {
PictureInPictureHelper.enterPictureInPictureMode(activity);
} else {
Toast.makeText(activity, R.string.popup_not_available, Toast.LENGTH_SHORT).show();
}
} else {
openPreNougatPopupPlayer(append);
}
}

private void openPreNougatPopupPlayer(final boolean append) {
if (!PermissionHelper.isPopupEnabled(activity)) {
return;
}

Expand Down Expand Up @@ -2431,6 +2469,13 @@ boolean isPlayerAvailable() {
return player != null;
}

public boolean canEnterAndroidPipMode() {
final var playerType = playerHolder.getType();
return player != null && player.isPlaying()
&& bottomSheetState == BottomSheetBehavior.STATE_EXPANDED
&& playerType != null && playerType != PlayerType.AUDIO;
}

boolean isPlayerServiceAvailable() {
return playerService != null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PictureInPictureHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;

Expand Down Expand Up @@ -374,7 +375,10 @@ private void onFragmentStopped() {
case MINIMIZE_ON_EXIT_MODE_POPUP:
getParentActivity().ifPresent(activity -> {
player.setRecovery();
NavigationHelper.playOnPopupPlayer(activity, player.getPlayQueue(), true);
if (!PictureInPictureHelper.isAndroidPictureInPictureEnabled(activity)) {
NavigationHelper.playOnPopupPlayer(activity, player.getPlayQueue(),
true);
}
});
break;
case MINIMIZE_ON_EXIT_MODE_NONE: default:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.schabi.newpipe.settings;

import static org.schabi.newpipe.util.PermissionHelper.checkSystemAlertWindowPermission;
import static org.schabi.newpipe.util.PictureInPictureHelper.isAndroidPictureInPictureEnabled;

import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
Expand All @@ -10,46 +14,60 @@

import androidx.preference.ListPreference;

import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;

import org.schabi.newpipe.R;
import org.schabi.newpipe.util.PermissionHelper;

import java.util.LinkedList;
import java.util.List;

public class VideoAudioSettingsFragment extends BasePreferenceFragment {
private SharedPreferences.OnSharedPreferenceChangeListener listener;
private final SharedPreferences.OnSharedPreferenceChangeListener listener =
(sharedPreferences, key) -> {
// on M and above, if user chooses to minimise to popup player on exit
// and the app doesn't have display over other apps permission,
// show a snackbar to let the user give permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& key.equals(getString(R.string.minimize_on_exit_key))
// If Android picture-in-picture is enabled, this permission is not needed.
&& !isAndroidPictureInPictureEnabled(requireContext())) {
final String newSetting = sharedPreferences.getString(key, null);
if (getString(R.string.minimize_on_exit_popup_key).equals(newSetting)
&& !Settings.canDrawOverlays(requireContext())) {
Snackbar.make(getListView(), R.string.permission_display_over_apps,
BaseTransientBottomBar.LENGTH_INDEFINITE)
.setAction(R.string.settings,
view -> checkSystemAlertWindowPermission(requireContext()))
.show();
}
} else if (getString(R.string.use_inexact_seek_key).equals(key)) {
updateSeekOptions();
}
};

@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResourceRegistry();

updateSeekOptions();

listener = (sharedPreferences, key) -> {

// on M and above, if user chooses to minimise to popup player on exit
// and the app doesn't have display over other apps permission,
// show a snackbar to let the user give permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& getString(R.string.minimize_on_exit_key).equals(key)) {
final String newSetting = sharedPreferences.getString(key, null);
if (newSetting != null
&& newSetting.equals(getString(R.string.minimize_on_exit_popup_key))
&& !Settings.canDrawOverlays(getContext())) {

Snackbar.make(getListView(), R.string.permission_display_over_apps,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.settings, view ->
PermissionHelper.checkSystemAlertWindowPermission(getContext()))
.show();

}
} else if (getString(R.string.use_inexact_seek_key).equals(key)) {
updateSeekOptions();
final boolean isPipUnavailable = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|| !requireContext().getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);

// Disable PiP configuration if the device is running Android < 8.0 or Android Go.
if (isPipUnavailable) {
final ListPreference popupConfig =
findPreference(getString(R.string.popup_configuration_key));
if (popupConfig != null) {
popupConfig.setEnabled(false);
// If the Android version is >= 8.0, then PiP is disabled when this point is
// reached.
popupConfig.setSummary(Build.VERSION.SDK_INT < Build.VERSION_CODES.O
? R.string.pip_unavailable : R.string.pip_disabled);
}
};
}
}

/**
Expand Down Expand Up @@ -86,8 +104,7 @@ private void updateSeekOptions() {
}
}

final ListPreference durations = findPreference(
getString(R.string.seek_duration_key));
final ListPreference durations = findPreference(getString(R.string.seek_duration_key));
durations.setEntryValues(displayedDurationValues.toArray(new CharSequence[0]));
durations.setEntries(displayedDescriptionValues.toArray(new CharSequence[0]));
final int selectedDuration = Integer.parseInt(durations.getValue());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.schabi.newpipe.util;

import android.app.Activity;
import android.app.PictureInPictureParams;
import android.content.Context;
import android.os.Build;

import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;

import org.schabi.newpipe.R;

public final class PictureInPictureHelper {
private PictureInPictureHelper() {
}

public static boolean isAndroidPictureInPictureEnabled(@NonNull final Context context) {
// The popup mode setting can't be changed if the device is running Android < 7.0 or Android
// Go.
final var popupMode = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.popup_configuration_key),
context.getString(R.string.popup_mode_legacy));
return popupMode.equals(context.getString(R.string.popup_mode_pip));
}

public static void enterPictureInPictureMode(@NonNull final Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
}
}
}
13 changes: 12 additions & 1 deletion app/src/main/res/values/settings_keys.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1317,7 +1317,6 @@
</string-array>

<string name="tablet_mode_key">tablet_mode</string>

<string name="tablet_mode_auto_key">auto</string>
<string name="tablet_mode_on_key">on</string>
<string name="tablet_mode_off_key">off</string>
Expand All @@ -1332,6 +1331,18 @@
<item>@string/off</item>
</string-array>

<string name="popup_configuration_key">popup_configuration</string>
<string name="popup_mode_pip">pip</string>
<string name="popup_mode_legacy">legacy</string>
<string-array name="popup_mode_values">
<item>@string/popup_mode_pip</item>
<item>@string/popup_mode_legacy</item>
</string-array>
<string-array name="popup_mode_description">
<item>@string/pip</item>
<item>@string/legacy</item>
</string-array>

<string name="recaptcha_cookies_key">recaptcha_cookies_key</string>

<string name="enable_streams_notifications">enable_streams_notifications</string>
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,11 @@
<string name="settings_category_updates_title">Updates</string>
<string name="settings_category_player_notification_title">Player notification</string>
<string name="settings_category_player_notification_summary">Configure current playing stream notification</string>
<string name="settings_category_player_popup_title">Popup player mode</string>
<string name="settings_category_player_popup_summary">Configure popup player mode</string>
<string name="background_player_playing_toast">Playing in background</string>
<string name="popup_playing_toast">Playing in popup mode</string>
<string name="popup_not_available">Popup playback not available in this mode / while paused</string>
<string name="content">Content</string>
<string name="show_age_restricted_content_title">Show age restricted content</string>
<string name="show_age_restricted_content_summary">Show content possibly unsuitable for children because it has an age limit (like 18+)</string>
Expand Down Expand Up @@ -564,6 +567,12 @@
<string name="grid">Grid</string>
<string name="card">Card</string>
<string name="auto">Auto</string>
<!-- Popup configuration -->
<string name="popup_mode">Popup mode</string>
<string name="pip">Android picture-in-picture</string>
<string name="legacy">NewPipe popup</string>
<string name="pip_unavailable">Not configurable on Android versions below 8.0</string>
<string name="pip_disabled">Not configurable on Android Go devices</string>
<!-- Seekbar Preview Thumbnail-->
<string name="seekbar_preview_thumbnail_title">Seekbar thumbnail preview</string>
<string name="high_quality_larger">High quality (larger)</string>
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/xml/video_audio_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,16 @@
app:singleLineTitle="false"
app:iconSpaceReserved="false" />

<ListPreference
android:defaultValue="@string/popup_mode_legacy"
android:entries="@array/popup_mode_description"
android:entryValues="@array/popup_mode_values"
android:key="@string/popup_configuration_key"
android:summary="%s"
android:title="@string/settings_category_player_popup_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />

<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/start_main_player_fullscreen_key"
Expand Down