1 /* 2 * Copyright 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.mediasession.service.players; 18 19 import android.content.Context; 20 import android.content.res.AssetFileDescriptor; 21 import android.media.MediaPlayer; 22 import android.os.SystemClock; 23 import android.support.v4.media.MediaMetadataCompat; 24 import android.support.v4.media.session.PlaybackStateCompat; 25 import android.util.Log; 26 27 import com.example.android.mediasession.service.PlaybackInfoListener; 28 import com.example.android.mediasession.service.PlayerAdapter; 29 import com.example.android.mediasession.service.contentcatalogs.MusicLibrary; 30 import com.example.android.mediasession.ui.MainActivity; 31 32 /** 33 * Exposes the functionality of the {@link MediaPlayer} and implements the {@link PlayerAdapter} 34 * so that {@link MainActivity} can control music playback. 35 */ 36 public final class MediaPlayerAdapter extends PlayerAdapter { 37 38 private final Context mContext; 39 private MediaPlayer mMediaPlayer; 40 private String mFilename; 41 private PlaybackInfoListener mPlaybackInfoListener; 42 private MediaMetadataCompat mCurrentMedia; 43 private int mState; 44 private boolean mCurrentMediaPlayedToCompletion; 45 46 // Work-around for a MediaPlayer bug related to the behavior of MediaPlayer.seekTo() 47 // while not playing. 48 private int mSeekWhileNotPlaying = -1; 49 50 public MediaPlayerAdapter(Context context, PlaybackInfoListener listener) { 51 super(context); 52 mContext = context.getApplicationContext(); 53 mPlaybackInfoListener = listener; 54 } 55 56 /** 57 * Once the {@link MediaPlayer} is released, it can't be used again, and another one has to be 58 * created. In the onStop() method of the {@link MainActivity} the {@link MediaPlayer} is 59 * released. Then in the onStart() of the {@link MainActivity} a new {@link MediaPlayer} 60 * object has to be created. That's why this method is private, and called by load(int) and 61 * not the constructor. 62 */ 63 private void initializeMediaPlayer() { 64 if (mMediaPlayer == null) { 65 mMediaPlayer = new MediaPlayer(); 66 mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 67 @Override 68 public void onCompletion(MediaPlayer mediaPlayer) { 69 mPlaybackInfoListener.onPlaybackCompleted(); 70 71 // Set the state to "paused" because it most closely matches the state 72 // in MediaPlayer with regards to available state transitions compared 73 // to "stop". 74 // Paused allows: seekTo(), start(), pause(), stop() 75 // Stop allows: stop() 76 setNewState(PlaybackStateCompat.STATE_PAUSED); 77 } 78 }); 79 } 80 } 81 82 // Implements PlaybackControl. 83 @Override 84 public void playFromMedia(MediaMetadataCompat metadata) { 85 mCurrentMedia = metadata; 86 final String mediaId = metadata.getDescription().getMediaId(); 87 playFile(MusicLibrary.getMusicFilename(mediaId)); 88 } 89 90 @Override 91 public MediaMetadataCompat getCurrentMedia() { 92 return mCurrentMedia; 93 } 94 95 private void playFile(String filename) { 96 boolean mediaChanged = (mFilename == null || !filename.equals(mFilename)); 97 if (mCurrentMediaPlayedToCompletion) { 98 // Last audio file was played to completion, the resourceId hasn't changed, but the 99 // player was released, so force a reload of the media file for playback. 100 mediaChanged = true; 101 mCurrentMediaPlayedToCompletion = false; 102 } 103 if (!mediaChanged) { 104 if (!isPlaying()) { 105 play(); 106 } 107 return; 108 } else { 109 release(); 110 } 111 112 mFilename = filename; 113 114 initializeMediaPlayer(); 115 116 try { 117 AssetFileDescriptor assetFileDescriptor = mContext.getAssets().openFd(mFilename); 118 mMediaPlayer.setDataSource( 119 assetFileDescriptor.getFileDescriptor(), 120 assetFileDescriptor.getStartOffset(), 121 assetFileDescriptor.getLength()); 122 } catch (Exception e) { 123 throw new RuntimeException("Failed to open file: " + mFilename, e); 124 } 125 126 try { 127 mMediaPlayer.prepare(); 128 } catch (Exception e) { 129 throw new RuntimeException("Failed to open file: " + mFilename, e); 130 } 131 132 play(); 133 } 134 135 @Override 136 public void onStop() { 137 // Regardless of whether or not the MediaPlayer has been created / started, the state must 138 // be updated, so that MediaNotificationManager can take down the notification. 139 setNewState(PlaybackStateCompat.STATE_STOPPED); 140 release(); 141 } 142 143 private void release() { 144 if (mMediaPlayer != null) { 145 mMediaPlayer.release(); 146 mMediaPlayer = null; 147 } 148 } 149 150 @Override 151 public boolean isPlaying() { 152 return mMediaPlayer != null && mMediaPlayer.isPlaying(); 153 } 154 155 @Override 156 protected void onPlay() { 157 if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { 158 mMediaPlayer.start(); 159 setNewState(PlaybackStateCompat.STATE_PLAYING); 160 } 161 } 162 163 @Override 164 protected void onPause() { 165 if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { 166 mMediaPlayer.pause(); 167 setNewState(PlaybackStateCompat.STATE_PAUSED); 168 } 169 } 170 171 // This is the main reducer for the player state machine. 172 private void setNewState(@PlaybackStateCompat.State int newPlayerState) { 173 mState = newPlayerState; 174 175 // Whether playback goes to completion, or whether it is stopped, the 176 // mCurrentMediaPlayedToCompletion is set to true. 177 if (mState == PlaybackStateCompat.STATE_STOPPED) { 178 mCurrentMediaPlayedToCompletion = true; 179 } 180 181 // Work around for MediaPlayer.getCurrentPosition() when it changes while not playing. 182 final long reportPosition; 183 if (mSeekWhileNotPlaying >= 0) { 184 reportPosition = mSeekWhileNotPlaying; 185 186 if (mState == PlaybackStateCompat.STATE_PLAYING) { 187 mSeekWhileNotPlaying = -1; 188 } 189 } else { 190 reportPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition(); 191 } 192 193 final PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder(); 194 stateBuilder.setActions(getAvailableActions()); 195 stateBuilder.setState(mState, 196 reportPosition, 197 1.0f, 198 SystemClock.elapsedRealtime()); 199 mPlaybackInfoListener.onPlaybackStateChange(stateBuilder.build()); 200 } 201 202 /** 203 * Set the current capabilities available on this session. Note: If a capability is not 204 * listed in the bitmask of capabilities then the MediaSession will not handle it. For 205 * example, if you don't want ACTION_STOP to be handled by the MediaSession, then don't 206 * included it in the bitmask that's returned. 207 */ 208 @PlaybackStateCompat.Actions 209 private long getAvailableActions() { 210 long actions = PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID 211 | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH 212 | PlaybackStateCompat.ACTION_SKIP_TO_NEXT 213 | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; 214 switch (mState) { 215 case PlaybackStateCompat.STATE_STOPPED: 216 actions |= PlaybackStateCompat.ACTION_PLAY 217 | PlaybackStateCompat.ACTION_PAUSE; 218 break; 219 case PlaybackStateCompat.STATE_PLAYING: 220 actions |= PlaybackStateCompat.ACTION_STOP 221 | PlaybackStateCompat.ACTION_PAUSE 222 | PlaybackStateCompat.ACTION_SEEK_TO; 223 break; 224 case PlaybackStateCompat.STATE_PAUSED: 225 actions |= PlaybackStateCompat.ACTION_PLAY 226 | PlaybackStateCompat.ACTION_STOP; 227 break; 228 default: 229 actions |= PlaybackStateCompat.ACTION_PLAY 230 | PlaybackStateCompat.ACTION_PLAY_PAUSE 231 | PlaybackStateCompat.ACTION_STOP 232 | PlaybackStateCompat.ACTION_PAUSE; 233 } 234 return actions; 235 } 236 237 @Override 238 public void seekTo(long position) { 239 if (mMediaPlayer != null) { 240 if (!mMediaPlayer.isPlaying()) { 241 mSeekWhileNotPlaying = (int) position; 242 } 243 mMediaPlayer.seekTo((int) position); 244 245 // Set the state (to the current state) because the position changed and should 246 // be reported to clients. 247 setNewState(mState); 248 } 249 } 250 251 @Override 252 public void setVolume(float volume) { 253 if (mMediaPlayer != null) { 254 mMediaPlayer.setVolume(volume, volume); 255 } 256 } 257 } 258