API for querying/revoking current NotificationAssistant

Additionally, NotificationAssistants guarantees that only one
NotificationAssistantService can be allowed per user at a time.

Test: added atest
Bug: 120852765
Change-Id: I19d2940e6e198a166963a2dbc05dbe8d9b8d084e
diff --git a/api/system-current.txt b/api/system-current.txt
index f203dbb8..b97abd5 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -499,6 +499,13 @@
     method public org.json.JSONObject toJson() throws org.json.JSONException;
   }
 
+  public class NotificationManager {
+    method @Nullable public android.content.ComponentName getAllowedNotificationAssistant();
+    method @Nullable public android.content.ComponentName getAllowedNotificationAssistantForUser(android.os.UserHandle);
+    method public void setNotificationAssistantAccessGranted(android.content.ComponentName, boolean);
+    method public void setNotificationAssistantAccessGrantedForUser(android.content.ComponentName, android.os.UserHandle, boolean);
+  }
+
   public final class StatsManager {
     method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void addConfig(long, byte[]) throws android.app.StatsManager.StatsUnavailableException;
     method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public boolean addConfiguration(long, byte[]);
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 199c133..250e446 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -149,6 +149,8 @@
     void setNotificationAssistantAccessGrantedForUser(in ComponentName assistant, int userId, boolean enabled);
     List<String> getEnabledNotificationListenerPackages();
     List<ComponentName> getEnabledNotificationListeners(int userId);
+    ComponentName getAllowedNotificationAssistantForUser(int userId);
+    ComponentName getAllowedNotificationAssistant();
 
     int getZenMode();
     ZenModeConfig getZenModeConfig();
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index c4b4b40..1334906 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -20,6 +20,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
 import android.annotation.UnsupportedAppUsage;
@@ -1137,6 +1138,19 @@
         }
     }
 
+    /**
+     * Checks whether the user has approved a given
+     * {@link android.service.notification.NotificationAssistantService}.
+     *
+     * <p>
+     * The assistant service must belong to the calling app.
+     *
+     * <p>
+     * Apps can request notification assistant access by sending the user to the activity that
+     * matches the system intent action
+     * TODO: STOPSHIP: Add correct intent
+     * {@link android.provider.Settings#ACTION_MANAGE_DEFAULT_APPS_SETTINGS}.
+     */
     public boolean isNotificationAssistantAccessGranted(ComponentName assistant) {
         INotificationManager service = getService();
         try {
@@ -1232,6 +1246,45 @@
         }
     }
 
+    /**
+     * Grants/revokes Notification Assistant access to {@code assistant} for current user.
+     *
+     * @param assistant Name of component to grant/revoke access or {@code null} to revoke access to
+     *                  current assistant
+     * @param granted Grant/revoke access
+     * @hide
+     */
+    @SystemApi
+    public void setNotificationAssistantAccessGranted(ComponentName assistant, boolean granted) {
+        INotificationManager service = getService();
+        try {
+            service.setNotificationAssistantAccessGranted(assistant, granted);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Grants/revokes Notification Assistant access to {@code assistant} for given user.
+     *
+     * @param assistant Name of component to grant/revoke access or {@code null} to revoke access to
+     *                  current assistant
+     * @param user handle to associate assistant with
+     * @param granted Grant/revoke access
+     * @hide
+     */
+    @SystemApi
+    public void setNotificationAssistantAccessGrantedForUser(ComponentName assistant,
+            UserHandle user, boolean granted) {
+        INotificationManager service = getService();
+        try {
+            service.setNotificationAssistantAccessGrantedForUser(assistant, user.getIdentifier(),
+                    granted);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     /** @hide */
     public List<ComponentName> getEnabledNotificationListeners(int userId) {
         INotificationManager service = getService();
@@ -1242,6 +1295,29 @@
         }
     }
 
+    /** @hide */
+    @SystemApi
+    public @Nullable ComponentName getAllowedNotificationAssistantForUser(UserHandle user) {
+        INotificationManager service = getService();
+        try {
+            return service.getAllowedNotificationAssistantForUser(user.getIdentifier());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    @SystemApi
+    public @Nullable ComponentName getAllowedNotificationAssistant() {
+        INotificationManager service = getService();
+        try {
+            return service.getAllowedNotificationAssistant();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+
     private Context mContext;
 
     private static void checkRequired(String name, Object value) {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index de3f50a..dee66e0 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -197,6 +197,7 @@
 import com.android.internal.os.SomeArgs;
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.Preconditions;
@@ -3618,6 +3619,22 @@
         }
 
         @Override
+        public ComponentName getAllowedNotificationAssistantForUser(int userId) {
+            checkCallerIsSystem();
+            List<ComponentName> allowedComponents = mAssistants.getAllowedComponents(userId);
+            if (allowedComponents.size() > 1) {
+                throw new IllegalStateException(
+                        "At most one NotificationAssistant: " + allowedComponents.size());
+            }
+            return CollectionUtils.firstOrNull(allowedComponents);
+        }
+
+        @Override
+        public ComponentName getAllowedNotificationAssistant() {
+            return getAllowedNotificationAssistantForUser(getCallingUserHandle().getIdentifier());
+        }
+
+        @Override
         public boolean isNotificationListenerAccessGranted(ComponentName listener) {
             Preconditions.checkNotNull(listener);
             checkCallerIsSystemOrSameApp(listener.getPackageName());
@@ -3685,8 +3702,15 @@
         @Override
         public void setNotificationAssistantAccessGrantedForUser(ComponentName assistant,
                 int userId, boolean granted) throws RemoteException {
-            Preconditions.checkNotNull(assistant);
             checkCallerIsSystemOrShell();
+            if (assistant == null) {
+                ComponentName allowedAssistant = CollectionUtils.firstOrNull(
+                        mAssistants.getAllowedComponents(userId));
+                if (allowedAssistant != null) {
+                    setNotificationAssistantAccessGrantedForUser(allowedAssistant, userId, false);
+                }
+                return;
+            }
             final long identity = Binder.clearCallingIdentity();
             try {
                 if (mAllowedManagedServicePackages.test(assistant.getPackageName())) {
@@ -7209,6 +7233,26 @@
                 }
             }
         }
+
+        @Override
+        protected void setPackageOrComponentEnabled(String pkgOrComponent, int userId,
+                boolean isPrimary, boolean enabled) {
+            // Ensures that only one component is enabled at a time
+            if (enabled) {
+                List<ComponentName> allowedComponents = getAllowedComponents(userId);
+                if (!allowedComponents.isEmpty()) {
+                    ComponentName currentComponent = CollectionUtils.firstOrNull(allowedComponents);
+                    if (currentComponent.flattenToString().equals(pkgOrComponent)) return;
+                    try {
+                        getBinderService().setNotificationAssistantAccessGrantedForUser(
+                                currentComponent, userId, false);
+                    } catch (RemoteException e) {
+                        e.rethrowFromSystemServer();
+                    }
+                }
+            }
+            super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled);
+        }
     }
 
     public class NotificationListeners extends ManagedServices {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
index 1de1e4e..0b488c0 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
@@ -25,6 +25,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.INotificationManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.IPackageManager;
@@ -36,22 +37,17 @@
 import android.util.IntArray;
 import android.util.Xml;
 
-import com.android.internal.util.FastXmlSerializer;
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.NotificationManagerService.NotificationAssistants;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlSerializer;
 
 import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -65,6 +61,8 @@
     private UserManager mUm;
     @Mock
     NotificationManagerService mNm;
+    @Mock
+    private INotificationManager mINm;
 
     NotificationAssistants mAssistants;
 
@@ -83,6 +81,7 @@
         getContext().setMockPackageManager(mPm);
         getContext().addMockSystemService(Context.USER_SERVICE, mUm);
         mAssistants = spy(mNm.new NotificationAssistants(getContext(), mLock, mUserProfiles, miPm));
+        when(mNm.getBinderService()).thenReturn(mINm);
 
         List<ResolveInfo> approved = new ArrayList<>();
         ResolveInfo resolve = new ResolveInfo();
@@ -136,4 +135,30 @@
         verify(mAssistants, times(1)).addApprovedList(
                 new ComponentName("b", "b").flattenToString(),10, true);
     }
+
+    @Test
+    public void testSetPackageOrComponentEnabled_onlyOnePackage() throws Exception {
+        ComponentName component1 = ComponentName.unflattenFromString("package/Component1");
+        ComponentName component2 = ComponentName.unflattenFromString("package/Component2");
+        mAssistants.setPackageOrComponentEnabled(component1.flattenToString(), mZero.id, true,
+                true);
+        verify(mINm, never()).setNotificationAssistantAccessGrantedForUser(any(ComponentName.class),
+                eq(mZero.id), anyBoolean());
+
+        mAssistants.setPackageOrComponentEnabled(component2.flattenToString(), mZero.id, true,
+                true);
+        verify(mINm, times(1)).setNotificationAssistantAccessGrantedForUser(component1, mZero.id,
+                false);
+    }
+
+    @Test
+    public void testSetPackageOrComponentEnabled_samePackage() throws Exception {
+        ComponentName component1 = ComponentName.unflattenFromString("package/Component1");
+        mAssistants.setPackageOrComponentEnabled(component1.flattenToString(), mZero.id, true,
+                true);
+        mAssistants.setPackageOrComponentEnabled(component1.flattenToString(), mZero.id, true,
+                true);
+        verify(mINm, never()).setNotificationAssistantAccessGrantedForUser(any(ComponentName.class),
+                eq(mZero.id), anyBoolean());
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 9c6ab0a..e5f119f 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -2029,6 +2029,31 @@
     }
 
     @Test
+    public void testGetAssistantAllowedForUser() throws Exception {
+        UserHandle user = UserHandle.of(10);
+        try {
+            mBinderService.getAllowedNotificationAssistantForUser(user.getIdentifier());
+        } catch (IllegalStateException e) {
+            if (!e.getMessage().contains("At most one NotificationAssistant")) {
+                throw e;
+            }
+        }
+        verify(mAssistants, times(1)).getAllowedComponents(user.getIdentifier());
+    }
+
+    @Test
+    public void testGetAssistantAllowed() throws Exception {
+        try {
+            mBinderService.getAllowedNotificationAssistant();
+        } catch (IllegalStateException e) {
+            if (!e.getMessage().contains("At most one NotificationAssistant")) {
+                throw e;
+            }
+        }
+        verify(mAssistants, times(1)).getAllowedComponents(0);
+    }
+
+    @Test
     public void testSetDndAccessForUser() throws Exception {
         UserHandle user = UserHandle.of(10);
         ComponentName c = ComponentName.unflattenFromString("package/Component");
@@ -2089,6 +2114,54 @@
     }
 
     @Test
+    public void testSetAssistantAccess_nullWithAllowedAssistant() throws Exception {
+        ArrayList<ComponentName> componentList = new ArrayList<>();
+        ComponentName c = ComponentName.unflattenFromString("package/Component");
+        componentList.add(c);
+        when(mAssistants.getAllowedComponents(anyInt())).thenReturn(componentList);
+
+        try {
+            mBinderService.setNotificationAssistantAccessGranted(null, true);
+        } catch (SecurityException e) {
+            if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
+                throw e;
+            }
+        }
+
+        verify(mAssistants, times(1)).setPackageOrComponentEnabled(
+                c.flattenToString(), 0, true, false);
+        verify(mConditionProviders, times(1)).setPackageOrComponentEnabled(
+                c.flattenToString(), 0, false,  false);
+        verify(mListeners, never()).setPackageOrComponentEnabled(
+                any(), anyInt(), anyBoolean(), anyBoolean());
+    }
+
+    @Test
+    public void testSetAssistantAccessForUser_nullWithAllowedAssistant() throws Exception {
+        UserHandle user = UserHandle.of(10);
+        ArrayList<ComponentName> componentList = new ArrayList<>();
+        ComponentName c = ComponentName.unflattenFromString("package/Component");
+        componentList.add(c);
+        when(mAssistants.getAllowedComponents(anyInt())).thenReturn(componentList);
+
+        try {
+            mBinderService.setNotificationAssistantAccessGrantedForUser(
+                    null, user.getIdentifier(), true);
+        } catch (SecurityException e) {
+            if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
+                throw e;
+            }
+        }
+
+        verify(mAssistants, times(1)).setPackageOrComponentEnabled(
+                c.flattenToString(), user.getIdentifier(), true, false);
+        verify(mConditionProviders, times(1)).setPackageOrComponentEnabled(
+                c.flattenToString(), user.getIdentifier(), false,  false);
+        verify(mListeners, never()).setPackageOrComponentEnabled(
+                any(), anyInt(), anyBoolean(), anyBoolean());
+    }
+
+    @Test
     public void testSetDndAccess() throws Exception {
         ComponentName c = ComponentName.unflattenFromString("package/Component");
         try {