Migrate ringtone playback to SystemUI.

Introduce IRingtonePlayer, which handles playback for both Ringtone
objects and Notifications. SystemUI now hosts this player, which it
registers with AudioService. It also keeps MediaPlayer instances
warm, and cleans them up after stop() or Binder death.

Move both Ringtone and NotificationManagerService to play back audio
through this new interface.

Bug: 6376128, 6350773
Change-Id: I1dcb86d16ee3c4f07cdb2248d33dcff4ead3609a
diff --git a/Android.mk b/Android.mk
index 7ebf4b5..678ae55 100644
--- a/Android.mk
+++ b/Android.mk
@@ -198,6 +198,7 @@
 	media/java/android/media/IMediaScannerService.aidl \
 	media/java/android/media/IRemoteControlClient.aidl \
 	media/java/android/media/IRemoteControlDisplay.aidl \
+	media/java/android/media/IRingtonePlayer.aidl \
 	telephony/java/com/android/internal/telephony/IPhoneStateListener.aidl \
 	telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl \
 	telephony/java/com/android/internal/telephony/ITelephony.aidl \
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 0a996df..69689c9 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
+import android.media.AudioManager;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -213,7 +214,7 @@
     /**
      * Use this constant as the value for audioStreamType to request that
      * the default stream type for notifications be used.  Currently the
-     * default stream type is STREAM_RING.
+     * default stream type is {@link AudioManager#STREAM_NOTIFICATION}.
      */
     public static final int STREAM_DEFAULT = -1;
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3ae2b4e..471a496 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -628,6 +628,11 @@
         android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
         android:protectionLevel="signature" />
 
+    <!-- Allows registration for remote audio playback. @hide -->
+    <permission android:name="android.permission.REMOTE_AUDIO_PLAYBACK"
+        android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
+        android:protectionLevel="signature" />
+
     <!-- =========================================== -->
     <!-- Permissions associated with telephony state -->
     <!-- =========================================== -->
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 2a006c6..f603525 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -2306,4 +2306,12 @@
         }
     }
 
+    /** {@hide} */
+    public IRingtonePlayer getRingtonePlayer() {
+        try {
+            return getService().getRingtonePlayer();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
 }
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index 3e958dc..dc9496f 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -16,6 +16,7 @@
 
 package android.media;
 
+import static android.Manifest.permission.REMOTE_AUDIO_PLAYBACK;
 import static android.media.AudioManager.RINGER_MODE_NORMAL;
 import static android.media.AudioManager.RINGER_MODE_SILENT;
 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
@@ -360,7 +361,6 @@
     private int mPrevVolDirection = AudioManager.ADJUST_SAME;
     // Keyguard manager proxy
     private KeyguardManager mKeyguardManager;
-
     // mVolumeControlStream is set by VolumePanel to temporarily force the stream type which volume
     // is controlled by Vol keys.
     private int  mVolumeControlStream = -1;
@@ -369,6 +369,8 @@
     // server process so in theory it is not necessary to monitor the client death.
     // However it is good to be ready for future evolutions.
     private ForceControlStreamClient mForceControlStreamClient = null;
+    // Used to play ringtones outside system_server
+    private volatile IRingtonePlayer mRingtonePlayer;
 
     ///////////////////////////////////////////////////////////////////////////
     // Construction
@@ -4231,6 +4233,17 @@
     }
 
     @Override
+    public void setRingtonePlayer(IRingtonePlayer player) {
+        mContext.enforceCallingOrSelfPermission(REMOTE_AUDIO_PLAYBACK, null);
+        mRingtonePlayer = player;
+    }
+
+    @Override
+    public IRingtonePlayer getRingtonePlayer() {
+        return mRingtonePlayer;
+    }
+
+    @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
 
@@ -4238,6 +4251,4 @@
         dumpFocusStack(pw);
         dumpRCStack(pw);
     }
-
-
 }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index df21040..1a2714e 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -21,6 +21,8 @@
 import android.media.IAudioFocusDispatcher;
 import android.media.IRemoteControlClient;
 import android.media.IRemoteControlDisplay;
+import android.media.IRingtonePlayer;
+import android.net.Uri;
 
 /**
  * {@hide}
@@ -113,10 +115,11 @@
     oneway void remoteControlDisplayUsesBitmapSize(in IRemoteControlDisplay rcd, int w, int h);
 
     void startBluetoothSco(IBinder cb);
-
     void stopBluetoothSco(IBinder cb);
 
     void forceVolumeControlStream(int streamType, IBinder cb);
 
+    void setRingtonePlayer(IRingtonePlayer player);
+    IRingtonePlayer getRingtonePlayer();
     int getMasterStreamType();
 }
diff --git a/media/java/android/media/IRingtonePlayer.aidl b/media/java/android/media/IRingtonePlayer.aidl
new file mode 100644
index 0000000..44a0333
--- /dev/null
+++ b/media/java/android/media/IRingtonePlayer.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012 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 android.media;
+
+import android.net.Uri;
+
+/**
+ * @hide
+ */
+interface IRingtonePlayer {
+    /** Used for Ringtone.java playback */
+    void play(IBinder token, in Uri uri, int streamType);
+    void stop(IBinder token);
+    boolean isPlaying(IBinder token);
+
+    /** Used for Notification sound playback. */
+    void playAsync(in Uri uri, boolean looping, int streamType);
+    void stopAsync();
+}
diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java
index f16ba36..57139d2 100644
--- a/media/java/android/media/Ringtone.java
+++ b/media/java/android/media/Ringtone.java
@@ -18,17 +18,15 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
-import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
-import android.media.AudioManager;
-import android.media.MediaPlayer;
 import android.net.Uri;
+import android.os.Binder;
+import android.os.RemoteException;
 import android.provider.DrmStore;
 import android.provider.MediaStore;
 import android.provider.Settings;
 import android.util.Log;
 
-import java.io.FileDescriptor;
 import java.io.IOException;
 
 /**
@@ -41,7 +39,8 @@
  * @see RingtoneManager
  */
 public class Ringtone {
-    private static String TAG = "Ringtone";
+    private static final String TAG = "Ringtone";
+    private static final boolean LOGD = true;
 
     private static final String[] MEDIA_COLUMNS = new String[] {
         MediaStore.Audio.Media._ID,
@@ -55,21 +54,26 @@
         DrmStore.Audio.TITLE
     };
 
-    private MediaPlayer mAudio;
+    private final Context mContext;
+    private final AudioManager mAudioManager;
+    private final boolean mAllowRemote;
+    private final IRingtonePlayer mRemotePlayer;
+    private final Binder mRemoteToken;
+
+    private MediaPlayer mLocalPlayer;
 
     private Uri mUri;
     private String mTitle;
-    private FileDescriptor mFileDescriptor;
-    private AssetFileDescriptor mAssetFileDescriptor;
 
     private int mStreamType = AudioManager.STREAM_RING;
-    private AudioManager mAudioManager;
 
-    private Context mContext;
-
-    Ringtone(Context context) {
+    /** {@hide} */
+    public Ringtone(Context context, boolean allowRemote) {
         mContext = context;
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mAllowRemote = allowRemote;
+        mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
+        mRemoteToken = allowRemote ? new Binder() : null;
     }
 
     /**
@@ -79,18 +83,10 @@
      */
     public void setStreamType(int streamType) {
         mStreamType = streamType;
-        
-        if (mAudio != null) {
-            /*
-             * The stream type has to be set before the media player is
-             * prepared. Re-initialize it.
-             */
-            try {
-                openMediaPlayer();
-            } catch (IOException e) {
-                Log.w(TAG, "Couldn't set the stream type", e);
-            }
-        }
+
+        // The stream type has to be set before the media player is prepared.
+        // Re-initialize it.
+        setUri(mUri);
     }
 
     /**
@@ -164,67 +160,75 @@
         
         return title;
     }
-    
-    private void openMediaPlayer() throws IOException {
-        if (mAudio != null) {
-            mAudio.release();
-        }
-        mAudio = new MediaPlayer();
-        if (mUri != null) {
-            mAudio.setDataSource(mContext, mUri);
-        } else if (mFileDescriptor != null) {
-            mAudio.setDataSource(mFileDescriptor);
-        } else if (mAssetFileDescriptor != null) {
-            // Note: using getDeclaredLength so that our behavior is the same
-            // as previous versions when the content provider is returning
-            // a full file.
-            if (mAssetFileDescriptor.getDeclaredLength() < 0) {
-                mAudio.setDataSource(mAssetFileDescriptor.getFileDescriptor());
-            } else {
-                mAudio.setDataSource(mAssetFileDescriptor.getFileDescriptor(),
-                        mAssetFileDescriptor.getStartOffset(),
-                        mAssetFileDescriptor.getDeclaredLength());
-            }
-        } else {
-            throw new IOException("No data source set.");
-        }
-        mAudio.setAudioStreamType(mStreamType);
-        mAudio.prepare();
-    }
 
-    void open(FileDescriptor fd) throws IOException {
-        mFileDescriptor = fd;
-        openMediaPlayer();
-    }
+    /**
+     * Set {@link Uri} to be used for ringtone playback. Attempts to open
+     * locally, otherwise will delegate playback to remote
+     * {@link IRingtonePlayer}.
+     *
+     * @hide
+     */
+    public void setUri(Uri uri) {
+        destroyLocalPlayer();
 
-    void open(AssetFileDescriptor fd) throws IOException {
-        mAssetFileDescriptor = fd;
-        openMediaPlayer();
-    }
-
-    void open(Uri uri) throws IOException {
         mUri = uri;
-        openMediaPlayer();
+        if (mUri == null) {
+            return;
+        }
+
+        // TODO: detect READ_EXTERNAL and specific content provider case, instead of relying on throwing
+
+        // try opening uri locally before delegating to remote player
+        mLocalPlayer = new MediaPlayer();
+        try {
+            mLocalPlayer.setDataSource(mContext, mUri);
+            mLocalPlayer.setAudioStreamType(mStreamType);
+            mLocalPlayer.prepare();
+
+        } catch (SecurityException e) {
+            destroyLocalPlayer();
+            if (!mAllowRemote) {
+                throw new IllegalStateException("Remote playback not allowed", e);
+            }
+        } catch (IOException e) {
+            destroyLocalPlayer();
+            if (!mAllowRemote) {
+                throw new IllegalStateException("Remote playback not allowed", e);
+            }
+        }
+
+        if (LOGD) {
+            if (mLocalPlayer != null) {
+                Log.d(TAG, "Successfully created local player");
+            } else {
+                Log.d(TAG, "Problem opening; delegating to remote player");
+            }
+        }
+    }
+
+    /** {@hide} */
+    public Uri getUri() {
+        return mUri;
     }
 
     /**
      * Plays the ringtone.
      */
     public void play() {
-        if (mAudio == null) {
-            try {
-                openMediaPlayer();
-            } catch (Exception ex) {
-                Log.e(TAG, "play() caught ", ex);
-                mAudio = null;
-            }
-        }
-        if (mAudio != null) {
-            // do not ringtones if stream volume is 0
+        if (mLocalPlayer != null) {
+            // do not play ringtones if stream volume is 0
             // (typically because ringer mode is silent).
             if (mAudioManager.getStreamVolume(mStreamType) != 0) {
-                mAudio.start();
+                mLocalPlayer.start();
             }
+        } else if (mAllowRemote) {
+            try {
+                mRemotePlayer.play(mRemoteToken, mUri, mStreamType);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Problem playing ringtone: " + e);
+            }
+        } else {
+            throw new IllegalStateException("Neither local nor remote playback available");
         }
     }
 
@@ -232,10 +236,22 @@
      * Stops a playing ringtone.
      */
     public void stop() {
-        if (mAudio != null) {
-            mAudio.reset();
-            mAudio.release();
-            mAudio = null;
+        if (mLocalPlayer != null) {
+            destroyLocalPlayer();
+        } else if (mAllowRemote) {
+            try {
+                mRemotePlayer.stop(mRemoteToken);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Problem stopping ringtone: " + e);
+            }
+        }
+    }
+
+    private void destroyLocalPlayer() {
+        if (mLocalPlayer != null) {
+            mLocalPlayer.reset();
+            mLocalPlayer.release();
+            mLocalPlayer = null;
         }
     }
 
@@ -245,7 +261,18 @@
      * @return True if playing, false otherwise.
      */
     public boolean isPlaying() {
-        return mAudio != null && mAudio.isPlaying();
+        if (mLocalPlayer != null) {
+            return mLocalPlayer.isPlaying();
+        } else if (mAllowRemote) {
+            try {
+                return mRemotePlayer.isPlaying(mRemoteToken);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Problem checking ringtone: " + e);
+                return false;
+            }
+        } else {
+            throw new IllegalStateException("Neither local nor remote playback available");
+        }
     }
 
     void setTitle(String title) {
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index a5b1f45..5e18bfa 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -606,16 +606,15 @@
      * @see #getRingtone(Context, Uri)
      */
     private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) {
-
         try {
-            Ringtone r = new Ringtone(context);
+            final Ringtone r = new Ringtone(context, true);
             if (streamType >= 0) {
                 r.setStreamType(streamType);
             }
-            r.open(ringtoneUri);
+            r.setUri(ringtoneUri);
             return r;
         } catch (Exception ex) {
-            Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
+            Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
         }
 
         return null;
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index a31c264..cef21b0 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -5,6 +5,7 @@
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
 
     <uses-permission android:name="android.permission.INJECT_EVENTS" />
     <uses-permission android:name="android.permission.WRITE_SETTINGS" />
@@ -12,6 +13,7 @@
     <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
     <uses-permission android:name="android.permission.STATUS_BAR" />
     <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
+    <uses-permission android:name="android.permission.REMOTE_AUDIO_PLAYBACK" />
 
     <!-- Networking and telephony -->
     <uses-permission android:name="android.permission.BLUETOOTH" />
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
index ae568f8..0a57499 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
@@ -41,6 +41,7 @@
     final Object[] SERVICES = new Object[] {
             0, // system bar or status bar, filled in below.
             com.android.systemui.power.PowerUI.class,
+            com.android.systemui.media.RingtonePlayer.class,
         };
 
     /**
diff --git a/services/java/com/android/server/NotificationPlayer.java b/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java
similarity index 98%
rename from services/java/com/android/server/NotificationPlayer.java
rename to packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java
index 52d2381..6a12eb1 100644
--- a/services/java/com/android/server/NotificationPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server;
+package com.android.systemui.media;
 
 import android.content.Context;
 import android.media.AudioManager;
@@ -36,7 +36,7 @@
 /**
  * @hide
  * This class is provides the same interface and functionality as android.media.AsyncPlayer
- * with the following differences: 
+ * 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.
  */
@@ -338,4 +338,3 @@
         }
     }
 }
-
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
new file mode 100644
index 0000000..9e273d4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2012 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.systemui.media;
+
+import android.content.Context;
+import android.media.IAudioService;
+import android.media.IRingtonePlayer;
+import android.media.Ringtone;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Slog;
+
+import com.android.systemui.SystemUI;
+import com.google.android.collect.Maps;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.HashMap;
+
+/**
+ * Service that offers to play ringtones by {@link Uri}, since our process has
+ * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}.
+ */
+public class RingtonePlayer extends SystemUI {
+    private static final String TAG = "RingtonePlayer";
+    private static final boolean LOGD = true;
+
+    // TODO: support Uri switching under same IBinder
+
+    private IAudioService mAudioService;
+
+    private final NotificationPlayer mAsyncPlayer = new NotificationPlayer(TAG);
+    private final HashMap<IBinder, Client> mClients = Maps.newHashMap();
+
+    @Override
+    public void start() {
+        mAsyncPlayer.setUsesWakeLock(mContext);
+
+        mAudioService = IAudioService.Stub.asInterface(
+                ServiceManager.getService(Context.AUDIO_SERVICE));
+        try {
+            mAudioService.setRingtonePlayer(mCallback);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Problem registering RingtonePlayer: " + e);
+        }
+    }
+
+    /**
+     * Represents an active remote {@link Ringtone} client.
+     */
+    private class Client implements IBinder.DeathRecipient {
+        private final IBinder mToken;
+        private final Ringtone mRingtone;
+
+        public Client(IBinder token, Uri uri, int streamType) {
+            mToken = token;
+            mRingtone = new Ringtone(mContext, false);
+            mRingtone.setStreamType(streamType);
+            mRingtone.setUri(uri);
+        }
+
+        @Override
+        public void binderDied() {
+            if (LOGD) Slog.d(TAG, "binderDied() token=" + mToken);
+            synchronized (mClients) {
+                mClients.remove(mToken);
+            }
+            mRingtone.stop();
+        }
+    }
+
+    private IRingtonePlayer mCallback = new IRingtonePlayer.Stub() {
+        @Override
+        public void play(IBinder token, Uri uri, int streamType) throws RemoteException {
+            if (LOGD) Slog.d(TAG, "play(token=" + token + ", uri=" + uri + ")");
+            Client client;
+            synchronized (mClients) {
+                client = mClients.get(token);
+                if (client == null) {
+                    client = new Client(token, uri, streamType);
+                    token.linkToDeath(client, 0);
+                    mClients.put(token, client);
+                }
+            }
+            client.mRingtone.play();
+        }
+
+        @Override
+        public void stop(IBinder token) {
+            if (LOGD) Slog.d(TAG, "stop(token=" + token + ")");
+            Client client;
+            synchronized (mClients) {
+                client = mClients.remove(token);
+            }
+            if (client != null) {
+                client.mToken.unlinkToDeath(client, 0);
+                client.mRingtone.stop();
+            }
+        }
+
+        @Override
+        public boolean isPlaying(IBinder token) {
+            if (LOGD) Slog.d(TAG, "isPlaying(token=" + token + ")");
+            Client client;
+            synchronized (mClients) {
+                client = mClients.get(token);
+            }
+            if (client != null) {
+                return client.mRingtone.isPlaying();
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public void playAsync(Uri uri, boolean looping, int streamType) {
+            if (LOGD) Slog.d(TAG, "playAsync(uri=" + uri + ")");
+            if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+                throw new SecurityException("Async playback only available from system UID.");
+            }
+            mAsyncPlayer.play(mContext, uri, looping, streamType);
+        }
+
+        @Override
+        public void stopAsync() {
+            if (LOGD) Slog.d(TAG, "stopAsync()");
+            if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+                throw new SecurityException("Async playback only available from system UID.");
+            }
+            mAsyncPlayer.stop();
+        }
+    };
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("Clients:");
+        synchronized (mClients) {
+            for (Client client : mClients.values()) {
+                pw.print("  mToken=");
+                pw.print(client.mToken);
+                pw.print(" mUri=");
+                pw.println(client.mRingtone.getUri());
+            }
+        }
+    }
+}
diff --git a/services/java/com/android/server/NotificationManagerService.java b/services/java/com/android/server/NotificationManagerService.java
index 1ba7e79..663a031 100755
--- a/services/java/com/android/server/NotificationManagerService.java
+++ b/services/java/com/android/server/NotificationManagerService.java
@@ -16,16 +16,15 @@
 
 package com.android.server;
 
-import com.android.internal.os.AtomicFile;
-import com.android.internal.statusbar.StatusBarNotification;
-import com.android.internal.util.FastXmlSerializer;
+import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.END_TAG;
+import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
 import android.app.ActivityManagerNative;
 import android.app.IActivityManager;
 import android.app.INotificationManager;
 import android.app.ITransientNotification;
 import android.app.Notification;
-import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.StatusBarManager;
 import android.content.BroadcastReceiver;
@@ -39,8 +38,8 @@
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.media.AudioManager;
-import android.net.NetworkPolicy;
-import android.net.NetworkTemplate;
+import android.media.IAudioService;
+import android.media.IRingtonePlayer;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Handler;
@@ -48,6 +47,7 @@
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserId;
 import android.os.Vibrator;
 import android.provider.Settings;
@@ -61,6 +61,14 @@
 import android.view.accessibility.AccessibilityManager;
 import android.widget.Toast;
 
+import com.android.internal.os.AtomicFile;
+import com.android.internal.statusbar.StatusBarNotification;
+import com.android.internal.util.FastXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
@@ -74,18 +82,6 @@
 
 import libcore.io.IoUtils;
 
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlSerializer;
-
-import static android.net.NetworkPolicyManager.POLICY_NONE;
-import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeBooleanAttribute;
-import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeIntAttribute;
-import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeLongAttribute;
-import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
-import static org.xmlpull.v1.XmlPullParser.END_TAG;
-import static org.xmlpull.v1.XmlPullParser.START_TAG;
-
 
 /** {@hide} */
 public class NotificationManagerService extends INotificationManager.Stub
@@ -126,12 +122,13 @@
     private int mDefaultNotificationLedOn;
     private int mDefaultNotificationLedOff;
 
-    private NotificationRecord mSoundNotification;
-    private NotificationPlayer mSound;
     private boolean mSystemReady;
     private int mDisabledNotifications;
 
+    private NotificationRecord mSoundNotification;
     private NotificationRecord mVibrateNotification;
+
+    private IAudioService mAudioService;
     private Vibrator mVibrator;
 
     // for enabling and disabling notification pulse behavior
@@ -409,17 +406,19 @@
                     // cancel whatever's going on
                     long identity = Binder.clearCallingIdentity();
                     try {
-                        mSound.stop();
-                    }
-                    finally {
+                        final IRingtonePlayer player = mAudioService.getRingtonePlayer();
+                        if (player != null) {
+                            player.stopAsync();
+                        }
+                    } catch (RemoteException e) {
+                    } finally {
                         Binder.restoreCallingIdentity(identity);
                     }
 
                     identity = Binder.clearCallingIdentity();
                     try {
                         mVibrator.cancel();
-                    }
-                    finally {
+                    } finally {
                         Binder.restoreCallingIdentity(identity);
                     }
                 }
@@ -445,11 +444,15 @@
             synchronized (mNotificationList) {
                 // sound
                 mSoundNotification = null;
+
                 long identity = Binder.clearCallingIdentity();
                 try {
-                    mSound.stop();
-                }
-                finally {
+                    final IRingtonePlayer player = mAudioService.getRingtonePlayer();
+                    if (player != null) {
+                        player.stopAsync();
+                    }
+                } catch (RemoteException e) {
+                } finally {
                     Binder.restoreCallingIdentity(identity);
                 }
 
@@ -458,8 +461,7 @@
                 identity = Binder.clearCallingIdentity();
                 try {
                     mVibrator.cancel();
-                }
-                finally {
+                } finally {
                     Binder.restoreCallingIdentity(identity);
                 }
 
@@ -570,8 +572,6 @@
         mContext = context;
         mVibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
         mAm = ActivityManagerNative.getDefault();
-        mSound = new NotificationPlayer(TAG);
-        mSound.setUsesWakeLock(context);
         mToastQueue = new ArrayList<ToastRecord>();
         mHandler = new WorkerHandler();
 
@@ -622,6 +622,9 @@
     }
 
     void systemReady() {
+        mAudioService = IAudioService.Stub.asInterface(
+                ServiceManager.getService(Context.AUDIO_SERVICE));
+
         // no beeping until we're basically done booting
         mSystemReady = true;
     }
@@ -1026,11 +1029,14 @@
                     // do not play notifications if stream volume is 0
                     // (typically because ringer mode is silent).
                     if (audioManager.getStreamVolume(audioStreamType) != 0) {
-                        long identity = Binder.clearCallingIdentity();
+                        final long identity = Binder.clearCallingIdentity();
                         try {
-                            mSound.play(mContext, uri, looping, audioStreamType);
-                        }
-                        finally {
+                            final IRingtonePlayer player = mAudioService.getRingtonePlayer();
+                            if (player != null) {
+                                player.playAsync(uri, looping, audioStreamType);
+                            }
+                        } catch (RemoteException e) {
+                        } finally {
                             Binder.restoreCallingIdentity(identity);
                         }
                     }
@@ -1121,11 +1127,14 @@
         // sound
         if (mSoundNotification == r) {
             mSoundNotification = null;
-            long identity = Binder.clearCallingIdentity();
+            final long identity = Binder.clearCallingIdentity();
             try {
-                mSound.stop();
-            }
-            finally {
+                final IRingtonePlayer player = mAudioService.getRingtonePlayer();
+                if (player != null) {
+                    player.stopAsync();
+                }
+            } catch (RemoteException e) {
+            } finally {
                 Binder.restoreCallingIdentity(identity);
             }
         }
@@ -1386,7 +1395,6 @@
             }
 
             pw.println("  mSoundNotification=" + mSoundNotification);
-            pw.println("  mSound=" + mSound);
             pw.println("  mVibrateNotification=" + mVibrateNotification);
             pw.println("  mDisabledNotifications=0x" + Integer.toHexString(mDisabledNotifications));
             pw.println("  mSystemReady=" + mSystemReady);