Merge "Allow NotificationListenerService to be unbound by default"
diff --git a/core/api/current.txt b/core/api/current.txt
index 1f99d47..8216f89 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -40779,6 +40779,7 @@
     method public final void requestInterruptionFilter(int);
     method public final void requestListenerHints(int);
     method public static void requestRebind(android.content.ComponentName);
+    method public static void requestUnbind(@NonNull android.content.ComponentName);
     method public final void requestUnbind();
     method public final void setNotificationsShown(String[]);
     method public final void snoozeNotification(String, long);
@@ -40796,6 +40797,7 @@
     field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3
     field public static final int INTERRUPTION_FILTER_PRIORITY = 2; // 0x2
     field public static final int INTERRUPTION_FILTER_UNKNOWN = 0; // 0x0
+    field public static final String META_DATA_DEFAULT_AUTOBIND = "android.service.notification.default_autobind_listenerservice";
     field public static final String META_DATA_DEFAULT_FILTER_TYPES = "android.service.notification.default_filter_types";
     field public static final String META_DATA_DISABLED_FILTER_TYPES = "android.service.notification.disabled_filter_types";
     field public static final int NOTIFICATION_CHANNEL_OR_GROUP_ADDED = 1; // 0x1
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index ab32f4d..ef9de18 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -147,6 +147,7 @@
 
     void requestBindListener(in ComponentName component);
     void requestUnbindListener(in INotificationListener token);
+    void requestUnbindListenerComponent(in ComponentName component);
     void requestBindProvider(in ComponentName component);
     void requestUnbindProvider(in IConditionProvider token);
 
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 4bc0d22..e55e2e5 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -141,6 +141,16 @@
             = "android.service.notification.disabled_filter_types";
 
     /**
+     * The name of the {@code meta-data} tag containing a boolean value that is used to decide if
+     * this listener should be automatically bound by default.
+     * If the value is 'false', the listener can be bound on demand using {@link #requestRebind}
+     * <p>An absent value means that the default is 'true'</p>
+     *
+     */
+    public static final String META_DATA_DEFAULT_AUTOBIND
+            = "android.service.notification.default_autobind_listenerservice";
+
+    /**
      * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
      *     Normal interruption filter.
      */
@@ -1326,6 +1336,21 @@
     /**
      * Request that the service be unbound.
      *
+     * <p>This method will fail for components that are not part of the calling app.
+     */
+    public static void requestUnbind(@NonNull ComponentName componentName) {
+        INotificationManager noMan = INotificationManager.Stub.asInterface(
+                ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+        try {
+            noMan.requestUnbindListenerComponent(componentName);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Request that the service be unbound.
+     *
      * <p>Once this is called, you will no longer receive updates and no method calls are
      * guaranteed to be successful, until you next receive the {@link #onListenerConnected()} event.
      * The service will likely be killed by the system after this call.
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 82625e4..53e841d 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -22,6 +22,7 @@
 import static android.content.Context.DEVICE_POLICY_SERVICE;
 import static android.os.UserHandle.USER_ALL;
 import static android.os.UserHandle.USER_SYSTEM;
+import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
@@ -60,6 +61,7 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseSetArray;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
@@ -390,14 +392,18 @@
             }
         }
 
+        final SparseSetArray<ComponentName> snoozingComponents;
         synchronized (mSnoozing) {
-            pw.println("    Snoozed " + getCaption() + "s ("
-                    + mSnoozing.size() + "):");
-            for (int i = 0; i < mSnoozing.size(); i++) {
-                pw.println("      User: " + mSnoozing.keyAt(i));
-                for (ComponentName name : mSnoozing.valuesAt(i)) {
-                    pw.println("        " + name.flattenToShortString());
-                }
+            snoozingComponents = new SparseSetArray<>(mSnoozing);
+        }
+        pw.println("    Snoozed " + getCaption() + "s ("
+                + snoozingComponents.size() + "):");
+        for (int i = 0; i < snoozingComponents.size(); i++) {
+            pw.println("      User: " + snoozingComponents.keyAt(i));
+            for (ComponentName name : snoozingComponents.valuesAt(i)) {
+                final ServiceInfo info = getServiceInfo(name, snoozingComponents.keyAt(i));
+                pw.println("        " + name.flattenToShortString() + (isAutobindAllowed(info) ? ""
+                        : " (META_DATA_DEFAULT_AUTOBIND=false)"));
             }
         }
     }
@@ -1432,28 +1438,30 @@
             final int userId = componentsToBind.keyAt(i);
             final Set<ComponentName> add = componentsToBind.get(userId);
             for (ComponentName component : add) {
-                try {
-                    ServiceInfo info = mPm.getServiceInfo(component,
-                            PackageManager.GET_META_DATA
-                                    | PackageManager.MATCH_DIRECT_BOOT_AWARE
-                                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
-                            userId);
-                    if (info == null) {
-                        Slog.w(TAG, "Not binding " + getCaption() + " service " + component
-                                + ": service not found");
-                        continue;
-                    }
-                    if (!mConfig.bindPermission.equals(info.permission)) {
-                        Slog.w(TAG, "Not binding " + getCaption() + " service " + component
-                                + ": it does not require the permission " + mConfig.bindPermission);
-                        continue;
-                    }
-                    Slog.v(TAG,
-                            "enabling " + getCaption() + " for " + userId + ": " + component);
-                    registerService(info, userId);
-                } catch (RemoteException e) {
-                    e.rethrowFromSystemServer();
+                ServiceInfo info = getServiceInfo(component, userId);
+                if (info == null) {
+                    Slog.w(TAG, "Not binding " + getCaption() + " service " + component
+                            + ": service not found");
+                    continue;
                 }
+                if (!mConfig.bindPermission.equals(info.permission)) {
+                    Slog.w(TAG, "Not binding " + getCaption() + " service " + component
+                            + ": it does not require the permission " + mConfig.bindPermission);
+                    continue;
+                }
+                // Do not (auto)bind if service has meta-data to explicitly disallow it
+                if (!isAutobindAllowed(info) && !isBoundOrRebinding(component, userId)) {
+                    synchronized (mSnoozing) {
+                        Slog.d(TAG, "Not binding " + getCaption() + " service " + component
+                                + ": has META_DATA_DEFAULT_AUTOBIND = false");
+                        mSnoozing.add(userId, component);
+                    }
+                    continue;
+                }
+
+                Slog.v(TAG,
+                        "enabling " + getCaption() + " for " + userId + ": " + component);
+                registerService(info, userId);
             }
         }
     }
@@ -1620,6 +1628,12 @@
         return mServicesBound.contains(servicesBindingTag);
     }
 
+    protected boolean isBoundOrRebinding(final ComponentName cn, final int userId) {
+        synchronized (mMutex) {
+            return isBound(cn, userId) || mServicesRebinding.contains(Pair.create(cn, userId));
+        }
+    }
+
     /**
      * Remove a service for the given user by ComponentName
      */
@@ -1718,6 +1732,27 @@
         }
     }
 
+    private ServiceInfo getServiceInfo(ComponentName component, int userId) {
+        try {
+            return mPm.getServiceInfo(component,
+                    PackageManager.GET_META_DATA
+                            | PackageManager.MATCH_DIRECT_BOOT_AWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+                    userId);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        return null;
+    }
+
+    private boolean isAutobindAllowed(ServiceInfo serviceInfo) {
+        if (serviceInfo != null && serviceInfo.metaData != null && serviceInfo.metaData.containsKey(
+                META_DATA_DEFAULT_AUTOBIND)) {
+            return serviceInfo.metaData.getBoolean(META_DATA_DEFAULT_AUTOBIND, true);
+        }
+        return true;
+    }
+
     public class ManagedServiceInfo implements IBinder.DeathRecipient {
         public IInterface service;
         public ComponentName component;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 81fcb61..bba1dbe 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -4547,6 +4547,27 @@
         }
 
         @Override
+        public void requestUnbindListenerComponent(ComponentName component) {
+            checkCallerIsSameApp(component.getPackageName());
+            int uid = Binder.getCallingUid();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mNotificationLock) {
+                    ManagedServices manager =
+                            mAssistants.isComponentEnabledForCurrentProfiles(component)
+                                    ? mAssistants
+                                    : mListeners;
+                    if (manager.isPackageOrComponentAllowed(component.flattenToString(),
+                            UserHandle.getUserId(uid))) {
+                        manager.setComponentState(component, UserHandle.getUserId(uid), false);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void setNotificationsShownFromListener(INotificationListener token, String[] keys) {
             final long identity = Binder.clearCallingIdentity();
             try {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 973d5d0..6f37e60 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -19,6 +19,7 @@
 import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;
 import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
 import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
+import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
 
 import static com.android.server.notification.ManagedServices.APPROVAL_BY_COMPONENT;
 import static com.android.server.notification.ManagedServices.APPROVAL_BY_PACKAGE;
@@ -35,6 +36,7 @@
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -52,8 +54,10 @@
 import android.content.pm.ServiceInfo;
 import android.content.pm.UserInfo;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IInterface;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -1820,6 +1824,156 @@
         assertTrue(profiles.isProfileUser(13));
     }
 
+    @Test
+    public void rebindServices_onlyBindsIfAutobindMetaDataTrue() throws Exception {
+        Context context = mock(Context.class);
+        PackageManager pm = mock(PackageManager.class);
+        ApplicationInfo ai = new ApplicationInfo();
+        ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
+        when(context.getPackageName()).thenReturn(mContext.getPackageName());
+        when(context.getUserId()).thenReturn(mContext.getUserId());
+        when(context.getPackageManager()).thenReturn(pm);
+        when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai);
+
+        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+        final ComponentName cn_allowed = ComponentName.unflattenFromString("anotherPackage/C1");
+        final ComponentName cn_disallowed = ComponentName.unflattenFromString("package/C1");
+
+        when(context.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            ServiceConnection sc = (ServiceConnection) args[1];
+            sc.onServiceConnected(cn_allowed, mock(IBinder.class));
+            return true;
+        });
+
+        List<ComponentName> componentNames = new ArrayList<>();
+        componentNames.add(cn_allowed);
+        componentNames.add(cn_disallowed);
+        ArrayMap<ComponentName, Bundle> metaDatas = new ArrayMap<>();
+        Bundle metaDataAutobindDisallow = new Bundle();
+        metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
+        metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
+        Bundle metaDataAutobindAllow = new Bundle();
+        metaDataAutobindAllow.putBoolean(META_DATA_DEFAULT_AUTOBIND, true);
+        metaDatas.put(cn_allowed, metaDataAutobindAllow);
+
+        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+
+        service.addApprovedList(cn_allowed.flattenToString(), 0, true);
+        service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
+
+        service.rebindServices(true, 0);
+
+        assertTrue(service.isBound(cn_allowed, 0));
+        assertFalse(service.isBound(cn_disallowed, 0));
+    }
+
+    @Test
+    public void rebindServices_bindsIfAutobindMetaDataFalseWhenServiceBound() throws Exception {
+        Context context = mock(Context.class);
+        PackageManager pm = mock(PackageManager.class);
+        ApplicationInfo ai = new ApplicationInfo();
+        ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
+        when(context.getPackageName()).thenReturn(mContext.getPackageName());
+        when(context.getUserId()).thenReturn(mContext.getUserId());
+        when(context.getPackageManager()).thenReturn(pm);
+        when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai);
+
+        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+        final ComponentName cn_disallowed = ComponentName.unflattenFromString("package/C1");
+
+        // mock isBoundOrRebinding => consider listener service bound
+        service = spy(service);
+        when(service.isBoundOrRebinding(cn_disallowed, 0)).thenReturn(true);
+
+        when(context.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            ServiceConnection sc = (ServiceConnection) args[1];
+            sc.onServiceConnected(cn_disallowed, mock(IBinder.class));
+            return true;
+        });
+
+        List<ComponentName> componentNames = new ArrayList<>();
+        componentNames.add(cn_disallowed);
+        ArrayMap<ComponentName, Bundle> metaDatas = new ArrayMap<>();
+        Bundle metaDataAutobindDisallow = new Bundle();
+        metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
+        metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
+
+        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+
+        service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
+
+        // Listener service should be bound by rebindService when forceRebind is false
+        service.rebindServices(false, 0);
+        assertTrue(service.isBound(cn_disallowed, 0));
+    }
+
+    @Test
+    public void setComponentState_ignoresAutobindMetaData() throws Exception {
+        Context context = mock(Context.class);
+        PackageManager pm = mock(PackageManager.class);
+        ApplicationInfo ai = new ApplicationInfo();
+        ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
+        when(context.getPackageName()).thenReturn(mContext.getPackageName());
+        when(context.getUserId()).thenReturn(mContext.getUserId());
+        when(context.getPackageManager()).thenReturn(pm);
+        when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai);
+
+        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+        final ComponentName cn_disallowed = ComponentName.unflattenFromString("package/C1");
+
+        when(context.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            ServiceConnection sc = (ServiceConnection) args[1];
+            sc.onServiceConnected(cn_disallowed, mock(IBinder.class));
+            return true;
+        });
+
+        List<ComponentName> componentNames = new ArrayList<>();
+        componentNames.add(cn_disallowed);
+        ArrayMap<ComponentName, Bundle> metaDatas = new ArrayMap<>();
+        Bundle metaDataAutobindDisallow = new Bundle();
+        metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
+        metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
+
+        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+
+        service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
+
+        // add component to snoozing list
+        service.setComponentState(cn_disallowed, 0, false);
+
+        // Test that setComponentState overrides the meta-data and service is bound
+        service.setComponentState(cn_disallowed, 0, true);
+        assertTrue(service.isBound(cn_disallowed, 0));
+    }
+
+    private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
+            ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
+            throws RemoteException {
+        when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
+                (Answer<ServiceInfo>) invocation -> {
+                    ComponentName invocationCn = invocation.getArgument(0);
+                    if (invocationCn != null && componentNames.contains(invocationCn)) {
+                        ServiceInfo serviceInfo = new ServiceInfo();
+                        serviceInfo.packageName = invocationCn.getPackageName();
+                        serviceInfo.name = invocationCn.getClassName();
+                        serviceInfo.permission = service.getConfig().bindPermission;
+                        serviceInfo.metaData = metaDatas.get(invocationCn);
+                        return serviceInfo;
+                    }
+                    return null;
+                }
+        );
+    }
+
     private void resetComponentsAndPackages() {
         ArrayMap<Integer, ArrayMap<Integer, String>> empty = new ArrayMap(1);
         ArrayMap<Integer, String> emptyPkgs = new ArrayMap(0);