Allow system apps to make channels that bypass DND

Test: runtest systemui-notification
Bug: 75429403
Change-Id: I3168eb7e7313b85c2c56935adb4eeea7880d4ef3
diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java
index b280bde..f163113 100644
--- a/services/core/java/com/android/server/notification/RankingHelper.java
+++ b/services/core/java/com/android/server/notification/RankingHelper.java
@@ -29,18 +29,23 @@
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ParceledListSlice;
+import android.content.pm.Signature;
+import android.content.res.Resources;
 import android.metrics.LogMaker;
 import android.os.Build;
 import android.os.UserHandle;
+import android.print.PrintManager;
 import android.provider.Settings.Secure;
 import android.service.notification.NotificationListenerService.Ranking;
 import android.service.notification.RankingHelperProto;
 import android.service.notification.RankingHelperProto.RecordProto;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
 import android.util.proto.ProtoOutputStream;
@@ -95,12 +100,18 @@
     private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
     private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
     private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record
+    private final ArrayMap<Pair<String, Integer>, Boolean> mSystemAppCache = new ArrayMap<>();
 
     private final Context mContext;
     private final RankingHandler mRankingHandler;
     private final PackageManager mPm;
     private SparseBooleanArray mBadgingEnabled;
 
+    private Signature[] mSystemSignature;
+    private String mPermissionControllerPackageName;
+    private String mServicesSystemSharedLibPackageName;
+    private String mSharedSystemSharedLibPackageName;
+
     public RankingHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
             ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames) {
         mContext = context;
@@ -130,6 +141,8 @@
                 Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
             }
         }
+
+        getSignatures();
     }
 
     @SuppressWarnings("unchecked")
@@ -571,7 +584,7 @@
         if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) {
             throw new IllegalArgumentException("Reserved id");
         }
-
+        final boolean isSystemApp = isSystemPackage(pkg, uid);
         NotificationChannel existing = r.channels.get(channel.getId());
         // Keep most of the existing settings
         if (existing != null && fromTargetApp) {
@@ -597,6 +610,11 @@
                 existing.setImportance(channel.getImportance());
             }
 
+            // system apps can bypass dnd if the user hasn't changed any fields on the channel yet
+            if (existing.getUserLockedFields() == 0 & isSystemApp) {
+                existing.setBypassDnd(channel.canBypassDnd());
+            }
+
             updateConfig();
             return;
         }
@@ -604,9 +622,12 @@
                 || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
             throw new IllegalArgumentException("Invalid importance level");
         }
+
         // Reset fields that apps aren't allowed to set.
-        if (fromTargetApp) {
+        if (fromTargetApp && !isSystemApp) {
             channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
+        }
+        if (fromTargetApp) {
             channel.setLockscreenVisibility(r.visibility);
         }
         clearLockedFields(channel);
@@ -616,6 +637,7 @@
         if (!r.showBadge) {
             channel.setShowBadge(false);
         }
+
         r.channels.put(channel.getId(), channel);
         MetricsLogger.action(getChannelLog(channel, pkg).setType(
                 MetricsProto.MetricsEvent.TYPE_OPEN));
@@ -625,6 +647,65 @@
         channel.unlockFields(channel.getUserLockedFields());
     }
 
+    /**
+     * Determine whether a package is a "system package", in which case certain things (like
+     * bypassing DND) should be allowed.
+     */
+    private boolean isSystemPackage(String pkg, int uid) {
+        Pair<String, Integer> app = new Pair(pkg, uid);
+        if (mSystemAppCache.containsKey(app)) {
+            return mSystemAppCache.get(app);
+        }
+
+        PackageInfo pi;
+        try {
+            pi = mPm.getPackageInfoAsUser(
+                    pkg, PackageManager.GET_SIGNATURES, UserHandle.getUserId(uid));
+        } catch (NameNotFoundException e) {
+            Slog.w(TAG, "Can't find pkg", e);
+            return false;
+        }
+        boolean isSystem = (mSystemSignature[0] != null
+                && mSystemSignature[0].equals(getFirstSignature(pi)))
+                || pkg.equals(mPermissionControllerPackageName)
+                || pkg.equals(mServicesSystemSharedLibPackageName)
+                || pkg.equals(mSharedSystemSharedLibPackageName)
+                || pkg.equals(PrintManager.PRINT_SPOOLER_PACKAGE_NAME)
+                || isDeviceProvisioningPackage(pkg);
+        mSystemAppCache.put(app, isSystem);
+        return isSystem;
+    }
+
+    private Signature getFirstSignature(PackageInfo pkg) {
+        if (pkg != null && pkg.signatures != null && pkg.signatures.length > 0) {
+            return pkg.signatures[0];
+        }
+        return null;
+    }
+
+    private Signature getSystemSignature() {
+        try {
+            final PackageInfo sys = mPm.getPackageInfoAsUser(
+                    "android", PackageManager.GET_SIGNATURES, UserHandle.USER_SYSTEM);
+            return getFirstSignature(sys);
+        } catch (NameNotFoundException e) {
+        }
+        return null;
+    }
+
+    private boolean isDeviceProvisioningPackage(String packageName) {
+        String deviceProvisioningPackage = mContext.getResources().getString(
+                com.android.internal.R.string.config_deviceProvisioningPackage);
+        return deviceProvisioningPackage != null && deviceProvisioningPackage.equals(packageName);
+    }
+
+    private void getSignatures() {
+        mSystemSignature = new Signature[]{getSystemSignature()};
+        mPermissionControllerPackageName = mPm.getPermissionControllerPackageName();
+        mServicesSystemSharedLibPackageName = mPm.getServicesSystemSharedLibraryPackageName();
+        mSharedSystemSharedLibPackageName = mPm.getSharedSystemSharedLibraryPackageName();
+    }
+
     @Override
     public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel,
             boolean fromUser) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
index 354d2d5..09d88fd 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
@@ -47,7 +47,9 @@
 import android.content.Context;
 import android.content.IContentProvider;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.Signature;
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.media.AudioAttributes;
@@ -97,6 +99,8 @@
     private static final UserHandle USER = UserHandle.of(0);
     private static final String UPDATED_PKG = "updatedPkg";
     private static final int UID2 = 1111;
+    private static final String SYSTEM_PKG = "android";
+    private static final int SYSTEM_UID= 1000;
     private static final UserHandle USER2 = UserHandle.of(10);
     private static final String TEST_CHANNEL_ID = "test_channel_id";
     private static final String TEST_AUTHORITY = "test";
@@ -136,8 +140,15 @@
         upgrade.targetSdkVersion = Build.VERSION_CODES.O;
         when(mPm.getApplicationInfoAsUser(eq(PKG), anyInt(), anyInt())).thenReturn(legacy);
         when(mPm.getApplicationInfoAsUser(eq(UPDATED_PKG), anyInt(), anyInt())).thenReturn(upgrade);
+        when(mPm.getApplicationInfoAsUser(eq(SYSTEM_PKG), anyInt(), anyInt())).thenReturn(upgrade);
         when(mPm.getPackageUidAsUser(eq(PKG), anyInt())).thenReturn(UID);
         when(mPm.getPackageUidAsUser(eq(UPDATED_PKG), anyInt())).thenReturn(UID2);
+        when(mPm.getPackageUidAsUser(eq(SYSTEM_PKG), anyInt())).thenReturn(SYSTEM_UID);
+        PackageInfo info = mock(PackageInfo.class);
+        info.signatures = new Signature[] {mock(Signature.class)};
+        when(mPm.getPackageInfoAsUser(eq(SYSTEM_PKG), anyInt(), anyInt())).thenReturn(info);
+        when(mPm.getPackageInfoAsUser(eq(PKG), anyInt(), anyInt()))
+                .thenReturn(mock(PackageInfo.class));
         when(mContext.getResources()).thenReturn(
                 InstrumentationRegistry.getContext().getResources());
         when(mContext.getContentResolver()).thenReturn(
@@ -1627,4 +1638,52 @@
         assertEquals(1, retrieved.getChannels().size());
         compareChannels(a, findChannel(retrieved.getChannels(), a.getId()));
     }
+
+    @Test
+    public void testAndroidPkgCanBypassDnd_creation() {
+
+        NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW);
+        test.setBypassDnd(true);
+
+        mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, test, true);
+
+        assertTrue(mHelper.getNotificationChannel(SYSTEM_PKG, SYSTEM_UID, "A", false)
+                .canBypassDnd());
+    }
+
+    @Test
+    public void testNormalPkgCannotBypassDnd_creation() {
+        NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW);
+        test.setBypassDnd(true);
+
+        mHelper.createNotificationChannel(PKG, 1000, test, true);
+
+        assertFalse(mHelper.getNotificationChannel(PKG, 1000, "A", false).canBypassDnd());
+    }
+
+    @Test
+    public void testAndroidPkgCanBypassDnd_update() throws Exception {
+        NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW);
+        mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, test, true);
+
+        NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW);
+        update.setBypassDnd(true);
+        mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, update, true);
+
+        assertTrue(mHelper.getNotificationChannel(SYSTEM_PKG, SYSTEM_UID, "A", false)
+                .canBypassDnd());
+
+        // setup + 1st check
+        verify(mPm, times(2)).getPackageInfoAsUser(any(), anyInt(), anyInt());
+    }
+
+    @Test
+    public void testNormalPkgCannotBypassDnd_update() {
+        NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW);
+        mHelper.createNotificationChannel(PKG, 1000, test, true);
+        NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW);
+        update.setBypassDnd(true);
+        mHelper.createNotificationChannel(PKG, 1000, update, true);
+        assertFalse(mHelper.getNotificationChannel(PKG, 1000, "A", false).canBypassDnd());
+    }
 }