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