1 /* 2 * Copyright (c) 2016, 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 package com.android.car.media.localmediaplayer; 17 18 import android.app.Notification; 19 import android.app.NotificationManager; 20 import android.app.PendingIntent; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.SharedPreferences; 24 import android.media.AudioManager; 25 import android.media.AudioManager.OnAudioFocusChangeListener; 26 import android.media.MediaDescription; 27 import android.media.MediaMetadata; 28 import android.media.MediaPlayer; 29 import android.media.MediaPlayer.OnCompletionListener; 30 import android.media.session.MediaSession; 31 import android.media.session.MediaSession.QueueItem; 32 import android.media.session.PlaybackState; 33 import android.media.session.PlaybackState.CustomAction; 34 import android.os.Bundle; 35 import android.util.Log; 36 37 import com.android.car.media.localmediaplayer.nano.Proto.Playlist; 38 import com.android.car.media.localmediaplayer.nano.Proto.Song; 39 40 // Proto should be available in AOSP. 41 import com.google.protobuf.nano.MessageNano; 42 import com.google.protobuf.nano.InvalidProtocolBufferNanoException; 43 44 import java.io.IOException; 45 import java.io.File; 46 import java.util.ArrayList; 47 import java.util.Base64; 48 import java.util.Collections; 49 import java.util.List; 50 51 /** 52 * TODO: Consider doing all content provider accesses and player operations asynchronously. 53 */ 54 public class Player extends MediaSession.Callback { 55 private static final String TAG = "LMPlayer"; 56 private static final String SHARED_PREFS_NAME = "com.android.car.media.localmediaplayer.prefs"; 57 private static final String CURRENT_PLAYLIST_KEY = "__CURRENT_PLAYLIST_KEY__"; 58 private static final int NOTIFICATION_ID = 42; 59 private static final int REQUEST_CODE = 94043; 60 61 private static final float PLAYBACK_SPEED = 1.0f; 62 private static final float PLAYBACK_SPEED_STOPPED = 1.0f; 63 private static final long PLAYBACK_POSITION_STOPPED = 0; 64 65 // Note: Queues loop around so next/previous are always available. 66 private static final long PLAYING_ACTIONS = PlaybackState.ACTION_PAUSE 67 | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_SKIP_TO_NEXT 68 | PlaybackState.ACTION_SKIP_TO_PREVIOUS | PlaybackState.ACTION_SKIP_TO_QUEUE_ITEM; 69 70 private static final long PAUSED_ACTIONS = PlaybackState.ACTION_PLAY 71 | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_SKIP_TO_NEXT 72 | PlaybackState.ACTION_SKIP_TO_PREVIOUS; 73 74 private static final long STOPPED_ACTIONS = PlaybackState.ACTION_PLAY 75 | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_SKIP_TO_NEXT 76 | PlaybackState.ACTION_SKIP_TO_PREVIOUS; 77 78 private static final String SHUFFLE = "android.car.media.localmediaplayer.shuffle"; 79 80 private final Context mContext; 81 private final MediaSession mSession; 82 private final AudioManager mAudioManager; 83 private final PlaybackState mErrorState; 84 private final DataModel mDataModel; 85 private final CustomAction mShuffle; 86 87 private List<QueueItem> mQueue; 88 private int mCurrentQueueIdx = 0; 89 private final SharedPreferences mSharedPrefs; 90 91 private NotificationManager mNotificationManager; 92 private Notification.Builder mPlayingNotificationBuilder; 93 private Notification.Builder mPausedNotificationBuilder; 94 95 // TODO: Use multiple media players for gapless playback. 96 private final MediaPlayer mMediaPlayer; 97 Player(Context context, MediaSession session, DataModel dataModel)98 public Player(Context context, MediaSession session, DataModel dataModel) { 99 mContext = context; 100 mDataModel = dataModel; 101 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 102 mSession = session; 103 mSharedPrefs = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); 104 105 mShuffle = new CustomAction.Builder(SHUFFLE, context.getString(R.string.shuffle), 106 R.drawable.shuffle).build(); 107 108 mMediaPlayer = new MediaPlayer(); 109 mMediaPlayer.reset(); 110 mMediaPlayer.setOnCompletionListener(mOnCompletionListener); 111 mErrorState = new PlaybackState.Builder() 112 .setState(PlaybackState.STATE_ERROR, 0, 0) 113 .setErrorMessage(context.getString(R.string.playback_error)) 114 .build(); 115 116 mNotificationManager = 117 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 118 119 // There are 2 forms of the media notification, when playing it needs to show the controls 120 // to pause & skip whereas when paused it needs to show controls to play & skip. Setup 121 // pre-populated builders for both of these up front. 122 Notification.Action prevAction = makeNotificationAction( 123 LocalMediaBrowserService.ACTION_PREV, R.drawable.ic_prev, R.string.prev); 124 Notification.Action nextAction = makeNotificationAction( 125 LocalMediaBrowserService.ACTION_NEXT, R.drawable.ic_next, R.string.next); 126 Notification.Action playAction = makeNotificationAction( 127 LocalMediaBrowserService.ACTION_PLAY, R.drawable.ic_play, R.string.play); 128 Notification.Action pauseAction = makeNotificationAction( 129 LocalMediaBrowserService.ACTION_PAUSE, R.drawable.ic_pause, R.string.pause); 130 131 // While playing, you need prev, pause, next. 132 mPlayingNotificationBuilder = new Notification.Builder(context) 133 .setVisibility(Notification.VISIBILITY_PUBLIC) 134 .setSmallIcon(R.drawable.ic_sd_storage_black) 135 .addAction(prevAction) 136 .addAction(pauseAction) 137 .addAction(nextAction); 138 139 // While paused, you need prev, play, next. 140 mPausedNotificationBuilder = new Notification.Builder(context) 141 .setVisibility(Notification.VISIBILITY_PUBLIC) 142 .setSmallIcon(R.drawable.ic_sd_storage_black) 143 .addAction(prevAction) 144 .addAction(playAction) 145 .addAction(nextAction); 146 } 147 makeNotificationAction(String action, int iconId, int stringId)148 private Notification.Action makeNotificationAction(String action, int iconId, int stringId) { 149 PendingIntent intent = PendingIntent.getBroadcast(mContext, REQUEST_CODE, 150 new Intent(action), PendingIntent.FLAG_UPDATE_CURRENT); 151 Notification.Action notificationAction = new Notification.Action.Builder(iconId, 152 mContext.getString(stringId), intent) 153 .build(); 154 return notificationAction; 155 } 156 requestAudioFocus(Runnable onSuccess)157 private boolean requestAudioFocus(Runnable onSuccess) { 158 int result = mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, 159 AudioManager.AUDIOFOCUS_GAIN); 160 if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 161 onSuccess.run(); 162 return true; 163 } 164 Log.e(TAG, "Failed to acquire audio focus"); 165 return false; 166 } 167 168 @Override onPlay()169 public void onPlay() { 170 super.onPlay(); 171 if (Log.isLoggable(TAG, Log.DEBUG)) { 172 Log.d(TAG, "onPlay"); 173 } 174 // Check permissions every time we try to play 175 if (!Utils.hasRequiredPermissions(mContext)) { 176 setMissingPermissionError(); 177 } else { 178 requestAudioFocus(() -> resumePlayback()); 179 } 180 } 181 182 @Override onPause()183 public void onPause() { 184 super.onPause(); 185 if (Log.isLoggable(TAG, Log.DEBUG)) { 186 Log.d(TAG, "onPause"); 187 } 188 pausePlayback(); 189 mAudioManager.abandonAudioFocus(mAudioFocusListener); 190 } 191 destroy()192 public void destroy() { 193 stopPlayback(); 194 mNotificationManager.cancelAll(); 195 mAudioManager.abandonAudioFocus(mAudioFocusListener); 196 mMediaPlayer.release(); 197 } 198 saveState()199 public void saveState() { 200 if (mQueue == null || mQueue.isEmpty()) { 201 return; 202 } 203 204 Playlist playlist = new Playlist(); 205 playlist.songs = new Song[mQueue.size()]; 206 207 int idx = 0; 208 for (QueueItem item : mQueue) { 209 Song song = new Song(); 210 song.queueId = item.getQueueId(); 211 MediaDescription description = item.getDescription(); 212 song.mediaId = description.getMediaId(); 213 song.title = description.getTitle().toString(); 214 song.subtitle = description.getSubtitle().toString(); 215 song.path = description.getExtras().getString(DataModel.PATH_KEY); 216 217 playlist.songs[idx] = song; 218 idx++; 219 } 220 playlist.currentQueueId = mQueue.get(mCurrentQueueIdx).getQueueId(); 221 playlist.currentSongPosition = mMediaPlayer.getCurrentPosition(); 222 playlist.name = CURRENT_PLAYLIST_KEY; 223 224 // Go to Base64 to ensure that we can actually store the string in a sharedpref. This is 225 // slightly wasteful because of the fact that base64 expands the size a bit but it's a 226 // lot less riskier than abusing the java string to directly store bytes coming out of 227 // proto encoding. 228 String serialized = Base64.getEncoder().encodeToString(MessageNano.toByteArray(playlist)); 229 SharedPreferences.Editor editor = mSharedPrefs.edit(); 230 editor.putString(CURRENT_PLAYLIST_KEY, serialized); 231 editor.commit(); 232 } 233 setMissingPermissionError()234 private void setMissingPermissionError() { 235 Intent prefsIntent = new Intent(); 236 prefsIntent.setClass(mContext, PermissionsActivity.class); 237 prefsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 238 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, prefsIntent, 0); 239 240 Bundle extras = new Bundle(); 241 extras.putString(Utils.ERROR_RESOLUTION_ACTION_LABEL, 242 mContext.getString(R.string.permission_error_resolve)); 243 extras.putParcelable(Utils.ERROR_RESOLUTION_ACTION_INTENT, pendingIntent); 244 245 PlaybackState state = new PlaybackState.Builder() 246 .setState(PlaybackState.STATE_ERROR, 0, 0) 247 .setErrorMessage(mContext.getString(R.string.permission_error)) 248 .setExtras(extras) 249 .build(); 250 mSession.setPlaybackState(state); 251 } 252 maybeRebuildQueue(Playlist playlist)253 private boolean maybeRebuildQueue(Playlist playlist) { 254 List<QueueItem> queue = new ArrayList<>(); 255 int foundIdx = 0; 256 // You need to check if the playlist actually is still valid because the user could have 257 // deleted files or taken out the sd card between runs so we might as well check this ahead 258 // of time before we load up the playlist. 259 for (Song song : playlist.songs) { 260 File tmp = new File(song.path); 261 if (!tmp.exists()) { 262 continue; 263 } 264 265 if (playlist.currentQueueId == song.queueId) { 266 foundIdx = queue.size(); 267 } 268 269 Bundle bundle = new Bundle(); 270 bundle.putString(DataModel.PATH_KEY, song.path); 271 MediaDescription description = new MediaDescription.Builder() 272 .setMediaId(song.mediaId) 273 .setTitle(song.title) 274 .setSubtitle(song.subtitle) 275 .setExtras(bundle) 276 .build(); 277 queue.add(new QueueItem(description, song.queueId)); 278 } 279 280 if (queue.isEmpty()) { 281 return false; 282 } 283 284 mQueue = queue; 285 mCurrentQueueIdx = foundIdx; // Resumes from beginning if last playing song was not found. 286 287 return true; 288 } 289 maybeRestoreState()290 public boolean maybeRestoreState() { 291 if (!Utils.hasRequiredPermissions(mContext)) { 292 setMissingPermissionError(); 293 return false; 294 } 295 String serialized = mSharedPrefs.getString(CURRENT_PLAYLIST_KEY, null); 296 if (serialized == null) { 297 return false; 298 } 299 300 try { 301 Playlist playlist = Playlist.parseFrom(Base64.getDecoder().decode(serialized)); 302 if (!maybeRebuildQueue(playlist)) { 303 return false; 304 } 305 updateSessionQueueState(); 306 307 requestAudioFocus(() -> { 308 try { 309 playCurrentQueueIndex(); 310 mMediaPlayer.seekTo(playlist.currentSongPosition); 311 updatePlaybackStatePlaying(); 312 } catch (IOException e) { 313 Log.e(TAG, "Restored queue, but couldn't resume playback."); 314 } 315 }); 316 } catch (IllegalArgumentException | InvalidProtocolBufferNanoException e) { 317 // Couldn't restore the playlist. Not the end of the world. 318 return false; 319 } 320 321 return true; 322 } 323 updateSessionQueueState()324 private void updateSessionQueueState() { 325 mSession.setQueueTitle(mContext.getString(R.string.playlist)); 326 mSession.setQueue(mQueue); 327 } 328 startPlayback(String key)329 private void startPlayback(String key) { 330 if (Log.isLoggable(TAG, Log.DEBUG)) { 331 Log.d(TAG, "startPlayback()"); 332 } 333 334 List<QueueItem> queue = mDataModel.getQueue(); 335 int idx = 0; 336 int foundIdx = -1; 337 for (QueueItem item : queue) { 338 if (item.getDescription().getMediaId().equals(key)) { 339 foundIdx = idx; 340 break; 341 } 342 idx++; 343 } 344 345 if (foundIdx == -1) { 346 mSession.setPlaybackState(mErrorState); 347 return; 348 } 349 350 mQueue = new ArrayList<>(queue); 351 mCurrentQueueIdx = foundIdx; 352 QueueItem current = mQueue.get(mCurrentQueueIdx); 353 String path = current.getDescription().getExtras().getString(DataModel.PATH_KEY); 354 MediaMetadata metadata = mDataModel.getMetadata(current.getDescription().getMediaId()); 355 updateSessionQueueState(); 356 357 try { 358 play(path, metadata); 359 } catch (IOException e) { 360 Log.e(TAG, "Playback failed.", e); 361 mSession.setPlaybackState(mErrorState); 362 } 363 } 364 resumePlayback()365 private void resumePlayback() { 366 if (Log.isLoggable(TAG, Log.DEBUG)) { 367 Log.d(TAG, "resumePlayback()"); 368 } 369 370 updatePlaybackStatePlaying(); 371 372 if (!mMediaPlayer.isPlaying()) { 373 mMediaPlayer.start(); 374 } 375 } 376 postMediaNotification(Notification.Builder builder)377 private void postMediaNotification(Notification.Builder builder) { 378 if (mQueue == null) { 379 return; 380 } 381 382 MediaDescription current = mQueue.get(mCurrentQueueIdx).getDescription(); 383 Notification notification = builder 384 .setStyle(new Notification.MediaStyle().setMediaSession(mSession.getSessionToken())) 385 .setContentTitle(current.getTitle()) 386 .setContentText(current.getSubtitle()) 387 .setShowWhen(false) 388 .build(); 389 notification.flags |= Notification.FLAG_NO_CLEAR; 390 mNotificationManager.notify(NOTIFICATION_ID, notification); 391 } 392 updatePlaybackStatePlaying()393 private void updatePlaybackStatePlaying() { 394 if (!mSession.isActive()) { 395 mSession.setActive(true); 396 } 397 398 // Update the state in the media session. 399 PlaybackState state = new PlaybackState.Builder() 400 .setState(PlaybackState.STATE_PLAYING, 401 mMediaPlayer.getCurrentPosition(), PLAYBACK_SPEED) 402 .setActions(PLAYING_ACTIONS) 403 .addCustomAction(mShuffle) 404 .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId()) 405 .build(); 406 mSession.setPlaybackState(state); 407 408 // Update the media styled notification. 409 postMediaNotification(mPlayingNotificationBuilder); 410 } 411 pausePlayback()412 private void pausePlayback() { 413 if (Log.isLoggable(TAG, Log.DEBUG)) { 414 Log.d(TAG, "pausePlayback()"); 415 } 416 417 long currentPosition = 0; 418 if (mMediaPlayer.isPlaying()) { 419 currentPosition = mMediaPlayer.getCurrentPosition(); 420 mMediaPlayer.pause(); 421 } 422 423 PlaybackState state = new PlaybackState.Builder() 424 .setState(PlaybackState.STATE_PAUSED, currentPosition, PLAYBACK_SPEED_STOPPED) 425 .setActions(PAUSED_ACTIONS) 426 .addCustomAction(mShuffle) 427 .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId()) 428 .build(); 429 mSession.setPlaybackState(state); 430 431 // Update the media styled notification. 432 postMediaNotification(mPausedNotificationBuilder); 433 } 434 stopPlayback()435 private void stopPlayback() { 436 if (Log.isLoggable(TAG, Log.DEBUG)) { 437 Log.d(TAG, "stopPlayback()"); 438 } 439 440 if (mMediaPlayer.isPlaying()) { 441 mMediaPlayer.stop(); 442 } 443 444 PlaybackState state = new PlaybackState.Builder() 445 .setState(PlaybackState.STATE_STOPPED, PLAYBACK_POSITION_STOPPED, 446 PLAYBACK_SPEED_STOPPED) 447 .setActions(STOPPED_ACTIONS) 448 .build(); 449 mSession.setPlaybackState(state); 450 } 451 advance()452 private void advance() throws IOException { 453 if (Log.isLoggable(TAG, Log.DEBUG)) { 454 Log.d(TAG, "advance()"); 455 } 456 // Go to the next song if one exists. Note that if you were to support gapless 457 // playback, you would have to change this code such that you had a currently 458 // playing and a loading MediaPlayer and juggled between them while also calling 459 // setNextMediaPlayer. 460 461 if (mQueue != null && !mQueue.isEmpty()) { 462 // Keep looping around when we run off the end of our current queue. 463 mCurrentQueueIdx = (mCurrentQueueIdx + 1) % mQueue.size(); 464 playCurrentQueueIndex(); 465 } else { 466 stopPlayback(); 467 } 468 } 469 retreat()470 private void retreat() throws IOException { 471 if (Log.isLoggable(TAG, Log.DEBUG)) { 472 Log.d(TAG, "retreat()"); 473 } 474 // Go to the next song if one exists. Note that if you were to support gapless 475 // playback, you would have to change this code such that you had a currently 476 // playing and a loading MediaPlayer and juggled between them while also calling 477 // setNextMediaPlayer. 478 if (mQueue != null) { 479 // Keep looping around when we run off the end of our current queue. 480 mCurrentQueueIdx--; 481 if (mCurrentQueueIdx < 0) { 482 mCurrentQueueIdx = mQueue.size() - 1; 483 } 484 playCurrentQueueIndex(); 485 } else { 486 stopPlayback(); 487 } 488 } 489 playCurrentQueueIndex()490 private void playCurrentQueueIndex() throws IOException { 491 MediaDescription next = mQueue.get(mCurrentQueueIdx).getDescription(); 492 String path = next.getExtras().getString(DataModel.PATH_KEY); 493 MediaMetadata metadata = mDataModel.getMetadata(next.getMediaId()); 494 495 play(path, metadata); 496 } 497 play(String path, MediaMetadata metadata)498 private void play(String path, MediaMetadata metadata) throws IOException { 499 if (Log.isLoggable(TAG, Log.DEBUG)) { 500 Log.d(TAG, "play path=" + path + " metadata=" + metadata); 501 } 502 503 mMediaPlayer.reset(); 504 mMediaPlayer.setDataSource(path); 505 mMediaPlayer.prepare(); 506 507 if (metadata != null) { 508 mSession.setMetadata(metadata); 509 } 510 boolean wasGrantedAudio = requestAudioFocus(() -> { 511 mMediaPlayer.start(); 512 updatePlaybackStatePlaying(); 513 }); 514 if (!wasGrantedAudio) { 515 // player.pause() isn't needed since it should not actually be playing, the 516 // other steps like, updating the notification and play state are needed, thus we 517 // call the pause method. 518 pausePlayback(); 519 } 520 } 521 safeAdvance()522 private void safeAdvance() { 523 try { 524 advance(); 525 } catch (IOException e) { 526 Log.e(TAG, "Failed to advance.", e); 527 mSession.setPlaybackState(mErrorState); 528 } 529 } 530 safeRetreat()531 private void safeRetreat() { 532 try { 533 retreat(); 534 } catch (IOException e) { 535 Log.e(TAG, "Failed to advance.", e); 536 mSession.setPlaybackState(mErrorState); 537 } 538 } 539 540 /** 541 * This is a naive implementation of shuffle, previously played songs may repeat after the 542 * shuffle operation. Only call this from the main thread. 543 */ shuffle()544 private void shuffle() { 545 if (Log.isLoggable(TAG, Log.DEBUG)) { 546 Log.d(TAG, "Shuffling"); 547 } 548 549 // rebuild the the queue in a shuffled form. 550 if (mQueue != null && mQueue.size() > 2) { 551 QueueItem current = mQueue.remove(mCurrentQueueIdx); 552 Collections.shuffle(mQueue); 553 mQueue.add(0, current); 554 // A QueueItem contains a queue id that's used as the key for when the user selects 555 // the current play list. This means the QueueItems must be rebuilt to have their new 556 // id's set. 557 for (int i = 0; i < mQueue.size(); i++) { 558 mQueue.set(i, new QueueItem(mQueue.get(i).getDescription(), i)); 559 } 560 mCurrentQueueIdx = 0; 561 updateSessionQueueState(); 562 } 563 } 564 565 @Override onPlayFromMediaId(String mediaId, Bundle extras)566 public void onPlayFromMediaId(String mediaId, Bundle extras) { 567 super.onPlayFromMediaId(mediaId, extras); 568 if (Log.isLoggable(TAG, Log.DEBUG)) { 569 Log.d(TAG, "onPlayFromMediaId mediaId" + mediaId + " extras=" + extras); 570 } 571 572 requestAudioFocus(() -> startPlayback(mediaId)); 573 } 574 575 @Override onSkipToNext()576 public void onSkipToNext() { 577 if (Log.isLoggable(TAG, Log.DEBUG)) { 578 Log.d(TAG, "onSkipToNext()"); 579 } 580 safeAdvance(); 581 } 582 583 @Override onSkipToPrevious()584 public void onSkipToPrevious() { 585 if (Log.isLoggable(TAG, Log.DEBUG)) { 586 Log.d(TAG, "onSkipToPrevious()"); 587 } 588 safeRetreat(); 589 } 590 591 @Override onSkipToQueueItem(long id)592 public void onSkipToQueueItem(long id) { 593 try { 594 mCurrentQueueIdx = (int) id; 595 playCurrentQueueIndex(); 596 } catch (IOException e) { 597 Log.e(TAG, "Failed to play.", e); 598 mSession.setPlaybackState(mErrorState); 599 } 600 } 601 602 @Override onCustomAction(String action, Bundle extras)603 public void onCustomAction(String action, Bundle extras) { 604 switch (action) { 605 case SHUFFLE: 606 shuffle(); 607 break; 608 default: 609 Log.e(TAG, "Unhandled custom action: " + action); 610 } 611 } 612 613 private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() { 614 @Override 615 public void onAudioFocusChange(int focus) { 616 switch (focus) { 617 case AudioManager.AUDIOFOCUS_GAIN: 618 resumePlayback(); 619 break; 620 case AudioManager.AUDIOFOCUS_LOSS: 621 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 622 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 623 pausePlayback(); 624 break; 625 default: 626 Log.e(TAG, "Unhandled audio focus type: " + focus); 627 } 628 } 629 }; 630 631 private OnCompletionListener mOnCompletionListener = new OnCompletionListener() { 632 @Override 633 public void onCompletion(MediaPlayer mediaPlayer) { 634 if (Log.isLoggable(TAG, Log.DEBUG)) { 635 Log.d(TAG, "onCompletion()"); 636 } 637 safeAdvance(); 638 } 639 }; 640 } 641