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