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