blob: fffb1e935622833eee20dce61cababd4caa1343b [file] [log] [blame]
/*
* Copyright 2018 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.media;
import android.Manifest.permission;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.media.AudioAttributes;
import android.media.IMediaSession2Callback;
import android.media.MediaItem2;
import android.media.MediaPlayerBase;
import android.media.MediaSession2;
import android.media.MediaSession2.Builder;
import android.media.MediaSession2.Command;
import android.media.MediaSession2.CommandButton;
import android.media.MediaSession2.CommandGroup;
import android.media.MediaSession2.ControllerInfo;
import android.media.MediaSession2.PlaylistParam;
import android.media.MediaSession2.SessionCallback;
import android.media.PlaybackState2;
import android.media.SessionToken2;
import android.media.VolumeProvider;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.media.update.MediaSession2Provider;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.ResultReceiver;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
public class MediaSession2Impl implements MediaSession2Provider {
private static final String TAG = "MediaSession2";
private static final boolean DEBUG = true;//Log.isLoggable(TAG, Log.DEBUG);
private final MediaSession2 mInstance;
private final Context mContext;
private final String mId;
private final Handler mHandler;
private final Executor mCallbackExecutor;
private final MediaSession2Stub mSessionStub;
private final SessionToken2 mSessionToken;
private MediaPlayerBase mPlayer;
private final List<PlaybackListenerHolder> mListeners = new ArrayList<>();
private MyPlaybackListener mListener;
private MediaSession2 instance;
/**
* Can be only called by the {@link Builder#build()}.
*
* @param instance
* @param context
* @param player
* @param id
* @param callback
* @param volumeProvider
* @param ratingType
* @param sessionActivity
*/
public MediaSession2Impl(MediaSession2 instance, Context context, MediaPlayerBase player,
String id, Executor callbackExecutor, SessionCallback callback,
VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity) {
mInstance = instance;
// TODO(jaewan): Keep other params.
// Argument checks are done by builder already.
// Initialize finals first.
mContext = context;
mId = id;
mHandler = new Handler(Looper.myLooper());
mCallbackExecutor = callbackExecutor;
mSessionStub = new MediaSession2Stub(this, callback);
// Ask server to create session token for following reasons.
// 1. Make session ID unique per package.
// Server can only know if the package has another process and has another session
// with the same id. Let server check this.
// Note that 'ID is unique per package' is important for controller to distinguish
// a session in another package.
// 2. Easier to know the type of session.
// Session created here can be the session service token. In order distinguish,
// we need to iterate AndroidManifest.xml but it's already done by the server.
// Let server to create token with the type.
MediaSessionManager manager =
(MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
mSessionToken = manager.createSessionToken(mContext.getPackageName(), mId, mSessionStub);
if (mSessionToken == null) {
throw new IllegalStateException("Session with the same id is already used by"
+ " another process. Use MediaController2 instead.");
}
setPlayerInternal(player);
}
// TODO(jaewan): Add explicit release() and do not remove session object with the
// setPlayer(null). Token can be available when player is null, and
// controller can also attach to session.
@Override
public void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider) throws IllegalArgumentException {
ensureCallingThread();
if (player == null) {
throw new IllegalArgumentException("player shouldn't be null");
}
setPlayerInternal(player);
}
private void setPlayerInternal(MediaPlayerBase player) {
if (mPlayer == player) {
// Player didn't changed. No-op.
return;
}
// TODO(jaewan): Find equivalent for the executor
//mHandler.removeCallbacksAndMessages(null);
if (mPlayer != null && mListener != null) {
// This might not work for a poorly implemented player.
mPlayer.removePlaybackListener(mListener);
}
mListener = new MyPlaybackListener(this, player);
player.addPlaybackListener(mCallbackExecutor, mListener);
notifyPlaybackStateChanged(player.getPlaybackState());
mPlayer = player;
}
@Override
public void close_impl() {
// Flush any pending messages.
mHandler.removeCallbacksAndMessages(null);
if (mSessionStub != null) {
if (DEBUG) {
Log.d(TAG, "session is now unavailable, id=" + mId);
}
// Invalidate previously published session stub.
mSessionStub.destroyNotLocked();
}
}
@Override
public MediaPlayerBase getPlayer_impl() {
return getPlayer();
}
// TODO(jaewan): Change this to @NonNull
@Override
public SessionToken2 getToken_impl() {
return mSessionToken;
}
@Override
public List<ControllerInfo> getConnectedControllers_impl() {
return mSessionStub.getControllers();
}
@Override
public void setAudioAttributes_impl(AudioAttributes attributes) {
// implement
}
@Override
public void setAudioFocusRequest_impl(int focusGain) {
// implement
}
@Override
public void play_impl() {
ensureCallingThread();
ensurePlayer();
mPlayer.play();
}
@Override
public void pause_impl() {
ensureCallingThread();
ensurePlayer();
mPlayer.pause();
}
@Override
public void stop_impl() {
ensureCallingThread();
ensurePlayer();
mPlayer.stop();
}
@Override
public void skipToPrevious_impl() {
ensureCallingThread();
ensurePlayer();
mPlayer.skipToPrevious();
}
@Override
public void skipToNext_impl() {
ensureCallingThread();
ensurePlayer();
mPlayer.skipToNext();
}
@Override
public void setCustomLayout_impl(ControllerInfo controller, List<CommandButton> layout) {
ensureCallingThread();
if (controller == null) {
throw new IllegalArgumentException("controller shouldn't be null");
}
if (layout == null) {
throw new IllegalArgumentException("layout shouldn't be null");
}
mSessionStub.notifyCustomLayoutNotLocked(controller, layout);
}
//////////////////////////////////////////////////////////////////////////////////////
// TODO(jaewan): Implement follows
//////////////////////////////////////////////////////////////////////////////////////
@Override
public void setPlayer_impl(MediaPlayerBase player) {
// TODO(jaewan): Implement
}
@Override
public void setAllowedCommands_impl(ControllerInfo controller, CommandGroup commands) {
// TODO(jaewan): Implement
}
@Override
public void notifyMetadataChanged_impl() {
// TODO(jaewan): Implement
}
@Override
public void sendCustomCommand_impl(ControllerInfo controller, Command command, Bundle args,
ResultReceiver receiver) {
// TODO(jaewan): Implement
}
@Override
public void sendCustomCommand_impl(Command command, Bundle args) {
// TODO(jaewan): Implement
}
@Override
public void setPlaylist_impl(List<MediaItem2> playlist, PlaylistParam param) {
// TODO(jaewan): Implement
}
@Override
public void prepare_impl() {
// TODO(jaewan): Implement
}
@Override
public void fastForward_impl() {
// TODO(jaewan): Implement
}
@Override
public void rewind_impl() {
// TODO(jaewan): Implement
}
@Override
public void seekTo_impl(long pos) {
// TODO(jaewan): Implement
}
@Override
public void setCurrentPlaylistItem_impl(int index) {
// TODO(jaewan): Implement
}
///////////////////////////////////////////////////
// Protected or private methods
///////////////////////////////////////////////////
// Enforces developers to call all the methods on the initially given thread
// because calls from the MediaController2 will be run on the thread.
// TODO(jaewan): Should we allow calls from the multiple thread?
// I prefer this way because allowing multiple thread may case tricky issue like
// b/63446360. If the {@link #setPlayer()} with {@code null} can be called from
// another thread, transport controls can be called after that.
// That's basically the developer's mistake, but they cannot understand what's
// happening behind until we tell them so.
// If enforcing callling thread doesn't look good, we can alternatively pick
// 1. Allow calls from random threads for all methods.
// 2. Allow calls from random threads for all methods, except for the
// {@link #setPlayer()}.
private void ensureCallingThread() {
// TODO(jaewan): Uncomment or remove
/*
if (mHandler.getLooper() != Looper.myLooper()) {
throw new IllegalStateException("Run this on the given thread");
}*/
}
private void ensurePlayer() {
// TODO(jaewan): Should we pend command instead? Follow the decision from MP2.
// Alternatively we can add a API like setAcceptsPendingCommands(boolean).
if (mPlayer == null) {
throw new IllegalStateException("Player isn't set");
}
}
Handler getHandler() {
return mHandler;
}
private void notifyPlaybackStateChanged(PlaybackState2 state) {
// Notify to listeners added directly to this session
for (int i = 0; i < mListeners.size(); i++) {
mListeners.get(i).postPlaybackChange(state);
}
// Notify to controllers as well.
mSessionStub.notifyPlaybackStateChangedNotLocked(state);
}
Context getContext() {
return mContext;
}
MediaSession2 getInstance() {
return mInstance;
}
MediaPlayerBase getPlayer() {
return mPlayer;
}
private static class MyPlaybackListener implements MediaPlayerBase.PlaybackListener {
private final WeakReference<MediaSession2Impl> mSession;
private final MediaPlayerBase mPlayer;
private MyPlaybackListener(MediaSession2Impl session, MediaPlayerBase player) {
mSession = new WeakReference<>(session);
mPlayer = player;
}
@Override
public void onPlaybackChanged(PlaybackState2 state) {
MediaSession2Impl session = mSession.get();
if (mPlayer != session.mInstance.getPlayer()) {
Log.w(TAG, "Unexpected playback state change notifications. Ignoring.",
new IllegalStateException());
return;
}
session.notifyPlaybackStateChanged(state);
}
}
public static class ControllerInfoImpl implements ControllerInfoProvider {
private final ControllerInfo mInstance;
private final int mUid;
private final String mPackageName;
private final boolean mIsTrusted;
private final IMediaSession2Callback mControllerBinder;
// Flag to indicate which callbacks should be returned for the controller binder.
// Either 0 or combination of {@link #CALLBACK_FLAG_PLAYBACK},
// {@link #CALLBACK_FLAG_SESSION_ACTIVENESS}
private int mFlag;
public ControllerInfoImpl(ControllerInfo instance, Context context, int uid,
int pid, String packageName, IMediaSession2Callback callback) {
mInstance = instance;
mUid = uid;
mPackageName = packageName;
// TODO(jaewan): Remove this workaround
if ("com.android.server.media".equals(packageName)) {
mIsTrusted = true;
} else if (context.checkPermission(permission.MEDIA_CONTENT_CONTROL, pid, uid) ==
PackageManager.PERMISSION_GRANTED) {
mIsTrusted = true;
} else {
// TODO(jaewan): Also consider enabled notification listener.
mIsTrusted = false;
// System apps may bind across the user so uid can be differ.
// Skip sanity check for the system app.
try {
int uidForPackage = context.getPackageManager().getPackageUid(packageName, 0);
if (uid != uidForPackage) {
throw new IllegalArgumentException("Illegal call from uid=" + uid +
", pkg=" + packageName + ". Expected uid" + uidForPackage);
}
} catch (NameNotFoundException e) {
// Rethrow exception with different name because binder methods only accept
// RemoteException.
throw new IllegalArgumentException(e);
}
}
mControllerBinder = callback;
}
@Override
public String getPackageName_impl() {
return mPackageName;
}
@Override
public int getUid_impl() {
return mUid;
}
@Override
public boolean isTrusted_impl() {
return mIsTrusted;
}
@Override
public int hashCode_impl() {
return mControllerBinder.hashCode();
}
@Override
public boolean equals_impl(ControllerInfoProvider obj) {
return equals(obj);
}
@Override
public int hashCode() {
return mControllerBinder.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ControllerInfoImpl)) {
return false;
}
ControllerInfoImpl other = (ControllerInfoImpl) obj;
return mControllerBinder.asBinder().equals(other.mControllerBinder.asBinder());
}
public ControllerInfo getInstance() {
return mInstance;
}
public IBinder getId() {
return mControllerBinder.asBinder();
}
public IMediaSession2Callback getControllerBinder() {
return mControllerBinder;
}
public boolean containsFlag(int flag) {
return (mFlag & flag) != 0;
}
public void addFlag(int flag) {
mFlag |= flag;
}
public void removeFlag(int flag) {
mFlag &= ~flag;
}
public static ControllerInfoImpl from(ControllerInfo controller) {
return (ControllerInfoImpl) controller.getProvider();
}
}
}