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), 151 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 152 Notification.Action notificationAction = new Notification.Action.Builder(iconId, 153 mContext.getString(stringId), intent) 154 .build(); 155 return notificationAction; 156 } 157 requestAudioFocus(Runnable onSuccess)158 private boolean requestAudioFocus(Runnable onSuccess) { 159 int result = mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, 160 AudioManager.AUDIOFOCUS_GAIN); 161 if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 162 onSuccess.run(); 163 return true; 164 } 165 Log.e(TAG, "Failed to acquire audio focus"); 166 return false; 167 } 168 169 @Override onPlay()170 public void onPlay() { 171 super.onPlay(); 172 if (Log.isLoggable(TAG, Log.DEBUG)) { 173 Log.d(TAG, "onPlay"); 174 } 175 // Check permissions every time we try to play 176 if (!Utils.hasRequiredPermissions(mContext)) { 177 setMissingPermissionError(); 178 } else { 179 requestAudioFocus(() -> resumePlayback()); 180 } 181 } 182 183 @Override onPause()184 public void onPause() { 185 super.onPause(); 186 if (Log.isLoggable(TAG, Log.DEBUG)) { 187 Log.d(TAG, "onPause"); 188 } 189 pausePlayback(); 190 mAudioManager.abandonAudioFocus(mAudioFocusListener); 191 } 192 destroy()193 public void destroy() { 194 stopPlayback(); 195 mNotificationManager.cancelAll(); 196 mAudioManager.abandonAudioFocus(mAudioFocusListener); 197 mMediaPlayer.release(); 198 } 199 saveState()200 public void saveState() { 201 if (mQueue == null || mQueue.isEmpty()) { 202 return; 203 } 204 205 Playlist playlist = new Playlist(); 206 playlist.songs = new Song[mQueue.size()]; 207 208 int idx = 0; 209 for (QueueItem item : mQueue) { 210 Song song = new Song(); 211 song.queueId = item.getQueueId(); 212 MediaDescription description = item.getDescription(); 213 song.mediaId = description.getMediaId(); 214 song.title = description.getTitle().toString(); 215 song.subtitle = description.getSubtitle().toString(); 216 song.path = description.getExtras().getString(DataModel.PATH_KEY); 217 218 playlist.songs[idx] = song; 219 idx++; 220 } 221 playlist.currentQueueId = mQueue.get(mCurrentQueueIdx).getQueueId(); 222 playlist.currentSongPosition = mMediaPlayer.getCurrentPosition(); 223 playlist.name = CURRENT_PLAYLIST_KEY; 224 225 // Go to Base64 to ensure that we can actually store the string in a sharedpref. This is 226 // slightly wasteful because of the fact that base64 expands the size a bit but it's a 227 // lot less riskier than abusing the java string to directly store bytes coming out of 228 // proto encoding. 229 String serialized = Base64.getEncoder().encodeToString(MessageNano.toByteArray(playlist)); 230 SharedPreferences.Editor editor = mSharedPrefs.edit(); 231 editor.putString(CURRENT_PLAYLIST_KEY, serialized); 232 editor.commit(); 233 } 234 setMissingPermissionError()235 private void setMissingPermissionError() { 236 Intent prefsIntent = new Intent(); 237 prefsIntent.setClass(mContext, PermissionsActivity.class); 238 prefsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 239 PendingIntent pendingIntent = 240 PendingIntent.getActivity(mContext, 0, prefsIntent, PendingIntent.FLAG_IMMUTABLE); 241 242 Bundle extras = new Bundle(); 243 extras.putString(Utils.ERROR_RESOLUTION_ACTION_LABEL, 244 mContext.getString(R.string.permission_error_resolve)); 245 extras.putParcelable(Utils.ERROR_RESOLUTION_ACTION_INTENT, pendingIntent); 246 247 PlaybackState state = new PlaybackState.Builder() 248 .setState(PlaybackState.STATE_ERROR, 0, 0) 249 .setErrorMessage(mContext.getString(R.string.permission_error)) 250 .setExtras(extras) 251 .build(); 252 mSession.setPlaybackState(state); 253 } 254 maybeRebuildQueue(Playlist playlist)255 private boolean maybeRebuildQueue(Playlist playlist) { 256 List<QueueItem> queue = new ArrayList<>(); 257 int foundIdx = 0; 258 // You need to check if the playlist actually is still valid because the user could have 259 // deleted files or taken out the sd card between runs so we might as well check this ahead 260 // of time before we load up the playlist. 261 for (Song song : playlist.songs) { 262 File tmp = new File(song.path); 263 if (!tmp.exists()) { 264 continue; 265 } 266 267 if (playlist.currentQueueId == song.queueId) { 268 foundIdx = queue.size(); 269 } 270 271 Bundle bundle = new Bundle(); 272 bundle.putString(DataModel.PATH_KEY, song.path); 273 MediaDescription description = new MediaDescription.Builder() 274 .setMediaId(song.mediaId) 275 .setTitle(song.title) 276 .setSubtitle(song.subtitle) 277 .setExtras(bundle) 278 .build(); 279 queue.add(new QueueItem(description, song.queueId)); 280 } 281 282 if (queue.isEmpty()) { 283 return false; 284 } 285 286 mQueue = queue; 287 mCurrentQueueIdx = foundIdx; // Resumes from beginning if last playing song was not found. 288 289 return true; 290 } 291 maybeRestoreState()292 public boolean maybeRestoreState() { 293 if (!Utils.hasRequiredPermissions(mContext)) { 294 setMissingPermissionError(); 295 return false; 296 } 297 String serialized = mSharedPrefs.getString(CURRENT_PLAYLIST_KEY, null); 298 if (serialized == null) { 299 return false; 300 } 301 302 try { 303 Playlist playlist = Playlist.parseFrom(Base64.getDecoder().decode(serialized)); 304 if (!maybeRebuildQueue(playlist)) { 305 return false; 306 } 307 updateSessionQueueState(); 308 309 requestAudioFocus(() -> { 310 try { 311 playCurrentQueueIndex(); 312 mMediaPlayer.seekTo(playlist.currentSongPosition); 313 updatePlaybackStatePlaying(); 314 } catch (IOException e) { 315 Log.e(TAG, "Restored queue, but couldn't resume playback."); 316 } 317 }); 318 } catch (IllegalArgumentException | InvalidProtocolBufferNanoException e) { 319 // Couldn't restore the playlist. Not the end of the world. 320 return false; 321 } 322 323 return true; 324 } 325 updateSessionQueueState()326 private void updateSessionQueueState() { 327 mSession.setQueueTitle(mContext.getString(R.string.playlist)); 328 mSession.setQueue(mQueue); 329 } 330 startPlayback(String key)331 private void startPlayback(String key) { 332 if (Log.isLoggable(TAG, Log.DEBUG)) { 333 Log.d(TAG, "startPlayback()"); 334 } 335 336 List<QueueItem> queue = mDataModel.getQueue(); 337 int idx = 0; 338 int foundIdx = -1; 339 for (QueueItem item : queue) { 340 if (item.getDescription().getMediaId().equals(key)) { 341 foundIdx = idx; 342 break; 343 } 344 idx++; 345 } 346 347 if (foundIdx == -1) { 348 mSession.setPlaybackState(mErrorState); 349 return; 350 } 351 352 mQueue = new ArrayList<>(queue); 353 mCurrentQueueIdx = foundIdx; 354 QueueItem current = mQueue.get(mCurrentQueueIdx); 355 String path = current.getDescription().getExtras().getString(DataModel.PATH_KEY); 356 MediaMetadata metadata = mDataModel.getMetadata(current.getDescription().getMediaId()); 357 updateSessionQueueState(); 358 359 try { 360 play(path, metadata); 361 } catch (IOException e) { 362 Log.e(TAG, "Playback failed.", e); 363 mSession.setPlaybackState(mErrorState); 364 } 365 } 366 resumePlayback()367 private void resumePlayback() { 368 if (Log.isLoggable(TAG, Log.DEBUG)) { 369 Log.d(TAG, "resumePlayback()"); 370 } 371 372 updatePlaybackStatePlaying(); 373 374 if (!mMediaPlayer.isPlaying()) { 375 mMediaPlayer.start(); 376 } 377 } 378 postMediaNotification(Notification.Builder builder)379 private void postMediaNotification(Notification.Builder builder) { 380 if (mQueue == null) { 381 return; 382 } 383 384 MediaDescription current = mQueue.get(mCurrentQueueIdx).getDescription(); 385 Notification notification = builder 386 .setStyle(new Notification.MediaStyle().setMediaSession(mSession.getSessionToken())) 387 .setContentTitle(current.getTitle()) 388 .setContentText(current.getSubtitle()) 389 .setShowWhen(false) 390 .build(); 391 notification.flags |= Notification.FLAG_NO_CLEAR; 392 mNotificationManager.notify(NOTIFICATION_ID, notification); 393 } 394 updatePlaybackStatePlaying()395 private void updatePlaybackStatePlaying() { 396 if (!mSession.isActive()) { 397 mSession.setActive(true); 398 } 399 400 // Update the state in the media session. 401 PlaybackState state = new PlaybackState.Builder() 402 .setState(PlaybackState.STATE_PLAYING, 403 mMediaPlayer.getCurrentPosition(), PLAYBACK_SPEED) 404 .setActions(PLAYING_ACTIONS) 405 .addCustomAction(mShuffle) 406 .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId()) 407 .build(); 408 mSession.setPlaybackState(state); 409 410 // Update the media styled notification. 411 postMediaNotification(mPlayingNotificationBuilder); 412 } 413 pausePlayback()414 private void pausePlayback() { 415 if (Log.isLoggable(TAG, Log.DEBUG)) { 416 Log.d(TAG, "pausePlayback()"); 417 } 418 419 long currentPosition = 0; 420 if (mMediaPlayer.isPlaying()) { 421 currentPosition = mMediaPlayer.getCurrentPosition(); 422 mMediaPlayer.pause(); 423 } 424 425 PlaybackState state = new PlaybackState.Builder() 426 .setState(PlaybackState.STATE_PAUSED, currentPosition, PLAYBACK_SPEED_STOPPED) 427 .setActions(PAUSED_ACTIONS) 428 .addCustomAction(mShuffle) 429 .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId()) 430 .build(); 431 mSession.setPlaybackState(state); 432 433 // Update the media styled notification. 434 postMediaNotification(mPausedNotificationBuilder); 435 } 436 stopPlayback()437 private void stopPlayback() { 438 if (Log.isLoggable(TAG, Log.DEBUG)) { 439 Log.d(TAG, "stopPlayback()"); 440 } 441 442 if (mMediaPlayer.isPlaying()) { 443 mMediaPlayer.stop(); 444 } 445 446 PlaybackState state = new PlaybackState.Builder() 447 .setState(PlaybackState.STATE_STOPPED, PLAYBACK_POSITION_STOPPED, 448 PLAYBACK_SPEED_STOPPED) 449 .setActions(STOPPED_ACTIONS) 450 .build(); 451 mSession.setPlaybackState(state); 452 } 453 advance()454 private void advance() throws IOException { 455 if (Log.isLoggable(TAG, Log.DEBUG)) { 456 Log.d(TAG, "advance()"); 457 } 458 // Go to the next song if one exists. Note that if you were to support gapless 459 // playback, you would have to change this code such that you had a currently 460 // playing and a loading MediaPlayer and juggled between them while also calling 461 // setNextMediaPlayer. 462 463 if (mQueue != null && !mQueue.isEmpty()) { 464 // Keep looping around when we run off the end of our current queue. 465 mCurrentQueueIdx = (mCurrentQueueIdx + 1) % mQueue.size(); 466 playCurrentQueueIndex(); 467 } else { 468 stopPlayback(); 469 } 470 } 471 retreat()472 private void retreat() throws IOException { 473 if (Log.isLoggable(TAG, Log.DEBUG)) { 474 Log.d(TAG, "retreat()"); 475 } 476 // Go to the next song if one exists. Note that if you were to support gapless 477 // playback, you would have to change this code such that you had a currently 478 // playing and a loading MediaPlayer and juggled between them while also calling 479 // setNextMediaPlayer. 480 if (mQueue != null) { 481 // Keep looping around when we run off the end of our current queue. 482 mCurrentQueueIdx--; 483 if (mCurrentQueueIdx < 0) { 484 mCurrentQueueIdx = mQueue.size() - 1; 485 } 486 playCurrentQueueIndex(); 487 } else { 488 stopPlayback(); 489 } 490 } 491 playCurrentQueueIndex()492 private void playCurrentQueueIndex() throws IOException { 493 MediaDescription next = mQueue.get(mCurrentQueueIdx).getDescription(); 494 String path = next.getExtras().getString(DataModel.PATH_KEY); 495 MediaMetadata metadata = mDataModel.getMetadata(next.getMediaId()); 496 497 play(path, metadata); 498 } 499 play(String path, MediaMetadata metadata)500 private void play(String path, MediaMetadata metadata) throws IOException { 501 if (Log.isLoggable(TAG, Log.DEBUG)) { 502 Log.d(TAG, "play path=" + path + " metadata=" + metadata); 503 } 504 505 mMediaPlayer.reset(); 506 mMediaPlayer.setDataSource(path); 507 mMediaPlayer.prepare(); 508 509 if (metadata != null) { 510 mSession.setMetadata(metadata); 511 } 512 boolean wasGrantedAudio = requestAudioFocus(() -> { 513 mMediaPlayer.start(); 514 updatePlaybackStatePlaying(); 515 }); 516 if (!wasGrantedAudio) { 517 // player.pause() isn't needed since it should not actually be playing, the 518 // other steps like, updating the notification and play state are needed, thus we 519 // call the pause method. 520 pausePlayback(); 521 } 522 } 523 safeAdvance()524 private void safeAdvance() { 525 try { 526 advance(); 527 } catch (IOException e) { 528 Log.e(TAG, "Failed to advance.", e); 529 mSession.setPlaybackState(mErrorState); 530 } 531 } 532 safeRetreat()533 private void safeRetreat() { 534 try { 535 retreat(); 536 } catch (IOException e) { 537 Log.e(TAG, "Failed to advance.", e); 538 mSession.setPlaybackState(mErrorState); 539 } 540 } 541 542 /** 543 * This is a naive implementation of shuffle, previously played songs may repeat after the 544 * shuffle operation. Only call this from the main thread. 545 */ shuffle()546 private void shuffle() { 547 if (Log.isLoggable(TAG, Log.DEBUG)) { 548 Log.d(TAG, "Shuffling"); 549 } 550 551 // rebuild the the queue in a shuffled form. 552 if (mQueue != null && mQueue.size() > 2) { 553 QueueItem current = mQueue.remove(mCurrentQueueIdx); 554 Collections.shuffle(mQueue); 555 mQueue.add(0, current); 556 // A QueueItem contains a queue id that's used as the key for when the user selects 557 // the current play list. This means the QueueItems must be rebuilt to have their new 558 // id's set. 559 for (int i = 0; i < mQueue.size(); i++) { 560 mQueue.set(i, new QueueItem(mQueue.get(i).getDescription(), i)); 561 } 562 mCurrentQueueIdx = 0; 563 updateSessionQueueState(); 564 } 565 } 566 567 @Override onPlayFromMediaId(String mediaId, Bundle extras)568 public void onPlayFromMediaId(String mediaId, Bundle extras) { 569 super.onPlayFromMediaId(mediaId, extras); 570 if (Log.isLoggable(TAG, Log.DEBUG)) { 571 Log.d(TAG, "onPlayFromMediaId mediaId" + mediaId + " extras=" + extras); 572 } 573 574 requestAudioFocus(() -> startPlayback(mediaId)); 575 } 576 577 @Override onSkipToNext()578 public void onSkipToNext() { 579 if (Log.isLoggable(TAG, Log.DEBUG)) { 580 Log.d(TAG, "onSkipToNext()"); 581 } 582 safeAdvance(); 583 } 584 585 @Override onSkipToPrevious()586 public void onSkipToPrevious() { 587 if (Log.isLoggable(TAG, Log.DEBUG)) { 588 Log.d(TAG, "onSkipToPrevious()"); 589 } 590 safeRetreat(); 591 } 592 593 @Override onSkipToQueueItem(long id)594 public void onSkipToQueueItem(long id) { 595 try { 596 mCurrentQueueIdx = (int) id; 597 playCurrentQueueIndex(); 598 } catch (IOException e) { 599 Log.e(TAG, "Failed to play.", e); 600 mSession.setPlaybackState(mErrorState); 601 } 602 } 603 604 @Override onCustomAction(String action, Bundle extras)605 public void onCustomAction(String action, Bundle extras) { 606 switch (action) { 607 case SHUFFLE: 608 shuffle(); 609 break; 610 default: 611 Log.e(TAG, "Unhandled custom action: " + action); 612 } 613 } 614 615 private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() { 616 @Override 617 public void onAudioFocusChange(int focus) { 618 switch (focus) { 619 case AudioManager.AUDIOFOCUS_GAIN: 620 resumePlayback(); 621 break; 622 case AudioManager.AUDIOFOCUS_LOSS: 623 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 624 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 625 pausePlayback(); 626 break; 627 default: 628 Log.e(TAG, "Unhandled audio focus type: " + focus); 629 } 630 } 631 }; 632 633 private OnCompletionListener mOnCompletionListener = new OnCompletionListener() { 634 @Override 635 public void onCompletion(MediaPlayer mediaPlayer) { 636 if (Log.isLoggable(TAG, Log.DEBUG)) { 637 Log.d(TAG, "onCompletion()"); 638 } 639 safeAdvance(); 640 } 641 }; 642 } 643