Allow sysui-managed remote volume controllers.

- Relax restriction on audio service calls that assume the volume
  ui is systemui, allow calls from a blessed component app.
- Blessed component app service saved in secure settings.
- SystemUI mediates requests to replace the volume dialog, prompts
  the user on activation.
- Show a low pri ongoing notification when the volume dialog is
  being replaced, to allow user restoration at any time.
- Replace the controller management code in VolumeUI to use a
  ServiceMonitor, backed by the new blessed app component setting.
- Add proper zen-related noman client wrappers, make avail to the
  registered volume controller.
- Everything is still @hidden, no api impact.

Bug: 19260237
Change-Id: Ie1383f57659090318a7eda737fdad5b8f88737d4
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 88b9080..5d864df 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -75,6 +75,7 @@
 
     ZenModeConfig getZenModeConfig();
     boolean setZenModeConfig(in ZenModeConfig config);
+    oneway void setZenMode(int mode);
     oneway void notifyConditions(String pkg, in IConditionProvider provider, in Condition[] conditions);
     oneway void requestZenModeConditions(in IConditionListener callback, int relevance);
     oneway void setZenModeCondition(in Condition condition);
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index cf54107..479327d 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -27,6 +27,9 @@
 import android.os.ServiceManager;
 import android.os.StrictMode;
 import android.os.UserHandle;
+import android.service.notification.Condition;
+import android.service.notification.IConditionListener;
+import android.service.notification.ZenModeConfig;
 import android.util.Log;
 
 /**
@@ -276,5 +279,53 @@
         }
     }
 
+    /**
+     * @hide
+     */
+    public void setZenMode(int mode) {
+        INotificationManager service = getService();
+        try {
+            service.setZenMode(mode);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void requestZenModeConditions(IConditionListener listener, int relevance) {
+        INotificationManager service = getService();
+        try {
+            service.requestZenModeConditions(listener, relevance);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void setZenModeCondition(Condition exitCondition) {
+        INotificationManager service = getService();
+        try {
+            service.setZenModeCondition(exitCondition);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public Condition getZenModeCondition() {
+        INotificationManager service = getService();
+        try {
+            final ZenModeConfig config = service.getZenModeConfig();
+            if (config != null) {
+                return config.exitCondition;
+            }
+        } catch (RemoteException e) {
+        }
+        return null;
+    }
+
     private Context mContext;
 }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index dddbe78..5380cd6 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -4762,6 +4762,10 @@
         public static final String BAR_SERVICE_COMPONENT = "bar_service_component";
 
         /** @hide */
+        public static final String VOLUME_CONTROLLER_SERVICE_COMPONENT
+                = "volume_controller_service_component";
+
+        /** @hide */
         public static final String IMMERSIVE_MODE_CONFIRMATIONS = "immersive_mode_confirmations";
 
         /**
diff --git a/media/java/android/media/AudioManagerInternal.java b/media/java/android/media/AudioManagerInternal.java
index 873c142..ef5710c 100644
--- a/media/java/android/media/AudioManagerInternal.java
+++ b/media/java/android/media/AudioManagerInternal.java
@@ -15,8 +15,6 @@
  */
 package android.media;
 
-import android.os.IBinder;
-
 import com.android.server.LocalServices;
 
 /**
@@ -47,6 +45,8 @@
 
     public abstract void setRingerModeInternal(int ringerMode, String caller);
 
+    public abstract int getVolumeControllerUid();
+
     public interface RingerModeDelegate {
         /** Called when external ringer mode is evaluated, returns the new internal ringer mode */
         int onSetRingerModeExternal(int ringerModeOld, int ringerModeNew, String caller,
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index aa53a3e..3fc75d2 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -40,6 +40,7 @@
     <item type="id" name="notification_power"/>
     <item type="id" name="notification_screenshot"/>
     <item type="id" name="notification_hidden"/>
+    <item type="id" name="notification_volumeui"/>
 
     <!-- Whether the icon is from a notification for which targetSdk < L -->
     <item type="id" name="icon_is_pre_L"/>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 6afca8a..0420d35 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -932,4 +932,19 @@
 
     <!-- Hide quick settings tile confirmation button -->
     <string name="quick_settings_reset_confirmation_button">Hide</string>
+
+    <!-- VolumeUI activation dialog: warning message -->
+    <string name="volumeui_prompt_message"><xliff:g id="app_name" example="Volume Prototype 1">%1$s</xliff:g> wants to be the volume dialog.</string>
+
+    <!-- VolumeUI activation dialog: allow button label -->
+    <string name="volumeui_prompt_allow">Allow</string>
+
+    <!-- VolumeUI activation dialog: deny button label -->
+    <string name="volumeui_prompt_deny">Deny</string>
+
+    <!-- VolumeUI restoration notification: title -->
+    <string name="volumeui_notification_title"><xliff:g id="app_name" example="Volume Prototype 1">%1$s</xliff:g> is the volume dialog</string>
+
+    <!-- VolumeUI restoration notification: text -->
+    <string name="volumeui_notification_text">Touch to restore the original.</string>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java b/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java
index aea9ec6..69a4932 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java
@@ -279,4 +279,14 @@
         }
         return sb.append('}').toString();
     }
+
+    public ComponentName getComponent() {
+        return getComponentNameFromSetting();
+    }
+
+    public void setComponent(ComponentName component) {
+        final String setting = component == null ? null : component.flattenToShortString();
+        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                mSettingKey, setting, UserHandle.USER_CURRENT);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index dbdb578..bea0c86 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.policy;
 
 import android.app.AlarmManager;
-import android.app.INotificationManager;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -28,8 +27,6 @@
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
-import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.provider.Settings.Global;
 import android.provider.Settings.Secure;
@@ -53,7 +50,7 @@
     private final Context mContext;
     private final GlobalSetting mModeSetting;
     private final GlobalSetting mConfigSetting;
-    private final INotificationManager mNoMan;
+    private final NotificationManager mNoMan;
     private final LinkedHashMap<Uri, Condition> mConditions = new LinkedHashMap<Uri, Condition>();
     private final AlarmManager mAlarmManager;
     private final SetupObserver mSetupObserver;
@@ -78,8 +75,7 @@
         };
         mModeSetting.setListening(true);
         mConfigSetting.setListening(true);
-        mNoMan = INotificationManager.Stub.asInterface(
-                ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+        mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
         mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
         mSetupObserver = new SetupObserver(handler);
         mSetupObserver.register();
@@ -113,11 +109,7 @@
     @Override
     public void requestConditions(boolean request) {
         mRequesting = request;
-        try {
-            mNoMan.requestZenModeConditions(mListener, request ? Condition.FLAG_RELEVANT_NOW : 0);
-        } catch (RemoteException e) {
-            // noop
-        }
+        mNoMan.requestZenModeConditions(mListener, request ? Condition.FLAG_RELEVANT_NOW : 0);
         if (!mRequesting) {
             mConditions.clear();
         }
@@ -125,24 +117,12 @@
 
     @Override
     public void setExitCondition(Condition exitCondition) {
-        try {
-            mNoMan.setZenModeCondition(exitCondition);
-        } catch (RemoteException e) {
-            // noop
-        }
+        mNoMan.setZenModeCondition(exitCondition);
     }
 
     @Override
     public Condition getExitCondition() {
-        try {
-            final ZenModeConfig config = mNoMan.getZenModeConfig();
-            if (config != null) {
-                return config.exitCondition;
-            }
-        } catch (RemoteException e) {
-            // noop
-        }
-        return null;
+        return mNoMan.getZenModeCondition();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
index 7102c2a..8048a48 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
@@ -1,31 +1,3 @@
-package com.android.systemui.volume;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.database.ContentObserver;
-import android.media.AudioManager;
-import android.media.IRemoteVolumeController;
-import android.media.IVolumeController;
-import android.media.session.ISessionController;
-import android.media.session.MediaController;
-import android.media.session.MediaSessionManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.RemoteException;
-import android.provider.Settings;
-import android.util.Log;
-
-import com.android.systemui.R;
-import com.android.systemui.SystemUI;
-import com.android.systemui.keyguard.KeyguardViewMediator;
-import com.android.systemui.statusbar.phone.PhoneStatusBar;
-import com.android.systemui.statusbar.policy.ZenModeController;
-import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-
 /*
  * Copyright (C) 2014 The Android Open Source Project
  *
@@ -42,19 +14,60 @@
  * limitations under the License.
  */
 
+package com.android.systemui.volume;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.media.IRemoteVolumeController;
+import android.media.IVolumeController;
+import android.media.session.ISessionController;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.systemui.R;
+import com.android.systemui.SystemUI;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.statusbar.ServiceMonitor;
+import com.android.systemui.statusbar.phone.PhoneStatusBar;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.statusbar.policy.ZenModeController;
+import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
 public class VolumeUI extends SystemUI {
     private static final String TAG = "VolumeUI";
-    private static final String SETTING = "systemui_volume_controller";  // for testing
-    private static final Uri SETTING_URI = Settings.Global.getUriFor(SETTING);
-    private static final int DEFAULT = 1;  // enabled by default
+    private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
 
     private final Handler mHandler = new Handler();
+    private final Receiver mReceiver = new Receiver();
+    private final RestorationNotification mRestorationNotification = new RestorationNotification();
 
     private boolean mEnabled;
     private AudioManager mAudioManager;
+    private NotificationManager mNotificationManager;
     private MediaSessionManager mMediaSessionManager;
     private VolumeController mVolumeController;
     private RemoteVolumeController mRemoteVolumeController;
+    private ServiceMonitor mVolumeControllerService;
 
     private VolumePanel mPanel;
     private int mDismissDelay;
@@ -64,14 +77,19 @@
         mEnabled = mContext.getResources().getBoolean(R.bool.enable_volume_ui);
         if (!mEnabled) return;
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mNotificationManager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
         mMediaSessionManager = (MediaSessionManager) mContext
                 .getSystemService(Context.MEDIA_SESSION_SERVICE);
         initPanel();
         mVolumeController = new VolumeController();
         mRemoteVolumeController = new RemoteVolumeController();
         putComponent(VolumeComponent.class, mVolumeController);
-        updateController();
-        mContext.getContentResolver().registerContentObserver(SETTING_URI, false, mObserver);
+        mReceiver.start();
+        mVolumeControllerService = new ServiceMonitor(TAG, LOGD,
+                mContext, Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT,
+                new ServiceMonitorCallbacks());
+        mVolumeControllerService.start();
     }
 
     @Override
@@ -85,18 +103,19 @@
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.print("mEnabled="); pw.println(mEnabled);
+        pw.print("mVolumeControllerService="); pw.println(mVolumeControllerService.getComponent());
         if (mPanel != null) {
             mPanel.dump(fd, pw, args);
         }
     }
 
-    private void updateController() {
-        if (Settings.Global.getInt(mContext.getContentResolver(), SETTING, DEFAULT) != 0) {
-            Log.d(TAG, "Registering volume controller");
+    private void setVolumeController(boolean register) {
+        if (register) {
+            if (LOGD) Log.d(TAG, "Registering volume controller");
             mAudioManager.setVolumeController(mVolumeController);
             mMediaSessionManager.setRemoteVolumeController(mRemoteVolumeController);
         } else {
-            Log.d(TAG, "Unregistering volume controller");
+            if (LOGD) Log.d(TAG, "Unregistering volume controller");
             mAudioManager.setVolumeController(null);
             mMediaSessionManager.setRemoteVolumeController(null);
         }
@@ -129,13 +148,32 @@
         });
     }
 
-    private final ContentObserver mObserver = new ContentObserver(mHandler) {
-        public void onChange(boolean selfChange, Uri uri) {
-            if (SETTING_URI.equals(uri)) {
-                updateController();
+    private String getAppLabel(ComponentName component) {
+        final String pkg = component.getPackageName();
+        try {
+            final ApplicationInfo ai = mContext.getPackageManager().getApplicationInfo(pkg, 0);
+            final String rt = mContext.getPackageManager().getApplicationLabel(ai).toString();
+            if (!TextUtils.isEmpty(rt)) {
+                return rt;
             }
+        } catch (Exception e) {
+            Log.w(TAG, "Error loading app label", e);
         }
-    };
+        return pkg;
+    }
+
+    private void showServiceActivationDialog(final ComponentName component) {
+        final SystemUIDialog d = new SystemUIDialog(mContext);
+        d.setMessage(mContext.getString(R.string.volumeui_prompt_message, getAppLabel(component)));
+        d.setPositiveButton(R.string.volumeui_prompt_allow, new OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                mVolumeControllerService.setComponent(component);
+            }
+        });
+        d.setNegativeButton(R.string.volumeui_prompt_deny, null);
+        d.show();
+    }
 
     private final Runnable mStartZenSettings = new Runnable() {
         @Override
@@ -213,4 +251,83 @@
             // than by remoteVolumeChanged.
         }
     }
+
+    private final class ServiceMonitorCallbacks implements ServiceMonitor.Callbacks {
+        @Override
+        public void onNoService() {
+            if (LOGD) Log.d(TAG, "onNoService");
+            setVolumeController(true);
+            mRestorationNotification.hide();
+        }
+
+        @Override
+        public long onServiceStartAttempt() {
+            if (LOGD) Log.d(TAG, "onServiceStartAttempt");
+            setVolumeController(false);
+            mVolumeController.dismissNow();
+            mRestorationNotification.show();
+            return 0;
+        }
+    }
+
+    private final class Receiver extends BroadcastReceiver {
+        private static final String ENABLE = "com.android.systemui.vui.ENABLE";
+        private static final String DISABLE = "com.android.systemui.vui.DISABLE";
+        private static final String EXTRA_COMPONENT = "component";
+
+        public void start() {
+            final IntentFilter filter = new IntentFilter();
+            filter.addAction(ENABLE);
+            filter.addAction(DISABLE);
+            mContext.registerReceiver(this, filter, null, mHandler);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            final ComponentName component = intent.getParcelableExtra(EXTRA_COMPONENT);
+            final boolean current = component.equals(mVolumeControllerService.getComponent());
+            if (ENABLE.equals(action) && component != null) {
+                if (!current) {
+                    showServiceActivationDialog(component);
+                }
+            }
+            if (DISABLE.equals(action) && component != null) {
+                if (current) {
+                    mVolumeControllerService.setComponent(null);
+                }
+            }
+        }
+    }
+
+    private final class RestorationNotification {
+        public void hide() {
+            mNotificationManager.cancel(R.id.notification_volumeui);
+        }
+
+        public void show() {
+            final ComponentName component = mVolumeControllerService.getComponent();
+            if (component == null) {
+                Log.w(TAG, "Not showing restoration notification, component not active");
+                return;
+            }
+            final Intent intent =  new Intent(Receiver.DISABLE)
+                    .putExtra(Receiver.EXTRA_COMPONENT, component);
+            mNotificationManager.notify(R.id.notification_volumeui,
+                    new Notification.Builder(mContext)
+                            .setSmallIcon(R.drawable.ic_ringer_audible)
+                            .setWhen(0)
+                            .setShowWhen(false)
+                            .setOngoing(true)
+                            .setContentTitle(mContext.getString(
+                                    R.string.volumeui_notification_title, getAppLabel(component)))
+                            .setContentText(mContext.getString(R.string.volumeui_notification_text))
+                            .setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0))
+                            .setPriority(Notification.PRIORITY_MIN)
+                            .setVisibility(Notification.VISIBILITY_PUBLIC)
+                            .setColor(mContext.getResources().getColor(
+                                    com.android.internal.R.color.system_notification_accent_color))
+                            .build());
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index eaece09..4301427 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -182,6 +182,7 @@
 
     /** The controller for the volume UI. */
     private final VolumeController mVolumeController = new VolumeController();
+    private final ControllerService mControllerService = new ControllerService();
 
     // sendMsg() flags
     /** If the msg is already queued, replace it with this one. */
@@ -708,6 +709,7 @@
                 SAFE_VOLUME_CONFIGURE_TIMEOUT_MS);
 
         StreamOverride.init(mContext);
+        mControllerService.init();
     }
 
     private void createAudioSystemThread() {
@@ -1833,7 +1835,7 @@
     }
 
     public void setRingerModeInternal(int ringerMode, String caller) {
-        enforceSelfOrSystemUI("setRingerModeInternal");
+        enforceVolumeController("setRingerModeInternal");
         setRingerMode(ringerMode, caller, false /*external*/);
     }
 
@@ -5013,7 +5015,7 @@
 
     @Override
     public void setRemoteStreamVolume(int index) {
-        enforceSelfOrSystemUI("set the remote stream volume");
+        enforceVolumeController("set the remote stream volume");
         mMediaFocusControl.setRemoteStreamVolume(index);
     }
 
@@ -5333,7 +5335,7 @@
 
     @Override
     public void disableSafeMediaVolume() {
-        enforceSelfOrSystemUI("disable the safe media volume");
+        enforceVolumeController("disable the safe media volume");
         synchronized (mSafeMediaVolumeState) {
             setSafeMediaVolumeEnabled(false);
             if (mPendingVolumeCommand != null) {
@@ -5505,6 +5507,7 @@
         pw.print("  mMusicActiveMs="); pw.println(mMusicActiveMs);
         pw.print("  mMcc="); pw.println(mMcc);
         pw.print("  mHasVibrator="); pw.println(mHasVibrator);
+        pw.print("  mControllerService="); pw.println(mControllerService);
 
         dumpAudioPolicies(pw);
     }
@@ -5528,14 +5531,17 @@
         }
     }
 
-    private void enforceSelfOrSystemUI(String action) {
+    private void enforceVolumeController(String action) {
+        if (mControllerService.mUid != 0 && Binder.getCallingUid() == mControllerService.mUid) {
+            return;
+        }
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.STATUS_BAR_SERVICE,
                 "Only SystemUI can " + action);
     }
 
     @Override
     public void setVolumeController(final IVolumeController controller) {
-        enforceSelfOrSystemUI("set the volume controller");
+        enforceVolumeController("set the volume controller");
 
         // return early if things are not actually changing
         if (mVolumeController.isSameBinder(controller)) {
@@ -5566,7 +5572,7 @@
 
     @Override
     public void notifyVolumeControllerVisible(final IVolumeController controller, boolean visible) {
-        enforceSelfOrSystemUI("notify about volume controller visibility");
+        enforceVolumeController("notify about volume controller visibility");
 
         // return early if the controller is not current
         if (!mVolumeController.isSameBinder(controller)) {
@@ -5751,6 +5757,11 @@
         public void setRingerModeInternal(int ringerMode, String caller) {
             AudioService.this.setRingerModeInternal(ringerMode, caller);
         }
+
+        @Override
+        public int getVolumeControllerUid() {
+            return mControllerService.mUid;
+        }
     }
 
     //==========================================================================================
@@ -5915,4 +5926,42 @@
     private HashMap<IBinder, AudioPolicyProxy> mAudioPolicies =
             new HashMap<IBinder, AudioPolicyProxy>();
     private int mAudioPolicyCounter = 0; // always accessed synchronized on mAudioPolicies
+
+    private class ControllerService extends ContentObserver {
+        private int mUid;
+        private ComponentName mComponent;
+
+        public ControllerService() {
+            super(null);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("{mUid=%d,mComponent=%s}", mUid, mComponent);
+        }
+
+        public void init() {
+            onChange(true);
+            mContentResolver.registerContentObserver(Settings.Secure.getUriFor(
+                    Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT), false, this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mUid = 0;
+            mComponent = null;
+            final String setting = Settings.Secure.getString(mContentResolver,
+                    Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT);
+            if (setting == null) return;
+            try {
+                mComponent = ComponentName.unflattenFromString(setting);
+                if (mComponent == null) return;
+                mUid = mContext.getPackageManager()
+                        .getApplicationInfo(mComponent.getPackageName(), 0).uid;
+            } catch (Exception e) {
+                Log.w(TAG, "Error loading controller service", e);
+            }
+            if (DEBUG_VOL) Log.d(TAG, "Reloaded controller service: " + this);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 02cacd9..ac3cd1a 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -50,6 +50,7 @@
 import android.database.ContentObserver;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
+import android.media.AudioManagerInternal;
 import android.media.AudioSystem;
 import android.media.IRingtonePlayer;
 import android.net.Uri;
@@ -179,6 +180,7 @@
 
     private IActivityManager mAm;
     AudioManager mAudioManager;
+    AudioManagerInternal mAudioManagerInternal;
     StatusBarManagerInternal mStatusBar;
     Vibrator mVibrator;
 
@@ -996,6 +998,7 @@
 
             // Grab our optional AudioService
             mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+            mAudioManagerInternal = getLocalService(AudioManagerInternal.class);
             mZenModeHelper.onSystemReady();
         } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
             // This observer will force an update when observe is called, causing us to
@@ -1468,7 +1471,7 @@
 
         @Override
         public ZenModeConfig getZenModeConfig() {
-            enforceSystemOrSystemUI("INotificationManager.getZenModeConfig");
+            enforceSystemOrSystemUIOrVolume("INotificationManager.getZenModeConfig");
             return mZenModeHelper.getConfig();
         }
 
@@ -1479,6 +1482,17 @@
         }
 
         @Override
+        public void setZenMode(int mode) throws RemoteException {
+            enforceSystemOrSystemUIOrVolume("INotificationManager.setZenMode");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mZenModeHelper.setZenMode(mode, "NotificationManager");
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void notifyConditions(String pkg, IConditionProvider provider,
                 Condition[] conditions) {
             final ManagedServiceInfo info = mConditionProviders.checkServiceToken(provider);
@@ -1493,13 +1507,13 @@
 
         @Override
         public void requestZenModeConditions(IConditionListener callback, int relevance) {
-            enforceSystemOrSystemUI("INotificationManager.requestZenModeConditions");
+            enforceSystemOrSystemUIOrVolume("INotificationManager.requestZenModeConditions");
             mConditionProviders.requestZenModeConditions(callback, relevance);
         }
 
         @Override
         public void setZenModeCondition(Condition condition) {
-            enforceSystemOrSystemUI("INotificationManager.setZenModeCondition");
+            enforceSystemOrSystemUIOrVolume("INotificationManager.setZenModeCondition");
             final long identity = Binder.clearCallingIdentity();
             try {
                 mConditionProviders.setZenModeCondition(condition, "binderCall");
@@ -1520,6 +1534,16 @@
             return mConditionProviders.getAutomaticZenModeConditions();
         }
 
+        private void enforceSystemOrSystemUIOrVolume(String message) {
+            if (mAudioManagerInternal != null) {
+                final int vcuid = mAudioManagerInternal.getVolumeControllerUid();
+                if (vcuid > 0 && Binder.getCallingUid() == vcuid) {
+                    return;
+                }
+            }
+            enforceSystemOrSystemUI(message);
+        }
+
         private void enforceSystemOrSystemUI(String message) {
             if (isCallerSystem()) return;
             getContext().enforceCallingPermission(android.Manifest.permission.STATUS_BAR_SERVICE,
@@ -1541,7 +1565,7 @@
 
         @Override
         public ComponentName getEffectsSuppressor() {
-            enforceSystemOrSystemUI("INotificationManager.getEffectsSuppressor");
+            enforceSystemOrSystemUIOrVolume("INotificationManager.getEffectsSuppressor");
             return mEffectsSuppressor;
         }
 
@@ -1558,7 +1582,7 @@
 
         @Override
         public boolean isSystemConditionProviderEnabled(String path) {
-            enforceSystemOrSystemUI("INotificationManager.isSystemConditionProviderEnabled");
+            enforceSystemOrSystemUIOrVolume("INotificationManager.isSystemConditionProviderEnabled");
             return mConditionProviders.isSystemConditionProviderEnabled(path);
         }
     };