Merge pull request #2506 from ByteHamster/picture-in-picture

Added Picure in picture
This commit is contained in:
Martin Fietz 2018-04-22 08:07:05 +02:00 committed by GitHub
commit a9e269b3bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 222 additions and 36 deletions

View File

@ -222,7 +222,8 @@
<activity
android:name=".activity.VideoplayerActivity"
android:configChanges="keyboardHidden|orientation"
android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize"
android:supportsPictureInPicture="true"
android:screenOrientation="sensorLandscape">
<meta-data
android:name="android.support.PARENT_ACTIVITY"

View File

@ -46,6 +46,7 @@ import de.danoeh.antennapod.core.util.Flavors;
import de.danoeh.antennapod.core.util.ShareUtils;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.Supplier;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
@ -225,10 +226,12 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
@Override
protected void onPause() {
if(controller != null) {
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
if (controller != null) {
controller.reinitServiceIfPaused();
controller.pause();
}
}
super.onPause();
}
@ -379,6 +382,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
} else {
startActivity(intent);
}
finish();
return true;
} else {
if (media != null) {

View File

@ -10,27 +10,31 @@ import android.support.v4.view.WindowCompat;
import android.support.v7.app.ActionBar;
import android.util.Log;
import android.util.Pair;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.view.AspectRatioVideoView;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Activity for playing video files.
*/
@ -52,6 +56,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
private LinearLayout videoOverlay;
private AspectRatioVideoView videoview;
private ProgressBar progressIndicator;
private FrameLayout videoframe;
@Override
protected void chooseTheme() {
@ -96,11 +101,28 @@ public class VideoplayerActivity extends MediaplayerActivity {
}
@Override
protected void onPause() {
protected void onStop() {
super.onStop();
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
videoControlsHider.stop();
}
}
@Override
public void onUserLeaveHint () {
if (!PictureInPictureUtil.isInPictureInPictureMode(this) && UserPreferences.getVideoBackgroundBehavior()
== UserPreferences.VideoBackgroundBehavior.PICTURE_IN_PICTURE) {
compatEnterPictureInPicture();
}
}
@Override
protected void onPause() {
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) {
controller.pause();
}
}
super.onPause();
}
@ -127,7 +149,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override
protected void setupGUI() {
if(isSetup.getAndSet(true)) {
if (isSetup.getAndSet(true)) {
return;
}
super.setupGUI();
@ -135,20 +157,23 @@ public class VideoplayerActivity extends MediaplayerActivity {
controls = (LinearLayout) findViewById(R.id.controls);
videoOverlay = (LinearLayout) findViewById(R.id.overlay);
videoview = (AspectRatioVideoView) findViewById(R.id.videoview);
videoframe = (FrameLayout) findViewById(R.id.videoframe);
progressIndicator = (ProgressBar) findViewById(R.id.progressIndicator);
videoview.getHolder().addCallback(surfaceHolderCallback);
videoview.setOnTouchListener(onVideoviewTouched);
videoframe.setOnTouchListener(onVideoviewTouched);
videoOverlay.setOnTouchListener((view, motionEvent) -> true); // To suppress touches directly below the slider
if (Build.VERSION.SDK_INT >= 16) {
videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
}
if (Build.VERSION.SDK_INT >= 14) {
videoOverlay.setFitsSystemWindows(true);
}
setupVideoControlsToggler();
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
videoframe.getViewTreeObserver().addOnGlobalLayoutListener(() ->
videoview.setAvailableSize(videoframe.getWidth(), videoframe.getHeight()));
}
@Override
@ -176,6 +201,9 @@ public class VideoplayerActivity extends MediaplayerActivity {
private final View.OnTouchListener onVideoviewTouched = (v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (PictureInPictureUtil.isInPictureInPictureMode(this)) {
return true;
}
videoControlsHider.stop();
toggleVideoControlsVisibility();
if (videoControlsShowing) {
@ -260,7 +288,9 @@ public class VideoplayerActivity extends MediaplayerActivity {
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "Videosurface was destroyed");
videoSurfaceCreated = false;
if (controller != null && !destroyingDueToReload) {
if (controller != null && !destroyingDueToReload
&& UserPreferences.getVideoBackgroundBehavior()
!= UserPreferences.VideoBackgroundBehavior.CONTINUE_PLAYING) {
controller.notifyVideoSurfaceAbandoned();
}
}
@ -269,6 +299,13 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override
protected void onReloadNotification(int notificationCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && PictureInPictureUtil.isInPictureInPictureMode(this)) {
if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO
|| notificationCode == PlaybackService.EXTRA_CODE_CAST) {
finish();
}
return;
}
if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) {
Log.d(TAG, "ReloadNotification received, switching to Audioplayer now");
destroyingDueToReload = true;
@ -313,28 +350,31 @@ public class VideoplayerActivity extends MediaplayerActivity {
videoOverlay.startAnimation(animation);
controls.startAnimation(animation);
}
if (Build.VERSION.SDK_INT >= 14) {
videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
}
}
@SuppressLint("NewApi")
private void hideVideoControls() {
private void hideVideoControls(boolean showAnimation) {
if (showAnimation) {
final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_out);
if (animation != null) {
videoOverlay.startAnimation(animation);
controls.startAnimation(animation);
}
if (Build.VERSION.SDK_INT >= 14) {
}
int videoviewFlag = (Build.VERSION.SDK_INT >= 16) ? View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION : 0;
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | videoviewFlag);
videoOverlay.setFitsSystemWindows(true);
}
videoOverlay.setVisibility(View.GONE);
controls.setVisibility(View.GONE);
}
private void hideVideoControls() {
hideVideoControls(true);
}
@Override
protected int getContentViewResourceId() {
return R.layout.videoplayer_activity;
@ -350,6 +390,32 @@ public class VideoplayerActivity extends MediaplayerActivity {
}
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
if (PictureInPictureUtil.supportsPictureInPicture(this)) {
menu.findItem(R.id.player_go_to_picture_in_picture).setVisible(true);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.player_go_to_picture_in_picture) {
compatEnterPictureInPicture();
return true;
}
return super.onOptionsItemSelected(item);
}
private void compatEnterPictureInPicture() {
if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
getSupportActionBar().hide();
hideVideoControls(false);
enterPictureInPictureMode();
}
}
private static class VideoControlsHider extends Handler {
private static final int DELAY = 2500;
@ -362,7 +428,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
private final Runnable hideVideoControls = () -> {
VideoplayerActivity vpa = activity != null ? activity.get() : null;
if(vpa == null) {
if (vpa == null) {
return;
}
if (vpa.videoControlsShowing) {

View File

@ -3,6 +3,7 @@ package de.danoeh.antennapod.config;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.AudioplayerActivity;
import de.danoeh.antennapod.activity.CastplayerActivity;
@ -18,7 +19,11 @@ public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks {
return new Intent(context, CastplayerActivity.class);
}
if (mediaType == MediaType.VIDEO) {
return new Intent(context, VideoplayerActivity.class);
Intent i = new Intent(context, VideoplayerActivity.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
i.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
}
return i;
} else {
return new Intent(context, AudioplayerActivity.class);
}

View File

@ -38,8 +38,10 @@ import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
import com.afollestad.materialdialogs.prefs.MaterialListPreference;
import de.danoeh.antennapod.activity.ImportExportActivity;
import de.danoeh.antennapod.activity.OpmlImportFromPathActivity;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import org.apache.commons.lang3.ArrayUtils;
import java.io.File;
@ -426,6 +428,11 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
return false;
}
);
if (!PictureInPictureUtil.supportsPictureInPicture(activity)) {
MaterialListPreference behaviour = (MaterialListPreference) ui.findPreference(UserPreferences.PREF_VIDEO_BEHAVIOR);
behaviour.setEntries(R.array.video_background_behavior_options_without_pip);
behaviour.setEntryValues(R.array.video_background_behavior_values_without_pip);
}
ui.findPreference(PREF_PROXY).setOnPreferenceClickListener(preference -> {
ProxyDialog dialog = new ProxyDialog(ui.getActivity());
dialog.createDialog().show();

View File

@ -25,6 +25,8 @@ public class AspectRatioVideoView extends VideoView {
private int mVideoWidth;
private int mVideoHeight;
private float mAvailableWidth = -1;
private float mAvailableHeight = -1;
public AspectRatioVideoView(Context context) {
this(context, null);
@ -48,8 +50,13 @@ public class AspectRatioVideoView extends VideoView {
return;
}
float heightRatio = (float) mVideoHeight / (float) getHeight();
float widthRatio = (float) mVideoWidth / (float) getWidth();
if (mAvailableWidth < 0 || mAvailableHeight < 0) {
mAvailableWidth = getWidth();
mAvailableHeight = getHeight();
}
float heightRatio = (float) mVideoHeight / mAvailableHeight;
float widthRatio = (float) mVideoWidth / mAvailableWidth;
int scaledHeight;
int scaledWidth;
@ -94,4 +101,15 @@ public class AspectRatioVideoView extends VideoView {
invalidate();
}
/**
* Sets the maximum size that the view might expand to
* @param width
* @param height
*/
public void setAvailableSize(float width, float height) {
mAvailableWidth = width;
mAvailableHeight = height;
requestLayout();
}
}

View File

@ -3,7 +3,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
android:orientation="vertical"
android:id="@+id/videoframe">
<de.danoeh.antennapod.view.AspectRatioVideoView
android:id="@+id/videoview"
@ -103,6 +104,7 @@
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/txtvLength"
android:layout_toRightOf="@+id/txtvPosition"
android:layout_centerInParent="true"
android:max="500" />
</RelativeLayout>

View File

@ -41,6 +41,14 @@
android:title="@string/visit_website_label"
android:visible="false">
</item>
<item
android:id="@+id/player_go_to_picture_in_picture"
custom:showAsAction="collapseActionView"
android:title="@string/player_go_to_picture_in_picture"
android:visible="false">
</item>
<item
android:id="@+id/share_item"
android:icon="?attr/social_share"

View File

@ -171,6 +171,14 @@
android:key="prefResumeAfterCall"
android:summary="@string/pref_resumeAfterCall_sum"
android:title="@string/pref_resumeAfterCall_title"/>
<com.afollestad.materialdialogs.prefs.MaterialListPreference
android:defaultValue="stop"
android:entries="@array/video_background_behavior_options"
android:entryValues="@array/video_background_behavior_values"
android:key="prefVideoBehavior"
android:summary="@string/pref_videoBehavior_sum"
android:title="@string/pref_videoBehavior_title"
app:useStockLayout="true"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/network_pref">

View File

@ -74,6 +74,7 @@ public class UserPreferences {
private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray";
private static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss";
private static final String PREF_RESUME_AFTER_CALL = "prefResumeAfterCall";
public static final String PREF_VIDEO_BEHAVIOR = "prefVideoBehavior";
// Network
private static final String PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded";
@ -661,6 +662,14 @@ public class UserPreferences {
.apply();
}
public static VideoBackgroundBehavior getVideoBackgroundBehavior() {
switch (prefs.getString(PREF_VIDEO_BEHAVIOR, "stop")) {
case "stop": return VideoBackgroundBehavior.STOP;
case "pip": return VideoBackgroundBehavior.PICTURE_IN_PICTURE;
case "continue": return VideoBackgroundBehavior.CONTINUE_PLAYING;
default: return VideoBackgroundBehavior.STOP;
}
}
public static EpisodeCleanupAlgorithm getEpisodeCleanupAlgorithm() {
int cleanupValue = Integer.parseInt(prefs.getString(PREF_EPISODE_CLEANUP, "-1"));
@ -839,4 +848,8 @@ public class UserPreferences {
public static boolean isCastEnabled() {
return prefs.getBoolean(PREF_CAST_ENABLED, false);
}
public enum VideoBackgroundBehavior {
STOP, PICTURE_IN_PICTURE, CONTINUE_PLAYING
}
}

View File

@ -0,0 +1,27 @@
package de.danoeh.antennapod.core.util.gui;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
public class PictureInPictureUtil {
private PictureInPictureUtil() {
}
public static boolean supportsPictureInPicture(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager packageManager = activity.getPackageManager();
return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
} else {
return false;
}
}
public static boolean isInPictureInPictureMode(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && supportsPictureInPicture(activity)) {
return activity.isInPictureInPictureMode();
} else {
return false;
}
}
}

View File

@ -224,4 +224,26 @@
<item>@string/fast_forward_label</item>
<item>@string/skip_episode_label</item>
</string-array>
<string-array name="video_background_behavior_options">
<item>@string/stop_playback</item>
<item>@string/player_go_to_picture_in_picture</item>
<item>@string/continue_playback</item>
</string-array>
<string-array name="video_background_behavior_values">
<item>stop</item>
<item>pip</item>
<item>continue</item>
</string-array>
<string-array name="video_background_behavior_options_without_pip">
<item>@string/stop_playback</item>
<item>@string/continue_playback</item>
</string-array>
<string-array name="video_background_behavior_values_without_pip">
<item>stop</item>
<item>continue</item>
</string-array>
</resources>

View File

@ -237,6 +237,7 @@
<string name="no_media_playing_label">No media playing</string>
<string name="position_default_label" translate="false">00:00:00</string>
<string name="player_buffering_msg">Buffering</string>
<string name="player_go_to_picture_in_picture">Picture-in-picture mode</string>
<string name="playbackservice_notification_title">Playing podcast</string>
<string name="unknown_media_key">AntennaPod - Unknown media key: %1$d</string>
@ -440,6 +441,10 @@
<string name="pref_cast_message_free_flavor">Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPod</string>
<string name="pref_enqueue_downloaded_title">Enqueue Downloaded</string>
<string name="pref_enqueue_downloaded_summary">Add downloaded episodes to the queue</string>
<string name="pref_videoBehavior_title">Video behavior</string>
<string name="pref_videoBehavior_sum">Behavior when leaving video playback</string>
<string name="stop_playback">Stop playback</string>
<string name="continue_playback">Continue playback</string>
<!-- Auto-Flattr dialog -->
<string name="auto_flattr_enable">Enable automatic flattring</string>