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