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