blob: 57e403c21ac8d635b254cd8063eac56887ea062e [file] [log] [blame]
/*
* Copyright (C) 2020 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.server.notification;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_SILENT;
import static com.android.server.notification.NotificationManagerService.NotificationListeners.TAG_REQUESTED_LISTENERS;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.content.pm.VersionedPackage;
import android.os.Bundle;
import android.os.UserHandle;
import android.service.notification.NotificationListenerFilter;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationRankingUpdate;
import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
import android.testing.TestableContext;
import android.util.ArraySet;
import android.util.Pair;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;
import com.android.server.UiServiceTestCase;
import com.google.common.collect.ImmutableList;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.internal.util.reflection.FieldSetter;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.List;
public class NotificationListenersTest extends UiServiceTestCase {
@Mock
private PackageManager mPm;
@Mock
private IPackageManager miPm;
@Mock
NotificationManagerService mNm;
@Mock
private INotificationManager mINm;
private TestableContext mContext = spy(getContext());
NotificationManagerService.NotificationListeners mListeners;
private ComponentName mCn1 = new ComponentName("pkg", "pkg.cmp");
private ComponentName mCn2 = new ComponentName("pkg2", "pkg2.cmp2");
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
getContext().setMockPackageManager(mPm);
doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any());
mListeners = spy(mNm.new NotificationListeners(
mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm));
when(mNm.getBinderService()).thenReturn(mINm);
}
@Test
public void testReadExtraTag() throws Exception {
String xml = "<" + TAG_REQUESTED_LISTENERS+ ">"
+ "<listener component=\"" + mCn1.flattenToString() + "\" user=\"0\">"
+ "<allowed types=\"7\" />"
+ "</listener>"
+ "<listener component=\"" + mCn2.flattenToString() + "\" user=\"10\">"
+ "<allowed types=\"4\" />"
+ "<disallowed pkg=\"pkg1\" uid=\"243\"/>"
+ "</listener>"
+ "</" + TAG_REQUESTED_LISTENERS + ">";
TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.getBytes())), null);
parser.nextTag();
mListeners.readExtraTag(TAG_REQUESTED_LISTENERS, parser);
validateListenersFromXml();
}
@Test
public void testWriteExtraTag() throws Exception {
NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>());
VersionedPackage a1 = new VersionedPackage("pkg1", 243);
NotificationListenerFilter nlf2 =
new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1}));
mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf);
mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2);
TypedXmlSerializer serializer = Xml.newFastSerializer();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
serializer.startDocument(null, true);
mListeners.writeExtraXmlTags(serializer);
serializer.endDocument();
serializer.flush();
TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(baos.toByteArray())), null);
parser.nextTag();
mListeners.readExtraTag("req_listeners", parser);
validateListenersFromXml();
}
private void validateListenersFromXml() {
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0)).getTypes())
.isEqualTo(7);
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0))
.getDisallowedPackages())
.isEmpty();
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 10)).getTypes())
.isEqualTo(4);
VersionedPackage a1 = new VersionedPackage("pkg1", 243);
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 10))
.getDisallowedPackages())
.contains(a1);
}
@Test
public void testOnUserRemoved() {
NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>());
VersionedPackage a1 = new VersionedPackage("pkg1", 243);
NotificationListenerFilter nlf2 =
new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1}));
mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf);
mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2);
mListeners.onUserRemoved(0);
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0))).isNull();
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 10)).getTypes())
.isEqualTo(4);
}
@Test
public void testEnsureFilters_newServiceNoMetadata() {
ServiceInfo si = new ServiceInfo();
si.packageName = "new2";
si.name = "comp2";
mListeners.ensureFilters(si, 0);
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 0))).isNull();
}
@Test
public void testEnsureFilters_preExisting() {
// one exists already, say from xml
VersionedPackage a1 = new VersionedPackage("pkg1", 243);
NotificationListenerFilter nlf =
new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1}));
mListeners.setNotificationListenerFilter(Pair.create(mCn2, 0), nlf);
ServiceInfo siOld = new ServiceInfo();
siOld.packageName = mCn2.getPackageName();
siOld.name = mCn2.getClassName();
mListeners.ensureFilters(siOld, 0);
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 0))).isEqualTo(nlf);
}
@Test
public void testEnsureFilters_newServiceWithMetadata() {
ServiceInfo si = new ServiceInfo();
si.packageName = "new";
si.name = "comp";
si.metaData = new Bundle();
si.metaData.putString(NotificationListenerService.META_DATA_DEFAULT_FILTER_TYPES, "1|2");
mListeners.ensureFilters(si, 0);
assertThat(mListeners.getNotificationListenerFilter(
Pair.create(si.getComponentName(), 0)).getTypes())
.isEqualTo(FLAG_FILTER_TYPE_CONVERSATIONS | FLAG_FILTER_TYPE_ALERTING);
}
@Test
public void testEnsureFilters_newServiceWithMetadata_namesNotNumbers() {
ServiceInfo si = new ServiceInfo();
si.packageName = "new";
si.name = "comp";
si.metaData = new Bundle();
si.metaData.putString(NotificationListenerService.META_DATA_DEFAULT_FILTER_TYPES,
"conversations|ALERTING");
mListeners.ensureFilters(si, 0);
assertThat(mListeners.getNotificationListenerFilter(
Pair.create(si.getComponentName(), 0)).getTypes())
.isEqualTo(FLAG_FILTER_TYPE_CONVERSATIONS | FLAG_FILTER_TYPE_ALERTING);
}
@Test
public void testEnsureFilters_newServiceWithMetadata_onlyOneListed() {
ServiceInfo si = new ServiceInfo();
si.packageName = "new";
si.name = "comp";
si.metaData = new Bundle();
si.metaData.putInt(NotificationListenerService.META_DATA_DEFAULT_FILTER_TYPES, 2);
mListeners.ensureFilters(si, 0);
assertThat(mListeners.getNotificationListenerFilter(
Pair.create(si.getComponentName(), 0)).getTypes())
.isEqualTo(FLAG_FILTER_TYPE_ALERTING);
}
@Test
public void testEnsureFilters_newServiceWithMetadata_disabledTypes() {
ServiceInfo si = new ServiceInfo();
si.packageName = "new";
si.name = "comp";
si.metaData = new Bundle();
si.metaData.putString(NotificationListenerService.META_DATA_DISABLED_FILTER_TYPES, "1|2");
mListeners.ensureFilters(si, 0);
assertThat(mListeners.getNotificationListenerFilter(
Pair.create(si.getComponentName(), 0)).getTypes())
.isEqualTo(FLAG_FILTER_TYPE_SILENT | FLAG_FILTER_TYPE_ONGOING);
}
@Test
public void testEnsureFilters_newServiceWithMetadata_disabledTypes_mixedText() {
ServiceInfo si = new ServiceInfo();
si.packageName = "new";
si.name = "comp";
si.metaData = new Bundle();
si.metaData.putString(NotificationListenerService.META_DATA_DISABLED_FILTER_TYPES,
"1|alerting");
mListeners.ensureFilters(si, 0);
assertThat(mListeners.getNotificationListenerFilter(
Pair.create(si.getComponentName(), 0)).getTypes())
.isEqualTo(FLAG_FILTER_TYPE_SILENT | FLAG_FILTER_TYPE_ONGOING);
}
@Test
public void testEnsureFilters_newServiceWithMetadata_metaDataDisagrees() {
ServiceInfo si = new ServiceInfo();
si.packageName = "new";
si.name = "comp";
si.metaData = new Bundle();
si.metaData.putString(NotificationListenerService.META_DATA_DEFAULT_FILTER_TYPES, "1|2");
si.metaData.putInt(NotificationListenerService.META_DATA_DISABLED_FILTER_TYPES, 1);
mListeners.ensureFilters(si, 0);
assertThat(mListeners.getNotificationListenerFilter(
Pair.create(si.getComponentName(), 0)).getTypes())
.isEqualTo(FLAG_FILTER_TYPE_ALERTING);
}
@Test
public void testEnsureFilters_newServiceWithEmptyMetadata() {
ServiceInfo si = new ServiceInfo();
si.packageName = "new";
si.name = "comp";
si.metaData = new Bundle();
si.metaData.putString(NotificationListenerService.META_DATA_DEFAULT_FILTER_TYPES, "");
mListeners.ensureFilters(si, 0);
assertThat(mListeners.getNotificationListenerFilter(
Pair.create(si.getComponentName(), 0)).getTypes())
.isEqualTo(0);
}
@Test
public void testOnPackageChanged() {
NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>());
VersionedPackage a1 = new VersionedPackage("pkg1", 243);
NotificationListenerFilter nlf2 =
new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1}));
mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf);
mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2);
String[] pkgs = new String[] {mCn1.getPackageName()};
int[] uids = new int[] {1};
mListeners.onPackagesChanged(false, pkgs, uids);
// not removing; no change
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0)).getTypes())
.isEqualTo(7);
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 10)).getTypes())
.isEqualTo(4);
}
@Test
public void testOnPackageChanged_removing() {
NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>());
VersionedPackage a1 = new VersionedPackage("pkg1", 243);
NotificationListenerFilter nlf2 =
new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1}));
mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf);
mListeners.setNotificationListenerFilter(Pair.create(mCn2, 0), nlf2);
String[] pkgs = new String[] {mCn1.getPackageName()};
int[] uids = new int[] {1};
mListeners.onPackagesChanged(true, pkgs, uids);
// only mCn1 removed
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0))).isNull();
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 0)).getTypes())
.isEqualTo(4);
}
@Test
public void testOnPackageChanged_removingDisallowedPackage() {
NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>());
VersionedPackage a1 = new VersionedPackage("pkg1", 243);
NotificationListenerFilter nlf2 =
new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1}));
mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf);
mListeners.setNotificationListenerFilter(Pair.create(mCn2, 0), nlf2);
String[] pkgs = new String[] {"pkg1"};
int[] uids = new int[] {243};
mListeners.onPackagesChanged(true, pkgs, uids);
assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0))
.getDisallowedPackages()).isEmpty();
}
@Test
public void testHasAllowedListener() {
final int uid1 = 1, uid2 = 2;
// enable mCn1 but not mCn2 for uid1
mListeners.addApprovedList(mCn1.flattenToString(), uid1, true);
// verify that:
// the package for mCn1 has an allowed listener for uid1 and not uid2
assertTrue(mListeners.hasAllowedListener(mCn1.getPackageName(), uid1));
assertFalse(mListeners.hasAllowedListener(mCn1.getPackageName(), uid2));
// and that mCn2 has no allowed listeners for either user id
assertFalse(mListeners.hasAllowedListener(mCn2.getPackageName(), uid1));
assertFalse(mListeners.hasAllowedListener(mCn2.getPackageName(), uid2));
}
@Test
public void testBroadcastUsers() {
int userId = 0;
mListeners.setPackageOrComponentEnabled(mCn1.flattenToString(), userId, true, false, true);
verify(mContext).sendBroadcastAsUser(
any(), eq(UserHandle.of(userId)), nullable(String.class));
}
@Test
public void testImplicitGrant() {
String pkg = "pkg";
int uid = 9;
NotificationChannel channel = new NotificationChannel("id", "name",
NotificationManager.IMPORTANCE_HIGH);
Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
.setContentTitle("foo")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
.setTimeoutAfter(1);
StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
nb.build(), UserHandle.getUserHandleForUid(uid), null, 0);
NotificationRecord r = new NotificationRecord(mContext, sbn, channel);
ManagedServices.ManagedServiceInfo info = mListeners.new ManagedServiceInfo(
null, new ComponentName("a", "a"), sbn.getUserId(), false, null, 33, 33);
List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(info);
when(mListeners.getServices()).thenReturn(services);
when(mNm.isVisibleToListener(any(), anyInt(), any())).thenReturn(true);
when(mNm.makeRankingUpdateLocked(info)).thenReturn(mock(NotificationRankingUpdate.class));
mNm.mPackageManagerInternal = mPmi;
mListeners.notifyPostedLocked(r, null);
verify(mPmi).grantImplicitAccess(sbn.getUserId(), null, UserHandle.getAppId(33),
sbn.getUid(), false, false);
}
@Test
public void testNotifyPostedLockedInLockdownMode() {
NotificationRecord r0 = mock(NotificationRecord.class);
NotificationRecord old0 = mock(NotificationRecord.class);
UserHandle uh0 = mock(UserHandle.class);
NotificationRecord r1 = mock(NotificationRecord.class);
NotificationRecord old1 = mock(NotificationRecord.class);
UserHandle uh1 = mock(UserHandle.class);
// Neither user0 and user1 is in the lockdown mode
when(r0.getUser()).thenReturn(uh0);
when(uh0.getIdentifier()).thenReturn(0);
when(mNm.isInLockDownMode(0)).thenReturn(false);
when(r1.getUser()).thenReturn(uh1);
when(uh1.getIdentifier()).thenReturn(1);
when(mNm.isInLockDownMode(1)).thenReturn(false);
mListeners.notifyPostedLocked(r0, old0, true);
mListeners.notifyPostedLocked(r0, old0, false);
verify(r0, atLeast(2)).getSbn();
mListeners.notifyPostedLocked(r1, old1, true);
mListeners.notifyPostedLocked(r1, old1, false);
verify(r1, atLeast(2)).getSbn();
// Reset
reset(r0);
reset(old0);
reset(r1);
reset(old1);
// Only user 0 is in the lockdown mode
when(r0.getUser()).thenReturn(uh0);
when(uh0.getIdentifier()).thenReturn(0);
when(mNm.isInLockDownMode(0)).thenReturn(true);
when(r1.getUser()).thenReturn(uh1);
when(uh1.getIdentifier()).thenReturn(1);
when(mNm.isInLockDownMode(1)).thenReturn(false);
mListeners.notifyPostedLocked(r0, old0, true);
mListeners.notifyPostedLocked(r0, old0, false);
verify(r0, never()).getSbn();
mListeners.notifyPostedLocked(r1, old1, true);
mListeners.notifyPostedLocked(r1, old1, false);
verify(r1, atLeast(2)).getSbn();
}
@Test
public void testNotifyRemovedLockedInLockdownMode() throws NoSuchFieldException {
NotificationRecord r0 = mock(NotificationRecord.class);
NotificationStats rs0 = mock(NotificationStats.class);
UserHandle uh0 = mock(UserHandle.class);
NotificationRecord r1 = mock(NotificationRecord.class);
NotificationStats rs1 = mock(NotificationStats.class);
UserHandle uh1 = mock(UserHandle.class);
StatusBarNotification sbn = mock(StatusBarNotification.class);
FieldSetter.setField(mNm,
NotificationManagerService.class.getDeclaredField("mHandler"),
mock(NotificationManagerService.WorkerHandler.class));
// Neither user0 and user1 is in the lockdown mode
when(r0.getUser()).thenReturn(uh0);
when(uh0.getIdentifier()).thenReturn(0);
when(mNm.isInLockDownMode(0)).thenReturn(false);
when(r0.getSbn()).thenReturn(sbn);
when(r1.getUser()).thenReturn(uh1);
when(uh1.getIdentifier()).thenReturn(1);
when(mNm.isInLockDownMode(1)).thenReturn(false);
when(r1.getSbn()).thenReturn(sbn);
mListeners.notifyRemovedLocked(r0, 0, rs0);
mListeners.notifyRemovedLocked(r0, 0, rs0);
verify(r0, atLeast(2)).getSbn();
mListeners.notifyRemovedLocked(r1, 0, rs1);
mListeners.notifyRemovedLocked(r1, 0, rs1);
verify(r1, atLeast(2)).getSbn();
// Reset
reset(r0);
reset(rs0);
reset(r1);
reset(rs1);
// Only user 0 is in the lockdown mode
when(r0.getUser()).thenReturn(uh0);
when(uh0.getIdentifier()).thenReturn(0);
when(mNm.isInLockDownMode(0)).thenReturn(true);
when(r0.getSbn()).thenReturn(sbn);
when(r1.getUser()).thenReturn(uh1);
when(uh1.getIdentifier()).thenReturn(1);
when(mNm.isInLockDownMode(1)).thenReturn(false);
when(r1.getSbn()).thenReturn(sbn);
mListeners.notifyRemovedLocked(r0, 0, rs0);
mListeners.notifyRemovedLocked(r0, 0, rs0);
verify(r0, never()).getSbn();
mListeners.notifyRemovedLocked(r1, 0, rs1);
mListeners.notifyRemovedLocked(r1, 0, rs1);
verify(r1, atLeast(2)).getSbn();
}
}