Home | History | Annotate | Download | only in media
      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
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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          */
    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          */
    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          */
    383         public int getNumberOfItemsInQueue() {
    384             return musicResources.size();
    385         }
    386 
    387     }
    388 }