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

Commit 9a8b2c83 authored by Brad Stenning's avatar Brad Stenning
Browse files

Add the ability to force car emergency and car warning notifications to the top

This changes the global sort string such these notifications can bypass all others

Design doc linked in the bug

Bug:111793232
Test: unit tests added
Change-Id: If47019ecf79f71caba6f4984d4cc745fd0d40da0
parent deee3950
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -2491,6 +2491,7 @@
        <item>com.android.server.notification.VisibilityExtractor</item>
        <!-- Depends on ZenModeExtractor -->
        <item>com.android.server.notification.BadgeExtractor</item>
        <item>com.android.server.notification.CriticalNotificationExtractor</item>

    </string-array>

+93 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.app.Notification;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Slog;

/**
 * Sets the criticality of a notification record. This is used to allow a bypass to all other
 * ranking signals. It is required in the automotive use case to facilitate placing emergency and
 * warning notifications above all others. It does not process notifications unless the system
 * has the automotive feature flag set.
 * <p>
 * Note: it is up to the notification ranking system to determine the effect of criticality values
 * on a notification record
 *
 */
public class CriticalNotificationExtractor implements NotificationSignalExtractor {

    private static final String TAG = "CriticalNotificationExt";
    private static final boolean DBG = false;
    private boolean mSupportsCriticalNotifications = false;
    /** 
     * Intended to bypass all other ranking, notification should be placed above all others.
     * In the automotive case, the notification would be used to tell a driver to pull over
     * immediately 
     */
    static final int CRITICAL = 0;
    /**
     * Indicates a notification should be place above all notifications except those marked as
     * critical. In the automotive case this is a check engine light. 
     */
    static final int CRITICAL_LOW = 1;
    /** Normal notification. */
    static final int NORMAL = 2;

    @Override
    public void initialize(Context context, NotificationUsageStats usageStats) {
        if (DBG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
        mSupportsCriticalNotifications = supportsCriticalNotifications(context);
    }

    private boolean supportsCriticalNotifications(Context context) {
        return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, 0);
    }

    @Override
    public RankingReconsideration process(NotificationRecord record) {
        if (!mSupportsCriticalNotifications) {
            if (DBG) Slog.d(TAG, "skipping since system does not support critical notification");
            return null;
        }
        if (record == null || record.getNotification() == null) {
            if (DBG) Slog.d(TAG, "skipping empty notification");
            return null;
        }
        // Note: The use of both CATEGORY_CAR_EMERGENCY and CATEGORY_CAR_WARNING is restricted to
        // System apps
        if (record.isCategory(Notification.CATEGORY_CAR_EMERGENCY)) {
            record.setCriticality(CRITICAL);
        } else if (record.isCategory(Notification.CATEGORY_CAR_WARNING)) {
            record.setCriticality(CRITICAL_LOW);
        } else {
            record.setCriticality(NORMAL);
        }
        return null;
    }

    @Override
    public void setConfig(RankingConfig config) {
    }

    @Override
    public void setZenHelper(ZenModeHelper helper) {
    }

}
+15 −0
Original line number Diff line number Diff line
@@ -144,6 +144,8 @@ public final class NotificationRecord {
    private int mPackageVisibility;
    private int mUserImportance = IMPORTANCE_UNSPECIFIED;
    private int mImportance = IMPORTANCE_UNSPECIFIED;
    // Field used in global sort key to bypass normal notifications
    private int mCriticality = CriticalNotificationExtractor.NORMAL;
    private CharSequence mImportanceExplanation = null;

    private int mSuppressedVisualEffects = 0;
@@ -746,6 +748,19 @@ public final class NotificationRecord {
        return mIntercept;
    }

    /**
     * Set to affect global sort key.
     *
     * @param criticality used in a string based sort thus 0 is the most critical
     */
    public void setCriticality(int criticality) {
        mCriticality = criticality;
    }

    public int getCriticality() {
        return mCriticality;
    }

    public boolean isIntercepted() {
        return mIntercept;
    }
+4 −1
Original line number Diff line number Diff line
@@ -106,6 +106,8 @@ public class RankingHelper {

        synchronized (mProxyByGroupTmp) {
            // record individual ranking result and nominate proxies for each group
            // Note: iteration is done backwards such that the index can be used as a sort key
            // in a string compare below
            for (int i = N - 1; i >= 0; i--) {
                final NotificationRecord record = notificationList.get(i);
                record.setAuthoritativeRank(i);
@@ -138,7 +140,8 @@ public class RankingHelper {

                boolean isGroupSummary = record.getNotification().isGroupSummary();
                record.setGlobalSortKey(
                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
                        String.format("crtcl=0x%04x:intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
                        record.getCriticality(),
                        record.isRecentlyIntrusive()
                                && record.getImportance() > NotificationManager.IMPORTANCE_MIN
                                ? '0' : '1',
+109 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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 android.app.NotificationManager.IMPORTANCE_LOW;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import android.app.Notification;
import android.app.NotificationChannel;
import android.content.pm.PackageManager;
import android.os.UserHandle;
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;

public class CriticalNotificationExtractorTest extends UiServiceTestCase {
    @Mock
    private PackageManager mPackageManagerClient;
    private TestableContext mContext = spy(getContext());
    private CriticalNotificationExtractor mExtractor;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mContext.setMockPackageManager(mPackageManagerClient);
        mExtractor = new CriticalNotificationExtractor();
    }

    /** confirm there is no affect on notifcations if the automotive feature flag is not set */
    @Test
    public void testExtractCritically_nonsupporting() throws Exception {
        when(mPackageManagerClient.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, 0))
                .thenReturn(false);
        mExtractor.initialize(mContext, null);

        assertCriticality(Notification.CATEGORY_CAR_EMERGENCY,
                CriticalNotificationExtractor.NORMAL);

        assertCriticality(Notification.CATEGORY_CAR_WARNING, CriticalNotificationExtractor.NORMAL);

        assertCriticality(Notification.CATEGORY_CAR_INFORMATION,
                CriticalNotificationExtractor.NORMAL);

        assertCriticality(Notification.CATEGORY_CALL,
                CriticalNotificationExtractor.NORMAL);
    }

    @Test
    public void testExtractCritically() throws Exception {
        when(mPackageManagerClient.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, 0))
                .thenReturn(true);
        mExtractor.initialize(mContext, null);

        assertCriticality(Notification.CATEGORY_CAR_EMERGENCY,
                CriticalNotificationExtractor.CRITICAL);

        assertCriticality(Notification.CATEGORY_CAR_WARNING,
                CriticalNotificationExtractor.CRITICAL_LOW);

        assertCriticality(Notification.CATEGORY_CAR_INFORMATION,
                CriticalNotificationExtractor.NORMAL);

        assertCriticality(Notification.CATEGORY_CALL,
                CriticalNotificationExtractor.NORMAL);
    }

    private void assertCriticality(String cat, int criticality) {
        NotificationRecord info = generateRecord(cat);
        mExtractor.process(info);
        assertThat(info.getCriticality(), is(criticality));
    }


    private NotificationRecord generateRecord(String category) {
        NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
        final Notification.Builder builder = new Notification.Builder(getContext())
                .setContentTitle("foo")
                .setCategory(category)
                .setSmallIcon(android.R.drawable.sym_def_app_icon);
        Notification n = builder.build();
        StatusBarNotification sbn = new StatusBarNotification("", "", 0, "", 0,
                0, n, UserHandle.ALL, null, System.currentTimeMillis());
        return new NotificationRecord(getContext(), sbn, channel);
    }
}
Loading