From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Wenhao Wang <wenhaowang@google.com>
Date: Wed, 2 Feb 2022 10:56:44 -0800
Subject: [PATCH] DO NOT MERGE Suppress notifications when device enter
 lockdown

This CL makes the following modifcations:
1. Add LockPatternUtils.StrongAuthTracker to monitor
the lockdown mode status of the phone.
2. Call mListeners.notifyRemovedLocked with all the
notifications in the mNotificationList when entering
the lockdown mode.
3. Call mListeners.notifyPostedLocked with all the
notifications in the mNotificationList when exiting
the lockdown mode.
4. Dismiss the function calls of notifyPostedLocked,
notifyRemovedLocked, and notifyRankingUpdateLocked
during the lockdown mode.

The CL also adds corresponding tests.

Bug: 173721373
Test: atest NotificationManagerServiceTest
Test: atest NotificationListenersTest
Test: manually verify the paired device cannot receive
notifications when the host phone is in lockdown mode.
Ignore-AOSP-First: pending fix for a security issue.

Change-Id: I7e83544863eeadf8272b6ff8a9bb8136d6466203
Merged-In: I7e83544863eeadf8272b6ff8a9bb8136d6466203
(cherry picked from commit 3cb6842a053e236cc98d7616ba4433c31ffda3ac)
(cherry picked from commit 85c00b98a6cac8d7286a70300ceff509693818f2)
Merged-In: I7e83544863eeadf8272b6ff8a9bb8136d6466203
---
 .../NotificationManagerService.java           | 109 +++++++++++++-
 .../tests/uiservicestests/AndroidManifest.xml |   1 +
 .../NotificationListenersTest.java            | 135 ++++++++++++++++++
 .../NotificationManagerServiceTest.java       |  66 +++++++++
 4 files changed, 305 insertions(+), 6 deletions(-)
 create mode 100644 services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java

diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index dd202a172d66..0dfc0ab0c3e0 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -181,6 +181,7 @@ import android.util.AtomicFile;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.TimeUtils;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
@@ -202,6 +203,7 @@ import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.internal.widget.LockPatternUtils;
 import com.android.server.DeviceIdleController;
 import com.android.server.EventLogTags;
 import com.android.server.LocalServices;
@@ -1272,6 +1274,54 @@ public class NotificationManagerService extends SystemService {
         return out;
     }
 
+    protected class StrongAuthTracker extends LockPatternUtils.StrongAuthTracker {
+
+        SparseBooleanArray mUserInLockDownMode = new SparseBooleanArray();
+        boolean mIsInLockDownMode = false;
+
+        StrongAuthTracker(Context context) {
+            super(context);
+        }
+
+        private boolean containsFlag(int haystack, int needle) {
+            return (haystack & needle) != 0;
+        }
+
+        public boolean isInLockDownMode() {
+            return mIsInLockDownMode;
+        }
+
+        @Override
+        public synchronized void onStrongAuthRequiredChanged(int userId) {
+            boolean userInLockDownModeNext = containsFlag(getStrongAuthForUser(userId),
+                    STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
+            mUserInLockDownMode.put(userId, userInLockDownModeNext);
+            boolean isInLockDownModeNext = mUserInLockDownMode.indexOfValue(true) != -1;
+
+            if (mIsInLockDownMode == isInLockDownModeNext) {
+                return;
+            }
+
+            if (isInLockDownModeNext) {
+                cancelNotificationsWhenEnterLockDownMode();
+            }
+
+            // When the mIsInLockDownMode is true, both notifyPostedLocked and
+            // notifyRemovedLocked will be dismissed. So we shall call
+            // cancelNotificationsWhenEnterLockDownMode before we set mIsInLockDownMode
+            // as true and call postNotificationsWhenExitLockDownMode after we set
+            // mIsInLockDownMode as false.
+            mIsInLockDownMode = isInLockDownModeNext;
+
+            if (!isInLockDownModeNext) {
+                postNotificationsWhenExitLockDownMode();
+            }
+        }
+    }
+
+    private LockPatternUtils mLockPatternUtils;
+    private StrongAuthTracker mStrongAuthTracker;
+
     public NotificationManagerService(Context context) {
         super(context);
         Notification.processWhitelistToken = WHITELIST_TOKEN;
@@ -1283,6 +1333,11 @@ public class NotificationManagerService extends SystemService {
         mAudioManager = audioMananger;
     }
 
+    @VisibleForTesting
+    void setStrongAuthTracker(StrongAuthTracker strongAuthTracker) {
+        mStrongAuthTracker = strongAuthTracker;
+    }
+
     @VisibleForTesting
     void setHints(int hints) {
         mListenerHints = hints;
@@ -1435,6 +1490,8 @@ public class NotificationManagerService extends SystemService {
 
         mHandler = new WorkerHandler(looper);
         mRankingThread.start();
+        mLockPatternUtils = new LockPatternUtils(getContext());
+        mStrongAuthTracker = new StrongAuthTracker(getContext());
         String[] extractorNames;
         try {
             extractorNames = resources.getStringArray(R.array.config_notificationSignalExtractors);
@@ -1563,7 +1620,8 @@ public class NotificationManagerService extends SystemService {
         init(Looper.myLooper(),
                 AppGlobals.getPackageManager(), getContext().getPackageManager(),
                 getLocalService(LightsManager.class),
-                new NotificationListeners(AppGlobals.getPackageManager()),
+                new NotificationListeners(getContext(), mNotificationLock, mUserProfiles,
+                        AppGlobals.getPackageManager()),
                 new NotificationAssistants(getContext(), mNotificationLock, mUserProfiles,
                         AppGlobals.getPackageManager()),
                 new ConditionProviders(getContext(), mUserProfiles, AppGlobals.getPackageManager()),
@@ -1679,6 +1737,7 @@ public class NotificationManagerService extends SystemService {
             mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
             mKeyguardManager = getContext().getSystemService(KeyguardManager.class);
             mZenModeHelper.onSystemReady();
+            mLockPatternUtils.registerStrongAuthTracker(mStrongAuthTracker);
         } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
             // This observer will force an update when observe is called, causing us to
             // bind to listener services.
@@ -6342,6 +6401,29 @@ public class NotificationManagerService extends SystemService {
         }
     }
 
+    private void cancelNotificationsWhenEnterLockDownMode() {
+        synchronized (mNotificationLock) {
+            int numNotifications = mNotificationList.size();
+            for (int i = 0; i < numNotifications; i++) {
+                NotificationRecord rec = mNotificationList.get(i);
+                mListeners.notifyRemovedLocked(rec, REASON_CANCEL_ALL,
+                        rec.getStats());
+            }
+
+        }
+    }
+
+    private void postNotificationsWhenExitLockDownMode() {
+        synchronized (mNotificationLock) {
+            int numNotifications = mNotificationList.size();
+            for (int i = 0; i < numNotifications; i++) {
+                NotificationRecord rec = mNotificationList.get(i);
+                mListeners.notifyPostedLocked(rec, rec);
+            }
+
+        }
+    }
+
     private void updateNotificationPulse() {
         synchronized (mNotificationLock) {
             updateLightsLocked();
@@ -6502,6 +6584,10 @@ public class NotificationManagerService extends SystemService {
                 channels, overridePeople, snoozeCriteria, showBadge, userSentiment, hidden);
     }
 
+    boolean isInLockDownMode() {
+        return mStrongAuthTracker.isInLockDownMode();
+    }
+
     boolean hasCompanionDevice(ManagedServiceInfo info) {
         if (mCompanionManager == null) {
             mCompanionManager = getCompanionManager();
@@ -6730,9 +6816,9 @@ public class NotificationManagerService extends SystemService {
 
         private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>();
 
-        public NotificationListeners(IPackageManager pm) {
-            super(getContext(), mNotificationLock, mUserProfiles, pm);
-
+        public NotificationListeners(Context context, Object lock, UserProfiles userProfiles,
+                IPackageManager pm) {
+            super(context, lock, userProfiles, pm);
         }
 
         @Override
@@ -6822,8 +6908,12 @@ public class NotificationManagerService extends SystemService {
          *                           targetting <= O_MR1
          */
         @GuardedBy("mNotificationLock")
-        private void notifyPostedLocked(NotificationRecord r, NotificationRecord old,
+        void notifyPostedLocked(NotificationRecord r, NotificationRecord old,
                 boolean notifyAllListeners) {
+            if (isInLockDownMode()) {
+                return;
+            }
+
             // Lazily initialized snapshots of the notification.
             StatusBarNotification sbn = r.sbn;
             StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
@@ -6886,8 +6976,11 @@ public class NotificationManagerService extends SystemService {
         @GuardedBy("mNotificationLock")
         public void notifyRemovedLocked(NotificationRecord r, int reason,
                 NotificationStats notificationStats) {
-            final StatusBarNotification sbn = r.sbn;
+            if (isInLockDownMode()) {
+                return;
+            }
 
+            final StatusBarNotification sbn = r.sbn;
             // make a copy in case changes are made to the underlying Notification object
             // NOTE: this copy is lightweight: it doesn't include heavyweight parts of the
             // notification
@@ -6938,6 +7031,10 @@ public class NotificationManagerService extends SystemService {
          */
         @GuardedBy("mNotificationLock")
         public void notifyRankingUpdateLocked(List<NotificationRecord> changedHiddenNotifications) {
+            if (isInLockDownMode()) {
+                return;
+            }
+
             boolean isHiddenRankingUpdate = changedHiddenNotifications != null
                     && changedHiddenNotifications.size() > 0;
 
diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml
index aa3135ff18da..4f81bfae9a7c 100644
--- a/services/tests/uiservicestests/AndroidManifest.xml
+++ b/services/tests/uiservicestests/AndroidManifest.xml
@@ -29,6 +29,7 @@
     <uses-permission android:name="android.permission.DEVICE_POWER" />
     <uses-permission android:name="android.permission.ACCESS_CONTENT_PROVIDERS_EXTERNALLY" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_KEYGUARD_SECURE_STORAGE" />
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
new file mode 100644
index 000000000000..793739bfe8f5
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2022 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 org.mockito.ArgumentMatchers.any;
+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.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.INotificationManager;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.service.notification.NotificationStats;
+import android.service.notification.StatusBarNotification;
+import android.testing.TestableContext;
+
+import com.android.server.UiServiceTestCase;
+
+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.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;
+
+    @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 testNotifyPostedLockedInLockdownMode() {
+        NotificationRecord r = mock(NotificationRecord.class);
+        NotificationRecord old = mock(NotificationRecord.class);
+
+        // before the lockdown mode
+        when(mNm.isInLockDownMode()).thenReturn(false);
+        mListeners.notifyPostedLocked(r, old, true);
+        mListeners.notifyPostedLocked(r, old, false);
+        verify(mListeners, times(2)).getServices();
+
+        // in the lockdown mode
+        reset(r);
+        reset(old);
+        reset(mListeners);
+        when(mNm.isInLockDownMode()).thenReturn(true);
+        mListeners.notifyPostedLocked(r, old, true);
+        mListeners.notifyPostedLocked(r, old, false);
+        verify(mListeners, never()).getServices();
+    }
+
+    @Test
+    public void testnotifyRankingUpdateLockedInLockdownMode() {
+        List chn = mock(List.class);
+
+        // before the lockdown mode
+        when(mNm.isInLockDownMode()).thenReturn(false);
+        mListeners.notifyRankingUpdateLocked(chn);
+        verify(chn, times(1)).size();
+
+        // in the lockdown mode
+        reset(chn);
+        when(mNm.isInLockDownMode()).thenReturn(true);
+        mListeners.notifyRankingUpdateLocked(chn);
+        verify(chn, never()).size();
+    }
+
+    @Test
+    public void testNotifyRemovedLockedInLockdownMode() throws NoSuchFieldException {
+        StatusBarNotification sbn = mock(StatusBarNotification.class);
+        NotificationRecord r = mock(NotificationRecord.class);
+        NotificationStats rs = mock(NotificationStats.class);
+        FieldSetter.setField(r,
+                NotificationRecord.class.getDeclaredField("sbn"),
+                sbn);
+        FieldSetter.setField(mNm,
+                NotificationManagerService.class.getDeclaredField("mHandler"),
+                mock(NotificationManagerService.WorkerHandler.class));
+
+        // before the lockdown mode
+        when(mNm.isInLockDownMode()).thenReturn(false);
+        mListeners.notifyRemovedLocked(r, 0, rs);
+        mListeners.notifyRemovedLocked(r, 0, rs);
+        verify(sbn, times(2)).cloneLight();
+
+        // in the lockdown mode
+        reset(sbn);
+        reset(r);
+        reset(rs);
+        when(mNm.isInLockDownMode()).thenReturn(true);
+        mListeners.notifyRemovedLocked(r, 0, rs);
+        mListeners.notifyRemovedLocked(r, 0, rs);
+        verify(sbn, never()).cloneLight();
+    }
+}
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 420bfbc042ff..ed3406fc95b4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -37,6 +37,9 @@ import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Build.VERSION_CODES.O_MR1;
 import static android.os.Build.VERSION_CODES.P;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
+
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -233,6 +236,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
         protected void reportUserInteraction(NotificationRecord r) {
             return;
         }
+
+        class StrongAuthTrackerFake extends NotificationManagerService.StrongAuthTracker {
+            private int mGetStrongAuthForUserReturnValue = 0;
+            StrongAuthTrackerFake(Context context) {
+                super(context);
+            }
+
+            public void setGetStrongAuthForUserReturnValue(int val) {
+                mGetStrongAuthForUserReturnValue = val;
+            }
+
+            @Override
+            public int getStrongAuthForUser(int userId) {
+                return mGetStrongAuthForUserReturnValue;
+            }
+        }
+    }
+
+    TestableNotificationManagerService.StrongAuthTrackerFake mStrongAuthTracker;
+
     }
 
     @Before
@@ -304,6 +327,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
         }
         mService.setAudioManager(mAudioManager);
 
+        mStrongAuthTracker = mService.new StrongAuthTrackerFake(mContext);
+        mService.setStrongAuthTracker(mStrongAuthTracker);
+
         // Tests call directly into the Binder.
         mBinderService = mService.getBinderService();
         mInternalService = mService.getInternalService();
@@ -3261,4 +3287,44 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
         mBinderService.areNotificationsEnabledForPackage(mContext.getPackageName(),
                 mUid + UserHandle.PER_USER_RANGE);
     }
+
+    @Test
+    public void testStrongAuthTracker_isInLockDownMode() {
+        mStrongAuthTracker.setGetStrongAuthForUserReturnValue(
+                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
+        mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId());
+        assertTrue(mStrongAuthTracker.isInLockDownMode());
+        mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0);
+        mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId());
+        assertFalse(mStrongAuthTracker.isInLockDownMode());
+    }
+
+    @Test
+    public void testCancelAndPostNotificationsWhenEnterAndExitLockDownMode() {
+        // post 2 notifications from 2 packages
+        NotificationRecord pkgA = new NotificationRecord(mContext,
+                generateSbn("a", 1000, 9, 0), mTestNotificationChannel);
+        mService.addNotification(pkgA);
+        NotificationRecord pkgB = new NotificationRecord(mContext,
+                generateSbn("b", 1001, 9, 0), mTestNotificationChannel);
+        mService.addNotification(pkgB);
+
+        // when entering the lockdown mode, cancel the 2 notifications.
+        mStrongAuthTracker.setGetStrongAuthForUserReturnValue(
+                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
+        mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId());
+        assertTrue(mStrongAuthTracker.isInLockDownMode());
+
+        // the notifyRemovedLocked function is called twice due to REASON_LOCKDOWN.
+        ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
+        verify(mListeners, times(2)).notifyRemovedLocked(any(), captor.capture(), any());
+        assertEquals(REASON_CANCEL_ALL, captor.getValue().intValue());
+
+        // exit lockdown mode.
+        mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0);
+        mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId());
+
+        // the notifyPostedLocked function is called twice.
+        verify(mListeners, times(2)).notifyPostedLocked(any(), any());
+    }
 }
