diff --git a/services/java/com/android/server/NotificationManagerService.java b/services/java/com/android/server/NotificationManagerService.java index 3e1adf12c6402..44c6018e77690 100755 --- a/services/java/com/android/server/NotificationManagerService.java +++ b/services/java/com/android/server/NotificationManagerService.java @@ -39,7 +39,6 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.ContentObserver; -import android.media.AsyncPlayer; import android.media.AudioManager; import android.net.Uri; import android.os.BatteryManager; @@ -98,7 +97,7 @@ class NotificationManagerService extends INotificationManager.Stub private int mDefaultNotificationLedOff; private NotificationRecord mSoundNotification; - private AsyncPlayer mSound; + private NotificationPlayer mSound; private boolean mSystemReady; private int mDisabledNotifications; @@ -413,7 +412,7 @@ class NotificationManagerService extends INotificationManager.Stub mContext = context; mLightsService = lights; mAm = ActivityManagerNative.getDefault(); - mSound = new AsyncPlayer(TAG); + mSound = new NotificationPlayer(TAG); mSound.setUsesWakeLock(context); mToastQueue = new ArrayList(); mHandler = new WorkerHandler(); diff --git a/services/java/com/android/server/NotificationPlayer.java b/services/java/com/android/server/NotificationPlayer.java new file mode 100644 index 0000000000000..4c4da5abbcb80 --- /dev/null +++ b/services/java/com/android/server/NotificationPlayer.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.Log; + +import java.io.IOException; +import java.lang.IllegalStateException; +import java.lang.Thread; +import java.util.LinkedList; + +/** + * @hide + * This class is provides the same interface and functionality as android.media.AsyncPlayer + * with the following differences: + * - whenever audio is played, audio focus is requested, + * - whenever audio playback is stopped or the playback completed, audio focus is abandoned. + */ +public class NotificationPlayer implements OnCompletionListener { + private static final int PLAY = 1; + private static final int STOP = 2; + private static final boolean mDebug = false; + + private static final class Command { + int code; + Context context; + Uri uri; + boolean looping; + int stream; + long requestTime; + + public String toString() { + return "{ code=" + code + " looping=" + looping + " stream=" + stream + + " uri=" + uri + " }"; + } + } + + private LinkedList mCmdQueue = new LinkedList(); + + private Looper mLooper; + + /* + * Besides the use of audio focus, the only implementation difference between AsyncPlayer and + * NotificationPlayer resides in the creation of the MediaPlayer. For the completion callback, + * OnCompletionListener, to be called at the end of the playback, the MediaPlayer needs to + * be created with a looper running so its event handler is not null. + */ + private final class PlayerCreationThread extends Thread { + public Command mCmd; + public PlayerCreationThread(Command cmd) { + super(); + mCmd = cmd; + } + + public void run() { + Looper.prepare(); + mLooper = Looper.myLooper(); + synchronized(this) { + AudioManager audioManager = + (AudioManager) mCmd.context.getSystemService(Context.AUDIO_SERVICE); + try { + MediaPlayer player = new MediaPlayer(); + player.setAudioStreamType(mCmd.stream); + player.setDataSource(mCmd.context, mCmd.uri); + player.setLooping(mCmd.looping); + player.prepare(); + if (mCmd.looping) { + audioManager.requestAudioFocus(null, mCmd.stream, + AudioManager.AUDIOFOCUS_GAIN); + } else { + audioManager.requestAudioFocus(null, mCmd.stream, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); + } + player.start(); + if (mPlayer != null) { + mPlayer.release(); + } + mPlayer = player; + } + catch (Exception e) { + Log.w(mTag, "error loading sound for " + mCmd.uri, e); + } + mAudioManager = audioManager; + this.notify(); + } + Looper.loop(); + } + }; + + private void startSound(Command cmd) { + // Preparing can be slow, so if there is something else + // is playing, let it continue until we're done, so there + // is less of a glitch. + try { + if (mDebug) Log.d(mTag, "Starting playback"); + //----------------------------------- + // This is were we deviate from the AsyncPlayer implementation and create the + // MediaPlayer in a new thread with which we're synchronized + PlayerCreationThread t = new PlayerCreationThread(cmd); + synchronized(t) { + t.start(); + t.wait(); + } + mPlayer.setOnCompletionListener(this); + //----------------------------------- + + long delay = SystemClock.uptimeMillis() - cmd.requestTime; + if (delay > 1000) { + Log.w(mTag, "Notification sound delayed by " + delay + "msecs"); + } + } + catch (Exception e) { + Log.w(mTag, "error loading sound for " + cmd.uri, e); + } + } + + private final class CmdThread extends java.lang.Thread { + CmdThread() { + super("NotificationPlayer-" + mTag); + } + + public void run() { + while (true) { + Command cmd = null; + + synchronized (mCmdQueue) { + if (mDebug) Log.d(mTag, "RemoveFirst"); + cmd = mCmdQueue.removeFirst(); + } + + switch (cmd.code) { + case PLAY: + if (mDebug) Log.d(mTag, "PLAY"); + startSound(cmd); + break; + case STOP: + if (mDebug) Log.d(mTag, "STOP"); + if (mPlayer != null) { + long delay = SystemClock.uptimeMillis() - cmd.requestTime; + if (delay > 1000) { + Log.w(mTag, "Notification stop delayed by " + delay + "msecs"); + } + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + mAudioManager.abandonAudioFocus(null); + mAudioManager = null; + mLooper.quit(); + } else { + Log.w(mTag, "STOP command without a player"); + } + break; + } + + synchronized (mCmdQueue) { + if (mCmdQueue.size() == 0) { + // nothing left to do, quit + // doing this check after we're done prevents the case where they + // added it during the operation from spawning two threads and + // trying to do them in parallel. + mThread = null; + releaseWakeLock(); + return; + } + } + } + } + } + + public void onCompletion(MediaPlayer mp) { + if (mAudioManager != null) { + mAudioManager.abandonAudioFocus(null); + } + } + + private String mTag; + private CmdThread mThread; + private MediaPlayer mPlayer; + private PowerManager.WakeLock mWakeLock; + private AudioManager mAudioManager; + + // The current state according to the caller. Reality lags behind + // because of the asynchronous nature of this class. + private int mState = STOP; + + /** + * Construct a NotificationPlayer object. + * + * @param tag a string to use for debugging + */ + public NotificationPlayer(String tag) { + if (tag != null) { + mTag = tag; + } else { + mTag = "NotificationPlayer"; + } + } + + /** + * Start playing the sound. It will actually start playing at some + * point in the future. There are no guarantees about latency here. + * Calling this before another audio file is done playing will stop + * that one and start the new one. + * + * @param context Your application's context. + * @param uri The URI to play. (see {@link MediaPlayer#setDataSource(Context, Uri)}) + * @param looping Whether the audio should loop forever. + * (see {@link MediaPlayer#setLooping(boolean)}) + * @param stream the AudioStream to use. + * (see {@link MediaPlayer#setAudioStreamType(int)}) + */ + public void play(Context context, Uri uri, boolean looping, int stream) { + Command cmd = new Command(); + cmd.requestTime = SystemClock.uptimeMillis(); + cmd.code = PLAY; + cmd.context = context; + cmd.uri = uri; + cmd.looping = looping; + cmd.stream = stream; + synchronized (mCmdQueue) { + enqueueLocked(cmd); + mState = PLAY; + } + } + + /** + * Stop a previously played sound. It can't be played again or unpaused + * at this point. Calling this multiple times has no ill effects. + */ + public void stop() { + synchronized (mCmdQueue) { + // This check allows stop to be called multiple times without starting + // a thread that ends up doing nothing. + if (mState != STOP) { + Command cmd = new Command(); + cmd.requestTime = SystemClock.uptimeMillis(); + cmd.code = STOP; + enqueueLocked(cmd); + mState = STOP; + } + } + } + + private void enqueueLocked(Command cmd) { + mCmdQueue.add(cmd); + if (mThread == null) { + acquireWakeLock(); + mThread = new CmdThread(); + mThread.start(); + } + } + + /** + * We want to hold a wake lock while we do the prepare and play. The stop probably is + * optional, but it won't hurt to have it too. The problem is that if you start a sound + * while you're holding a wake lock (e.g. an alarm starting a notification), you want the + * sound to play, but if the CPU turns off before mThread gets to work, it won't. The + * simplest way to deal with this is to make it so there is a wake lock held while the + * thread is starting or running. You're going to need the WAKE_LOCK permission if you're + * going to call this. + * + * This must be called before the first time play is called. + * + * @hide + */ + public void setUsesWakeLock(Context context) { + if (mWakeLock != null || mThread != null) { + // if either of these has happened, we've already played something. + // and our releases will be out of sync. + throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock + + " mThread=" + mThread); + } + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag); + } + + private void acquireWakeLock() { + if (mWakeLock != null) { + mWakeLock.acquire(); + } + } + + private void releaseWakeLock() { + if (mWakeLock != null) { + mWakeLock.release(); + } + } +} +