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