Android MediaSession使用解析

???????前段時間在做項目中接手了一項功能,具體就是有音樂播放時,需要在狀態欄上進行實時展示,并進行雙向控制,實現上使用到了MediaSession,為什么使用MediaSession呢?主要是為了解耦,減少了狀態欄與各音樂應用進程的直接通信,主流音樂應用都會使用MediaSession,閑暇之余看了一下MediaSession的相關邏輯實現,本文針對Android11對MediaSession的主要功能進行分析。
???????在開始分析之前,先對本文進行概括一下,主要分為4部分:
???????1.創建MediaSession服務端,實現對應的功能;
???????2.客戶端接收MediaSession創建回調,獲取MediaSession對應的MediaController;
???????3.MediaSession服務端控制媒體播放;
???????4.客戶端控制MediaSession服務端媒體播放;
???????主要涉及的類有:MediaSession、MediaController、MediaSessionManager、MediaSessionService、MediaSessionRecord等,先看一下類關系圖:


MediaSession類圖.png

1. 創建MediaSession

???????音樂應用需要創建MediaSession,簡單看一下實現:

    private void initMediaSession() {
        MediaSession mediaSession = new MediaSession(this, getPackageName());
        mediaSession.setCallback(new MediaSession.Callback() {
            @Override
            public void onCommand(@NonNull String command, @Nullable Bundle args, @Nullable ResultReceiver cb) {
                super.onCommand(command, args, cb);
            }

            @Override
            public void onPlay() {
                super.onPlay();
            }

            @Override
            public void onPause() {
                super.onPause();
            }
        });
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(getPackageName(), "com.hly.testActivity"));
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mediaSession.setSessionActivity(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
        mediaSession.setActive(true);
    }

???????在創建MediaSession時,主要做了三件事:
???????1.setCallBack():客戶端控制媒體播放時,會進行通知回調,執行對應的播放控制動作;
???????2.setSessionActivity():傳入媒體播放界面對應的PendingIntent,客戶端通過該PendingIntent可以快速切換到播放界面;
???????3.setActive():設置該MediaSession為active即活躍狀態,每次設置都會觸發客戶端onActiveSessionChanged(),獲取新的MediaController列表;
???????接下來根據源碼來看一下具體的邏輯實現:

1.1.MediaSession.java

???????首先看一下構造方法:

    public MediaSession(@NonNull Context context, @NonNull String tag,
            @Nullable Bundle sessionInfo) {
        ........
        mMaxBitmapSize = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize);
        mCbStub = new CallbackStub(this);
        MediaSessionManager manager = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
        try {
            mBinder = manager.createSession(mCbStub, tag, sessionInfo);
            mSessionToken = new Token(Process.myUid(), mBinder.getController());
            mController = new MediaController(context, mSessionToken);
        } catch (RemoteException e) {
            throw new RuntimeException("Remote error creating session.", e);
        }
    }

???????在構造方法內,進行一些變量的初始化及實例化:
???????1.mMaxBitmapSize:對應Metadata中Bitmap的最大寬度或高度,此處對應的是320dp;
???????2.創建CallbackStub實例mCbStub,CallbackStub繼承ISessionCallback.Stub,用來接收客戶端發生的控制命令,后面章節再進行分析;
???????3.通過MediaSessionManager的createSession()來創建ISession實例mBinder;接下來進行分析;
???????4.創建Token實例mSessionToken;
???????5.創建MediaSession對應的MediaController;
???????接下來先分析createSession():

1.2.MediaSessionManager.java

   public ISession createSession(@NonNull MediaSession.CallbackStub cbStub, @NonNull String tag,
            @Nullable Bundle sessionInfo) {
        try {
            return mService.createSession(mContext.getPackageName(), cbStub, tag, sessionInfo,
                    UserHandle.myUserId());
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }
    }

???????mService是ISessionManager.Stub實例,ISessionManager.Stub是在MediaSessionService內部進行實現的;

1.3.MediaSessionService.java

class SessionManagerImpl extends ISessionManager.Stub {
    .....................
    .....................
    Override
    public ISession createSession(String packageName, ISessionCallback cb, String tag,
                Bundle sessionInfo, int userId) throws RemoteException {
        .....................
        try {
            enforcePackageName(packageName, uid);
            int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
                    false /* allowAll */, true /* requireFull */, "createSession", packageName);
            if (cb == null) {
                throw new IllegalArgumentException("Controller callback cannot be null");
            }
            MediaSessionRecord session = createSessionInternal(
                    pid, uid, resolvedUserId, packageName, cb, tag, sessionInfo);
            if (session == null) {
                 hrow new IllegalStateException("Failed to create a new session record");
            }
            ISession sessionBinder = session.getSessionBinder();
            if (sessionBinder == null) {
                throw new IllegalStateException("Invalid session record");
            }
            return sessionBinder;
        } catch (Exception e) {
            Slog.w(TAG, "Exception in creating a new session", e);
            throw e;
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }
}

???????在該方法內主要兩項工作:
???????1.通過createSessionInternal()創建MediaSessionRecord實例session;
???????2.通過session的getSessionBinder()返回ISession實例;

    private MediaSessionRecord createSessionInternal(int callerPid, int callerUid, int userId,
            String callerPackageName, ISessionCallback cb, String tag, Bundle sessionInfo) {
        synchronized (mLock) {
            int policies = 0;
            if (mCustomSessionPolicyProvider != null) {
                policies = mCustomSessionPolicyProvider.getSessionPoliciesForApplication(
                        callerUid, callerPackageName);
            }

            FullUserRecord user = getFullUserRecordLocked(userId);
            if (user == null) {
                Log.w(TAG, "Request from invalid user: " +  userId + ", pkg=" + callerPackageName);
                throw new RuntimeException("Session request from invalid user.");
            }

            final int sessionCount = user.mUidToSessionCount.get(callerUid, 0);
            if (sessionCount >= SESSION_CREATION_LIMIT_PER_UID
                    && !hasMediaControlPermission(callerPid, callerUid)) {
                throw new RuntimeException("Created too many sessions. count="
                        + sessionCount + ")");
            }

            final MediaSessionRecord session;
            try {
                session = new MediaSessionRecord(callerPid, callerUid, userId,
                        callerPackageName, cb, tag, sessionInfo, this,
                        mRecordThread.getLooper(), policies);
            } catch (RemoteException e) {
                throw new RuntimeException("Media Session owner died prematurely.", e);
            }

            user.mUidToSessionCount.put(callerUid, sessionCount + 1);

            user.mPriorityStack.addSession(session);
            mHandler.postSessionsChanged(session);

            if (DEBUG) {
                Log.d(TAG, "Created session for " + callerPackageName + " with tag " + tag);
            }
            return session;
        }
    }

???????通過以上代碼可以看到:
???????1.先進行一些判斷,FullUserRecord不能為空,sessionCount不能超過100;
???????2.創建MediaSessionRecord實例;
???????3.將進程的sessionCount交給mUidToSessionCount進行管理,將先創建的session交給mPriorityStack進行管理,后續在客戶端回調時會用到;
???????4.執行mHandler.postSessionsChanged(session)來通知客戶端activeSessions回調,后面再講;

1.4.MediaSessionRecord.java

    public MediaSessionRecord(int ownerPid, int ownerUid, int userId, String ownerPackageName,
            ISessionCallback cb, String tag, Bundle sessionInfo,
            MediaSessionService service, Looper handlerLooper, int policies)
            throws RemoteException {
        mOwnerPid = ownerPid;
        mOwnerUid = ownerUid;
        mUserId = userId;
        mPackageName = ownerPackageName;
        mTag = tag;
        mSessionInfo = sessionInfo;
        mController = new ControllerStub();
        mSessionToken = new MediaSession.Token(ownerUid, mController);
        mSession = new SessionStub();
        mSessionCb = new SessionCb(cb);
        mService = service;
        mContext = mService.getContext();
        mHandler = new MessageHandler(handlerLooper);
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class);
        mAudioAttrs = DEFAULT_ATTRIBUTES;
        mPolicies = policies;

        // May throw RemoteException if the session app is killed.
        mSessionCb.mCb.asBinder().linkToDeath(this, 0);
    }

???????在構造方法內,進行了實例初始化:
???????1.創建ControllerStub實例mController,ControllerStub繼承了ISessionController.Stub,主要用來接收來自客戶端的控制;
???????2.創建MediaSession.Token實例mSessionToken;
???????3.創建SessionStub實例mSession,SessionStub繼承ISession.Stub,主要用來初始化MediaSession實例變量,比如:setSessionActivity()等;
???????4.創建SessionCb實例mSessionCb,mSessionCb接收到來自客戶端的控制調用mSessionCb,然后在其內部調用cb最終調用到MediaSession構造方法內部的CallbackStub實例mCbStub;
???????前面在MediaSessionService內部的createSession()創建MediaSessionRecord實例,然后調用getSessionBinder()返回ISession實例,即對應SessionStub實例mSession;

1.5.總結

???????MediaSession構造方法內部的createSession()最終返回的是MediaSessionRecord的實例mSession,用一張圖總結一下執行過程:


MediaSession創建流程.png

2.MediaSession創建完成回調

???????在MediaSession創建完成后,會回調給客戶端進行實時監聽,關于回調在上面已經分析到,具體實現是在MediaSessionService的createSessionInternal()內部創建完MediaSessionRecord實例后會執行mHandler.postSessionsChanged(session),一起看一下:

2.1.MediaSessionService.java

    public void postSessionsChanged(MediaSessionRecordImpl record) {
            // Use object instead of the arguments when posting message to remove pending requests.
            Integer userIdInteger = mIntegerCache.get(record.getUserId());
            if (userIdInteger == null) {
                userIdInteger = Integer.valueOf(record.getUserId());
                mIntegerCache.put(record.getUserId(), userIdInteger);
            }

            int msg = (record instanceof MediaSessionRecord)
                    ? MSG_SESSIONS_1_CHANGED : MSG_SESSIONS_2_CHANGED;
            removeMessages(msg, userIdInteger);
            obtainMessage(msg, userIdInteger).sendToTarget();
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_SESSIONS_1_CHANGED:
                    pushSession1Changed((int) msg.obj);
                    break;
                case MSG_SESSIONS_2_CHANGED:
                    pushSession2Changed((int) msg.obj);
                    break;
            }
        }

???????跟隨調用關系:

    private void pushSession1Changed(int userId) {
        synchronized (mLock) {
            FullUserRecord user = getFullUserRecordLocked(userId);
            if (user == null) {
                Log.w(TAG, "pushSession1ChangedOnHandler failed. No user with id=" + userId);
                return;
            }
            List<MediaSessionRecord> records = getActiveSessionsLocked(userId);
            int size = records.size();
            ArrayList<MediaSession.Token> tokens = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                tokens.add(records.get(i).getSessionToken());
            }
            pushRemoteVolumeUpdateLocked(userId);
            for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
                SessionsListenerRecord record = mSessionsListeners.get(i);
                if (record.userId == USER_ALL || record.userId == userId) {
                    try {
                        record.listener.onActiveSessionsChanged(tokens);
                    } catch (RemoteException e) {
                        Log.w(TAG, "Dead ActiveSessionsListener in pushSessionsChanged, removing",
                                e);
                        mSessionsListeners.remove(i);
                    }
                }
            }
        }
    }

???????1.先通過getActiveSessionsLocked()獲取到MediaSessionRecord列表,是通過mPriorityStack中獲取的;
???????2.創建MediaSession.Token列表,遍歷執行MediaSessionRecord的getSessionToken()方法來獲取對應的MediaSession.Token;
???????3.遍歷mSessionsListeners執行record.listener.onActiveSessionsChanged(tokens)回調給客戶端;
???????當然了,客戶端必須先注冊,才能接收到回調,先看一下客戶端注冊過程;

2.2.MediaSessionManager.java

    public void addOnActiveSessionsChangedListener(
            @NonNull OnActiveSessionsChangedListener sessionListener,
            @Nullable ComponentName notificationListener, int userId, @Nullable Handler handler) {
        if (sessionListener == null) {
            throw new IllegalArgumentException("listener may not be null");
        }
        if (handler == null) {
            handler = new Handler();
        }
        synchronized (mLock) {
            if (mListeners.get(sessionListener) != null) {
                Log.w(TAG, "Attempted to add session listener twice, ignoring.");
                return;
            }
            SessionsChangedWrapper wrapper = new SessionsChangedWrapper(mContext, sessionListener,
                    handler);
            try {
                mService.addSessionsListener(wrapper.mStub, notificationListener, userId);
                mListeners.put(sessionListener, wrapper);
            } catch (RemoteException e) {
                Log.e(TAG, "Error in addOnActiveSessionsChangedListener.", e);
            }
        }
    }

???????先創建內部類SessionsChangedWrapper實例wrapper,然后將其內部變量mStub作為參數傳遞給MediaSessionService的addSessionsListener方法;

    @Override
    public void addSessionsListener(IActiveSessionsListener listener,
                ComponentName componentName, int userId) throws RemoteException {
        final int pid = Binder.getCallingPid();
        final int uid = Binder.getCallingUid();
        final long token = Binder.clearCallingIdentity();

        try {
            int resolvedUserId = verifySessionsRequest(componentName, userId, pid, uid);
            synchronized (mLock) {
                int index = findIndexOfSessionsListenerLocked(listener);
                if (index != -1) {
                    Log.w(TAG, "ActiveSessionsListener is already added, ignoring");
                    return;
                }
                SessionsListenerRecord record = new SessionsListenerRecord(listener,
                        componentName, resolvedUserId, pid, uid);
                try {
                    listener.asBinder().linkToDeath(record, 0);
                } catch (RemoteException e) {
                    Log.e(TAG, "ActiveSessionsListener is dead, ignoring it", e);
                    return;
                }
                mSessionsListeners.add(record);
            }
       } finally {
            Binder.restoreCallingIdentity(token);
       }
   }

???????將listener封裝成SessionsListenerRecord對象,最終存入mSessionsListeners進行管理,用來后續通知回調(前面可以看到);
???????再看一下SessionsChangedWrapper的實現:

2.2.1.SessionsChangedWrapper

private static final class SessionsChangedWrapper {
    private Context mContext;
    private OnActiveSessionsChangedListener mListener;
    private Handler mHandler;

    public SessionsChangedWrapper(Context context, OnActiveSessionsChangedListener listener,
                Handler handler) {
        mContext = context;
        mListener = listener;
        mHandler = handler;
    }

    private final IActiveSessionsListener.Stub mStub = new IActiveSessionsListener.Stub() {
        @Override
        public void onActiveSessionsChanged(final List<MediaSession.Token> tokens) {
            final Handler handler = mHandler;
            if (handler != null) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        final Context context = mContext;
                        if (context != null) {
                            ArrayList<MediaController> controllers = new ArrayList<>();
                            int size = tokens.size();
                            for (int i = 0; i < size; i++) {
                                controllers.add(new MediaController(context, tokens.get(i)));
                            }
                            final OnActiveSessionsChangedListener listener = mListener;
                            if (listener != null) {
                                listener.onActiveSessionsChanged(controllers);
                            }
                        }
                    }
                });
            }
        }
    };

    private void release() {
        mListener = null;
        mContext = null;
        mHandler = null;
    }
}

???????跟隨調用關系,最終會調用到該類的onActiveSessionsChanged()方法,在該方法內會遍歷tokens來獲取MediaSessionRecord對應的MediaSession.Token,創建MediaController[注意一下:每次有ActivieSession變化,都會返回不同的MediaController列表],最終回調到客戶端的是與MediaSession相關聯的MediaController列表;

3.客戶端控制

3.1.MediaController.java

    public MediaController(@NonNull Context context, @NonNull MediaSession.Token token) {
        if (context == null) {
            throw new IllegalArgumentException("context shouldn't be null");
        }
        if (token == null) {
            throw new IllegalArgumentException("token shouldn't be null");
        }
        if (token.getBinder() == null) {
            throw new IllegalArgumentException("token.getBinder() shouldn't be null");
        }
        mSessionBinder = token.getBinder();
        mTransportControls = new TransportControls();
        mToken = token;
        mContext = context;
    }

???????在MediaController構造方法內部,主要執行了兩項工作:
???????1.通過getBinder()獲取ISessionController實例mSessionBinder,從名字可以看到是用來控制MediaSession的;
???????2.創建TransportControls()實例mTransportControls,客戶端用來執行控制;

3.1.1. TransportControls

public final class TransportControls {
    ..............
    public void play() {
        try {
            mSessionBinder.play(mContext.getPackageName());
        } catch (RemoteException e) {
            Log.wtf(TAG, "Error calling play.", e);
        }
    }
    .....................
    public void pause() {
        try {
            mSessionBinder.pause(mContext.getPackageName());
        } catch (RemoteException e) {
            Log.wtf(TAG, "Error calling pause.", e);
        }
    }
    .....................
}

???????TransportControls最終是通過mSessionBinder來進行命令控制,mSessionBinder是通過token.getBinder()來獲取,反過來看一下MediaSession.Token實現:

3.2.MediaSession.Token

public static final class Token implements Parcelable {

    private final int mUid;
    private final ISessionController mBinder;

    public Token(int uid, ISessionController binder) {
        mUid = uid;
        mBinder = binder;
    }

    Token(Parcel in) {
        mUid = in.readInt();
        mBinder = ISessionController.Stub.asInterface(in.readStrongBinder());
    }

    public ISessionController getBinder() {
        return mBinder;
    }

}

???????再回到MediaSessionRecord內部看一下該mBinder的由來:

3.3.MediaSessionRecord.java

public MediaSessionRecord(int ownerPid, int ownerUid, int userId, String ownerPackageName,
            ISessionCallback cb, String tag, Bundle sessionInfo,
            MediaSessionService service, Looper handlerLooper, int policies)
            throws RemoteException {
    ..............
    mController = new ControllerStub();
    mSessionToken = new MediaSession.Token(ownerUid, mController);
    mSessionCb = new SessionCb(cb);
    ............
}

??????? ISessionController實例是由ControllerStub創建而來,所以當執行控制時,調用的是ControllerStub內部的方法:

3.3.1.ControllerStub

class ControllerStub extends ISessionController.Stub {
    @Override
    public void sendCommand(String packageName, String command, Bundle args,
                ResultReceiver cb) {
        mSessionCb.sendCommand(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                command, args, cb);
    }
    ..................
    @Override
    public void play(String packageName) {
        mSessionCb.play(packageName, Binder.getCallingPid(), Binder.getCallingUid());
    }
    ....................
   @Override
    public void pause(String packageName) {
        mSessionCb.pause(packageName, Binder.getCallingPid(), Binder.getCallingUid());
   }
}

??????? 在ControllerStub內部的方法又會調用到SessionCb的方法:

3.3.2.SessionCb

class SessionCb {
    private final ISessionCallback mCb;

    SessionCb(ISessionCallback cb) {
        mCb = cb;
    }

    public void sendCommand(String packageName, int pid, int uid, String command, Bundle args,
                ResultReceiver cb) {
            try {
                mCb.onCommand(packageName, pid, uid, command, args, cb);
            } catch (RemoteException e) {
                Slog.e(TAG, "Remote failure in sendCommand.", e);
            }
        }


    public void play(String packageName, int pid, int uid) {
            try {
                mCb.onPlay(packageName, pid, uid);
            } catch (RemoteException e) {
                Slog.e(TAG, "Remote failure in play.", e);
            }
        }

    public void pause(String packageName, int pid, int uid) {
            try {
                mCb.onPause(packageName, pid, uid);
            } catch (RemoteException e) {
                Slog.e(TAG, "Remote failure in pause.", e);
            }
        }

??????? SessionCb會調用到mCb,即MediaSession內部的CallbackStub實例;

3.4.MediaSession.CallbackStub

public static class CallbackStub extends ISessionCallback.Stub {
    .............
    @Override
    public void onCommand(String packageName, int pid, int uid, String command, Bundle args,
                ResultReceiver cb) {
        MediaSession session = mMediaSession.get();
        if (session != null) {
            session.dispatchCommand(createRemoteUserInfo(packageName, pid, uid),
                    command, args, cb);
        }
    }

    @Override
    public void onPlay(String packageName, int pid, int uid) {
        MediaSession session = mMediaSession.get();
        if (session != null) {
            session.dispatchPlay(createRemoteUserInfo(packageName, pid, uid));
        }
    }

    @Override
    public void onPause(String packageName, int pid, int uid) {
        MediaSession session = mMediaSession.get();
        if (session != null) {
            session.dispatchPause(createRemoteUserInfo(packageName, pid, uid));
        }
   }
}

???????跟隨調用關系,最終會調用到mCallback的方法,前面已經講到,mCallBack是音樂應用在創建MediaSession時,傳入的本地實現;

    case MSG_COMMAND:
         Command cmd = (Command) obj;
          mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
          break;
     case MSG_PLAY:
          mCallback.onPlay();
          break;
      case MSG_PAUSE:
          mCallback.onPause();
          break;

???????音樂應用內部對應實現對應的方法,那么控制就生效了。

3.5.總結

???????客戶端MediaController通過TransportControls來進行控制,最終會對應到服務端MediaSession的Callback,對應關系如下:

TransportControls MediaSession.Callback
play() onPlay()
pause() onPause
stop() onStop
skipToNext() onSkipToNext()

4.MediaSession端控制

4.1.MediaSession.java

???????直接看代碼,播放暫停相關控制通過setPlaybackState(),PlaybackState:STATE_PAUSED = 2、STATE_PLAYING = 3;

    public void setPlaybackState(@Nullable PlaybackState state) {
        mPlaybackState = state;
        try {
            mBinder.setPlaybackState(state);
        } catch (RemoteException e) {
            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
        }
    }

???????媒體信息發生變化時,可以通過setMetadata來更新MediaMetadata信息就可以了;

    public void setMetadata(@Nullable MediaMetadata metadata) {
        long duration = -1;
        int fields = 0;
        MediaDescription description = null;
        if (metadata != null) {
            metadata = (new MediaMetadata.Builder(metadata, mMaxBitmapSize)).build();
            if (metadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
                duration = metadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
            }
            fields = metadata.size();
            description = metadata.getDescription();
        }
        String metadataDescription = "size=" + fields + ", description=" + description;

        try {
            mBinder.setMetadata(metadata, duration, metadataDescription);
        } catch (RemoteException e) {
            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
        }
    }

???????通過上面的分析,mBinder是通過MediaSessionRecord的getSessionBinder()來返回的,接下來一起看一下執行過程:

4.2.MediaSessionRecord.java

???????getSessionBinder()返回的是SessionStub實例:

private final class SessionStub extends ISession.Stub {
    .............
    @Override
    public void setPlaybackState(PlaybackState state) throws RemoteException {
        int oldState = mPlaybackState == null
                ? PlaybackState.STATE_NONE : mPlaybackState.getState();
        int newState = state == null
                ? PlaybackState.STATE_NONE : state.getState();
        boolean shouldUpdatePriority = ALWAYS_PRIORITY_STATES.contains(newState)
                || (!TRANSITION_PRIORITY_STATES.contains(oldState)
                && TRANSITION_PRIORITY_STATES.contains(newState));
        synchronized (mLock) {
            mPlaybackState = state;
        }
        final long token = Binder.clearCallingIdentity();
        try {
            mService.onSessionPlaybackStateChanged(
                    MediaSessionRecord.this, shouldUpdatePriority);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
        mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);
    }
    .................
}

???????通過該方法可以看到,主要有四項工作:
???????1.首先判斷當前的playState及新的playState,然后來確定shouldUpdatePriority的值,該值表示當前的MediaSession處于活躍狀態(比如:由暫停到播放);
???????2.更新mPlaybackState值為最新的playbackstate;
???????3.執行mService.onSessionPlaybackStateChanged()來通知MediaSessionSession來根據shouldUpdatePriority來確定是否需要更新相關狀態;

    void onSessionPlaybackStateChanged(MediaSessionRecordImpl record,
            boolean shouldUpdatePriority) {
        synchronized (mLock) {
            FullUserRecord user = getFullUserRecordLocked(record.getUserId());
            if (user == null || !user.mPriorityStack.contains(record)) {
                Log.d(TAG, "Unknown session changed playback state. Ignoring.");
                return;
            }
            user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority);
        }
    }

???????如果shouldUpdatePriority為true,則將record放在mSessions的首位,反之將mCachedVolumeDefault置空;

    public void onPlaybackStateChanged(
            MediaSessionRecordImpl record, boolean shouldUpdatePriority) {
        if (shouldUpdatePriority) {
            mSessions.remove(record);
            mSessions.add(0, record);
            clearCache(record.getUserId());
        } else if (record.checkPlaybackActiveState(false)) {
            // Just clear the volume cache when a state goes inactive
            mCachedVolumeDefault = null;
        }
        ..........
    }

???????4.執行mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE)來通知客戶端進行狀態更新;

    private void pushPlaybackStateUpdate() {
        synchronized (mLock) {
            if (mDestroyed) {
                return;
            }
            for (int i = mControllerCallbackHolders.size() - 1; i >= 0; i--) {
                ISessionControllerCallbackHolder holder = mControllerCallbackHolders.get(i);
                try {
                    holder.mCallback.onPlaybackStateChanged(mPlaybackState);
                } catch (DeadObjectException e) {
                    mControllerCallbackHolders.remove(i);
                    logCallbackException("Removing dead callback in pushPlaybackStateUpdate",
                            holder, e);
                } catch (RemoteException e) {
                    logCallbackException("unexpected exception in pushPlaybackStateUpdate",
                            holder, e);
                }
            }
        }
    }

???????可以看到,發生消息來通知回調,當然了,客戶端要想接收回調,肯定需要先進行注冊,通過MediaController來進行注冊,一起看一下:

4.3.MediaController.java

    public void registerCallback(@NonNull Callback callback) {
        registerCallback(callback, null);
    }

    public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
        .........
        synchronized (mLock) {
            addCallbackLocked(callback, handler);
        }
    }

    private void addCallbackLocked(Callback cb, Handler handler) {
        if (getHandlerForCallbackLocked(cb) != null) {
            Log.w(TAG, "Callback is already added, ignoring");
            return;
        }
        MessageHandler holder = new MessageHandler(handler.getLooper(), cb);
        mCallbacks.add(holder);
        holder.mRegistered = true;

        if (!mCbRegistered) {
            try {
                mSessionBinder.registerCallback(mContext.getPackageName(), mCbStub);
                mCbRegistered = true;
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in registerCallback", e);
            }
        }
    }

???????跟隨調用關系,可以看到:
???????1.首先將cb封裝到MessageHandler內部,然后放入mCallbacks進行管理;
???????2.執行mSessionBinder的registerCallback()方法,mCbStub對應的是CallbackStub實例,用來接收回調:

private static final class CallbackStub extends ISessionControllerCallback.Stub {
    private final WeakReference<MediaController> mController;

    CallbackStub(MediaController controller) {
        mController = new WeakReference<MediaController>(controller);
    }
    .................
    @Override
    public void onPlaybackStateChanged(PlaybackState state) {
        MediaController controller = mController.get();
        if (controller != null) {
            controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null);
        }
    }

    @Override
    public void onMetadataChanged(MediaMetadata metadata) {
        MediaController controller = mController.get();
        if (controller != null) {
            controller.postMessage(MSG_UPDATE_METADATA, metadata, null);
        }
    }
    ..................
}

前面分析到,mSessionBinders是通過token.getBinder()來獲得的,最終返回的是MediaSessionRecord的ControllerStub實例mController,看一下對應實現:

4.4.MediaSessionRecord.ControllerStub

class ControllerStub extends ISessionController.Stub {
    ................
    @Override
    public void registerCallback(String packageName, ISessionControllerCallback cb) {
        synchronized (mLock) {
            // If this session is already destroyed tell the caller and
            // don't add them.
            if (mDestroyed) {
                try {
                    cb.onSessionDestroyed();
                } catch (Exception e) {
                    // ignored
                }
                return;
            }
            if (getControllerHolderIndexForCb(cb) < 0) {
                mControllerCallbackHolders.add(new ISessionControllerCallbackHolder(cb,
                        packageName, Binder.getCallingUid()));
            }
        }
    }
}

???????將ISessionControllerCallback封裝到ISessionControllerCallbackHolder中,然后加入到mControllerCallbackHolders進行管理,根據前面講到的,有狀態變化時通知回調就是對mControllerCallbackHolders進行holder.mCallback.onPlaybackStateChanged(mPlaybackState)遍歷通知,再返回到MediaController內部的CallbackStub:

    public void onPlaybackStateChanged(PlaybackState state) {
        MediaController controller = mController.get();
        if (controller != null) {
            controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null);
        }
    }

    case MSG_UPDATE_PLAYBACK_STATE:
        mCallback.onPlaybackStateChanged((PlaybackState) msg.obj);
        break;

4.5.總結

???????服務端MediaSession進行控制,最終會對應到客戶端MediaController的Callback,對應關系如下:

MediaSession MediaController.Callback
setMetadata(MediaMetadata) onMetadataChanged(MediaMetadata)
setPlaybackState(PlaybackState) onPlaybackStateChanged(PlaybackState)

???????客戶端MediaController與服務端MediaSession雙向控制圖如下:


MediaController與MediaSession雙向控制.png

???????以上就是MediaSession工作的相關流程,詳細邏輯還需要通過源碼進一步了解!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容