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