• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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