1 /* 2 * Copyright (C) 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.googlecode.android_scripting.facade.bluetooth.media; 18 19 import android.media.AudioAttributes; 20 import android.media.MediaMetadata; 21 import android.media.MediaMetadataRetriever; 22 import android.media.MediaPlayer; 23 import android.media.session.MediaSession; 24 import android.media.session.PlaybackState; 25 import android.net.Uri; 26 import android.os.Environment; 27 import android.os.SystemClock; 28 29 //import com.googlecode.android_scripting.R; 30 import com.googlecode.android_scripting.facade.bluetooth.BluetoothMediaFacade; 31 import com.googlecode.android_scripting.Log; 32 33 import java.io.File; 34 import java.io.IOException; 35 import java.util.ArrayList; 36 import java.util.HashMap; 37 import java.util.List; 38 39 /** 40 * This is a UI-less MediaPlayer that is used in testing Bluetooth Media related test cases. 41 * 42 * This class handles media playback commands coming from the MediaBrowserService. 43 * This is responsible for dealing with getting the media content and creating a MediaPlayer 44 * on the MediaBrowserService's MediaSession. 45 * This codepath would be exercised an an Audio source (Phone). 46 * 47 * The nested MusicProvider utility class takes care of reading the media files and maintaining 48 * the Playing Queue. It expects the media files to have been pushed to /sdcard/Music/test 49 */ 50 51 public class BluetoothMediaPlayback { 52 private MediaPlayer mMediaPlayer = null; 53 private MediaSession playbackSession = null; 54 private MusicProvider musicProvider = null; 55 private int queueIndex; 56 private static final String TAG = "BluetoothMediaPlayback"; 57 private int mState; 58 private long mCurrentPosition = 0; 59 60 // Passing in the Resources BluetoothMediaPlayback()61 public BluetoothMediaPlayback() { 62 queueIndex = 0; 63 musicProvider = new MusicProvider(); 64 mState = PlaybackState.STATE_NONE; 65 } 66 67 /** 68 * MediaPlayer Callback for Completion. Used to move to the next track. 69 */ 70 private MediaPlayer.OnCompletionListener mCompletionListener = 71 new MediaPlayer.OnCompletionListener() { 72 @Override 73 public void onCompletion(MediaPlayer player) { 74 queueIndex++; 75 // If we were playing the last item in the Queue, reset back to the first 76 // item. 77 if (queueIndex >= musicProvider.getNumberOfItemsInQueue()) { 78 queueIndex = 0; 79 } 80 mCurrentPosition = 0; 81 play(); 82 } 83 }; 84 85 /** 86 * MediaPlayer Callback for Error Handling 87 */ 88 private MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() { 89 @Override 90 public boolean onError(MediaPlayer mp, int what, int extra) { 91 Log.d(TAG + " MediaPlayer Error " + what); 92 // Release the resources 93 mMediaPlayer.stop(); 94 releaseMediaPlayer(); 95 mMediaPlayer.release(); 96 mMediaPlayer = null; 97 return false; 98 } 99 }; 100 101 /** 102 * Build & Return the AudioAtrributes for the MediaPlayer. 103 * 104 * @return {@link AudioAttributes} 105 */ createAudioAttributes(int contentType, int usage)106 private AudioAttributes createAudioAttributes(int contentType, int usage) { 107 AudioAttributes.Builder builder = new AudioAttributes.Builder(); 108 return builder.setContentType(contentType).setUsage(usage).build(); 109 } 110 111 /** 112 * Update the Current Playback State on the Media Session 113 * 114 * @param state - the state to set to. 115 */ updatePlaybackState(int state)116 private void updatePlaybackState(int state) { 117 PlaybackState.Builder stateBuilder = new PlaybackState.Builder(); 118 Log.d(TAG + " Update Playback Status Curr Posn: " + mCurrentPosition); 119 stateBuilder.setState(state, mCurrentPosition, 1.0f, SystemClock.elapsedRealtime()); 120 stateBuilder.setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE | 121 PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS); 122 playbackSession.setPlaybackState(stateBuilder.build()); 123 } 124 125 /** 126 * The core method that handles loading the media file from the raw resources 127 * and sets up and prepares the MediaPlayer to play the file. 128 * 129 * @param newTrack - the MediaMetadata to update the MediaSession with. 130 */ handlePlayMedia(MediaMetadata newTrack)131 private void handlePlayMedia(MediaMetadata newTrack) { 132 createMediaPlayerIfNeeded(); 133 // Updates the MediaBrowserService's MediaSession's metadata 134 playbackSession.setMetadata(newTrack); 135 String url = newTrack.getString(MusicProvider.CUSTOM_URL); 136 try { 137 mMediaPlayer.setDataSource( 138 BluetoothSL4AAudioSrcMBS.getAvrcpMediaBrowserService().getApplicationContext(), 139 Uri.parse(url)); 140 mMediaPlayer.prepare(); 141 } catch (IOException e) { 142 throw new RuntimeException(e); 143 } 144 Log.d(TAG + " MediaPlayer Start"); 145 mMediaPlayer.start(); 146 } 147 148 /** 149 * Sets the MediaSession to operate on 150 */ setMediaSession(MediaSession session)151 public void setMediaSession(MediaSession session) { 152 playbackSession = session; 153 } 154 155 /** 156 * Create MediaPlayer on demand if necessary. 157 * It also sets the appropriate callbacks for Completion and Error Handling 158 */ createMediaPlayerIfNeeded()159 public void createMediaPlayerIfNeeded() { 160 if (mMediaPlayer == null) { 161 mMediaPlayer = new MediaPlayer(); 162 mMediaPlayer.setOnCompletionListener(mCompletionListener); 163 mMediaPlayer.setOnErrorListener(mErrorListener); 164 } else { 165 mMediaPlayer.reset(); 166 } 167 } 168 169 /** 170 * Release the current Media Player 171 */ releaseMediaPlayer()172 public void releaseMediaPlayer() { 173 if (mMediaPlayer == null) { 174 return; 175 } 176 mMediaPlayer.reset(); 177 mMediaPlayer.release(); 178 mMediaPlayer = null; 179 } 180 181 /** 182 * Sets the Volume for the MediaSession 183 */ setVolume(float leftVolume, float rightVolume)184 public void setVolume(float leftVolume, float rightVolume) { 185 if (mMediaPlayer != null) { 186 mMediaPlayer.setVolume(leftVolume, rightVolume); 187 } 188 } 189 190 /** 191 * Gets the item to play from the MusicProvider's PlayQueue 192 * Also dispatches a "I received a Play Command" acknowledgement through the Facade. 193 */ play()194 public void play() { 195 Log.d(TAG + " play queIndex: " + queueIndex); 196 BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_PLAYING); 197 MediaMetadata newMetaData = musicProvider.getItemToPlay(queueIndex); 198 if (newMetaData == null) { 199 //Error logged in getItemToPlay already. 200 return; 201 } 202 handlePlayMedia(newMetaData); 203 updatePlaybackState(PlaybackState.STATE_PLAYING); 204 205 } 206 207 /** 208 * Gets the currently playing MediaItem to pause 209 * Also dispatches a "I received a Pause Command" acknowledgement through the Facade. 210 */ pause()211 public void pause() { 212 BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_PAUSED); 213 if (mMediaPlayer == null) { 214 Log.d(TAG + " MediaPlayer not yet created."); 215 return; 216 } 217 mMediaPlayer.pause(); 218 // Cache the current position to use when play resumes 219 mCurrentPosition = mMediaPlayer.getCurrentPosition(); 220 updatePlaybackState(PlaybackState.STATE_PAUSED); 221 } 222 223 /** 224 * Skips to the next item in the MusicProvider's PlayQueue 225 * Also dispatches a "I received a SkipNext Command" acknowledgement through the Facade 226 */ skipNext()227 public void skipNext() { 228 BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_SKIPPING_TO_NEXT); 229 queueIndex++; 230 if (queueIndex >= musicProvider.getNumberOfItemsInQueue()) { 231 queueIndex = 0; 232 } 233 Log.d(TAG + " skipNext queIndex: " + queueIndex); 234 MediaMetadata newMetaData = musicProvider.getItemToPlay(queueIndex); 235 if (newMetaData == null) { 236 //Error logged in getItemToPlay already. 237 return; 238 } 239 mCurrentPosition = 0; 240 handlePlayMedia(newMetaData); 241 242 } 243 244 /** 245 * Skips to the previous item in the MusicProvider's PlayQueue 246 * Also dispatches a "I received a SkipPrev Command" acknowledgement through the Facade. 247 */ 248 skipPrev()249 public void skipPrev() { 250 BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_SKIPPING_TO_PREVIOUS); 251 queueIndex--; 252 if (queueIndex < 0) { 253 queueIndex = 0; 254 } 255 Log.d(TAG + " skipPrev queIndex: " + queueIndex); 256 MediaMetadata newMetaData = musicProvider.getItemToPlay(queueIndex); 257 if (newMetaData == null) { 258 //Error logged in getItemToPlay already. 259 return; 260 } 261 mCurrentPosition = 0; 262 handlePlayMedia(newMetaData); 263 264 } 265 266 /** 267 * Resets and releases the MediaPlayer 268 */ 269 stop()270 public void stop() { 271 queueIndex = 0; 272 releaseMediaPlayer(); 273 updatePlaybackState(PlaybackState.STATE_STOPPED); 274 } 275 276 277 /** 278 * Utility Class to abstract retrieving and providing Playback with the appropriate MediaFile 279 * This looks for Media files used for the test to be present in /sdcard/Music/test directory 280 * It is the responsibility of the client side to push the media files to the above directory 281 * before or as part of the test. 282 */ 283 private class MusicProvider { 284 List<String> mediaFilesPath; 285 HashMap musicResources; 286 public static final String CUSTOM_URL = "__MUSIC_URL__"; 287 private static final String TAG = "BluetoothMediaMusicProvider"; 288 // The test samples for the test is expected to be in the /sdcard/Music/test directory 289 private static final String MEDIA_TEST_PATH = "/Music/test"; 290 MusicProvider()291 public MusicProvider() { 292 mediaFilesPath = new ArrayList<String>(); 293 // Get the Media file names from the Music directory 294 List<String> mediaFileNames = new ArrayList<String>(); 295 String musicPath = 296 Environment.getExternalStorageDirectory().toString() + MEDIA_TEST_PATH; 297 File musicDir = new File(musicPath); 298 if (musicDir != null) { 299 if (musicDir.listFiles() != null) { 300 for (File f : musicDir.listFiles()) { 301 if (f.isFile()) { 302 mediaFileNames.add(f.getName()); 303 } 304 } 305 } 306 musicResources = new HashMap(); 307 // Extract the metadata from the media files and build a hashmap 308 // of <filename, mediametadata> called musicResources. 309 for (String song : mediaFileNames) { 310 String songPath = musicPath + "/" + song; 311 mediaFilesPath.add(songPath); 312 Log.d(TAG + " Retrieving Meta Data for " + songPath); 313 MediaMetadata track = retrieveMetaData(songPath); 314 musicResources.put(songPath, track); 315 } 316 Log.d(TAG + "MusicProvider Num of Songs : " + mediaFilesPath.size()); 317 } else { 318 Log.e(TAG + " No media files found"); 319 } 320 } 321 322 /** 323 * Opens the Media File from the resources and retrieves the Metadata information 324 * 325 * @param song - the resource path of the file 326 * @return {@link MediaMetadata} corresponding to the media file loaded. 327 */ retrieveMetaData(String song)328 private MediaMetadata retrieveMetaData(String song) { 329 MediaMetadata.Builder newMetaData = new MediaMetadata.Builder(); 330 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 331 retriever.setDataSource( 332 BluetoothSL4AAudioSrcMBS.getAvrcpMediaBrowserService().getApplicationContext(), 333 Uri.parse(song)); 334 335 // Extract from the mediafile and build the MediaMetadata 336 newMetaData.putString(MediaMetadata.METADATA_KEY_TITLE, 337 retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)); 338 Log.d(TAG + " Retriever : " + retriever.extractMetadata( 339 MediaMetadataRetriever.METADATA_KEY_TITLE)); 340 newMetaData.putString(MediaMetadata.METADATA_KEY_ALBUM, 341 retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)); 342 newMetaData.putString(MediaMetadata.METADATA_KEY_ARTIST, 343 retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)); 344 newMetaData.putLong(MediaMetadata.METADATA_KEY_DURATION, Long.parseLong( 345 retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION))); 346 newMetaData.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, Long.parseLong( 347 retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS))); 348 //newMetaData.putLong(CUSTOM_MUSIC_PROVIDER_RESOURCE_ID, resourceId); 349 newMetaData.putString(CUSTOM_URL, song); 350 return newMetaData.build(); 351 352 } 353 354 /** 355 * Returns the MediaMetadata of the song that corresponds to the index in the Queue. 356 * 357 * @return {@link MediaMetadata} 358 */ getItemToPlay(int queueIndex)359 public MediaMetadata getItemToPlay(int queueIndex) { 360 // We have 2 data structures in this utility class - 361 // 1. A String List called mediaFilesPath - holds the file names (incl path) of the 362 // media files 363 // 2. A hashmap called musicResources that has been built where the keys are from 364 // the List mediaFilesPath above and the values are the corresponding extracted 365 // MediaMetadata. 366 // mediaFilesPath doubles up as the Playing Queue. The index that is passed here 367 // is used to retrieve the filename which is then keyed into the musicResources 368 // to return the MediaMetadata. 369 if (mediaFilesPath.size() == 0) { 370 Log.e(TAG + " No Media to play"); 371 return null; 372 } 373 String song = mediaFilesPath.get(queueIndex); 374 MediaMetadata track = (MediaMetadata) musicResources.get(song); 375 return track; 376 } 377 378 /** 379 * Number of items we have in the Play Queue 380 * 381 * @return Number of items. 382 */ getNumberOfItemsInQueue()383 public int getNumberOfItemsInQueue() { 384 return musicResources.size(); 385 } 386 387 } 388 }