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 android.media;
18
19import android.content.Context;
20import android.net.Uri;
21import android.os.PowerManager;
22import android.os.SystemClock;
23import android.util.Log;
24
25import java.io.IOException;
26import java.lang.IllegalStateException;
27import java.util.LinkedList;
28
29/**
30 * Plays a series of audio URIs, but does all the hard work on another thread
31 * so that any slowness with preparing or loading doesn't block the calling thread.
32 */
33public class AsyncPlayer {
34    private static final int PLAY = 1;
35    private static final int STOP = 2;
36    private static final boolean mDebug = false;
37
38    private static final class Command {
39        int code;
40        Context context;
41        Uri uri;
42        boolean looping;
43        int stream;
44        long requestTime;
45
46        public String toString() {
47            return "{ code=" + code + " looping=" + looping + " stream=" + stream
48                    + " uri=" + uri + " }";
49        }
50    }
51
52    private final LinkedList<Command> mCmdQueue = new LinkedList();
53
54    private void startSound(Command cmd) {
55        // Preparing can be slow, so if there is something else
56        // is playing, let it continue until we're done, so there
57        // is less of a glitch.
58        try {
59            if (mDebug) Log.d(mTag, "Starting playback");
60            MediaPlayer player = new MediaPlayer();
61            player.setAudioStreamType(cmd.stream);
62            player.setDataSource(cmd.context, cmd.uri);
63            player.setLooping(cmd.looping);
64            player.prepare();
65            player.start();
66            if (mPlayer != null) {
67                mPlayer.release();
68            }
69            mPlayer = player;
70            long delay = SystemClock.uptimeMillis() - cmd.requestTime;
71            if (delay > 1000) {
72                Log.w(mTag, "Notification sound delayed by " + delay + "msecs");
73            }
74        }
75        catch (Exception e) {
76            Log.w(mTag, "error loading sound for " + cmd.uri, e);
77        }
78    }
79
80    private final class Thread extends java.lang.Thread {
81        Thread() {
82            super("AsyncPlayer-" + mTag);
83        }
84
85        public void run() {
86            while (true) {
87                Command cmd = null;
88
89                synchronized (mCmdQueue) {
90                    if (mDebug) Log.d(mTag, "RemoveFirst");
91                    cmd = mCmdQueue.removeFirst();
92                }
93
94                switch (cmd.code) {
95                case PLAY:
96                    if (mDebug) Log.d(mTag, "PLAY");
97                    startSound(cmd);
98                    break;
99                case STOP:
100                    if (mDebug) Log.d(mTag, "STOP");
101                    if (mPlayer != null) {
102                        long delay = SystemClock.uptimeMillis() - cmd.requestTime;
103                        if (delay > 1000) {
104                            Log.w(mTag, "Notification stop delayed by " + delay + "msecs");
105                        }
106                        mPlayer.stop();
107                        mPlayer.release();
108                        mPlayer = null;
109                    } else {
110                        Log.w(mTag, "STOP command without a player");
111                    }
112                    break;
113                }
114
115                synchronized (mCmdQueue) {
116                    if (mCmdQueue.size() == 0) {
117                        // nothing left to do, quit
118                        // doing this check after we're done prevents the case where they
119                        // added it during the operation from spawning two threads and
120                        // trying to do them in parallel.
121                        mThread = null;
122                        releaseWakeLock();
123                        return;
124                    }
125                }
126            }
127        }
128    }
129
130    private String mTag;
131    private Thread mThread;
132    private MediaPlayer mPlayer;
133    private PowerManager.WakeLock mWakeLock;
134
135    // The current state according to the caller.  Reality lags behind
136    // because of the asynchronous nature of this class.
137    private int mState = STOP;
138
139    /**
140     * Construct an AsyncPlayer object.
141     *
142     * @param tag a string to use for debugging
143     */
144    public AsyncPlayer(String tag) {
145        if (tag != null) {
146            mTag = tag;
147        } else {
148            mTag = "AsyncPlayer";
149        }
150    }
151
152    /**
153     * Start playing the sound.  It will actually start playing at some
154     * point in the future.  There are no guarantees about latency here.
155     * Calling this before another audio file is done playing will stop
156     * that one and start the new one.
157     *
158     * @param context Your application's context.
159     * @param uri The URI to play.  (see {@link MediaPlayer#setDataSource(Context, Uri)})
160     * @param looping Whether the audio should loop forever.
161     *          (see {@link MediaPlayer#setLooping(boolean)})
162     * @param stream the AudioStream to use.
163     *          (see {@link MediaPlayer#setAudioStreamType(int)})
164     */
165    public void play(Context context, Uri uri, boolean looping, int stream) {
166        Command cmd = new Command();
167        cmd.requestTime = SystemClock.uptimeMillis();
168        cmd.code = PLAY;
169        cmd.context = context;
170        cmd.uri = uri;
171        cmd.looping = looping;
172        cmd.stream = stream;
173        synchronized (mCmdQueue) {
174            enqueueLocked(cmd);
175            mState = PLAY;
176        }
177    }
178
179    /**
180     * Stop a previously played sound.  It can't be played again or unpaused
181     * at this point.  Calling this multiple times has no ill effects.
182     */
183    public void stop() {
184        synchronized (mCmdQueue) {
185            // This check allows stop to be called multiple times without starting
186            // a thread that ends up doing nothing.
187            if (mState != STOP) {
188                Command cmd = new Command();
189                cmd.requestTime = SystemClock.uptimeMillis();
190                cmd.code = STOP;
191                enqueueLocked(cmd);
192                mState = STOP;
193            }
194        }
195    }
196
197    private void enqueueLocked(Command cmd) {
198        mCmdQueue.add(cmd);
199        if (mThread == null) {
200            acquireWakeLock();
201            mThread = new Thread();
202            mThread.start();
203        }
204    }
205
206    /**
207     * We want to hold a wake lock while we do the prepare and play.  The stop probably is
208     * optional, but it won't hurt to have it too.  The problem is that if you start a sound
209     * while you're holding a wake lock (e.g. an alarm starting a notification), you want the
210     * sound to play, but if the CPU turns off before mThread gets to work, it won't.  The
211     * simplest way to deal with this is to make it so there is a wake lock held while the
212     * thread is starting or running.  You're going to need the WAKE_LOCK permission if you're
213     * going to call this.
214     *
215     * This must be called before the first time play is called.
216     *
217     * @hide
218     */
219    public void setUsesWakeLock(Context context) {
220        if (mWakeLock != null || mThread != null) {
221            // if either of these has happened, we've already played something.
222            // and our releases will be out of sync.
223            throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock
224                    + " mThread=" + mThread);
225        }
226        PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
227        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
228    }
229
230    private void acquireWakeLock() {
231        if (mWakeLock != null) {
232            mWakeLock.acquire();
233        }
234    }
235
236    private void releaseWakeLock() {
237        if (mWakeLock != null) {
238            mWakeLock.release();
239        }
240    }
241}
242
243