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 
toString()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;
CreationAndCompletionThread(Command cmd)78         public CreationAndCompletionThread(Command cmd) {
79             super();
80             mCmd = cmd;
81         }
82 
run()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 
abandonAudioFocusAfterError()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 
startSound(Command cmd)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 {
CmdThread()214         CmdThread() {
215             super("NotificationPlayer-" + mTag);
216         }
217 
run()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 
onCompletion(MediaPlayer mp)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 
onError(MediaPlayer mp, int what, int extra)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      */
NotificationPlayer(String tag)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
play(Context context, Uri uri, boolean looping, int stream)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      */
play(Context context, Uri uri, boolean looping, AudioAttributes attributes)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      */
stop()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")
enqueueLocked(Command cmd)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      */
setUsesWakeLock(Context context)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")
acquireWakeLock()475     private void acquireWakeLock() {
476         if (mWakeLock != null) {
477             mWakeLock.acquire();
478         }
479     }
480 
481     @GuardedBy("mCmdQueue")
releaseWakeLock()482     private void releaseWakeLock() {
483         if (mWakeLock != null) {
484             mWakeLock.release();
485         }
486     }
487 }
488