blob: ba21afdfab0f171daaf1432a6e516b02924f7391 [file] [log] [blame]
/*
* Copyright (C) 2017 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.systemui;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
import static junit.framework.TestCase.fail;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.UserIdInt;
import android.app.AppOpsManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.widget.RemoteViews;
import androidx.test.filters.SmallTest;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.systemui.appops.AppOpsController;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class ForegroundServiceControllerTest extends SysuiTestCase {
private ForegroundServiceController mFsc;
private ForegroundServiceNotificationListener mListener;
private NotificationEntryListener mEntryListener;
private final FakeSystemClock mClock = new FakeSystemClock();
@Mock private NotificationEntryManager mEntryManager;
@Mock private AppOpsController mAppOpsController;
@Mock private Handler mMainHandler;
@Mock private NotifPipeline mNotifPipeline;
@Before
public void setUp() throws Exception {
// allow the TestLooper to be asserted as the main thread these tests
allowTestableLooperAsMainThread();
MockitoAnnotations.initMocks(this);
mFsc = new ForegroundServiceController(mAppOpsController, mMainHandler);
mListener = new ForegroundServiceNotificationListener(
mContext, mFsc, mEntryManager, mNotifPipeline, mClock);
ArgumentCaptor<NotificationEntryListener> entryListenerCaptor =
ArgumentCaptor.forClass(NotificationEntryListener.class);
verify(mEntryManager).addNotificationEntryListener(
entryListenerCaptor.capture());
mEntryListener = entryListenerCaptor.getValue();
}
@Test
public void testAppOpsChangedCalledFromBgThread() {
try {
// WHEN onAppOpChanged is called from a different thread than the MainLooper
disallowTestableLooperAsMainThread();
NotificationEntry entry = createFgEntry();
mFsc.onAppOpChanged(
AppOpsManager.OP_CAMERA,
entry.getSbn().getUid(),
entry.getSbn().getPackageName(),
true);
// This test is run on the TestableLooper, which is not the MainLooper, so
// we expect an exception to be thrown
fail("onAppOpChanged shouldn't be allowed to be called from a bg thread.");
} catch (IllegalStateException e) {
// THEN expect an exception
}
}
@Test
public void testAppOpsCRUD() {
// no crash on remove that doesn't exist
mFsc.onAppOpChanged(9, 1000, "pkg1", false);
assertNull(mFsc.getAppOps(0, "pkg1"));
// multiuser & multipackage
mFsc.onAppOpChanged(8, 50, "pkg1", true);
mFsc.onAppOpChanged(1, 60, "pkg3", true);
mFsc.onAppOpChanged(7, 500000, "pkg2", true);
assertEquals(1, mFsc.getAppOps(0, "pkg1").size());
assertTrue(mFsc.getAppOps(0, "pkg1").contains(8));
assertEquals(1, mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").size());
assertTrue(mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").contains(7));
assertEquals(1, mFsc.getAppOps(0, "pkg3").size());
assertTrue(mFsc.getAppOps(0, "pkg3").contains(1));
// multiple ops for the same package
mFsc.onAppOpChanged(9, 50, "pkg1", true);
mFsc.onAppOpChanged(5, 50, "pkg1", true);
assertEquals(3, mFsc.getAppOps(0, "pkg1").size());
assertTrue(mFsc.getAppOps(0, "pkg1").contains(8));
assertTrue(mFsc.getAppOps(0, "pkg1").contains(9));
assertTrue(mFsc.getAppOps(0, "pkg1").contains(5));
assertEquals(1, mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").size());
assertTrue(mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").contains(7));
// remove one of the multiples
mFsc.onAppOpChanged(9, 50, "pkg1", false);
assertEquals(2, mFsc.getAppOps(0, "pkg1").size());
assertTrue(mFsc.getAppOps(0, "pkg1").contains(8));
assertTrue(mFsc.getAppOps(0, "pkg1").contains(5));
// remove last op
mFsc.onAppOpChanged(1, 60, "pkg3", false);
assertNull(mFsc.getAppOps(0, "pkg3"));
}
@Test
public void testDisclosurePredicate() {
StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, "com.example.app1",
5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
StatusBarNotification sbn_user1_disclosure = makeMockSBN(USERID_ONE, "android",
SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
null, Notification.FLAG_NO_CLEAR);
assertTrue(mFsc.isDisclosureNotification(sbn_user1_disclosure));
assertFalse(mFsc.isDisclosureNotification(sbn_user1_app1));
}
@Test
public void testNeedsDisclosureAfterRemovingUnrelatedNotification() {
final String PKG1 = "com.example.app100";
StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1,
5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, PKG1);
// first add a normal notification
entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
// nothing required yet
assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
// now the app starts a fg service
entryAdded(makeMockDisclosure(USERID_ONE, new String[]{PKG1}),
NotificationManager.IMPORTANCE_DEFAULT);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
// add the fg notification
entryAdded(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); // app1 has got it covered
// remove the boring notification
entryRemoved(sbn_user1_app1);
assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); // app1 has STILL got it covered
entryRemoved(sbn_user1_app1_fg);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
}
@Test
public void testSimpleAddRemove() {
final String PKG1 = "com.example.app1";
final String PKG2 = "com.example.app2";
StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1,
5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
// no services are "running"
entryAdded(makeMockDisclosure(USERID_ONE, null),
NotificationManager.IMPORTANCE_DEFAULT);
assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
entryUpdated(makeMockDisclosure(USERID_ONE, new String[]{PKG1}),
NotificationManager.IMPORTANCE_DEFAULT);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
// switch to different package
entryUpdated(makeMockDisclosure(USERID_ONE, new String[]{PKG2}),
NotificationManager.IMPORTANCE_DEFAULT);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
entryUpdated(makeMockDisclosure(USERID_TWO, new String[]{PKG1}),
NotificationManager.IMPORTANCE_DEFAULT);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
assertTrue(mFsc.isDisclosureNeededForUser(USERID_TWO)); // finally user2 needs one too
entryUpdated(makeMockDisclosure(USERID_ONE, new String[]{PKG2, PKG1}),
NotificationManager.IMPORTANCE_DEFAULT);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
assertTrue(mFsc.isDisclosureNeededForUser(USERID_TWO));
entryRemoved(makeMockDisclosure(USERID_ONE, null /*unused*/));
assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
assertTrue(mFsc.isDisclosureNeededForUser(USERID_TWO));
entryRemoved(makeMockDisclosure(USERID_TWO, null /*unused*/));
assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
}
@Test
public void testDisclosureBasic() {
final String PKG1 = "com.example.app0";
StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1,
5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, PKG1);
entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT); // not fg
entryAdded(makeMockDisclosure(USERID_ONE, new String[]{PKG1}),
NotificationManager.IMPORTANCE_DEFAULT);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
entryAdded(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); // app1 has got it covered
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
// let's take out the other notification and see what happens.
entryRemoved(sbn_user1_app1);
assertFalse(
mFsc.isDisclosureNeededForUser(USERID_ONE)); // still covered by sbn_user1_app1_fg
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
// let's attempt to downgrade the notification from FLAG_FOREGROUND and see what we get
StatusBarNotification sbn_user1_app1_fg_sneaky = makeMockFgSBN(USERID_ONE, PKG1);
sbn_user1_app1_fg_sneaky.getNotification().flags = 0;
entryUpdated(sbn_user1_app1_fg_sneaky,
NotificationManager.IMPORTANCE_DEFAULT);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
// ok, ok, we'll put it back
sbn_user1_app1_fg_sneaky.getNotification().flags = Notification.FLAG_FOREGROUND_SERVICE;
entryUpdated(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
entryRemoved(sbn_user1_app1_fg_sneaky);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
// now let's test an upgrade
entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE;
entryUpdated(sbn_user1_app1,
NotificationManager.IMPORTANCE_DEFAULT); // this is now a fg notification
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
// remove it, make sure we're out of compliance again
entryRemoved(sbn_user1_app1); // was fg, should return true
entryRemoved(sbn_user1_app1);
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
// importance upgrade
entryAdded(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN);
assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE;
entryUpdated(sbn_user1_app1_fg,
NotificationManager.IMPORTANCE_DEFAULT); // this is now a fg notification
// finally, let's turn off the service
entryAdded(makeMockDisclosure(USERID_ONE, null),
NotificationManager.IMPORTANCE_DEFAULT);
assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
}
@Test
public void testNoNotifsNorAppOps_noSystemAlertWarningRequired() {
// no notifications nor app op signals that this package/userId requires system alert
// warning
assertFalse(mFsc.isSystemAlertWarningNeeded(USERID_ONE, "any"));
}
@Test
public void testCustomLayouts_systemAlertWarningRequired() {
// GIVEN a notification with a custom layout
final String pkg = "com.example.app0";
StatusBarNotification customLayoutNotif = makeMockSBN(USERID_ONE, pkg, 0,
false);
// WHEN the custom layout entry is added
entryAdded(customLayoutNotif, NotificationManager.IMPORTANCE_MIN);
// THEN a system alert warning is required since there aren't any notifications that can
// display the app ops
assertTrue(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg));
}
@Test
public void testStandardLayoutExists_noSystemAlertWarningRequired() {
// GIVEN two notifications (one with a custom layout, the other with a standard layout)
final String pkg = "com.example.app0";
StatusBarNotification customLayoutNotif = makeMockSBN(USERID_ONE, pkg, 0,
false);
StatusBarNotification standardLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, true);
// WHEN the entries are added
entryAdded(customLayoutNotif, NotificationManager.IMPORTANCE_MIN);
entryAdded(standardLayoutNotif, NotificationManager.IMPORTANCE_MIN);
// THEN no system alert warning is required, since there is at least one notification
// with a standard layout that can display the app ops on the notification
assertFalse(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg));
}
@Test
public void testStandardLayoutRemoved_systemAlertWarningRequired() {
// GIVEN two notifications (one with a custom layout, the other with a standard layout)
final String pkg = "com.example.app0";
StatusBarNotification customLayoutNotif = makeMockSBN(USERID_ONE, pkg, 0,
false);
StatusBarNotification standardLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, true);
// WHEN the entries are added and then the standard layout notification is removed
entryAdded(customLayoutNotif, NotificationManager.IMPORTANCE_MIN);
entryAdded(standardLayoutNotif, NotificationManager.IMPORTANCE_MIN);
entryRemoved(standardLayoutNotif);
// THEN a system alert warning is required since there aren't any notifications that can
// display the app ops
assertTrue(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg));
}
@Test
public void testStandardLayoutUpdatedToCustomLayout_systemAlertWarningRequired() {
// GIVEN a standard layout notification and then an updated version with a customLayout
final String pkg = "com.example.app0";
StatusBarNotification standardLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, true);
StatusBarNotification updatedToCustomLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, false);
// WHEN the entries is added and then updated to a custom layout
entryAdded(standardLayoutNotif, NotificationManager.IMPORTANCE_MIN);
entryUpdated(updatedToCustomLayoutNotif, NotificationManager.IMPORTANCE_MIN);
// THEN a system alert warning is required since there aren't any notifications that can
// display the app ops
assertTrue(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg));
}
private StatusBarNotification makeMockSBN(int userId, String pkg, int id, String tag,
int flags) {
final Notification n = mock(Notification.class);
n.extras = new Bundle();
n.flags = flags;
return makeMockSBN(userId, pkg, id, tag, n);
}
private StatusBarNotification makeMockSBN(int userid, String pkg, int id, String tag,
Notification n) {
final StatusBarNotification sbn = mock(StatusBarNotification.class);
when(sbn.getNotification()).thenReturn(n);
when(sbn.getId()).thenReturn(id);
when(sbn.getPackageName()).thenReturn(pkg);
when(sbn.getTag()).thenReturn(tag);
when(sbn.getUserId()).thenReturn(userid);
when(sbn.getUser()).thenReturn(new UserHandle(userid));
when(sbn.getKey()).thenReturn("MOCK:"+userid+"|"+pkg+"|"+id+"|"+tag);
return sbn;
}
private StatusBarNotification makeMockSBN(int uid, String pkg, int id,
boolean usesStdLayout) {
StatusBarNotification sbn = makeMockSBN(uid, pkg, id, "foo", 0);
if (usesStdLayout) {
sbn.getNotification().contentView = null;
sbn.getNotification().headsUpContentView = null;
sbn.getNotification().bigContentView = null;
} else {
sbn.getNotification().contentView = mock(RemoteViews.class);
}
return sbn;
}
private StatusBarNotification makeMockFgSBN(int uid, String pkg, int id,
boolean usesStdLayout) {
StatusBarNotification sbn =
makeMockSBN(uid, pkg, id, "foo", Notification.FLAG_FOREGROUND_SERVICE);
if (usesStdLayout) {
sbn.getNotification().contentView = null;
sbn.getNotification().headsUpContentView = null;
sbn.getNotification().bigContentView = null;
} else {
sbn.getNotification().contentView = mock(RemoteViews.class);
}
return sbn;
}
private StatusBarNotification makeMockFgSBN(int uid, String pkg) {
return makeMockSBN(uid, pkg, 1000, "foo", Notification.FLAG_FOREGROUND_SERVICE);
}
private StatusBarNotification makeMockDisclosure(int userid, String[] pkgs) {
final Notification n = mock(Notification.class);
n.flags = Notification.FLAG_ONGOING_EVENT;
final Bundle extras = new Bundle();
if (pkgs != null) extras.putStringArray(Notification.EXTRA_FOREGROUND_APPS, pkgs);
n.extras = extras;
n.when = System.currentTimeMillis() - 10000; // ten seconds ago
final StatusBarNotification sbn = makeMockSBN(userid, "android",
SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
null, n);
sbn.getNotification().extras = extras;
return sbn;
}
private NotificationEntry addFgEntry() {
NotificationEntry entry = createFgEntry();
mEntryListener.onPendingEntryAdded(entry);
return entry;
}
private NotificationEntry createFgEntry() {
return new NotificationEntryBuilder()
.setSbn(makeMockFgSBN(0, TEST_PACKAGE_NAME, 1000, true))
.setImportance(NotificationManager.IMPORTANCE_DEFAULT)
.build();
}
private void entryRemoved(StatusBarNotification notification) {
mEntryListener.onEntryRemoved(
new NotificationEntryBuilder()
.setSbn(notification)
.build(),
null,
false,
REASON_APP_CANCEL);
}
private void entryAdded(StatusBarNotification notification, int importance) {
NotificationEntry entry = new NotificationEntryBuilder()
.setSbn(notification)
.setImportance(importance)
.build();
mEntryListener.onPendingEntryAdded(entry);
}
private void entryUpdated(StatusBarNotification notification, int importance) {
NotificationEntry entry = new NotificationEntryBuilder()
.setSbn(notification)
.setImportance(importance)
.build();
mEntryListener.onPreEntryUpdated(entry);
}
@UserIdInt private static final int USERID_ONE = 10; // UserManagerService.MIN_USER_ID;
@UserIdInt private static final int USERID_TWO = USERID_ONE + 1;
private static final String TEST_PACKAGE_NAME = "test";
}