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);