Merge "Moving shared classes to common.pip package 3/N" into udc-qpr-dev
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.wm.shell.common.pip
|
||||
|
||||
import android.annotation.DrawableRes
|
||||
import android.annotation.StringRes
|
||||
import android.app.PendingIntent
|
||||
import android.app.RemoteAction
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.drawable.Icon
|
||||
import android.media.MediaMetadata
|
||||
import android.media.session.MediaController
|
||||
import android.media.session.MediaSession
|
||||
import android.media.session.MediaSessionManager
|
||||
import android.media.session.PlaybackState
|
||||
import android.os.Handler
|
||||
import android.os.HandlerExecutor
|
||||
import android.os.UserHandle
|
||||
import com.android.wm.shell.R
|
||||
import com.android.wm.shell.pip.PipUtils
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* Interfaces with the [MediaSessionManager] to compose the right set of actions to show (only
|
||||
* if there are no actions from the PiP activity itself). The active media controller is only set
|
||||
* when there is a media session from the top PiP activity.
|
||||
*/
|
||||
class PipMediaController(private val mContext: Context, private val mMainHandler: Handler) {
|
||||
/**
|
||||
* A listener interface to receive notification on changes to the media actions.
|
||||
*/
|
||||
interface ActionListener {
|
||||
/**
|
||||
* Called when the media actions changed.
|
||||
*/
|
||||
fun onMediaActionsChanged(actions: List<RemoteAction?>?)
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener interface to receive notification on changes to the media metadata.
|
||||
*/
|
||||
interface MetadataListener {
|
||||
/**
|
||||
* Called when the media metadata changed.
|
||||
*/
|
||||
fun onMediaMetadataChanged(metadata: MediaMetadata?)
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener interface to receive notification on changes to the media session token.
|
||||
*/
|
||||
interface TokenListener {
|
||||
/**
|
||||
* Called when the media session token changed.
|
||||
*/
|
||||
fun onMediaSessionTokenChanged(token: MediaSession.Token?)
|
||||
}
|
||||
|
||||
private val mHandlerExecutor: HandlerExecutor = HandlerExecutor(mMainHandler)
|
||||
private val mMediaSessionManager: MediaSessionManager?
|
||||
private var mMediaController: MediaController? = null
|
||||
private val mPauseAction: RemoteAction
|
||||
private val mPlayAction: RemoteAction
|
||||
private val mNextAction: RemoteAction
|
||||
private val mPrevAction: RemoteAction
|
||||
private val mMediaActionReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (mMediaController == null) {
|
||||
// no active media session, bail early.
|
||||
return
|
||||
}
|
||||
when (intent.action) {
|
||||
ACTION_PLAY -> mMediaController!!.transportControls.play()
|
||||
ACTION_PAUSE -> mMediaController!!.transportControls.pause()
|
||||
ACTION_NEXT -> mMediaController!!.transportControls.skipToNext()
|
||||
ACTION_PREV -> mMediaController!!.transportControls.skipToPrevious()
|
||||
}
|
||||
}
|
||||
}
|
||||
private val mPlaybackChangedListener: MediaController.Callback =
|
||||
object : MediaController.Callback() {
|
||||
override fun onPlaybackStateChanged(state: PlaybackState?) {
|
||||
notifyActionsChanged()
|
||||
}
|
||||
|
||||
override fun onMetadataChanged(metadata: MediaMetadata?) {
|
||||
notifyMetadataChanged(metadata)
|
||||
}
|
||||
}
|
||||
private val mSessionsChangedListener =
|
||||
MediaSessionManager.OnActiveSessionsChangedListener { controllers: List<MediaController>? ->
|
||||
resolveActiveMediaController(controllers)
|
||||
}
|
||||
private val mActionListeners = ArrayList<ActionListener>()
|
||||
private val mMetadataListeners = ArrayList<MetadataListener>()
|
||||
private val mTokenListeners = ArrayList<TokenListener>()
|
||||
|
||||
init {
|
||||
val mediaControlFilter = IntentFilter()
|
||||
mediaControlFilter.addAction(ACTION_PLAY)
|
||||
mediaControlFilter.addAction(ACTION_PAUSE)
|
||||
mediaControlFilter.addAction(ACTION_NEXT)
|
||||
mediaControlFilter.addAction(ACTION_PREV)
|
||||
mContext.registerReceiverForAllUsers(
|
||||
mMediaActionReceiver, mediaControlFilter,
|
||||
SYSTEMUI_PERMISSION, mMainHandler, Context.RECEIVER_EXPORTED
|
||||
)
|
||||
|
||||
// Creates the standard media buttons that we may show.
|
||||
mPauseAction = getDefaultRemoteAction(
|
||||
R.string.pip_pause,
|
||||
R.drawable.pip_ic_pause_white, ACTION_PAUSE
|
||||
)
|
||||
mPlayAction = getDefaultRemoteAction(
|
||||
R.string.pip_play,
|
||||
R.drawable.pip_ic_play_arrow_white, ACTION_PLAY
|
||||
)
|
||||
mNextAction = getDefaultRemoteAction(
|
||||
R.string.pip_skip_to_next,
|
||||
R.drawable.pip_ic_skip_next_white, ACTION_NEXT
|
||||
)
|
||||
mPrevAction = getDefaultRemoteAction(
|
||||
R.string.pip_skip_to_prev,
|
||||
R.drawable.pip_ic_skip_previous_white, ACTION_PREV
|
||||
)
|
||||
mMediaSessionManager = mContext.getSystemService(
|
||||
MediaSessionManager::class.java
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when an activity is pinned.
|
||||
*/
|
||||
fun onActivityPinned() {
|
||||
// Once we enter PiP, try to find the active media controller for the top most activity
|
||||
resolveActiveMediaController(
|
||||
mMediaSessionManager!!.getActiveSessionsForUser(
|
||||
null,
|
||||
UserHandle.CURRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new media action listener.
|
||||
*/
|
||||
fun addActionListener(listener: ActionListener) {
|
||||
if (!mActionListeners.contains(listener)) {
|
||||
mActionListeners.add(listener)
|
||||
listener.onMediaActionsChanged(mediaActions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a media action listener.
|
||||
*/
|
||||
fun removeActionListener(listener: ActionListener) {
|
||||
listener.onMediaActionsChanged(emptyList<RemoteAction>())
|
||||
mActionListeners.remove(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new media metadata listener.
|
||||
*/
|
||||
fun addMetadataListener(listener: MetadataListener) {
|
||||
if (!mMetadataListeners.contains(listener)) {
|
||||
mMetadataListeners.add(listener)
|
||||
listener.onMediaMetadataChanged(mediaMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a media metadata listener.
|
||||
*/
|
||||
fun removeMetadataListener(listener: MetadataListener) {
|
||||
listener.onMediaMetadataChanged(null)
|
||||
mMetadataListeners.remove(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new token listener.
|
||||
*/
|
||||
fun addTokenListener(listener: TokenListener) {
|
||||
if (!mTokenListeners.contains(listener)) {
|
||||
mTokenListeners.add(listener)
|
||||
listener.onMediaSessionTokenChanged(token)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a token listener.
|
||||
*/
|
||||
fun removeTokenListener(listener: TokenListener) {
|
||||
listener.onMediaSessionTokenChanged(null)
|
||||
mTokenListeners.remove(listener)
|
||||
}
|
||||
|
||||
private val token: MediaSession.Token?
|
||||
get() = if (mMediaController == null) {
|
||||
null
|
||||
} else mMediaController!!.sessionToken
|
||||
private val mediaMetadata: MediaMetadata?
|
||||
get() = if (mMediaController != null) mMediaController!!.metadata else null
|
||||
|
||||
private val mediaActions: List<RemoteAction?>
|
||||
/**
|
||||
* Gets the set of media actions currently available.
|
||||
*/
|
||||
get() {
|
||||
if (mMediaController == null) {
|
||||
return emptyList<RemoteAction>()
|
||||
}
|
||||
// Cache the PlaybackState since it's a Binder call.
|
||||
// Safe because mMediaController is guaranteed non-null here.
|
||||
val playbackState: PlaybackState = mMediaController!!.playbackState
|
||||
?: return emptyList<RemoteAction>()
|
||||
val mediaActions = ArrayList<RemoteAction?>()
|
||||
val isPlaying = playbackState.isActive
|
||||
val actions = playbackState.actions
|
||||
|
||||
// Prev action
|
||||
mPrevAction.isEnabled =
|
||||
actions and PlaybackState.ACTION_SKIP_TO_PREVIOUS != 0L
|
||||
mediaActions.add(mPrevAction)
|
||||
|
||||
// Play/pause action
|
||||
if (!isPlaying && actions and PlaybackState.ACTION_PLAY != 0L) {
|
||||
mediaActions.add(mPlayAction)
|
||||
} else if (isPlaying && actions and PlaybackState.ACTION_PAUSE != 0L) {
|
||||
mediaActions.add(mPauseAction)
|
||||
}
|
||||
|
||||
// Next action
|
||||
mNextAction.isEnabled =
|
||||
actions and PlaybackState.ACTION_SKIP_TO_NEXT != 0L
|
||||
mediaActions.add(mNextAction)
|
||||
return mediaActions
|
||||
}
|
||||
|
||||
/** @return Default [RemoteAction] sends broadcast back to SysUI.
|
||||
*/
|
||||
private fun getDefaultRemoteAction(
|
||||
@StringRes titleAndDescription: Int,
|
||||
@DrawableRes icon: Int,
|
||||
action: String
|
||||
): RemoteAction {
|
||||
val titleAndDescriptionStr = mContext.getString(titleAndDescription)
|
||||
val intent = Intent(action)
|
||||
intent.setPackage(mContext.packageName)
|
||||
return RemoteAction(
|
||||
Icon.createWithResource(mContext, icon),
|
||||
titleAndDescriptionStr, titleAndDescriptionStr,
|
||||
PendingIntent.getBroadcast(
|
||||
mContext, 0 /* requestCode */, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-registers the session listener for the current user.
|
||||
*/
|
||||
fun registerSessionListenerForCurrentUser() {
|
||||
mMediaSessionManager!!.removeOnActiveSessionsChangedListener(mSessionsChangedListener)
|
||||
mMediaSessionManager.addOnActiveSessionsChangedListener(
|
||||
null, UserHandle.CURRENT,
|
||||
mHandlerExecutor, mSessionsChangedListener
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find and set the active media controller for the top PiP activity.
|
||||
*/
|
||||
private fun resolveActiveMediaController(controllers: List<MediaController>?) {
|
||||
if (controllers != null) {
|
||||
val topActivity = PipUtils.getTopPipActivity(mContext).first
|
||||
if (topActivity != null) {
|
||||
for (i in controllers.indices) {
|
||||
val controller = controllers[i]
|
||||
if (controller.packageName == topActivity.packageName) {
|
||||
setActiveMediaController(controller)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setActiveMediaController(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active media controller for the top PiP activity.
|
||||
*/
|
||||
private fun setActiveMediaController(controller: MediaController?) {
|
||||
if (controller != mMediaController) {
|
||||
if (mMediaController != null) {
|
||||
mMediaController!!.unregisterCallback(mPlaybackChangedListener)
|
||||
}
|
||||
mMediaController = controller
|
||||
controller?.registerCallback(mPlaybackChangedListener, mMainHandler)
|
||||
notifyActionsChanged()
|
||||
notifyMetadataChanged(mediaMetadata)
|
||||
notifyTokenChanged(token)
|
||||
|
||||
// TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all listeners that the actions have changed.
|
||||
*/
|
||||
private fun notifyActionsChanged() {
|
||||
if (mActionListeners.isNotEmpty()) {
|
||||
val actions = mediaActions
|
||||
mActionListeners.forEach(
|
||||
Consumer { l: ActionListener -> l.onMediaActionsChanged(actions) })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all listeners that the metadata have changed.
|
||||
*/
|
||||
private fun notifyMetadataChanged(metadata: MediaMetadata?) {
|
||||
if (mMetadataListeners.isNotEmpty()) {
|
||||
mMetadataListeners.forEach(Consumer { l: MetadataListener ->
|
||||
l.onMediaMetadataChanged(
|
||||
metadata
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyTokenChanged(token: MediaSession.Token?) {
|
||||
if (mTokenListeners.isNotEmpty()) {
|
||||
mTokenListeners.forEach(Consumer { l: TokenListener ->
|
||||
l.onMediaSessionTokenChanged(
|
||||
token
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"
|
||||
private const val ACTION_PLAY = "com.android.wm.shell.pip.PLAY"
|
||||
private const val ACTION_PAUSE = "com.android.wm.shell.pip.PAUSE"
|
||||
private const val ACTION_NEXT = "com.android.wm.shell.pip.NEXT"
|
||||
private const val ACTION_PREV = "com.android.wm.shell.pip.PREV"
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ import com.android.wm.shell.common.annotations.ShellAnimationThread;
|
||||
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
|
||||
import com.android.wm.shell.common.annotations.ShellMainThread;
|
||||
import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
|
||||
import com.android.wm.shell.common.pip.PipMediaController;
|
||||
import com.android.wm.shell.common.pip.PipUiEventLogger;
|
||||
import com.android.wm.shell.compatui.CompatUIConfiguration;
|
||||
import com.android.wm.shell.compatui.CompatUIController;
|
||||
@@ -332,6 +333,13 @@ public abstract class WMShellBaseModule {
|
||||
return new PipUiEventLogger(uiEventLogger, packageManager);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
@Provides
|
||||
static PipMediaController providePipMediaController(Context context,
|
||||
@ShellMainThread Handler mainHandler) {
|
||||
return new PipMediaController(context, mainHandler);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Bubbles (optional feature)
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
import com.android.wm.shell.common.annotations.ShellMainThread;
|
||||
import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
|
||||
import com.android.wm.shell.common.pip.PipAppOpsListener;
|
||||
import com.android.wm.shell.common.pip.PipMediaController;
|
||||
import com.android.wm.shell.common.pip.PipUiEventLogger;
|
||||
import com.android.wm.shell.common.pip.SizeSpecSource;
|
||||
import com.android.wm.shell.dagger.WMShellBaseModule;
|
||||
@@ -42,7 +43,6 @@ import com.android.wm.shell.pip.PipAnimationController;
|
||||
import com.android.wm.shell.pip.PipBoundsAlgorithm;
|
||||
import com.android.wm.shell.pip.PipBoundsState;
|
||||
import com.android.wm.shell.pip.PipDisplayLayoutState;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipParamsChangedForwarder;
|
||||
import com.android.wm.shell.pip.PipSnapAlgorithm;
|
||||
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
|
||||
|
||||
@@ -17,11 +17,8 @@
|
||||
package com.android.wm.shell.dagger.pip;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
|
||||
import com.android.wm.shell.common.annotations.ShellMainThread;
|
||||
import com.android.wm.shell.dagger.WMSingleton;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
|
||||
|
||||
import dagger.Module;
|
||||
@@ -33,14 +30,6 @@ import dagger.Provides;
|
||||
*/
|
||||
@Module
|
||||
public abstract class Pip1SharedModule {
|
||||
// Needs handler for registering broadcast receivers
|
||||
@WMSingleton
|
||||
@Provides
|
||||
static PipMediaController providePipMediaController(Context context,
|
||||
@ShellMainThread Handler mainHandler) {
|
||||
return new PipMediaController(context, mainHandler);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
@Provides
|
||||
static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper(Context context) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
import com.android.wm.shell.common.annotations.ShellMainThread;
|
||||
import com.android.wm.shell.common.pip.LegacySizeSpecSource;
|
||||
import com.android.wm.shell.common.pip.PipAppOpsListener;
|
||||
import com.android.wm.shell.common.pip.PipMediaController;
|
||||
import com.android.wm.shell.common.pip.PipUiEventLogger;
|
||||
import com.android.wm.shell.common.pip.SizeSpecSource;
|
||||
import com.android.wm.shell.dagger.WMShellBaseModule;
|
||||
@@ -37,7 +38,6 @@ import com.android.wm.shell.dagger.WMSingleton;
|
||||
import com.android.wm.shell.pip.Pip;
|
||||
import com.android.wm.shell.pip.PipAnimationController;
|
||||
import com.android.wm.shell.pip.PipDisplayLayoutState;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipParamsChangedForwarder;
|
||||
import com.android.wm.shell.pip.PipSnapAlgorithm;
|
||||
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.pip;
|
||||
|
||||
import static android.app.PendingIntent.FLAG_IMMUTABLE;
|
||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
|
||||
import android.annotation.DrawableRes;
|
||||
import android.annotation.StringRes;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.RemoteAction;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.media.MediaMetadata;
|
||||
import android.media.session.MediaController;
|
||||
import android.media.session.MediaSession;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerExecutor;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interfaces with the {@link MediaSessionManager} to compose the right set of actions to show (only
|
||||
* if there are no actions from the PiP activity itself). The active media controller is only set
|
||||
* when there is a media session from the top PiP activity.
|
||||
*/
|
||||
public class PipMediaController {
|
||||
private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
|
||||
|
||||
private static final String ACTION_PLAY = "com.android.wm.shell.pip.PLAY";
|
||||
private static final String ACTION_PAUSE = "com.android.wm.shell.pip.PAUSE";
|
||||
private static final String ACTION_NEXT = "com.android.wm.shell.pip.NEXT";
|
||||
private static final String ACTION_PREV = "com.android.wm.shell.pip.PREV";
|
||||
|
||||
/**
|
||||
* A listener interface to receive notification on changes to the media actions.
|
||||
*/
|
||||
public interface ActionListener {
|
||||
/**
|
||||
* Called when the media actions changed.
|
||||
*/
|
||||
void onMediaActionsChanged(List<RemoteAction> actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener interface to receive notification on changes to the media metadata.
|
||||
*/
|
||||
public interface MetadataListener {
|
||||
/**
|
||||
* Called when the media metadata changed.
|
||||
*/
|
||||
void onMediaMetadataChanged(MediaMetadata metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener interface to receive notification on changes to the media session token.
|
||||
*/
|
||||
public interface TokenListener {
|
||||
/**
|
||||
* Called when the media session token changed.
|
||||
*/
|
||||
void onMediaSessionTokenChanged(MediaSession.Token token);
|
||||
}
|
||||
|
||||
private final Context mContext;
|
||||
private final Handler mMainHandler;
|
||||
private final HandlerExecutor mHandlerExecutor;
|
||||
|
||||
private final MediaSessionManager mMediaSessionManager;
|
||||
private MediaController mMediaController;
|
||||
|
||||
private RemoteAction mPauseAction;
|
||||
private RemoteAction mPlayAction;
|
||||
private RemoteAction mNextAction;
|
||||
private RemoteAction mPrevAction;
|
||||
|
||||
private final BroadcastReceiver mMediaActionReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (mMediaController == null || mMediaController.getTransportControls() == null) {
|
||||
// no active media session, bail early.
|
||||
return;
|
||||
}
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_PLAY:
|
||||
mMediaController.getTransportControls().play();
|
||||
break;
|
||||
case ACTION_PAUSE:
|
||||
mMediaController.getTransportControls().pause();
|
||||
break;
|
||||
case ACTION_NEXT:
|
||||
mMediaController.getTransportControls().skipToNext();
|
||||
break;
|
||||
case ACTION_PREV:
|
||||
mMediaController.getTransportControls().skipToPrevious();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final MediaController.Callback mPlaybackChangedListener =
|
||||
new MediaController.Callback() {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(PlaybackState state) {
|
||||
notifyActionsChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataChanged(@Nullable MediaMetadata metadata) {
|
||||
notifyMetadataChanged(metadata);
|
||||
}
|
||||
};
|
||||
|
||||
private final MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener =
|
||||
this::resolveActiveMediaController;
|
||||
|
||||
private final ArrayList<ActionListener> mActionListeners = new ArrayList<>();
|
||||
private final ArrayList<MetadataListener> mMetadataListeners = new ArrayList<>();
|
||||
private final ArrayList<TokenListener> mTokenListeners = new ArrayList<>();
|
||||
|
||||
public PipMediaController(Context context, Handler mainHandler) {
|
||||
mContext = context;
|
||||
mMainHandler = mainHandler;
|
||||
mHandlerExecutor = new HandlerExecutor(mMainHandler);
|
||||
if (!PipUtils.isPip2ExperimentEnabled()) {
|
||||
IntentFilter mediaControlFilter = new IntentFilter();
|
||||
mediaControlFilter.addAction(ACTION_PLAY);
|
||||
mediaControlFilter.addAction(ACTION_PAUSE);
|
||||
mediaControlFilter.addAction(ACTION_NEXT);
|
||||
mediaControlFilter.addAction(ACTION_PREV);
|
||||
mContext.registerReceiverForAllUsers(mMediaActionReceiver, mediaControlFilter,
|
||||
SYSTEMUI_PERMISSION, mainHandler, Context.RECEIVER_EXPORTED);
|
||||
}
|
||||
|
||||
// Creates the standard media buttons that we may show.
|
||||
mPauseAction = getDefaultRemoteAction(R.string.pip_pause,
|
||||
R.drawable.pip_ic_pause_white, ACTION_PAUSE);
|
||||
mPlayAction = getDefaultRemoteAction(R.string.pip_play,
|
||||
R.drawable.pip_ic_play_arrow_white, ACTION_PLAY);
|
||||
mNextAction = getDefaultRemoteAction(R.string.pip_skip_to_next,
|
||||
R.drawable.pip_ic_skip_next_white, ACTION_NEXT);
|
||||
mPrevAction = getDefaultRemoteAction(R.string.pip_skip_to_prev,
|
||||
R.drawable.pip_ic_skip_previous_white, ACTION_PREV);
|
||||
|
||||
mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when an activity is pinned.
|
||||
*/
|
||||
public void onActivityPinned() {
|
||||
// Once we enter PiP, try to find the active media controller for the top most activity
|
||||
resolveActiveMediaController(mMediaSessionManager.getActiveSessionsForUser(null,
|
||||
UserHandle.CURRENT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new media action listener.
|
||||
*/
|
||||
public void addActionListener(ActionListener listener) {
|
||||
if (!mActionListeners.contains(listener)) {
|
||||
mActionListeners.add(listener);
|
||||
listener.onMediaActionsChanged(getMediaActions());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a media action listener.
|
||||
*/
|
||||
public void removeActionListener(ActionListener listener) {
|
||||
listener.onMediaActionsChanged(Collections.emptyList());
|
||||
mActionListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new media metadata listener.
|
||||
*/
|
||||
public void addMetadataListener(MetadataListener listener) {
|
||||
if (!mMetadataListeners.contains(listener)) {
|
||||
mMetadataListeners.add(listener);
|
||||
listener.onMediaMetadataChanged(getMediaMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a media metadata listener.
|
||||
*/
|
||||
public void removeMetadataListener(MetadataListener listener) {
|
||||
listener.onMediaMetadataChanged(null);
|
||||
mMetadataListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new token listener.
|
||||
*/
|
||||
public void addTokenListener(TokenListener listener) {
|
||||
if (!mTokenListeners.contains(listener)) {
|
||||
mTokenListeners.add(listener);
|
||||
listener.onMediaSessionTokenChanged(getToken());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a token listener.
|
||||
*/
|
||||
public void removeTokenListener(TokenListener listener) {
|
||||
listener.onMediaSessionTokenChanged(null);
|
||||
mTokenListeners.remove(listener);
|
||||
}
|
||||
|
||||
private MediaSession.Token getToken() {
|
||||
if (mMediaController == null) {
|
||||
return null;
|
||||
}
|
||||
return mMediaController.getSessionToken();
|
||||
}
|
||||
|
||||
private MediaMetadata getMediaMetadata() {
|
||||
return mMediaController != null ? mMediaController.getMetadata() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the set of media actions currently available.
|
||||
*/
|
||||
// This is due to using PlaybackState#isActive, which is added in API 31.
|
||||
// It can be removed when min_sdk of the app is set to 31 or greater.
|
||||
@SuppressLint("NewApi")
|
||||
private List<RemoteAction> getMediaActions() {
|
||||
// Cache the PlaybackState since it's a Binder call.
|
||||
final PlaybackState playbackState;
|
||||
if (mMediaController == null
|
||||
|| (playbackState = mMediaController.getPlaybackState()) == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
ArrayList<RemoteAction> mediaActions = new ArrayList<>();
|
||||
boolean isPlaying = playbackState.isActive();
|
||||
long actions = playbackState.getActions();
|
||||
|
||||
// Prev action
|
||||
mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
|
||||
mediaActions.add(mPrevAction);
|
||||
|
||||
// Play/pause action
|
||||
if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
|
||||
mediaActions.add(mPlayAction);
|
||||
} else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
|
||||
mediaActions.add(mPauseAction);
|
||||
}
|
||||
|
||||
// Next action
|
||||
mNextAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0);
|
||||
mediaActions.add(mNextAction);
|
||||
return mediaActions;
|
||||
}
|
||||
|
||||
/** @return Default {@link RemoteAction} sends broadcast back to SysUI. */
|
||||
private RemoteAction getDefaultRemoteAction(@StringRes int titleAndDescription,
|
||||
@DrawableRes int icon, String action) {
|
||||
final String titleAndDescriptionStr = mContext.getString(titleAndDescription);
|
||||
final Intent intent = new Intent(action);
|
||||
intent.setPackage(mContext.getPackageName());
|
||||
return new RemoteAction(Icon.createWithResource(mContext, icon),
|
||||
titleAndDescriptionStr, titleAndDescriptionStr,
|
||||
PendingIntent.getBroadcast(mContext, 0 /* requestCode */, intent,
|
||||
FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-registers the session listener for the current user.
|
||||
*/
|
||||
public void registerSessionListenerForCurrentUser() {
|
||||
mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsChangedListener);
|
||||
mMediaSessionManager.addOnActiveSessionsChangedListener(null, UserHandle.CURRENT,
|
||||
mHandlerExecutor, mSessionsChangedListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find and set the active media controller for the top PiP activity.
|
||||
*/
|
||||
private void resolveActiveMediaController(List<MediaController> controllers) {
|
||||
if (controllers != null) {
|
||||
final ComponentName topActivity = PipUtils.getTopPipActivity(mContext).first;
|
||||
if (topActivity != null) {
|
||||
for (int i = 0; i < controllers.size(); i++) {
|
||||
final MediaController controller = controllers.get(i);
|
||||
if (controller.getPackageName().equals(topActivity.getPackageName())) {
|
||||
setActiveMediaController(controller);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setActiveMediaController(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active media controller for the top PiP activity.
|
||||
*/
|
||||
private void setActiveMediaController(MediaController controller) {
|
||||
if (controller != mMediaController) {
|
||||
if (mMediaController != null) {
|
||||
mMediaController.unregisterCallback(mPlaybackChangedListener);
|
||||
}
|
||||
mMediaController = controller;
|
||||
if (controller != null) {
|
||||
controller.registerCallback(mPlaybackChangedListener, mMainHandler);
|
||||
}
|
||||
notifyActionsChanged();
|
||||
notifyMetadataChanged(getMediaMetadata());
|
||||
notifyTokenChanged(getToken());
|
||||
|
||||
// TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all listeners that the actions have changed.
|
||||
*/
|
||||
private void notifyActionsChanged() {
|
||||
if (!mActionListeners.isEmpty()) {
|
||||
List<RemoteAction> actions = getMediaActions();
|
||||
mActionListeners.forEach(l -> l.onMediaActionsChanged(actions));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all listeners that the metadata have changed.
|
||||
*/
|
||||
private void notifyMetadataChanged(MediaMetadata metadata) {
|
||||
if (!mMetadataListeners.isEmpty()) {
|
||||
mMetadataListeners.forEach(l -> l.onMediaMetadataChanged(metadata));
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyTokenChanged(MediaSession.Token token) {
|
||||
if (!mTokenListeners.isEmpty()) {
|
||||
mTokenListeners.forEach(l -> l.onMediaSessionTokenChanged(token));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,10 @@ import android.view.WindowManagerGlobal;
|
||||
import com.android.internal.protolog.common.ProtoLog;
|
||||
import com.android.wm.shell.common.ShellExecutor;
|
||||
import com.android.wm.shell.common.SystemWindows;
|
||||
import com.android.wm.shell.common.pip.PipMediaController;
|
||||
import com.android.wm.shell.common.pip.PipMediaController.ActionListener;
|
||||
import com.android.wm.shell.common.pip.PipUiEventLogger;
|
||||
import com.android.wm.shell.pip.PipBoundsState;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipMediaController.ActionListener;
|
||||
import com.android.wm.shell.pip.PipMenuController;
|
||||
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
|
||||
import com.android.wm.shell.protolog.ShellProtoLogGroup;
|
||||
|
||||
@@ -76,6 +76,7 @@ import com.android.wm.shell.common.TabletopModeController;
|
||||
import com.android.wm.shell.common.TaskStackListenerCallback;
|
||||
import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
import com.android.wm.shell.common.pip.PipAppOpsListener;
|
||||
import com.android.wm.shell.common.pip.PipMediaController;
|
||||
import com.android.wm.shell.onehanded.OneHandedController;
|
||||
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
|
||||
import com.android.wm.shell.pip.IPip;
|
||||
@@ -87,7 +88,6 @@ import com.android.wm.shell.pip.PipBoundsAlgorithm;
|
||||
import com.android.wm.shell.pip.PipBoundsState;
|
||||
import com.android.wm.shell.pip.PipDisplayLayoutState;
|
||||
import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipParamsChangedForwarder;
|
||||
import com.android.wm.shell.pip.PipSnapAlgorithm;
|
||||
import com.android.wm.shell.pip.PipTaskOrganizer;
|
||||
|
||||
@@ -35,7 +35,7 @@ import android.content.Context;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.protolog.common.ProtoLog;
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.common.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipUtils;
|
||||
import com.android.wm.shell.protolog.ShellProtoLogGroup;
|
||||
|
||||
|
||||
@@ -48,11 +48,11 @@ import com.android.wm.shell.common.ShellExecutor;
|
||||
import com.android.wm.shell.common.TaskStackListenerCallback;
|
||||
import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
import com.android.wm.shell.common.pip.PipAppOpsListener;
|
||||
import com.android.wm.shell.common.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
|
||||
import com.android.wm.shell.pip.Pip;
|
||||
import com.android.wm.shell.pip.PipAnimationController;
|
||||
import com.android.wm.shell.pip.PipDisplayLayoutState;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipParamsChangedForwarder;
|
||||
import com.android.wm.shell.pip.PipTaskOrganizer;
|
||||
import com.android.wm.shell.pip.PipTransitionController;
|
||||
|
||||
@@ -36,7 +36,7 @@ import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
|
||||
import com.android.internal.protolog.common.ProtoLog;
|
||||
import com.android.internal.util.ImageUtils;
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.common.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipParamsChangedForwarder;
|
||||
import com.android.wm.shell.pip.PipUtils;
|
||||
import com.android.wm.shell.protolog.ShellProtoLogGroup;
|
||||
|
||||
@@ -56,12 +56,12 @@ import com.android.wm.shell.common.ShellExecutor;
|
||||
import com.android.wm.shell.common.TabletopModeController;
|
||||
import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
import com.android.wm.shell.common.pip.PipAppOpsListener;
|
||||
import com.android.wm.shell.common.pip.PipMediaController;
|
||||
import com.android.wm.shell.onehanded.OneHandedController;
|
||||
import com.android.wm.shell.pip.PipAnimationController;
|
||||
import com.android.wm.shell.pip.PipBoundsAlgorithm;
|
||||
import com.android.wm.shell.pip.PipBoundsState;
|
||||
import com.android.wm.shell.pip.PipDisplayLayoutState;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipParamsChangedForwarder;
|
||||
import com.android.wm.shell.pip.PipSnapAlgorithm;
|
||||
import com.android.wm.shell.pip.PipTaskOrganizer;
|
||||
|
||||
@@ -37,7 +37,7 @@ import android.testing.TestableLooper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.common.pip.PipMediaController;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
Reference in New Issue
Block a user