• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.media;
18 
19 import android.annotation.NonNull;
20 import android.compat.annotation.UnsupportedAppUsage;
21 import android.content.Context;
22 import android.net.Uri;
23 import android.os.PowerManager;
24 import android.os.SystemClock;
25 import android.util.Log;
26 
27 import 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  */
33 public 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 
toString()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 
startSound(Command cmd)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 {
Thread()81         Thread() {
82             super("AsyncPlayer-" + mTag);
83         }
84 
run()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      */
AsyncPlayer(String tag)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      */
play(Context context, Uri uri, boolean looping, int stream)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      */
play(@onNull Context context, @NonNull Uri uri, boolean looping, @NonNull AudioAttributes attributes)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      */
stop()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 
enqueueLocked(Command cmd)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
setUsesWakeLock(Context context)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 
acquireWakeLock()263     private void acquireWakeLock() {
264         if (mWakeLock != null) {
265             mWakeLock.acquire();
266         }
267     }
268 
releaseWakeLock()269     private void releaseWakeLock() {
270         if (mWakeLock != null) {
271             mWakeLock.release();
272         }
273     }
274 }
275 
276