Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 056e801c authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Prevent apps from modifing reserved channels

Test: NotificationManagerServiceTest
Test: PreferencesHelperTest
Flag: android.service.notification.notification_classification
Fixes: 362327403
Change-Id: Id04f11e11a02207ee42fadb578b96c0b04d39fbd
parent 81110986
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -55,7 +55,9 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
@@ -100,6 +102,11 @@ public final class NotificationChannel implements Parcelable {
    @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
    public static final String RECS_ID = "android.app.recs";

    /** @hide */
    @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
    public static final ArrayList<String> SYSTEM_RESERVED_IDS = new ArrayList<>(
            List.of(NEWS_ID, SOCIAL_MEDIA_ID, PROMOTIONS_ID, RECS_ID));

    /**
     * The formatter used by the system to create an id for notification
     * channels when it automatically creates conversation channels on behalf of an app. The format
+11 −0
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ import static android.app.NotificationChannel.NEWS_ID;
import static android.app.NotificationChannel.PROMOTIONS_ID;
import static android.app.NotificationChannel.RECS_ID;
import static android.app.NotificationChannel.SOCIAL_MEDIA_ID;
import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
import static android.app.NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED;
import static android.app.NotificationManager.ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED;
import static android.app.NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED;
@@ -109,6 +110,7 @@ import static android.service.notification.Adjustment.TYPE_NEWS;
import static android.service.notification.Adjustment.TYPE_PROMOTION;
import static android.service.notification.Adjustment.TYPE_SOCIAL_MEDIA;
import static android.service.notification.Flags.callstyleCallbackApi;
import static android.service.notification.Flags.notificationClassification;
import static android.service.notification.Flags.notificationForceGrouping;
import static android.service.notification.Flags.redactSensitiveNotificationsBigTextStyle;
import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners;
@@ -4404,6 +4406,15 @@ public class NotificationManagerService extends SystemService {
            if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) {
                throw new IllegalArgumentException("Cannot delete default channel");
            }
            if (notificationClassification()) {
                // Check for all reserved channels, but do not throw because it's a common
                // preexisting pattern for apps to (try to) delete all channels that don't match
                //  their current desired channel structure
                if (SYSTEM_RESERVED_IDS.contains(channelId)) {
                    Log.v(TAG, "Package " + pkg + " cannot delete a reserved channel");
                    return;
                }
            }
            enforceDeletingChannelHasNoFgService(pkg, callingUser, channelId);
            enforceDeletingChannelHasNoUserInitiatedJob(pkg, callingUser, channelId);
            cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0,
+12 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import static android.app.NotificationChannel.PLACEHOLDER_CONVERSATION_ID;
import static android.app.NotificationChannel.PROMOTIONS_ID;
import static android.app.NotificationChannel.RECS_ID;
import static android.app.NotificationChannel.SOCIAL_MEDIA_ID;
import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
import static android.app.NotificationChannel.USER_LOCKED_IMPORTANCE;
import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE;
@@ -440,6 +441,12 @@ public class PreferencesHelper implements RankingConfig {
                channel.setImportanceLockedByCriticalDeviceFunction(
                        r.defaultAppLockedImportance || r.fixedImportance);

                if (notificationClassification()) {
                    if (SYSTEM_RESERVED_IDS.contains(id) && channel.isDeleted() ) {
                        channel.setDeleted(false);
                    }
                }

                if (isShortcutOk(channel) && isDeletionOk(channel)) {
                    r.channels.put(id, channel);
                }
@@ -1023,6 +1030,11 @@ public class PreferencesHelper implements RankingConfig {
            if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) {
                throw new IllegalArgumentException("Reserved id");
            }
            // Only the user can update bundle channel settings
            if (notificationClassification() && !fromSystemOrSystemUi
                    && SYSTEM_RESERVED_IDS.contains(channel.getId())) {
                return false;
            }
            NotificationChannel existing = r.channels.get(channel.getId());
            if (existing != null && fromTargetApp) {
                // Actually modifying an existing channel - keep most of the existing settings
+20 −1
Original line number Diff line number Diff line
@@ -99,6 +99,7 @@ import static android.service.notification.Adjustment.TYPE_NEWS;
import static android.service.notification.Condition.SOURCE_CONTEXT;
import static android.service.notification.Condition.SOURCE_USER_ACTION;
import static android.service.notification.Condition.STATE_TRUE;
import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION;
import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING;
import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
@@ -4420,6 +4421,24 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
                eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED));
    }
    @Test
    @EnableFlags(FLAG_NOTIFICATION_CLASSIFICATION)
    public void testAppsCannotDeleteBundleChannel() throws Exception {
        when(mCompanionMgr.getAssociations(mPkg, mUserId))
                .thenReturn(singletonList(mock(AssociationInfo.class)));
        mService.setPreferencesHelper(mPreferencesHelper);
        when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(),
                eq(NEWS_ID), anyBoolean()))
                .thenReturn(mTestNotificationChannel);
        when(mPreferencesHelper.deleteNotificationChannel(eq(mPkg), anyInt(),
                eq(NEWS_ID), anyInt(), anyBoolean())).thenReturn(true);
        reset(mListeners);
        mBinderService.deleteNotificationChannel(mPkg, NEWS_ID);
        verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg),
                eq(Process.myUserHandle()), any(),
                eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED));
    }
    @Test
    public void testDeleteChannelOnlyDoExtraWorkIfExisted() throws Exception {
        when(mCompanionMgr.getAssociations(mPkg, mUserId))
+41 −0
Original line number Diff line number Diff line
@@ -6222,6 +6222,47 @@ public class PreferencesHelperTest extends UiServiceTestCase {
                .isEqualTo(IMPORTANCE_LOW);
    }

    @Test
    @EnableFlags(FLAG_NOTIFICATION_CLASSIFICATION)
    public void testNotificationBundles_appsCannotUpdate() {
        // do something that triggers settings creation for an app
        mHelper.setShowBadge(PKG_O, UID_O, true);

        NotificationChannel fromApp =
                new NotificationChannel(NEWS_ID, "The best channel", IMPORTANCE_HIGH);
        mHelper.createNotificationChannel(PKG_O, UID_O, fromApp, true, false, UID_O, false);

        assertThat(mHelper.getNotificationChannel(PKG_O, UID_O, NEWS_ID, false).getImportance())
                .isEqualTo(IMPORTANCE_LOW);
    }

    @Test
    @EnableFlags(FLAG_NOTIFICATION_CLASSIFICATION)
    public void testNotificationBundles_osCanAllowToBypassDnd() {
        // do something that triggers settings creation for an app
        mHelper.setShowBadge(PKG_O, UID_O, true);

        NotificationChannel fromApp =
                new NotificationChannel(NEWS_ID, "The best channel", IMPORTANCE_HIGH);
        mHelper.createNotificationChannel(PKG_O, UID_O, fromApp, true, false, UID_O, false);
    }

    @Test
    @EnableFlags(FLAG_NOTIFICATION_CLASSIFICATION)
    public void testUnDeleteBundleChannelsOnLoadIfNotUserChange() throws Exception {
        mHelper.setShowBadge(PKG_P, UID_P, true);
        // the public create/update methods should prevent this, so take advantage of the fact that
        // the object is in the same process
        mHelper.getNotificationChannel(PKG_P, UID_P, SOCIAL_MEDIA_ID, true).setDeleted(true);

        ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, false,
                UserHandle.USER_ALL, SOCIAL_MEDIA_ID);

        loadStreamXml(baos, false, UserHandle.USER_ALL);

        assertThat(mXmlHelper.getNotificationChannel(PKG_P, UID_P, SOCIAL_MEDIA_ID, true).
                isDeleted()).isFalse();
    }

    @Test
    public void testRestoredWithoutUid_threadSafety() throws Exception {