1/* 2 * Copyright (C) 2015 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 com.android.messaging.util; 18 19import android.content.Context; 20import android.media.AudioManager; 21import android.media.MediaPlayer; 22import android.media.MediaPlayer.OnCompletionListener; 23import android.net.Uri; 24import android.os.Looper; 25import android.os.PowerManager; 26import android.os.SystemClock; 27 28import com.android.messaging.Factory; 29 30import java.util.LinkedList; 31 32/** 33 * This class is provides the same interface and functionality as android.media.AsyncPlayer 34 * with the following differences: 35 * - whenever audio is played, audio focus is requested, 36 * - whenever audio playback is stopped or the playback completed, audio focus is abandoned. 37 * 38 * This file has been copied from com.android.server.NotificationPlayer. The only modification is 39 * the addition of a volume parameter. Hopefully the framework will adapt AsyncPlayer to support 40 * all the functionality in this class, at which point this one can be deleted. 41 */ 42public class NotificationPlayer implements OnCompletionListener { 43 private static final int PLAY = 1; 44 private static final int STOP = 2; 45 private static final boolean mDebug = false; 46 47 private static final class Command { 48 int code; 49 Uri uri; 50 boolean looping; 51 int stream; 52 float volume; 53 long requestTime; 54 boolean releaseFocus; 55 56 @Override 57 public String toString() { 58 return "{ code=" + code + " looping=" + looping + " stream=" + stream 59 + " uri=" + uri + " }"; 60 } 61 } 62 63 private final LinkedList<Command> mCmdQueue = new LinkedList<Command>(); 64 65 private Looper mLooper; 66 67 /* 68 * Besides the use of audio focus, the only implementation difference between AsyncPlayer and 69 * NotificationPlayer resides in the creation of the MediaPlayer. For the completion callback, 70 * OnCompletionListener, to be called at the end of the playback, the MediaPlayer needs to 71 * be created with a looper running so its event handler is not null. 72 */ 73 private final class CreationAndCompletionThread extends Thread { 74 public Command mCmd; 75 public CreationAndCompletionThread(final Command cmd) { 76 super(); 77 mCmd = cmd; 78 } 79 80 @Override 81 public void run() { 82 Looper.prepare(); 83 mLooper = Looper.myLooper(); 84 synchronized (this) { 85 final AudioManager audioManager = 86 (AudioManager) Factory.get().getApplicationContext() 87 .getSystemService(Context.AUDIO_SERVICE); 88 try { 89 final MediaPlayer player = new MediaPlayer(); 90 player.setAudioStreamType(mCmd.stream); 91 player.setDataSource(Factory.get().getApplicationContext(), mCmd.uri); 92 player.setLooping(mCmd.looping); 93 player.setVolume(mCmd.volume, mCmd.volume); 94 player.prepare(); 95 if ((mCmd.uri != null) && (mCmd.uri.getEncodedPath() != null) 96 && (mCmd.uri.getEncodedPath().length() > 0)) { 97 audioManager.requestAudioFocus(null, mCmd.stream, 98 mCmd.looping ? AudioManager.AUDIOFOCUS_GAIN_TRANSIENT 99 : AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); 100 } 101 player.setOnCompletionListener(NotificationPlayer.this); 102 player.start(); 103 if (mPlayer != null) { 104 mPlayer.release(); 105 } 106 mPlayer = player; 107 } catch (final Exception e) { 108 LogUtil.w(mTag, "error loading sound for " + mCmd.uri, e); 109 } 110 mAudioManager = audioManager; 111 this.notify(); 112 } 113 Looper.loop(); 114 } 115 } 116 117 private void startSound(final Command cmd) { 118 // Preparing can be slow, so if there is something else 119 // is playing, let it continue until we're done, so there 120 // is less of a glitch. 121 try { 122 if (mDebug) { 123 LogUtil.d(mTag, "Starting playback"); 124 } 125 //----------------------------------- 126 // This is were we deviate from the AsyncPlayer implementation and create the 127 // MediaPlayer in a new thread with which we're synchronized 128 synchronized (mCompletionHandlingLock) { 129 // if another sound was already playing, it doesn't matter we won't get notified 130 // of the completion, since only the completion notification of the last sound 131 // matters 132 if ((mLooper != null) 133 && (mLooper.getThread().getState() != Thread.State.TERMINATED)) { 134 mLooper.quit(); 135 } 136 mCompletionThread = new CreationAndCompletionThread(cmd); 137 synchronized (mCompletionThread) { 138 mCompletionThread.start(); 139 mCompletionThread.wait(); 140 } 141 } 142 //----------------------------------- 143 144 final long delay = SystemClock.elapsedRealtime() - cmd.requestTime; 145 if (delay > 1000) { 146 LogUtil.w(mTag, "Notification sound delayed by " + delay + "msecs"); 147 } 148 } catch (final Exception e) { 149 LogUtil.w(mTag, "error loading sound for " + cmd.uri, e); 150 } 151 } 152 153 private void stopSound(final Command cmd) { 154 if (mPlayer == null) { 155 return; 156 } 157 final long delay = SystemClock.elapsedRealtime() - cmd.requestTime; 158 if (delay > 1000) { 159 LogUtil.w(mTag, "Notification stop delayed by " + delay + "msecs"); 160 } 161 mPlayer.stop(); 162 mPlayer.release(); 163 mPlayer = null; 164 if (cmd.releaseFocus && mAudioManager != null) { 165 mAudioManager.abandonAudioFocus(null); 166 } 167 mAudioManager = null; 168 if ((mLooper != null) && (mLooper.getThread().getState() != Thread.State.TERMINATED)) { 169 mLooper.quit(); 170 } 171 } 172 173 private final class CmdThread extends java.lang.Thread { 174 CmdThread() { 175 super("NotificationPlayer-" + mTag); 176 } 177 178 @Override 179 public void run() { 180 while (true) { 181 Command cmd = null; 182 183 synchronized (mCmdQueue) { 184 if (mDebug) { 185 LogUtil.d(mTag, "RemoveFirst"); 186 } 187 cmd = mCmdQueue.removeFirst(); 188 } 189 190 switch (cmd.code) { 191 case PLAY: 192 if (mDebug) { 193 LogUtil.d(mTag, "PLAY"); 194 } 195 startSound(cmd); 196 break; 197 case STOP: 198 if (mDebug) { 199 LogUtil.d(mTag, "STOP"); 200 } 201 stopSound(cmd); 202 break; 203 } 204 205 synchronized (mCmdQueue) { 206 if (mCmdQueue.size() == 0) { 207 // nothing left to do, quit 208 // doing this check after we're done prevents the case where they 209 // added it during the operation from spawning two threads and 210 // trying to do them in parallel. 211 mThread = null; 212 releaseWakeLock(); 213 return; 214 } 215 } 216 } 217 } 218 } 219 220 @Override 221 public void onCompletion(final MediaPlayer mp) { 222 if (mAudioManager != null) { 223 mAudioManager.abandonAudioFocus(null); 224 } 225 // if there are no more sounds to play, end the Looper to listen for media completion 226 synchronized (mCmdQueue) { 227 if (mCmdQueue.size() == 0) { 228 synchronized (mCompletionHandlingLock) { 229 if (mLooper != null) { 230 mLooper.quit(); 231 } 232 mCompletionThread = null; 233 } 234 } 235 } 236 } 237 238 private String mTag; 239 private CmdThread mThread; 240 private CreationAndCompletionThread mCompletionThread; 241 private final Object mCompletionHandlingLock = new Object(); 242 private MediaPlayer mPlayer; 243 private PowerManager.WakeLock mWakeLock; 244 private AudioManager mAudioManager; 245 246 // The current state according to the caller. Reality lags behind 247 // because of the asynchronous nature of this class. 248 private int mState = STOP; 249 250 /** 251 * Construct a NotificationPlayer object. 252 * 253 * @param tag a string to use for debugging 254 */ 255 public NotificationPlayer(final String tag) { 256 if (tag != null) { 257 mTag = tag; 258 } else { 259 mTag = "NotificationPlayer"; 260 } 261 } 262 263 /** 264 * Start playing the sound. It will actually start playing at some 265 * point in the future. There are no guarantees about latency here. 266 * Calling this before another audio file is done playing will stop 267 * that one and start the new one. 268 * 269 * @param uri The URI to play. (see {@link MediaPlayer#setDataSource(Context, Uri)}) 270 * @param looping Whether the audio should loop forever. 271 * (see {@link MediaPlayer#setLooping(boolean)}) 272 * @param stream the AudioStream to use. 273 * (see {@link MediaPlayer#setAudioStreamType(int)}) 274 * @param volume The volume at which to play this sound, as a fraction of the system volume for 275 * the relevant stream type. A value of 1 is the maximum and means play at the system 276 * volume with no attenuation. 277 */ 278 public void play(final Uri uri, final boolean looping, final int stream, final float volume) { 279 final Command cmd = new Command(); 280 cmd.requestTime = SystemClock.elapsedRealtime(); 281 cmd.code = PLAY; 282 cmd.uri = uri; 283 cmd.looping = looping; 284 cmd.stream = stream; 285 cmd.volume = volume; 286 synchronized (mCmdQueue) { 287 enqueueLocked(cmd); 288 mState = PLAY; 289 } 290 } 291 292 /** Same as calling stop(true) */ 293 public void stop() { 294 stop(true); 295 } 296 297 /** 298 * Stop a previously played sound. It can't be played again or unpaused 299 * at this point. Calling this multiple times has no ill effects. 300 * @param releaseAudioFocus whether to release audio focus 301 */ 302 public void stop(final boolean releaseAudioFocus) { 303 synchronized (mCmdQueue) { 304 // This check allows stop to be called multiple times without starting 305 // a thread that ends up doing nothing. 306 if (mState != STOP) { 307 final Command cmd = new Command(); 308 cmd.requestTime = SystemClock.elapsedRealtime(); 309 cmd.code = STOP; 310 cmd.releaseFocus = releaseAudioFocus; 311 enqueueLocked(cmd); 312 mState = STOP; 313 } 314 } 315 } 316 317 private void enqueueLocked(final Command cmd) { 318 mCmdQueue.add(cmd); 319 if (mThread == null) { 320 acquireWakeLock(); 321 mThread = new CmdThread(); 322 mThread.start(); 323 } 324 } 325 326 /** 327 * We want to hold a wake lock while we do the prepare and play. The stop probably is 328 * optional, but it won't hurt to have it too. The problem is that if you start a sound 329 * while you're holding a wake lock (e.g. an alarm starting a notification), you want the 330 * sound to play, but if the CPU turns off before mThread gets to work, it won't. The 331 * simplest way to deal with this is to make it so there is a wake lock held while the 332 * thread is starting or running. You're going to need the WAKE_LOCK permission if you're 333 * going to call this. 334 * 335 * This must be called before the first time play is called. 336 * 337 * @hide 338 */ 339 public void setUsesWakeLock() { 340 if (mWakeLock != null || mThread != null) { 341 // if either of these has happened, we've already played something. 342 // and our releases will be out of sync. 343 throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock 344 + " mThread=" + mThread); 345 } 346 final PowerManager pm = (PowerManager) Factory.get().getApplicationContext() 347 .getSystemService(Context.POWER_SERVICE); 348 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag); 349 } 350 351 private void acquireWakeLock() { 352 if (mWakeLock != null) { 353 mWakeLock.acquire(); 354 } 355 } 356 357 private void releaseWakeLock() { 358 if (mWakeLock != null) { 359 mWakeLock.release(); 360 } 361 } 362} 363 364