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