Home | History | Annotate | Download | only in media
      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.content.Context;
     21 import android.media.PlayerBase;
     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 
     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     public void setUsesWakeLock(Context context) {
    252         if (mWakeLock != null || mThread != null) {
    253             // if either of these has happened, we've already played something.
    254             // and our releases will be out of sync.
    255             throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock
    256                     + " mThread=" + mThread);
    257         }
    258         PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
    259         mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
    260     }
    261 
    262     private void acquireWakeLock() {
    263         if (mWakeLock != null) {
    264             mWakeLock.acquire();
    265         }
    266     }
    267 
    268     private void releaseWakeLock() {
    269         if (mWakeLock != null) {
    270             mWakeLock.release();
    271         }
    272     }
    273 }
    274 
    275