blob: c3dc118fec7e63db7bb0aaa0abf83152e9644731 [file] [log] [blame]
Alan Viverette3da604b2020-06-10 18:34:39 +00001/*
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.compat.annotation.UnsupportedAppUsage;
21import android.content.Context;
22import android.net.Uri;
23import android.os.PowerManager;
24import android.os.SystemClock;
25import android.util.Log;
26
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 AudioAttributes attributes;
44 long requestTime;
45
46 public String toString() {
47 return "{ code=" + code + " looping=" + looping + " attr=" + attributes
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.setAudioAttributes(cmd.attributes);
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 * @deprecated use {@link #play(Context, Uri, boolean, AudioAttributes)} instead
165 */
166 public void play(Context context, Uri uri, boolean looping, int stream) {
167 PlayerBase.deprecateStreamTypeForPlayback(stream, "AsyncPlayer", "play()");
168 if (context == null || uri == null) {
169 return;
170 }
171 try {
172 play(context, uri, looping,
173 new AudioAttributes.Builder().setInternalLegacyStreamType(stream).build());
174 } catch (IllegalArgumentException e) {
175 Log.e(mTag, "Call to deprecated AsyncPlayer.play() method caused:", e);
176 }
177 }
178
179 /**
180 * Start playing the sound. It will actually start playing at some
181 * point in the future. There are no guarantees about latency here.
182 * Calling this before another audio file is done playing will stop
183 * that one and start the new one.
184 *
185 * @param context the non-null application's context.
186 * @param uri the non-null URI to play. (see {@link MediaPlayer#setDataSource(Context, Uri)})
187 * @param looping whether the audio should loop forever.
188 * (see {@link MediaPlayer#setLooping(boolean)})
189 * @param attributes the non-null {@link AudioAttributes} to use.
190 * (see {@link MediaPlayer#setAudioAttributes(AudioAttributes)})
191 * @throws IllegalArgumentException
192 */
193 public void play(@NonNull Context context, @NonNull Uri uri, boolean looping,
194 @NonNull AudioAttributes attributes) throws IllegalArgumentException {
195 if (context == null || uri == null || attributes == null) {
196 throw new IllegalArgumentException("Illegal null AsyncPlayer.play() argument");
197 }
198 Command cmd = new Command();
199 cmd.requestTime = SystemClock.uptimeMillis();
200 cmd.code = PLAY;
201 cmd.context = context;
202 cmd.uri = uri;
203 cmd.looping = looping;
204 cmd.attributes = attributes;
205 synchronized (mCmdQueue) {
206 enqueueLocked(cmd);
207 mState = PLAY;
208 }
209 }
210
211 /**
212 * Stop a previously played sound. It can't be played again or unpaused
213 * at this point. Calling this multiple times has no ill effects.
214 */
215 public void stop() {
216 synchronized (mCmdQueue) {
217 // This check allows stop to be called multiple times without starting
218 // a thread that ends up doing nothing.
219 if (mState != STOP) {
220 Command cmd = new Command();
221 cmd.requestTime = SystemClock.uptimeMillis();
222 cmd.code = STOP;
223 enqueueLocked(cmd);
224 mState = STOP;
225 }
226 }
227 }
228
229 private void enqueueLocked(Command cmd) {
230 mCmdQueue.add(cmd);
231 if (mThread == null) {
232 acquireWakeLock();
233 mThread = new Thread();
234 mThread.start();
235 }
236 }
237
238 /**
239 * We want to hold a wake lock while we do the prepare and play. The stop probably is
240 * optional, but it won't hurt to have it too. The problem is that if you start a sound
241 * while you're holding a wake lock (e.g. an alarm starting a notification), you want the
242 * sound to play, but if the CPU turns off before mThread gets to work, it won't. The
243 * simplest way to deal with this is to make it so there is a wake lock held while the
244 * thread is starting or running. You're going to need the WAKE_LOCK permission if you're
245 * going to call this.
246 *
247 * This must be called before the first time play is called.
248 *
249 * @hide
250 */
251 @UnsupportedAppUsage
252 public void setUsesWakeLock(Context context) {
253 if (mWakeLock != null || mThread != null) {
254 // if either of these has happened, we've already played something.
255 // and our releases will be out of sync.
256 throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock
257 + " mThread=" + mThread);
258 }
259 PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
260 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
261 }
262
263 private void acquireWakeLock() {
264 if (mWakeLock != null) {
265 mWakeLock.acquire();
266 }
267 }
268
269 private void releaseWakeLock() {
270 if (mWakeLock != null) {
271 mWakeLock.release();
272 }
273 }
274}
275