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

Commit f19de905 authored by Matías Hernández's avatar Matías Hernández Committed by Android (Google) Code Review
Browse files

Merge "Cap the number of channels that an NLS can create for other packages" into main

parents dd203253 16b68360
Loading
Loading
Loading
Loading
+134 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 android.util.SparseIntArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.notification.ManagedServices.ManagedServiceInfo;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.PrintWriter;

class NotificationListenerStats {
    /**
     * Maximum number of channels that a single NLS is allowed to create for <em>all</em> installed
     * packages. Should be much higher than reasonably necessary, but still prevent runaway channel
     * creation abuse.
     *
     * @see PreferencesHelper#NOTIFICATION_CHANNEL_COUNT_LIMIT
     */
    private static final int MAX_CHANNELS = 5_000;

    private static final String TAG_STATS = "nlsStats";
    private static final String TAG_NLS = "nls";
    private static final String ATT_UID = "uid";
    private static final String ATT_OWNED_CHANNEL_COUNT = "channelCount";

    private final Object mLock = new Object();

    // nlsUid => count of channels created by that (privileged) NLS.
    @GuardedBy("mLock")
    private final SparseIntArray mChannelsCreated = new SparseIntArray();

    private final int mMaxChannelsAllowed;

    NotificationListenerStats() {
        this(MAX_CHANNELS);
    }

    @VisibleForTesting
    NotificationListenerStats(int maxChannelsAllowed) {
        mMaxChannelsAllowed = maxChannelsAllowed;
    }

    boolean isAllowedToCreateChannel(ManagedServiceInfo nls) {
        synchronized (mLock) {
            int numCreated = mChannelsCreated.get(nls.uid);
            return numCreated < mMaxChannelsAllowed;
        }
    }

    void logCreatedChannels(ManagedServiceInfo nls, int increase) {
        synchronized (mLock) {
            int prevCreated = mChannelsCreated.get(nls.uid);
            mChannelsCreated.put(nls.uid, prevCreated + increase);
        }
    }

    void onPackageRemoved(int uid, String packageName) {
        // If the uninstalled package was an NLS, drop its stats.
        synchronized (mLock) {
            mChannelsCreated.delete(uid);
        }
    }

    static boolean isXmlTag(String tag) {
        return TAG_STATS.equals(tag);
    }

    void writeXml(TypedXmlSerializer out) throws IOException {
        out.startTag(null, TAG_STATS);
        synchronized (mLock) {
            for (int i = 0; i < mChannelsCreated.size(); i++) {
                out.startTag(null, TAG_NLS);
                out.attributeInt(null, ATT_UID, mChannelsCreated.keyAt(i));
                out.attributeInt(null, ATT_OWNED_CHANNEL_COUNT, mChannelsCreated.valueAt(i));
                out.endTag(null, TAG_NLS);
            }
        }
        out.endTag(null, TAG_STATS);
    }

    void readXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException {
        int type = parser.getEventType();
        if (type != XmlPullParser.START_TAG) return;
        String tag = parser.getName();
        if (!TAG_STATS.equals(tag)) return;

        synchronized (mLock) {
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
                tag = parser.getName();
                if (type == XmlPullParser.END_TAG && TAG_STATS.equals(tag)) {
                    break;
                }
                if (type == XmlPullParser.START_TAG) {
                    if (TAG_NLS.equals(tag)) {
                        int uid = parser.getAttributeInt(null, ATT_UID, 0);
                        int channelCount = parser.getAttributeInt(null, ATT_OWNED_CHANNEL_COUNT, 0);
                        mChannelsCreated.put(uid, channelCount);
                    }
                }
            }
        }
    }

    void dump(PrintWriter pw, String prefix) {
        synchronized (mLock) {
            for (int i = 0; i < mChannelsCreated.size(); i++) {
                pw.println(prefix + "NLS with uid " + mChannelsCreated.keyAt(i));
                pw.println(prefix + "  created channel count: " + mChannelsCreated.valueAt(i));
            }
        }
    }
}
+63 −16
Original line number Diff line number Diff line
@@ -787,6 +787,7 @@ public class NotificationManagerService extends SystemService {
    @VisibleForTesting
    NotificationAssistants mAssistants;
    private ConditionProviders mConditionProviders;
    private NotificationListenerStats mNotificationListenerStats;
    private NotificationUsageStats mUsageStats;
    private boolean mLockScreenAllowSecureNotifications = true;
    final ArrayMap<String, ArrayMap<Integer,
@@ -1239,6 +1240,9 @@ public class NotificationManagerService extends SystemService {
                mLockScreenAllowSecureNotifications = parser.getAttributeBoolean(null,
                        LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_VALUE, true);
            }
            if (NotificationListenerStats.isXmlTag(parser.getName())) {
                mNotificationListenerStats.readXml(parser);
            }
        }
        if (!migratedManagedServices) {
@@ -1359,6 +1363,9 @@ public class NotificationManagerService extends SystemService {
        if (!forBackup || userId == USER_SYSTEM) {
            writeSecureNotificationsPolicy(out);
        }
        if (!forBackup) {
            mNotificationListenerStats.writeXml(out);
        }
        out.endTag(null, TAG_NOTIFICATION_POLICY);
        out.endDocument();
    }
@@ -2796,7 +2803,8 @@ public class NotificationManagerService extends SystemService {
            SystemUiSystemPropertiesFlags.FlagResolver flagResolver,
            PermissionManager permissionManager, PowerManager powerManager,
            PostNotificationTrackerFactory postNotificationTrackerFactory,
            UiEventLogger uiEventLogger, BitmapOffloadInternal bitmapOffloader) {
            UiEventLogger uiEventLogger, BitmapOffloadInternal bitmapOffloader,
            NotificationListenerStats notificationListenerStats) {
        mHandler = handler;
        if (Flags.nmBinderPerfThrottleEffectsSuppressorBroadcast()) {
            mBroadcastsHandler = broadcastsHandler;
@@ -2847,6 +2855,7 @@ public class NotificationManagerService extends SystemService {
        mMetricsLogger = new MetricsLogger();
        mRankingHandler = rankingHandler;
        mConditionProviders = conditionProviders;
        mNotificationListenerStats = notificationListenerStats;
        mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), Clock.systemUTC(),
                mConditionProviders, flagResolver, new ZenModeEventLogger(mPackageManagerClient));
        mZenModeHelper.addCallback(new ZenModeHelper.Callback() {
@@ -3170,7 +3179,7 @@ public class NotificationManagerService extends SystemService {
                getContext().getSystemService(PermissionManager.class),
                getContext().getSystemService(PowerManager.class),
                new PostNotificationTrackerFactory() {}, new UiEventLoggerImpl(),
                bitmapOffloader);
                bitmapOffloader, new NotificationListenerStats());
        publishBinderService(Context.NOTIFICATION_SERVICE, mService, /* allowIsolated= */ false,
                DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL);
@@ -7377,7 +7386,15 @@ public class NotificationManagerService extends SystemService {
            Objects.requireNonNull(user);
            Objects.requireNonNull(parentId);
            Objects.requireNonNull(conversationId);
            verifyPrivilegedListener(token, user, true);
            ManagedServiceInfo nlsInfo = verifyPrivilegedListener(token, user, true);
            if (!nlsInfo.isSystemUi()
                    && !isNotificationAssistant(nlsInfo.service)
                    && !mNotificationListenerStats.isAllowedToCreateChannel(nlsInfo)) {
                Slog.e(TAG, "NLS " + nlsInfo + " has created too many channels already! "
                        + "Rejecting " + pkg + "/" + user + "/" + parentId + "/" + conversationId);
                return null;
            }
            if (notificationClassification()) {
                if (SYSTEM_RESERVED_IDS.contains(parentId)) {
@@ -7388,18 +7405,39 @@ public class NotificationManagerService extends SystemService {
                }
            }
            int uid = getUidForPackageAndUser(pkg, user);
            NotificationChannel conversationChannel =
                    mPreferencesHelper.getNotificationChannel(pkg, uid, parentId, false).copy();
            String conversationChannelId = String.format(
                    CONVERSATION_CHANNEL_ID_FORMAT, parentId, conversationId);
            if (uid == INVALID_UID) {
                return null;
            }
            NotificationChannel parentChannel =
                    mPreferencesHelper.getNotificationChannel(pkg, uid, parentId, false);
            if (parentChannel == null) {
                return null;
            }
            NotificationChannel previous = mPreferencesHelper.getConversationNotificationChannel(
                    pkg, uid, parentId, conversationId, false, false);
            if (previous != null) {
                // If the conversation already exists, we're done. Continuing is worse since
                // it would override any conversation customizations with the parent's values.
                return previous;
            }
            String conversationChannelId = String.format(CONVERSATION_CHANNEL_ID_FORMAT, parentId,
                    conversationId);
            NotificationChannel conversationChannel = parentChannel.copy();
            conversationChannel.setId(conversationChannelId);
            conversationChannel.setConversationId(parentId, conversationId);
            createNotificationChannelsImpl(
                    pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel)));
                    pkg, uid, new ParceledListSlice<>(Arrays.asList(conversationChannel)));
            NotificationChannel created = mPreferencesHelper.getConversationNotificationChannel(
                    pkg, uid, parentId, conversationId, false, false);
            if (created != null) {
                mNotificationListenerStats.logCreatedChannels(nlsInfo, /* increase= */ 1);
                handleSavePolicyFile();
            }
            return mPreferencesHelper.getConversationNotificationChannel(
                    pkg, uid, parentId, conversationId, false, false).copy();
            return created;
        }
        @Override
@@ -7491,8 +7529,9 @@ public class NotificationManagerService extends SystemService {
            return mPermissionHelper.isPermissionFixed(pkg, userId);
        }
        private void verifyPrivilegedListener(INotificationListener token, UserHandle user,
                boolean assistantAllowed) {
        @NonNull
        private ManagedServiceInfo verifyPrivilegedListener(INotificationListener token,
                UserHandle user, boolean assistantAllowed) {
            ManagedServiceInfo info;
            synchronized (mNotificationLock) {
                info = mListeners.checkServiceTokenLocked(token);
@@ -7507,6 +7546,7 @@ public class NotificationManagerService extends SystemService {
            if (!info.enabledAndUserMatches(user.getIdentifier())) {
                throw new SecurityException(info + " does not have access");
            }
            return info;
        }
        private void verifyPrivilegedListenerUriPermission(int sourceUid,
@@ -8218,6 +8258,7 @@ public class NotificationManagerService extends SystemService {
                pw.println("\n  Notification listeners:");
                mListeners.dump(pw, filter);
                pw.print("    mListenerHints: "); pw.println(mListenerHints);
                pw.print("    mListenersDisablingEffects: (");
                N = mListenersDisablingEffects.size();
                for (int i = 0; i < N; i++) {
@@ -8237,6 +8278,10 @@ public class NotificationManagerService extends SystemService {
                    }
                }
                pw.println(')');
                pw.println("\n  NotificationListenerStats:");
                mNotificationListenerStats.dump(pw, "    ");
                pw.println("\n  Notification assistant services:");
                mAssistants.dump(pw, filter);
            }
@@ -11072,6 +11117,8 @@ public class NotificationManagerService extends SystemService {
                // (recently dismissed notifications) and notification history.
                mArchive.removePackageNotifications(pkg, userHandle);
                mHistoryManager.onPackageRemoved(userHandle, pkg);
                // Remove from NLS Stats (in case the package included an NLS).
                mNotificationListenerStats.onPackageRemoved(uid, pkg);
            }
        }
        if (preferencesChanged) {
@@ -12419,11 +12466,11 @@ public class NotificationManagerService extends SystemService {
     */
    @VisibleForTesting
    boolean isInteractionVisibleToListener(ManagedServiceInfo info, int userId) {
        boolean isAssistantService = isServiceTokenValid(info.getService());
        boolean isAssistantService = isNotificationAssistant(info.getService());
        return !isAssistantService || info.isSameUser(userId);
    }
    private boolean isServiceTokenValid(IInterface service) {
    private boolean isNotificationAssistant(IInterface service) {
        synchronized (mNotificationLock) {
            return mAssistants.isServiceTokenValidLocked(service);
        }
@@ -14671,7 +14718,7 @@ public class NotificationManagerService extends SystemService {
                BackgroundThread.getHandler().post(() -> {
                    if (info.isSystem
                            || hasCompanionDevice(info)
                            || isServiceTokenValid(info.service)) {
                            || isNotificationAssistant(info.service)) {
                        notifyNotificationChannelChanged(
                                info, pkg, user, channel, modificationType);
                    }
+105 −121

File changed.

Preview size limit exceeded, changes collapsed.

+1 −1
Original line number Diff line number Diff line
@@ -175,7 +175,7 @@ public class RoleObserverTest extends UiServiceTestCase {
                    mock(PowerManager.class),
                    new NotificationManagerService.PostNotificationTrackerFactory() {},
                    mock(UiEventLogger.class),
                    mock(BitmapOffloadInternal.class));
                    mock(BitmapOffloadInternal.class), new NotificationListenerStats());
        } catch (SecurityException e) {
            if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
                throw e;