Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2008 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.android.systemui.media;
     18 
     19 import android.content.Context;
     20 import android.media.AudioAttributes;
     21 import android.media.AudioManager;
     22 import android.media.MediaPlayer;
     23 import android.media.MediaPlayer.OnCompletionListener;
     24 import android.media.MediaPlayer.OnErrorListener;
     25 import android.media.PlayerBase;
     26 import android.net.Uri;
     27 import android.os.Looper;
     28 import android.os.PowerManager;
     29 import android.os.SystemClock;
     30 import android.util.Log;
     31 
     32 import com.android.internal.annotations.GuardedBy;
     33 
     34 import java.util.LinkedList;
     35 
     36 /**
     37  * @hide
     38  * This class is provides the same interface and functionality as android.media.AsyncPlayer
     39  * with the following differences:
     40  * - whenever audio is played, audio focus is requested,
     41  * - whenever audio playback is stopped or the playback completed, audio focus is abandoned.
     42  */
     43 public class NotificationPlayer implements OnCompletionListener, OnErrorListener {
     44     private static final int PLAY = 1;
     45     private static final int STOP = 2;
     46     private static final boolean DEBUG = false;
     47 
     48     private static final class Command {
     49         int code;
     50         Context context;
     51         Uri uri;
     52         boolean looping;
     53         AudioAttributes attributes;
     54         long requestTime;
     55 
     56         public String toString() {
     57             return "{ code=" + code + " looping=" + looping + " attributes=" + attributes
     58                     + " uri=" + uri + " }";
     59         }
     60     }
     61 
     62     private final LinkedList<Command> mCmdQueue = new LinkedList<Command>();
     63 
     64     private final Object mCompletionHandlingLock = new Object();
     65     @GuardedBy("mCompletionHandlingLock")
     66     private CreationAndCompletionThread mCompletionThread;
     67     @GuardedBy("mCompletionHandlingLock")
     68     private Looper mLooper;
     69 
     70     /*
     71      * Besides the use of audio focus, the only implementation difference between AsyncPlayer and
     72      * NotificationPlayer resides in the creation of the MediaPlayer. For the completion callback,
     73      * OnCompletionListener, to be called at the end of the playback, the MediaPlayer needs to
     74      * be created with a looper running so its event handler is not null.
     75      */
     76     private final class CreationAndCompletionThread extends Thread {
     77         public Command mCmd;
     78         public CreationAndCompletionThread(Command cmd) {
     79             super();
     80             mCmd = cmd;
     81         }
     82 
     83         public void run() {
     84             Looper.prepare();
     85             // ok to modify mLooper as here we are
     86             // synchronized on mCompletionHandlingLock due to the Object.wait() in startSound(cmd)
     87             mLooper = Looper.myLooper();
     88             if (DEBUG) Log.d(mTag, "in run: new looper " + mLooper);
     89             MediaPlayer player = null;
     90             synchronized(this) {
     91                 AudioManager audioManager =
     92                     (AudioManager) mCmd.context.getSystemService(Context.AUDIO_SERVICE);
     93                 try {
     94                     player = new MediaPlayer();
     95                     if (mCmd.attributes == null) {
     96                         mCmd.attributes = new AudioAttributes.Builder()
     97                                 .setUsage(AudioAttributes.USAGE_NOTIFICATION)
     98                                 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
     99                                 .build();
    100                     }
    101                     player.setAudioAttributes(mCmd.attributes);
    102                     player.setDataSource(mCmd.context, mCmd.uri);
    103                     player.setLooping(mCmd.looping);
    104                     player.setOnCompletionListener(NotificationPlayer.this);
    105                     player.setOnErrorListener(NotificationPlayer.this);
    106                     player.prepare();
    107                     if ((mCmd.uri != null) && (mCmd.uri.getEncodedPath() != null)
    108                             && (mCmd.uri.getEncodedPath().length() > 0)) {
    109                         if (!audioManager.isMusicActiveRemotely()) {
    110                             synchronized (mQueueAudioFocusLock) {
    111                                 if (mAudioManagerWithAudioFocus == null) {
    112                                     if (DEBUG) Log.d(mTag, "requesting AudioFocus");
    113                                     int focusGain = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
    114                                     if (mCmd.looping) {
    115                                         focusGain = AudioManager.AUDIOFOCUS_GAIN;
    116                                     }
    117                                     mNotificationRampTimeMs = audioManager.getFocusRampTimeMs(
    118                                             focusGain, mCmd.attributes);
    119                                     audioManager.requestAudioFocus(null, mCmd.attributes,
    120                                                 focusGain, 0);
    121                                     mAudioManagerWithAudioFocus = audioManager;
    122                                 } else {
    123                                     if (DEBUG) Log.d(mTag, "AudioFocus was previously requested");
    124                                 }
    125                             }
    126                         }
    127                     }
    128                     // FIXME Having to start a new thread so we can receive completion callbacks
    129                     //  is wrong, as we kill this thread whenever a new sound is to be played. This
    130                     //  can lead to AudioFocus being released too early, before the second sound is
    131                     //  done playing. This class should be modified to use a single thread, on which
    132                     //  command are issued, and on which it receives the completion callbacks.
    133                     if (DEBUG)  { Log.d(mTag, "notification will be delayed by "
    134                             + mNotificationRampTimeMs + "ms"); }
    135                     try {
    136                         Thread.sleep(mNotificationRampTimeMs);
    137                     } catch (InterruptedException e) {
    138                         Log.e(mTag, "Exception while sleeping to sync notification playback"
    139                                 + " with ducking", e);
    140                     }
    141                     player.start();
    142                     if (DEBUG) { Log.d(mTag, "player.start"); }
    143                 } catch (Exception e) {
    144                     if (player != null) {
    145                         player.release();
    146                         player = null;
    147                     }
    148                     Log.w(mTag, "error loading sound for " + mCmd.uri, e);
    149                     // playing the notification didn't work, revert the focus request
    150                     abandonAudioFocusAfterError();
    151                 }
    152                 final MediaPlayer mp;
    153                 synchronized (mPlayerLock) {
    154                     mp = mPlayer;
    155                     mPlayer = player;
    156                 }
    157                 if (mp != null) {
    158                     if (DEBUG) { Log.d(mTag, "mPlayer.release"); }
    159                     mp.release();
    160                 }
    161                 this.notify();
    162             }
    163             Looper.loop();
    164         }
    165     };
    166 
    167     private void abandonAudioFocusAfterError() {
    168         synchronized (mQueueAudioFocusLock) {
    169             if (mAudioManagerWithAudioFocus != null) {
    170                 if (DEBUG) Log.d(mTag, "abandoning focus after playback error");
    171                 mAudioManagerWithAudioFocus.abandonAudioFocus(null);
    172                 mAudioManagerWithAudioFocus = null;
    173             }
    174         }
    175     }
    176 
    177     private void startSound(Command cmd) {
    178         // Preparing can be slow, so if there is something else
    179         // is playing, let it continue until we're done, so there
    180         // is less of a glitch.
    181         try {
    182             if (DEBUG) { Log.d(mTag, "startSound()"); }
    183             //-----------------------------------
    184             // This is were we deviate from the AsyncPlayer implementation and create the
    185             // MediaPlayer in a new thread with which we're synchronized
    186             synchronized(mCompletionHandlingLock) {
    187                 // if another sound was already playing, it doesn't matter we won't get notified
    188                 // of the completion, since only the completion notification of the last sound
    189                 // matters
    190                 if((mLooper != null)
    191                         && (mLooper.getThread().getState() != Thread.State.TERMINATED)) {
    192                     if (DEBUG) { Log.d(mTag, "in startSound quitting looper " + mLooper); }
    193                     mLooper.quit();
    194                 }
    195                 mCompletionThread = new CreationAndCompletionThread(cmd);
    196                 synchronized (mCompletionThread) {
    197                     mCompletionThread.start();
    198                     mCompletionThread.wait();
    199                 }
    200             }
    201             //-----------------------------------
    202 
    203             long delay = SystemClock.uptimeMillis() - cmd.requestTime;
    204             if (delay > 1000) {
    205                 Log.w(mTag, "Notification sound delayed by " + delay + "msecs");
    206             }
    207         }
    208         catch (Exception e) {
    209             Log.w(mTag, "error loading sound for " + cmd.uri, e);
    210         }
    211     }
    212 
    213     private final class CmdThread extends java.lang.Thread {
    214         CmdThread() {
    215             super("NotificationPlayer-" + mTag);
    216         }
    217 
    218         public void run() {
    219             while (true) {
    220                 Command cmd = null;
    221 
    222                 synchronized (mCmdQueue) {
    223                     if (DEBUG) Log.d(mTag, "RemoveFirst");
    224                     cmd = mCmdQueue.removeFirst();
    225                 }
    226 
    227                 switch (cmd.code) {
    228                 case PLAY:
    229                     if (DEBUG) Log.d(mTag, "PLAY");
    230                     startSound(cmd);
    231                     break;
    232                 case STOP:
    233                     if (DEBUG) Log.d(mTag, "STOP");
    234                     final MediaPlayer mp;
    235                     synchronized (mPlayerLock) {
    236                         mp = mPlayer;
    237                         mPlayer = null;
    238                     }
    239                     if (mp != null) {
    240                         long delay = SystemClock.uptimeMillis() - cmd.requestTime;
    241                         if (delay > 1000) {
    242                             Log.w(mTag, "Notification stop delayed by " + delay + "msecs");
    243                         }
    244                         try {
    245                             mp.stop();
    246                         } catch (Exception e) { }
    247                         mp.release();
    248                         synchronized(mQueueAudioFocusLock) {
    249                             if (mAudioManagerWithAudioFocus != null) {
    250                                 if (DEBUG) { Log.d(mTag, "in STOP: abandonning AudioFocus"); }
    251                                 mAudioManagerWithAudioFocus.abandonAudioFocus(null);
    252                                 mAudioManagerWithAudioFocus = null;
    253                             }
    254                         }
    255                         synchronized (mCompletionHandlingLock) {
    256                             if ((mLooper != null) &&
    257                                     (mLooper.getThread().getState() != Thread.State.TERMINATED))
    258                             {
    259                                 if (DEBUG) { Log.d(mTag, "in STOP: quitting looper "+ mLooper); }
    260                                 mLooper.quit();
    261                             }
    262                         }
    263                     } else {
    264                         Log.w(mTag, "STOP command without a player");
    265                     }
    266                     break;
    267                 }
    268 
    269                 synchronized (mCmdQueue) {
    270                     if (mCmdQueue.size() == 0) {
    271                         // nothing left to do, quit
    272                         // doing this check after we're done prevents the case where they
    273                         // added it during the operation from spawning two threads and
    274                         // trying to do them in parallel.
    275                         mThread = null;
    276                         releaseWakeLock();
    277                         return;
    278                     }
    279                 }
    280             }
    281         }
    282     }
    283 
    284     public void onCompletion(MediaPlayer mp) {
    285         synchronized(mQueueAudioFocusLock) {
    286             if (mAudioManagerWithAudioFocus != null) {
    287                 if (DEBUG) Log.d(mTag, "onCompletion() abandonning AudioFocus");
    288                 mAudioManagerWithAudioFocus.abandonAudioFocus(null);
    289                 mAudioManagerWithAudioFocus = null;
    290             } else {
    291                 if (DEBUG) Log.d(mTag, "onCompletion() no need to abandon AudioFocus");
    292             }
    293         }
    294         // if there are no more sounds to play, end the Looper to listen for media completion
    295         synchronized (mCmdQueue) {
    296             synchronized(mCompletionHandlingLock) {
    297                 if (DEBUG) { Log.d(mTag, "onCompletion queue size=" + mCmdQueue.size()); }
    298                 if ((mCmdQueue.size() == 0)) {
    299                     if (mLooper != null) {
    300                         if (DEBUG) { Log.d(mTag, "in onCompletion quitting looper " + mLooper); }
    301                         mLooper.quit();
    302                     }
    303                     mCompletionThread = null;
    304                 }
    305             }
    306         }
    307         synchronized (mPlayerLock) {
    308             if (mp == mPlayer) {
    309                 mPlayer = null;
    310             }
    311         }
    312         if (mp != null) {
    313             mp.release();
    314         }
    315     }
    316 
    317     public boolean onError(MediaPlayer mp, int what, int extra) {
    318         Log.e(mTag, "error " + what + " (extra=" + extra + ") playing notification");
    319         // error happened, handle it just like a completion
    320         onCompletion(mp);
    321         return true;
    322     }
    323 
    324     private String mTag;
    325 
    326     @GuardedBy("mCmdQueue")
    327     private CmdThread mThread;
    328 
    329     private final Object mPlayerLock = new Object();
    330     @GuardedBy("mPlayerLock")
    331     private MediaPlayer mPlayer;
    332 
    333 
    334     @GuardedBy("mCmdQueue")
    335     private PowerManager.WakeLock mWakeLock;
    336 
    337     private final Object mQueueAudioFocusLock = new Object();
    338     @GuardedBy("mQueueAudioFocusLock")
    339     private AudioManager mAudioManagerWithAudioFocus;
    340 
    341     private int mNotificationRampTimeMs = 0;
    342 
    343     // The current state according to the caller.  Reality lags behind
    344     // because of the asynchronous nature of this class.
    345     private int mState = STOP;
    346 
    347     /**
    348      * Construct a NotificationPlayer object.
    349      *
    350      * @param tag a string to use for debugging
    351      */
    352     public NotificationPlayer(String tag) {
    353         if (tag != null) {
    354             mTag = tag;
    355         } else {
    356             mTag = "NotificationPlayer";
    357         }
    358     }
    359 
    360     /**
    361      * Start playing the sound.  It will actually start playing at some
    362      * point in the future.  There are no guarantees about latency here.
    363      * Calling this before another audio file is done playing will stop
    364      * that one and start the new one.
    365      *
    366      * @param context Your application's context.
    367      * @param uri The URI to play.  (see {@link MediaPlayer#setDataSource(Context, Uri)})
    368      * @param looping Whether the audio should loop forever.
    369      *          (see {@link MediaPlayer#setLooping(boolean)})
    370      * @param stream the AudioStream to use.
    371      *          (see {@link MediaPlayer#setAudioStreamType(int)})
    372      * @deprecated use {@link #play(Context, Uri, boolean, AudioAttributes)} instead.
    373      */
    374     @Deprecated
    375     public void play(Context context, Uri uri, boolean looping, int stream) {
    376         if (DEBUG) { Log.d(mTag, "play uri=" + uri.toString()); }
    377         PlayerBase.deprecateStreamTypeForPlayback(stream, "NotificationPlayer", "play");
    378         Command cmd = new Command();
    379         cmd.requestTime = SystemClock.uptimeMillis();
    380         cmd.code = PLAY;
    381         cmd.context = context;
    382         cmd.uri = uri;
    383         cmd.looping = looping;
    384         cmd.attributes = new AudioAttributes.Builder().setInternalLegacyStreamType(stream).build();
    385         synchronized (mCmdQueue) {
    386             enqueueLocked(cmd);
    387             mState = PLAY;
    388         }
    389     }
    390 
    391     /**
    392      * Start playing the sound.  It will actually start playing at some
    393      * point in the future.  There are no guarantees about latency here.
    394      * Calling this before another audio file is done playing will stop
    395      * that one and start the new one.
    396      *
    397      * @param context Your application's context.
    398      * @param uri The URI to play.  (see {@link MediaPlayer#setDataSource(Context, Uri)})
    399      * @param looping Whether the audio should loop forever.
    400      *          (see {@link MediaPlayer#setLooping(boolean)})
    401      * @param attributes the AudioAttributes to use.
    402      *          (see {@link MediaPlayer#setAudioAttributes(AudioAttributes)})
    403      */
    404     public void play(Context context, Uri uri, boolean looping, AudioAttributes attributes) {
    405         if (DEBUG) { Log.d(mTag, "play uri=" + uri.toString()); }
    406         Command cmd = new Command();
    407         cmd.requestTime = SystemClock.uptimeMillis();
    408         cmd.code = PLAY;
    409         cmd.context = context;
    410         cmd.uri = uri;
    411         cmd.looping = looping;
    412         cmd.attributes = attributes;
    413         synchronized (mCmdQueue) {
    414             enqueueLocked(cmd);
    415             mState = PLAY;
    416         }
    417     }
    418 
    419     /**
    420      * Stop a previously played sound.  It can't be played again or unpaused
    421      * at this point.  Calling this multiple times has no ill effects.
    422      */
    423     public void stop() {
    424         if (DEBUG) { Log.d(mTag, "stop"); }
    425         synchronized (mCmdQueue) {
    426             // This check allows stop to be called multiple times without starting
    427             // a thread that ends up doing nothing.
    428             if (mState != STOP) {
    429                 Command cmd = new Command();
    430                 cmd.requestTime = SystemClock.uptimeMillis();
    431                 cmd.code = STOP;
    432                 enqueueLocked(cmd);
    433                 mState = STOP;
    434             }
    435         }
    436     }
    437 
    438     @GuardedBy("mCmdQueue")
    439     private void enqueueLocked(Command cmd) {
    440         mCmdQueue.add(cmd);
    441         if (mThread == null) {
    442             acquireWakeLock();
    443             mThread = new CmdThread();
    444             mThread.start();
    445         }
    446     }
    447 
    448     /**
    449      * We want to hold a wake lock while we do the prepare and play.  The stop probably is
    450      * optional, but it won't hurt to have it too.  The problem is that if you start a sound
    451      * while you're holding a wake lock (e.g. an alarm starting a notification), you want the
    452      * sound to play, but if the CPU turns off before mThread gets to work, it won't.  The
    453      * simplest way to deal with this is to make it so there is a wake lock held while the
    454      * thread is starting or running.  You're going to need the WAKE_LOCK permission if you're
    455      * going to call this.
    456      *
    457      * This must be called before the first time play is called.
    458      *
    459      * @hide
    460      */
    461     public void setUsesWakeLock(Context context) {
    462         synchronized (mCmdQueue) {
    463             if (mWakeLock != null || mThread != null) {
    464                 // if either of these has happened, we've already played something.
    465                 // and our releases will be out of sync.
    466                 throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock
    467                         + " mThread=" + mThread);
    468             }
    469             PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
    470             mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
    471         }
    472     }
    473 
    474     @GuardedBy("mCmdQueue")
    475     private void acquireWakeLock() {
    476         if (mWakeLock != null) {
    477             mWakeLock.acquire();
    478         }
    479     }
    480 
    481     @GuardedBy("mCmdQueue")
    482     private void releaseWakeLock() {
    483         if (mWakeLock != null) {
    484             mWakeLock.release();
    485         }
    486     }
    487 }
    488