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
17package com.android.systemui.media;
18
19import android.content.Context;
20import android.media.AudioAttributes;
21import android.media.AudioManager;
22import android.media.MediaPlayer;
23import android.media.MediaPlayer.OnCompletionListener;
24import android.media.MediaPlayer.OnErrorListener;
25import android.media.PlayerBase;
26import android.net.Uri;
27import android.os.Looper;
28import android.os.PowerManager;
29import android.os.SystemClock;
30import android.util.Log;
31
32import 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 */
41public 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
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;
72        public CreationAndCompletionThread(Command cmd) {
73            super();
74            mCmd = cmd;
75        }
76
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
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 {
182        CmdThread() {
183            super("NotificationPlayer-" + mTag);
184        }
185
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
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
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     */
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
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     */
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     */
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
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     */
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
406    private void acquireWakeLock() {
407        if (mWakeLock != null) {
408            mWakeLock.acquire();
409        }
410    }
411
412    private void releaseWakeLock() {
413        if (mWakeLock != null) {
414            mWakeLock.release();
415        }
416    }
417}
418