1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.media.testmediaapp; 18 19 import static android.media.AudioManager.AUDIOFOCUS_GAIN; 20 import static android.media.AudioManager.AUDIOFOCUS_LOSS; 21 import static android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT; 22 import static android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK; 23 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 24 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE; 25 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY; 26 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID; 27 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE; 28 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SEEK_TO; 29 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT; 30 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; 31 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; 32 import static android.support.v4.media.session.PlaybackStateCompat.ERROR_CODE_APP_ERROR; 33 import static android.support.v4.media.session.PlaybackStateCompat.STATE_ERROR; 34 35 import android.media.AudioManager.OnAudioFocusChangeListener; 36 import androidx.annotation.Nullable; 37 38 import android.app.PendingIntent; 39 import android.content.Intent; 40 import android.media.AudioFocusRequest; 41 import android.media.AudioManager; 42 import android.os.Bundle; 43 import android.os.Handler; 44 import android.support.v4.media.session.MediaSessionCompat; 45 import android.support.v4.media.session.PlaybackStateCompat; 46 import android.text.TextUtils; 47 import android.util.Log; 48 import android.widget.Toast; 49 50 import com.android.car.media.testmediaapp.TmaMediaEvent.Action; 51 import com.android.car.media.testmediaapp.TmaMediaEvent.EventState; 52 import com.android.car.media.testmediaapp.TmaMediaEvent.ResolutionIntent; 53 import com.android.car.media.testmediaapp.TmaMediaItem.TmaCustomAction; 54 import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaAccountType; 55 import com.android.car.media.testmediaapp.prefs.TmaPrefs; 56 import com.android.car.media.testmediaapp.prefs.TmaPrefsActivity; 57 58 59 /** 60 * This class simulates all media interactions (no sound is actually played). 61 */ 62 public class TmaPlayer extends MediaSessionCompat.Callback { 63 64 private static final String TAG = "TmaPlayer"; 65 66 private final TmaBrowser mBrowser; 67 private final TmaPrefs mPrefs; 68 private final TmaLibrary mLibrary; 69 private final AudioManager mAudioManager; 70 private final Handler mHandler; 71 private final Runnable mTrackTimer = this::onStop; 72 private final Runnable mEventTrigger = this::onProcessMediaEvent; 73 private final MediaSessionCompat mSession; 74 private final AudioFocusRequest mAudioFocusRequest; 75 76 /** Only updated when the state changes. */ 77 private long mCurrentPositionMs = 0; 78 private float mPlaybackSpeed = 1.0f; // TODO: make variable. 79 private long mPlaybackStartTimeMs; 80 private boolean mIsPlaying; 81 @Nullable 82 private TmaMediaItem mActiveItem; 83 private int mNextEventIndex = -1; 84 private boolean mResumeOnFocusGain; 85 TmaPlayer(TmaBrowser browser, TmaLibrary library, AudioManager audioManager, Handler handler, MediaSessionCompat session)86 TmaPlayer(TmaBrowser browser, TmaLibrary library, AudioManager audioManager, Handler handler, 87 MediaSessionCompat session) { 88 mBrowser = browser; 89 mPrefs = TmaPrefs.getInstance(mBrowser); 90 mLibrary = library; 91 mAudioManager = audioManager; 92 mHandler = handler; 93 mSession = session; 94 95 mAudioFocusRequest = new AudioFocusRequest.Builder(AUDIOFOCUS_GAIN) 96 .setOnAudioFocusChangeListener(this::onAudioFocusChange, mHandler) 97 .build(); 98 } 99 100 /** Updates the state in the media session based on the given {@link TmaMediaEvent}. */ setPlaybackState(TmaMediaEvent event)101 void setPlaybackState(TmaMediaEvent event) { 102 Log.i(TAG, "setPlaybackState " + event); 103 104 PlaybackStateCompat.Builder state = new PlaybackStateCompat.Builder() 105 .setState(event.mState.mValue, mCurrentPositionMs, mPlaybackSpeed) 106 .setErrorMessage(event.mErrorCode.mValue, event.mErrorMessage) 107 .setActions(addActions(ACTION_PAUSE)); 108 if (ResolutionIntent.PREFS.equals(event.mResolutionIntent)) { 109 Intent prefsIntent = new Intent(); 110 prefsIntent.setClass(mBrowser, TmaPrefsActivity.class); 111 prefsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 112 PendingIntent pendingIntent = PendingIntent.getActivity(mBrowser, 0, prefsIntent, 113 PendingIntent.FLAG_IMMUTABLE); 114 115 Bundle extras = new Bundle(); 116 extras.putString(MediaKeys.ERROR_RESOLUTION_ACTION_LABEL, event.mActionLabel); 117 extras.putParcelable(MediaKeys.ERROR_RESOLUTION_ACTION_INTENT, pendingIntent); 118 state.setExtras(extras); 119 } 120 121 setActiveItemState(state); 122 mSession.setPlaybackState(state.build()); 123 } 124 125 /** Sets custom action, queue id, etc. */ setActiveItemState(PlaybackStateCompat.Builder state)126 private void setActiveItemState(PlaybackStateCompat.Builder state) { 127 if (mActiveItem != null) { 128 for (TmaCustomAction action : mActiveItem.mCustomActions) { 129 String name = mBrowser.getResources().getString(action.mNameId); 130 state.addCustomAction(action.mId, name, action.mIcon); 131 } 132 state.setActiveQueueItemId(mActiveItem.getQueueId()); 133 } 134 } 135 playItem(@ullable TmaMediaItem item)136 private void playItem(@Nullable TmaMediaItem item) { 137 if (item != null && item.getParent() != null) { 138 if (mIsPlaying) { 139 stopPlayback(); 140 } 141 mActiveItem = item; 142 mSession.setQueue(item.getParent().buildQueue()); 143 startPlayBack(true); 144 } 145 } 146 147 @Override onPlayFromMediaId(String mediaId, Bundle extras)148 public void onPlayFromMediaId(String mediaId, Bundle extras) { 149 super.onPlayFromMediaId(mediaId, extras); 150 playItem(mLibrary.getMediaItemById(mediaId)); 151 } 152 153 @Override onPrepareFromMediaId(String mediaId, Bundle extras)154 public void onPrepareFromMediaId(String mediaId, Bundle extras) { 155 super.onPrepareFromMediaId(mediaId, extras); 156 157 TmaMediaItem item = mLibrary.getMediaItemById(mediaId); 158 prepareMediaItem(item); 159 } 160 161 @Override onPrepare()162 public void onPrepare() { 163 super.onPrepare(); 164 if (!mSession.isActive()) { 165 mSession.setActive(true); 166 } 167 // Prepare the first playable item (at root level) as the active item 168 if (mActiveItem == null) { 169 TmaMediaItem root = mLibrary.getRoot(mPrefs.mRootNodeType.getValue()); 170 if (root != null) { 171 prepareMediaItem(root.getPlayableByIndex(0)); 172 } 173 } 174 } 175 prepareMediaItem(@ullable TmaMediaItem item)176 void prepareMediaItem(@Nullable TmaMediaItem item) { 177 if (item != null && item.getParent() != null) { 178 if (mIsPlaying) { 179 stopPlayback(); 180 } 181 mActiveItem = item; 182 mActiveItem.updateSessionMetadata(mSession); 183 mSession.setQueue(item.getParent().buildQueue()); 184 185 PlaybackStateCompat.Builder state = new PlaybackStateCompat.Builder() 186 .setState(PlaybackStateCompat.STATE_PAUSED, mCurrentPositionMs, mPlaybackSpeed) 187 .setActions(addActions(ACTION_PLAY)); 188 setActiveItemState(state); 189 mSession.setPlaybackState(state.build()); 190 } 191 } 192 193 @Override onSkipToQueueItem(long id)194 public void onSkipToQueueItem(long id) { 195 super.onSkipToQueueItem(id); 196 if (mActiveItem != null && mActiveItem.getParent() != null) { 197 playItem(mActiveItem.getParent().getPlayableByIndex(id)); 198 } 199 } 200 201 @Override onSkipToNext()202 public void onSkipToNext() { 203 super.onSkipToNext(); 204 if (mActiveItem != null) { 205 playItem(mActiveItem.getNext()); 206 } 207 } 208 209 @Override onSkipToPrevious()210 public void onSkipToPrevious() { 211 super.onSkipToPrevious(); 212 if (mActiveItem != null) { 213 playItem(mActiveItem.getPrevious()); 214 } 215 } 216 217 @Override onPlay()218 public void onPlay() { 219 super.onPlay(); 220 startPlayBack(true); 221 } 222 223 @Override onSeekTo(long pos)224 public void onSeekTo(long pos) { 225 super.onSeekTo(pos); 226 boolean wasPlaying = mIsPlaying; 227 if (wasPlaying) { 228 mHandler.removeCallbacks(mTrackTimer); 229 } 230 mCurrentPositionMs = pos; 231 boolean requestAudioFocus = !wasPlaying; 232 startPlayBack(requestAudioFocus); 233 } 234 235 @Override onPause()236 public void onPause() { 237 super.onPause(); 238 pausePlayback(); 239 } 240 241 @Override onStop()242 public void onStop() { 243 super.onStop(); 244 stopPlayback(); 245 sendStopPlaybackState(); 246 } 247 248 @Override onCustomAction(String action, Bundle extras)249 public void onCustomAction(String action, Bundle extras) { 250 super.onCustomAction(action, extras); 251 if (mActiveItem != null) { 252 if (TmaCustomAction.HEART_PLUS_PLUS.mId.equals(action)) { 253 mActiveItem.mHearts++; 254 toast("" + mActiveItem.mHearts); 255 } else if (TmaCustomAction.HEART_LESS_LESS.mId.equals(action)) { 256 mActiveItem.mHearts--; 257 toast("" + mActiveItem.mHearts); 258 } else if (TmaCustomAction.REQUEST_LOCATION.mId.equals(action)) { 259 mBrowser.startService(new Intent(mBrowser, TmaForegroundService.class)); 260 } 261 } 262 } 263 264 /** Note: this is for quick feedback implementation, media apps should avoid toasts... */ toast(String message)265 private void toast(String message) { 266 Toast.makeText(mBrowser, message, Toast.LENGTH_LONG).show(); 267 } 268 audioFocusGranted()269 private boolean audioFocusGranted() { 270 return mAudioManager.requestAudioFocus(mAudioFocusRequest) == AUDIOFOCUS_REQUEST_GRANTED; 271 } 272 onProcessMediaEvent()273 private void onProcessMediaEvent() { 274 if (mActiveItem == null) return; 275 276 TmaMediaEvent event = mActiveItem.mMediaEvents.get(mNextEventIndex); 277 event.maybeThrow(); 278 if (!TextUtils.isEmpty(event.mMediaItemIdToToggle)) { 279 mBrowser.toggleItem(mLibrary.getMediaItemById(event.mMediaItemIdToToggle)); 280 } 281 282 if (event.premiumAccountRequired() && 283 TmaAccountType.PAID.equals(mPrefs.mAccountType.getValue())) { 284 Log.i(TAG, "Ignoring even for paid account"); 285 return; 286 } else if (Action.RESET_METADATA.equals(event.mAction)) { 287 mSession.setMetadata(mSession.getController().getMetadata()); 288 } else { 289 setPlaybackState(event); 290 } 291 292 if (event.mState == EventState.PLAYING) { 293 if (!mSession.isActive()) { 294 mSession.setActive(true); 295 } 296 297 long trackDurationMs = mActiveItem.getDuration(); 298 if (trackDurationMs > 0) { 299 mPlaybackStartTimeMs = System.currentTimeMillis(); 300 long remainingMs = (long) ((trackDurationMs - mCurrentPositionMs) / mPlaybackSpeed); 301 mHandler.postDelayed(mTrackTimer, remainingMs); 302 } 303 mIsPlaying = true; 304 } else if (mIsPlaying) { 305 stopPlayback(); 306 } 307 308 mNextEventIndex++; 309 if (mNextEventIndex < mActiveItem.mMediaEvents.size()) { 310 mHandler.postDelayed(mEventTrigger, 311 mActiveItem.mMediaEvents.get(mNextEventIndex).mPostDelayMs); 312 } 313 } 314 startPlayBack(boolean requestAudioFocus)315 private void startPlayBack(boolean requestAudioFocus) { 316 if (requestAudioFocus && !audioFocusGranted()) return; 317 318 if (mActiveItem == null || mActiveItem.mMediaEvents.size() <= 0) { 319 PlaybackStateCompat state = new PlaybackStateCompat.Builder() 320 .setState(STATE_ERROR, mCurrentPositionMs, mPlaybackSpeed) 321 .setErrorMessage(ERROR_CODE_APP_ERROR, "null mActiveItem or empty events") 322 .build(); 323 mSession.setPlaybackState(state); 324 return; 325 } 326 327 mActiveItem.updateSessionMetadata(mSession); 328 329 mHandler.removeCallbacks(mEventTrigger); 330 mNextEventIndex = 0; 331 mHandler.postDelayed(mEventTrigger, mActiveItem.mMediaEvents.get(0).mPostDelayMs); 332 } 333 pausePlayback()334 private void pausePlayback() { 335 mCurrentPositionMs += (System.currentTimeMillis() - mPlaybackStartTimeMs) / mPlaybackSpeed; 336 mHandler.removeCallbacks(mTrackTimer); 337 PlaybackStateCompat.Builder state = new PlaybackStateCompat.Builder() 338 .setState(PlaybackStateCompat.STATE_PAUSED, mCurrentPositionMs, mPlaybackSpeed) 339 .setActions(addActions(ACTION_PLAY)); 340 setActiveItemState(state); 341 mSession.setPlaybackState(state.build()); 342 mIsPlaying = false; 343 } 344 345 /** Doesn't change the playback state. */ stopPlayback()346 private void stopPlayback() { 347 mCurrentPositionMs = 0; 348 mHandler.removeCallbacks(mTrackTimer); 349 mIsPlaying = false; 350 } 351 sendStopPlaybackState()352 private void sendStopPlaybackState() { 353 PlaybackStateCompat.Builder state = new PlaybackStateCompat.Builder() 354 .setState(PlaybackStateCompat.STATE_STOPPED, mCurrentPositionMs, mPlaybackSpeed) 355 .setActions(addActions(ACTION_PLAY)); 356 setActiveItemState(state); 357 mSession.setPlaybackState(state.build()); 358 } 359 addActions(long actions)360 private long addActions(long actions) { 361 actions |= ACTION_PLAY_FROM_MEDIA_ID | ACTION_SKIP_TO_QUEUE_ITEM | ACTION_SEEK_TO 362 | ACTION_PREPARE; 363 364 if (mActiveItem != null) { 365 if (mActiveItem.getNext() != null) { 366 actions |= ACTION_SKIP_TO_NEXT; 367 } 368 if (mActiveItem.getPrevious() != null) { 369 actions |= ACTION_SKIP_TO_PREVIOUS; 370 } 371 } 372 373 return actions; 374 } 375 onAudioFocusChange(int focusChange)376 private void onAudioFocusChange(int focusChange) { 377 // Adapted from samples at https://developer.android.com/guide/topics/media-apps/audio-focus 378 // Android Auto emulator tests rely on the app pausing and resuming in response to focus 379 // transient loss and focus gain, respectively. 380 switch (focusChange) { 381 case AUDIOFOCUS_GAIN: 382 if (mResumeOnFocusGain) { 383 mResumeOnFocusGain = false; 384 startPlayBack(/* requestAudioFocus= */ false); 385 } 386 break; 387 case AUDIOFOCUS_LOSS: 388 mResumeOnFocusGain = false; 389 pausePlayback(); 390 break; 391 case AUDIOFOCUS_LOSS_TRANSIENT: 392 case AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 393 mResumeOnFocusGain = mIsPlaying; 394 pausePlayback(); 395 break; 396 default: 397 Log.w(TAG, "Unknown audio focus change " + focusChange); 398 } 399 } 400 } 401