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