|  | /* | 
|  | * 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 androidx.media; | 
|  |  | 
|  | import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN; | 
|  | import static androidx.media.MediaSession2.ControllerInfo; | 
|  | import static androidx.media.MediaSession2.ErrorCode; | 
|  | import static androidx.media.MediaSession2.OnDataSourceMissingHelper; | 
|  | import static androidx.media.MediaSession2.SessionCallback; | 
|  | import static androidx.media.SessionToken2.TYPE_LIBRARY_SERVICE; | 
|  | import static androidx.media.SessionToken2.TYPE_SESSION; | 
|  | import static androidx.media.SessionToken2.TYPE_SESSION_SERVICE; | 
|  |  | 
|  | import android.annotation.TargetApi; | 
|  | import android.app.PendingIntent; | 
|  | import android.content.Context; | 
|  | import android.content.Intent; | 
|  | import android.content.pm.PackageManager; | 
|  | import android.content.pm.ResolveInfo; | 
|  | import android.media.AudioFocusRequest; | 
|  | import android.media.AudioManager; | 
|  | import android.os.Build; | 
|  | import android.os.Bundle; | 
|  | import android.os.Handler; | 
|  | import android.os.HandlerThread; | 
|  | import android.os.Process; | 
|  | import android.os.ResultReceiver; | 
|  | import android.support.v4.media.session.MediaSessionCompat; | 
|  | import android.support.v4.media.session.PlaybackStateCompat; | 
|  | import android.text.TextUtils; | 
|  | import android.util.Log; | 
|  |  | 
|  | import androidx.annotation.GuardedBy; | 
|  | import androidx.annotation.NonNull; | 
|  | import androidx.annotation.Nullable; | 
|  | import androidx.media.MediaController2.PlaybackInfo; | 
|  | import androidx.media.MediaPlayerBase.PlayerEventCallback; | 
|  | import androidx.media.MediaPlaylistAgent.PlaylistEventCallback; | 
|  |  | 
|  | import java.lang.ref.WeakReference; | 
|  | import java.util.List; | 
|  | import java.util.NoSuchElementException; | 
|  | import java.util.concurrent.Executor; | 
|  | import java.util.concurrent.RejectedExecutionException; | 
|  |  | 
|  | @TargetApi(Build.VERSION_CODES.KITKAT) | 
|  | class MediaSession2ImplBase extends MediaSession2.SupportLibraryImpl { | 
|  | static final String TAG = "MS2ImplBase"; | 
|  | static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); | 
|  |  | 
|  | private final Object mLock = new Object(); | 
|  |  | 
|  | private final Context mContext; | 
|  | private final HandlerThread mHandlerThread; | 
|  | private final Handler mHandler; | 
|  | private final MediaSessionCompat mSessionCompat; | 
|  | private final MediaSession2StubImplBase mSession2Stub; | 
|  | private final String mId; | 
|  | private final Executor mCallbackExecutor; | 
|  | private final SessionCallback mCallback; | 
|  | private final SessionToken2 mSessionToken; | 
|  | private final AudioManager mAudioManager; | 
|  | private final MediaPlayerBase.PlayerEventCallback mPlayerEventCallback; | 
|  | private final MediaPlaylistAgent.PlaylistEventCallback mPlaylistEventCallback; | 
|  |  | 
|  | private WeakReference<MediaSession2> mInstance; | 
|  |  | 
|  | @GuardedBy("mLock") | 
|  | private MediaPlayerBase mPlayer; | 
|  | @GuardedBy("mLock") | 
|  | private MediaPlaylistAgent mPlaylistAgent; | 
|  | @GuardedBy("mLock") | 
|  | private SessionPlaylistAgentImplBase mSessionPlaylistAgent; | 
|  | @GuardedBy("mLock") | 
|  | private VolumeProviderCompat mVolumeProvider; | 
|  | @GuardedBy("mLock") | 
|  | private OnDataSourceMissingHelper mDsmHelper; | 
|  | @GuardedBy("mLock") | 
|  | private PlaybackStateCompat mPlaybackStateCompat; | 
|  | @GuardedBy("mLock") | 
|  | private PlaybackInfo mPlaybackInfo; | 
|  |  | 
|  | MediaSession2ImplBase(Context context, MediaSessionCompat sessionCompat, String id, | 
|  | MediaPlayerBase player, MediaPlaylistAgent playlistAgent, | 
|  | VolumeProviderCompat volumeProvider, PendingIntent sessionActivity, | 
|  | Executor callbackExecutor, SessionCallback callback) { | 
|  | mContext = context; | 
|  | mHandlerThread = new HandlerThread("MediaController2_Thread"); | 
|  | mHandlerThread.start(); | 
|  | mHandler = new Handler(mHandlerThread.getLooper()); | 
|  |  | 
|  | mSessionCompat = sessionCompat; | 
|  | mSession2Stub = new MediaSession2StubImplBase(this); | 
|  | mSessionCompat.setCallback(mSession2Stub, mHandler); | 
|  | mSessionCompat.setSessionActivity(sessionActivity); | 
|  |  | 
|  | mId = id; | 
|  | mCallback = callback; | 
|  | mCallbackExecutor = callbackExecutor; | 
|  | mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); | 
|  |  | 
|  | // TODO: Set callback values properly | 
|  | mPlayerEventCallback = new MyPlayerEventCallback(this); | 
|  | mPlaylistEventCallback = new MyPlaylistEventCallback(this); | 
|  |  | 
|  | // Infer type from the id and package name. | 
|  | String libraryService = getServiceName(context, MediaLibraryService2.SERVICE_INTERFACE, id); | 
|  | String sessionService = getServiceName(context, MediaSessionService2.SERVICE_INTERFACE, id); | 
|  | if (sessionService != null && libraryService != null) { | 
|  | throw new IllegalArgumentException("Ambiguous session type. Multiple" | 
|  | + " session services define the same id=" + id); | 
|  | } else if (libraryService != null) { | 
|  | mSessionToken = new SessionToken2(Process.myUid(), TYPE_LIBRARY_SERVICE, | 
|  | context.getPackageName(), libraryService, id, mSessionCompat.getSessionToken()); | 
|  | } else if (sessionService != null) { | 
|  | mSessionToken = new SessionToken2(Process.myUid(), TYPE_SESSION_SERVICE, | 
|  | context.getPackageName(), sessionService, id, mSessionCompat.getSessionToken()); | 
|  | } else { | 
|  | mSessionToken = new SessionToken2(Process.myUid(), TYPE_SESSION, | 
|  | context.getPackageName(), null, id, mSessionCompat.getSessionToken()); | 
|  | } | 
|  | updatePlayer(player, playlistAgent, volumeProvider); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void updatePlayer(@NonNull MediaPlayerBase player, | 
|  | @Nullable MediaPlaylistAgent playlistAgent, | 
|  | @Nullable VolumeProviderCompat volumeProvider) { | 
|  | if (player == null) { | 
|  | throw new IllegalArgumentException("player shouldn't be null"); | 
|  | } | 
|  | final MediaPlayerBase oldPlayer; | 
|  | final MediaPlaylistAgent oldAgent; | 
|  | final PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes()); | 
|  | synchronized (mLock) { | 
|  | oldPlayer = mPlayer; | 
|  | oldAgent = mPlaylistAgent; | 
|  | mPlayer = player; | 
|  | if (playlistAgent == null) { | 
|  | mSessionPlaylistAgent = new SessionPlaylistAgentImplBase(this, mPlayer); | 
|  | if (mDsmHelper != null) { | 
|  | mSessionPlaylistAgent.setOnDataSourceMissingHelper(mDsmHelper); | 
|  | } | 
|  | playlistAgent = mSessionPlaylistAgent; | 
|  | } | 
|  | mPlaylistAgent = playlistAgent; | 
|  | mVolumeProvider = volumeProvider; | 
|  | mPlaybackInfo = info; | 
|  | } | 
|  | if (player != oldPlayer) { | 
|  | player.registerPlayerEventCallback(mCallbackExecutor, mPlayerEventCallback); | 
|  | if (oldPlayer != null) { | 
|  | // Warning: Poorly implement player may ignore this | 
|  | oldPlayer.unregisterPlayerEventCallback(mPlayerEventCallback); | 
|  | } | 
|  | } | 
|  | if (playlistAgent != oldAgent) { | 
|  | playlistAgent.registerPlaylistEventCallback(mCallbackExecutor, mPlaylistEventCallback); | 
|  | if (oldAgent != null) { | 
|  | // Warning: Poorly implement player may ignore this | 
|  | oldAgent.unregisterPlaylistEventCallback(mPlaylistEventCallback); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (oldPlayer != null) { | 
|  | mSession2Stub.notifyPlaybackInfoChanged(info); | 
|  | notifyPlayerUpdatedNotLocked(oldPlayer); | 
|  | } | 
|  | // TODO(jaewan): Repeat the same thing for the playlist agent. | 
|  | } | 
|  |  | 
|  | private PlaybackInfo createPlaybackInfo(VolumeProviderCompat volumeProvider, | 
|  | AudioAttributesCompat attrs) { | 
|  | PlaybackInfo info; | 
|  | if (volumeProvider == null) { | 
|  | int stream; | 
|  | if (attrs == null) { | 
|  | stream = AudioManager.STREAM_MUSIC; | 
|  | } else { | 
|  | stream = attrs.getVolumeControlStream(); | 
|  | if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) { | 
|  | // It may happen if the AudioAttributes doesn't have usage. | 
|  | // Change it to the STREAM_MUSIC because it's not supported by audio manager | 
|  | // for querying volume level. | 
|  | stream = AudioManager.STREAM_MUSIC; | 
|  | } | 
|  | } | 
|  |  | 
|  | int controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; | 
|  | if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) { | 
|  | controlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED; | 
|  | } | 
|  | info = PlaybackInfo.createPlaybackInfo( | 
|  | PlaybackInfo.PLAYBACK_TYPE_LOCAL, | 
|  | attrs, | 
|  | controlType, | 
|  | mAudioManager.getStreamMaxVolume(stream), | 
|  | mAudioManager.getStreamVolume(stream)); | 
|  | } else { | 
|  | info = PlaybackInfo.createPlaybackInfo( | 
|  | PlaybackInfo.PLAYBACK_TYPE_REMOTE, | 
|  | attrs, | 
|  | volumeProvider.getVolumeControl(), | 
|  | volumeProvider.getMaxVolume(), | 
|  | volumeProvider.getCurrentVolume()); | 
|  | } | 
|  | return info; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void close() { | 
|  | synchronized (mLock) { | 
|  | if (mPlayer == null) { | 
|  | return; | 
|  | } | 
|  | mPlayer.unregisterPlayerEventCallback(mPlayerEventCallback); | 
|  | mPlayer = null; | 
|  | mSessionCompat.release(); | 
|  | mHandler.removeCallbacksAndMessages(null); | 
|  | if (mHandlerThread.isAlive()) { | 
|  | mHandlerThread.quitSafely(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public @NonNull MediaPlayerBase getPlayer() { | 
|  | synchronized (mLock) { | 
|  | return mPlayer; | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public @NonNull MediaPlaylistAgent getPlaylistAgent() { | 
|  | synchronized (mLock) { | 
|  | return mPlaylistAgent; | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public @Nullable VolumeProviderCompat getVolumeProvider() { | 
|  | synchronized (mLock) { | 
|  | return mVolumeProvider; | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public @NonNull SessionToken2 getToken() { | 
|  | return mSessionToken; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public @NonNull List<MediaSession2.ControllerInfo> getConnectedControllers() { | 
|  | return mSession2Stub.getConnectedControllers(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) { | 
|  | // TODO(jaewan): implement this (b/72529899) | 
|  | // mProvider.setAudioFocusRequest_impl(focusGain); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setCustomLayout(@NonNull ControllerInfo controller, | 
|  | @NonNull List<MediaSession2.CommandButton> layout) { | 
|  | if (controller == null) { | 
|  | throw new IllegalArgumentException("controller shouldn't be null"); | 
|  | } | 
|  | if (layout == null) { | 
|  | throw new IllegalArgumentException("layout shouldn't be null"); | 
|  | } | 
|  | mSession2Stub.notifyCustomLayout(controller, layout); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setAllowedCommands(@NonNull ControllerInfo controller, | 
|  | @NonNull SessionCommandGroup2 commands) { | 
|  | if (controller == null) { | 
|  | throw new IllegalArgumentException("controller shouldn't be null"); | 
|  | } | 
|  | if (commands == null) { | 
|  | throw new IllegalArgumentException("commands shouldn't be null"); | 
|  | } | 
|  | mSession2Stub.setAllowedCommands(controller, commands); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) { | 
|  | if (command == null) { | 
|  | throw new IllegalArgumentException("command shouldn't be null"); | 
|  | } | 
|  | mSession2Stub.sendCustomCommand(command, args); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void sendCustomCommand(@NonNull ControllerInfo controller, | 
|  | @NonNull SessionCommand2 command, @Nullable Bundle args, | 
|  | @Nullable ResultReceiver receiver) { | 
|  | if (controller == null) { | 
|  | throw new IllegalArgumentException("controller shouldn't be null"); | 
|  | } | 
|  | if (command == null) { | 
|  | throw new IllegalArgumentException("command shouldn't be null"); | 
|  | } | 
|  | mSession2Stub.sendCustomCommand(controller, command, args, receiver); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void play() { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | player.play(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void pause() { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | player.pause(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void reset() { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | player.reset(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void prepare() { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | player.prepare(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void seekTo(long pos) { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | player.seekTo(pos); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void skipForward() { | 
|  | // To match with KEYCODE_MEDIA_SKIP_FORWARD | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void skipBackward() { | 
|  | // To match with KEYCODE_MEDIA_SKIP_BACKWARD | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) { | 
|  | mSession2Stub.notifyError(errorCode, extras); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller, | 
|  | @Nullable List<Bundle> routes) { | 
|  | mSession2Stub.notifyRoutesInfoChanged(controller, routes); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public @MediaPlayerBase.PlayerState int getPlayerState() { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | return player.getPlayerState(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | return MediaPlayerBase.PLAYER_STATE_ERROR; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public long getCurrentPosition() { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | return player.getCurrentPosition(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | return MediaPlayerBase.UNKNOWN_TIME; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public long getDuration() { | 
|  | // TODO: implement | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public long getBufferedPosition() { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | return player.getBufferedPosition(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | return MediaPlayerBase.UNKNOWN_TIME; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public @MediaPlayerBase.BuffState int getBufferingState() { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | return player.getBufferingState(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | return BUFFERING_STATE_UNKNOWN; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public float getPlaybackSpeed() { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | return player.getPlaybackSpeed(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | return 1.0f; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setPlaybackSpeed(float speed) { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | if (player != null) { | 
|  | player.setPlaybackSpeed(speed); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setOnDataSourceMissingHelper( | 
|  | @NonNull OnDataSourceMissingHelper helper) { | 
|  | if (helper == null) { | 
|  | throw new IllegalArgumentException("helper shouldn't be null"); | 
|  | } | 
|  | synchronized (mLock) { | 
|  | mDsmHelper = helper; | 
|  | if (mSessionPlaylistAgent != null) { | 
|  | mSessionPlaylistAgent.setOnDataSourceMissingHelper(helper); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void clearOnDataSourceMissingHelper() { | 
|  | synchronized (mLock) { | 
|  | mDsmHelper = null; | 
|  | if (mSessionPlaylistAgent != null) { | 
|  | mSessionPlaylistAgent.clearOnDataSourceMissingHelper(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public List<MediaItem2> getPlaylist() { | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | return agent.getPlaylist(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { | 
|  | if (list == null) { | 
|  | throw new IllegalArgumentException("list shouldn't be null"); | 
|  | } | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | agent.setPlaylist(list, metadata); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void skipToPlaylistItem(@NonNull MediaItem2 item) { | 
|  | if (item == null) { | 
|  | throw new IllegalArgumentException("item shouldn't be null"); | 
|  | } | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | agent.skipToPlaylistItem(item); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void skipToPreviousItem() { | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | agent.skipToPreviousItem(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void skipToNextItem() { | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | agent.skipToNextItem(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public MediaMetadata2 getPlaylistMetadata() { | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | return agent.getPlaylistMetadata(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void addPlaylistItem(int index, @NonNull MediaItem2 item) { | 
|  | if (index < 0) { | 
|  | throw new IllegalArgumentException("index shouldn't be negative"); | 
|  | } | 
|  | if (item == null) { | 
|  | throw new IllegalArgumentException("item shouldn't be null"); | 
|  | } | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | agent.addPlaylistItem(index, item); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void removePlaylistItem(@NonNull MediaItem2 item) { | 
|  | if (item == null) { | 
|  | throw new IllegalArgumentException("item shouldn't be null"); | 
|  | } | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | agent.removePlaylistItem(item); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void replacePlaylistItem(int index, @NonNull MediaItem2 item) { | 
|  | if (index < 0) { | 
|  | throw new IllegalArgumentException("index shouldn't be negative"); | 
|  | } | 
|  | if (item == null) { | 
|  | throw new IllegalArgumentException("item shouldn't be null"); | 
|  | } | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | agent.replacePlaylistItem(index, item); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public MediaItem2 getCurrentMediaItem() { | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | return agent.getCurrentMediaItem(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) { | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | agent.updatePlaylistMetadata(metadata); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public @MediaPlaylistAgent.RepeatMode int getRepeatMode() { | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | return agent.getRepeatMode(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | return MediaPlaylistAgent.REPEAT_MODE_NONE; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setRepeatMode(@MediaPlaylistAgent.RepeatMode int repeatMode) { | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | agent.setRepeatMode(repeatMode); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public @MediaPlaylistAgent.ShuffleMode int getShuffleMode() { | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | return agent.getShuffleMode(); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | return MediaPlaylistAgent.SHUFFLE_MODE_NONE; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setShuffleMode(int shuffleMode) { | 
|  | MediaPlaylistAgent agent; | 
|  | synchronized (mLock) { | 
|  | agent = mPlaylistAgent; | 
|  | } | 
|  | if (agent != null) { | 
|  | agent.setShuffleMode(shuffleMode); | 
|  | } else if (DEBUG) { | 
|  | Log.d(TAG, "API calls after the close()", new IllegalStateException()); | 
|  | } | 
|  | } | 
|  |  | 
|  | /////////////////////////////////////////////////// | 
|  | // package private and private methods | 
|  | /////////////////////////////////////////////////// | 
|  |  | 
|  | @Override | 
|  | void setInstance(MediaSession2 session) { | 
|  | mInstance = new WeakReference<>(session); | 
|  |  | 
|  | } | 
|  |  | 
|  | @Override | 
|  | MediaSession2 getInstance() { | 
|  | return mInstance.get(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | Context getContext() { | 
|  | return mContext; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | Executor getCallbackExecutor() { | 
|  | return mCallbackExecutor; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | SessionCallback getCallback() { | 
|  | return mCallback; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | boolean isClosed() { | 
|  | return !mHandlerThread.isAlive(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | PlaybackStateCompat getPlaybackStateCompat() { | 
|  | synchronized (mLock) { | 
|  | int state = MediaUtils2.createPlaybackStateCompatState(getPlayerState(), | 
|  | getBufferingState()); | 
|  | // TODO: Consider following missing stuff | 
|  | //       - setCustomAction(): Fill custom layout | 
|  | //       - setErrorMessage(): Fill error message when notifyError() is called. | 
|  | //       - setActiveQueueItemId(): Fill here with the current media item... | 
|  | //       - setExtra(): No idea at this moment. | 
|  | // TODO: generate actions from the allowed commands. | 
|  | long allActions = PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PAUSE | 
|  | | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND | 
|  | | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | 
|  | | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | 
|  | | PlaybackStateCompat.ACTION_FAST_FORWARD | 
|  | | PlaybackStateCompat.ACTION_SET_RATING | 
|  | | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_PLAY_PAUSE | 
|  | | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID | 
|  | | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH | 
|  | | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM | 
|  | | PlaybackStateCompat.ACTION_PLAY_FROM_URI | PlaybackStateCompat.ACTION_PREPARE | 
|  | | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID | 
|  | | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH | 
|  | | PlaybackStateCompat.ACTION_PREPARE_FROM_URI | 
|  | | PlaybackStateCompat.ACTION_SET_REPEAT_MODE | 
|  | | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE | 
|  | | PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED; | 
|  | return new PlaybackStateCompat.Builder() | 
|  | .setState(state, getCurrentPosition(), getPlaybackSpeed()) | 
|  | .setActions(allActions) | 
|  | .setBufferedPosition(getBufferedPosition()) | 
|  | .build(); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | PlaybackInfo getPlaybackInfo() { | 
|  | synchronized (mLock) { | 
|  | return mPlaybackInfo; | 
|  | } | 
|  | } | 
|  |  | 
|  | MediaSession2StubImplBase getSession2Stub() { | 
|  | return mSession2Stub; | 
|  | } | 
|  |  | 
|  | private static String getServiceName(Context context, String serviceAction, String id) { | 
|  | PackageManager manager = context.getPackageManager(); | 
|  | Intent serviceIntent = new Intent(serviceAction); | 
|  | serviceIntent.setPackage(context.getPackageName()); | 
|  | List<ResolveInfo> services = manager.queryIntentServices(serviceIntent, | 
|  | PackageManager.GET_META_DATA); | 
|  | String serviceName = null; | 
|  | if (services != null) { | 
|  | for (int i = 0; i < services.size(); i++) { | 
|  | String serviceId = SessionToken2.getSessionId(services.get(i)); | 
|  | if (serviceId != null && TextUtils.equals(id, serviceId)) { | 
|  | if (services.get(i).serviceInfo == null) { | 
|  | continue; | 
|  | } | 
|  | if (serviceName != null) { | 
|  | throw new IllegalArgumentException("Ambiguous session type. Multiple" | 
|  | + " session services define the same id=" + id); | 
|  | } | 
|  | serviceName = services.get(i).serviceInfo.name; | 
|  | } | 
|  | } | 
|  | } | 
|  | return serviceName; | 
|  | } | 
|  |  | 
|  | private void notifyPlayerUpdatedNotLocked(MediaPlayerBase oldPlayer) { | 
|  | MediaPlayerBase player; | 
|  | synchronized (mLock) { | 
|  | player = mPlayer; | 
|  | } | 
|  | // TODO(jaewan): (Can be post-P) Find better way for player.getPlayerState() // | 
|  | //               In theory, Session.getXXX() may not be the same as Player.getXXX() | 
|  | //               and we should notify information of the session.getXXX() instead of | 
|  | //               player.getXXX() | 
|  | // Notify to controllers as well. | 
|  | final int state = player.getPlayerState(); | 
|  | if (state != oldPlayer.getPlayerState()) { | 
|  | // TODO: implement | 
|  | mSession2Stub.notifyPlayerStateChanged(state); | 
|  | } | 
|  |  | 
|  | final long currentTimeMs = System.currentTimeMillis(); | 
|  | final long position = player.getCurrentPosition(); | 
|  | if (position != oldPlayer.getCurrentPosition()) { | 
|  | // TODO: implement | 
|  | //mSession2Stub.notifyPositionChangedNotLocked(currentTimeMs, position); | 
|  | } | 
|  |  | 
|  | final float speed = player.getPlaybackSpeed(); | 
|  | if (speed != oldPlayer.getPlaybackSpeed()) { | 
|  | // TODO: implement | 
|  | //mSession2Stub.notifyPlaybackSpeedChangedNotLocked(speed); | 
|  | } | 
|  |  | 
|  | final long bufferedPosition = player.getBufferedPosition(); | 
|  | if (bufferedPosition != oldPlayer.getBufferedPosition()) { | 
|  | // TODO: implement | 
|  | //mSession2Stub.notifyBufferedPositionChangedNotLocked(bufferedPosition); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent, | 
|  | List<MediaItem2> list, MediaMetadata2 metadata) { | 
|  | synchronized (mLock) { | 
|  | if (playlistAgent != mPlaylistAgent) { | 
|  | // Ignore calls from the old agent. | 
|  | return; | 
|  | } | 
|  | } | 
|  | MediaSession2 session2 = mInstance.get(); | 
|  | if (session2 != null) { | 
|  | mCallback.onPlaylistChanged(session2, playlistAgent, list, metadata); | 
|  | mSession2Stub.notifyPlaylistChanged(list, metadata); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent, | 
|  | MediaMetadata2 metadata) { | 
|  | synchronized (mLock) { | 
|  | if (playlistAgent != mPlaylistAgent) { | 
|  | // Ignore calls from the old agent. | 
|  | return; | 
|  | } | 
|  | } | 
|  | MediaSession2 session2 = mInstance.get(); | 
|  | if (session2 != null) { | 
|  | mCallback.onPlaylistMetadataChanged(session2, playlistAgent, metadata); | 
|  | mSession2Stub.notifyPlaylistMetadataChanged(metadata); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, | 
|  | int repeatMode) { | 
|  | synchronized (mLock) { | 
|  | if (playlistAgent != mPlaylistAgent) { | 
|  | // Ignore calls from the old agent. | 
|  | return; | 
|  | } | 
|  | } | 
|  | MediaSession2 session2 = mInstance.get(); | 
|  | if (session2 != null) { | 
|  | mCallback.onRepeatModeChanged(session2, playlistAgent, repeatMode); | 
|  | mSession2Stub.notifyRepeatModeChanged(repeatMode); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, | 
|  | int shuffleMode) { | 
|  | synchronized (mLock) { | 
|  | if (playlistAgent != mPlaylistAgent) { | 
|  | // Ignore calls from the old agent. | 
|  | return; | 
|  | } | 
|  | } | 
|  | MediaSession2 session2 = mInstance.get(); | 
|  | if (session2 != null) { | 
|  | mCallback.onShuffleModeChanged(session2, playlistAgent, shuffleMode); | 
|  | mSession2Stub.notifyShuffleModeChanged(shuffleMode); | 
|  | } | 
|  | } | 
|  |  | 
|  | /////////////////////////////////////////////////// | 
|  | // Inner classes | 
|  | /////////////////////////////////////////////////// | 
|  |  | 
|  | private static class MyPlayerEventCallback extends PlayerEventCallback { | 
|  | private final WeakReference<MediaSession2ImplBase> mSession; | 
|  |  | 
|  | private MyPlayerEventCallback(MediaSession2ImplBase session) { | 
|  | mSession = new WeakReference<>(session); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onCurrentDataSourceChanged(final MediaPlayerBase mpb, | 
|  | final DataSourceDesc dsd) { | 
|  | final MediaSession2ImplBase session = getSession(); | 
|  | // TODO: handle properly when dsd == null | 
|  | if (session == null || dsd == null) { | 
|  | return; | 
|  | } | 
|  | session.getCallbackExecutor().execute(new Runnable() { | 
|  | @Override | 
|  | public void run() { | 
|  | MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd); | 
|  | if (item == null) { | 
|  | return; | 
|  | } | 
|  | session.getCallback().onCurrentMediaItemChanged(session.getInstance(), mpb, | 
|  | item); | 
|  | if (item.equals(session.getCurrentMediaItem())) { | 
|  | session.getSession2Stub().notifyCurrentMediaItemChanged(item); | 
|  | } | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onMediaPrepared(final MediaPlayerBase mpb, final DataSourceDesc dsd) { | 
|  | final MediaSession2ImplBase session = getSession(); | 
|  | if (session == null || dsd == null) { | 
|  | return; | 
|  | } | 
|  | session.getCallbackExecutor().execute(new Runnable() { | 
|  | @Override | 
|  | public void run() { | 
|  | MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd); | 
|  | if (item == null) { | 
|  | return; | 
|  | } | 
|  | session.getCallback().onMediaPrepared(session.getInstance(), mpb, item); | 
|  | // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936) | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onPlayerStateChanged(final MediaPlayerBase mpb, final int state) { | 
|  | final MediaSession2ImplBase session = getSession(); | 
|  | if (session == null) { | 
|  | return; | 
|  | } | 
|  | session.getCallbackExecutor().execute(new Runnable() { | 
|  | @Override | 
|  | public void run() { | 
|  | session.getCallback().onPlayerStateChanged(session.getInstance(), mpb, state); | 
|  | session.getSession2Stub().notifyPlayerStateChanged(state); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onBufferingStateChanged(final MediaPlayerBase mpb, final DataSourceDesc dsd, | 
|  | final int state) { | 
|  | final MediaSession2ImplBase session = getSession(); | 
|  | if (session == null || dsd == null) { | 
|  | return; | 
|  | } | 
|  | session.getCallbackExecutor().execute(new Runnable() { | 
|  | @Override | 
|  | public void run() { | 
|  | MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd); | 
|  | if (item == null) { | 
|  | return; | 
|  | } | 
|  | session.getCallback().onBufferingStateChanged( | 
|  | session.getInstance(), mpb, item, state); | 
|  | session.getSession2Stub().notifyBufferingStateChanged(item, state); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onPlaybackSpeedChanged(final MediaPlayerBase mpb, final float speed) { | 
|  | final MediaSession2ImplBase session = getSession(); | 
|  | if (session == null) { | 
|  | return; | 
|  | } | 
|  | session.getCallbackExecutor().execute(new Runnable() { | 
|  | @Override | 
|  | public void run() { | 
|  | session.getCallback().onPlaybackSpeedChanged(session.getInstance(), mpb, speed); | 
|  | session.getSession2Stub().notifyPlaybackSpeedChanged(speed); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | private MediaSession2ImplBase getSession() { | 
|  | final MediaSession2ImplBase session = mSession.get(); | 
|  | if (session == null && DEBUG) { | 
|  | Log.d(TAG, "Session is closed", new IllegalStateException()); | 
|  | } | 
|  | return session; | 
|  | } | 
|  |  | 
|  | private MediaItem2 getMediaItem(MediaSession2ImplBase session, DataSourceDesc dsd) { | 
|  | MediaPlaylistAgent agent = session.getPlaylistAgent(); | 
|  | if (agent == null) { | 
|  | if (DEBUG) { | 
|  | Log.d(TAG, "Session is closed", new IllegalStateException()); | 
|  | } | 
|  | return null; | 
|  | } | 
|  | MediaItem2 item = agent.getMediaItem(dsd); | 
|  | if (item == null) { | 
|  | if (DEBUG) { | 
|  | Log.d(TAG, "Could not find matching item for dsd=" + dsd, | 
|  | new NoSuchElementException()); | 
|  | } | 
|  | } | 
|  | return item; | 
|  | } | 
|  | } | 
|  |  | 
|  | private static class MyPlaylistEventCallback extends PlaylistEventCallback { | 
|  | private final WeakReference<MediaSession2ImplBase> mSession; | 
|  |  | 
|  | private MyPlaylistEventCallback(MediaSession2ImplBase session) { | 
|  | mSession = new WeakReference<>(session); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list, | 
|  | MediaMetadata2 metadata) { | 
|  | final MediaSession2ImplBase session = mSession.get(); | 
|  | if (session == null) { | 
|  | return; | 
|  | } | 
|  | session.notifyPlaylistChangedOnExecutor(playlistAgent, list, metadata); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent, | 
|  | MediaMetadata2 metadata) { | 
|  | final MediaSession2ImplBase session = mSession.get(); | 
|  | if (session == null) { | 
|  | return; | 
|  | } | 
|  | session.notifyPlaylistMetadataChangedOnExecutor(playlistAgent, metadata); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode) { | 
|  | final MediaSession2ImplBase session = mSession.get(); | 
|  | if (session == null) { | 
|  | return; | 
|  | } | 
|  | session.notifyRepeatModeChangedOnExecutor(playlistAgent, repeatMode); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode) { | 
|  | final MediaSession2ImplBase session = mSession.get(); | 
|  | if (session == null) { | 
|  | return; | 
|  | } | 
|  | session.notifyShuffleModeChangedOnExecutor(playlistAgent, shuffleMode); | 
|  | } | 
|  | } | 
|  |  | 
|  | abstract static class BuilderBase | 
|  | <T extends MediaSession2, C extends SessionCallback> { | 
|  | final Context mContext; | 
|  | MediaPlayerBase mPlayer; | 
|  | String mId; | 
|  | Executor mCallbackExecutor; | 
|  | C mCallback; | 
|  | MediaPlaylistAgent mPlaylistAgent; | 
|  | VolumeProviderCompat mVolumeProvider; | 
|  | PendingIntent mSessionActivity; | 
|  |  | 
|  | BuilderBase(Context context) { | 
|  | if (context == null) { | 
|  | throw new IllegalArgumentException("context shouldn't be null"); | 
|  | } | 
|  | mContext = context; | 
|  | // Ensure MediaSessionCompat non-null or empty | 
|  | mId = TAG; | 
|  | } | 
|  |  | 
|  | void setPlayer(@NonNull MediaPlayerBase player) { | 
|  | if (player == null) { | 
|  | throw new IllegalArgumentException("player shouldn't be null"); | 
|  | } | 
|  | mPlayer = player; | 
|  | } | 
|  |  | 
|  | void setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) { | 
|  | if (playlistAgent == null) { | 
|  | throw new IllegalArgumentException("playlistAgent shouldn't be null"); | 
|  | } | 
|  | mPlaylistAgent = playlistAgent; | 
|  | } | 
|  |  | 
|  | void setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) { | 
|  | mVolumeProvider = volumeProvider; | 
|  | } | 
|  |  | 
|  | void setSessionActivity(@Nullable PendingIntent pi) { | 
|  | mSessionActivity = pi; | 
|  | } | 
|  |  | 
|  | void setId(@NonNull String id) { | 
|  | if (id == null) { | 
|  | throw new IllegalArgumentException("id shouldn't be null"); | 
|  | } | 
|  | mId = id; | 
|  | } | 
|  |  | 
|  | void setSessionCallback(@NonNull Executor executor, @NonNull C callback) { | 
|  | if (executor == null) { | 
|  | throw new IllegalArgumentException("executor shouldn't be null"); | 
|  | } | 
|  | if (callback == null) { | 
|  | throw new IllegalArgumentException("callback shouldn't be null"); | 
|  | } | 
|  | mCallbackExecutor = executor; | 
|  | mCallback = callback; | 
|  | } | 
|  |  | 
|  | abstract @NonNull T build(); | 
|  | } | 
|  |  | 
|  | static final class Builder extends | 
|  | BuilderBase<MediaSession2, MediaSession2.SessionCallback> { | 
|  | Builder(Context context) { | 
|  | super(context); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public @NonNull MediaSession2 build() { | 
|  | if (mCallbackExecutor == null) { | 
|  | mCallbackExecutor = new MainHandlerExecutor(mContext); | 
|  | } | 
|  | if (mCallback == null) { | 
|  | mCallback = new SessionCallback() {}; | 
|  | } | 
|  | return new MediaSession2(new MediaSession2ImplBase(mContext, | 
|  | new MediaSessionCompat(mContext, mId), mId, mPlayer, mPlaylistAgent, | 
|  | mVolumeProvider, mSessionActivity, mCallbackExecutor, mCallback)); | 
|  | } | 
|  | } | 
|  |  | 
|  | static class MainHandlerExecutor implements Executor { | 
|  | private final Handler mHandler; | 
|  |  | 
|  | MainHandlerExecutor(Context context) { | 
|  | mHandler = new Handler(context.getMainLooper()); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void execute(Runnable command) { | 
|  | if (!mHandler.post(command)) { | 
|  | throw new RejectedExecutionException(mHandler + " is shutting down"); | 
|  | } | 
|  | } | 
|  | } | 
|  | } |