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