Allow NAS to mark when sensitive notification content was detected.

Bug: 268019420
Test: NotificationListenerServiceTest
Test: NotificationRecordExtractorDataTest
Test: NotificationRecordTest
Test: CTS in same topic
Change-Id: I5741aa01993cd31b551e07f4969e8d115e56671d
This commit is contained in:
Ioana Alexandru
2023-02-06 14:12:09 +00:00
parent bef72a409e
commit a00fb9ba1c
13 changed files with 238 additions and 73 deletions

View File

@@ -12063,6 +12063,7 @@ package android.service.notification {
field public static final String KEY_NOT_CONVERSATION = "key_not_conversation";
field public static final String KEY_PEOPLE = "key_people";
field public static final String KEY_RANKING_SCORE = "key_ranking_score";
field public static final String KEY_SENSITIVE_CONTENT = "key_sensitive_content";
field public static final String KEY_SNOOZE_CRITERIA = "key_snooze_criteria";
field public static final String KEY_TEXT_REPLIES = "key_text_replies";
field public static final String KEY_USER_SENTIMENT = "key_user_sentiment";
@@ -12102,6 +12103,7 @@ package android.service.notification {
public static class NotificationListenerService.Ranking {
method public int getProposedImportance();
method public boolean hasSensitiveContent();
}
public final class NotificationStats implements android.os.Parcelable {

View File

@@ -52,7 +52,7 @@ public final class Adjustment implements Parcelable {
/** @hide */
@StringDef (prefix = { "KEY_" }, value = {
KEY_CONTEXTUAL_ACTIONS, KEY_GROUP_KEY, KEY_IMPORTANCE, KEY_PEOPLE, KEY_SNOOZE_CRITERIA,
KEY_TEXT_REPLIES, KEY_USER_SENTIMENT, KEY_IMPORTANCE_PROPOSAL
KEY_TEXT_REPLIES, KEY_USER_SENTIMENT, KEY_IMPORTANCE_PROPOSAL, KEY_SENSITIVE_CONTENT
})
@Retention(RetentionPolicy.SOURCE)
public @interface Keys {}
@@ -133,6 +133,13 @@ public final class Adjustment implements Parcelable {
*/
public static final String KEY_IMPORTANCE_PROPOSAL = "key_importance_proposal";
/**
* Data type: boolean, when true it suggests that the content text of this notification is
* sensitive. A notification listener can use this information to redact notifications on locked
* devices.
*/
public static final String KEY_SENSITIVE_CONTENT = "key_sensitive_content";
/**
* Data type: float, a ranking score from 0 (lowest) to 1 (highest).
* Used to rank notifications inside that fall under the same classification (i.e. alerting,

View File

@@ -1732,10 +1732,13 @@ public abstract class NotificationListenerService extends Service {
private boolean mIsBubble;
// Notification assistant importance suggestion
private int mProposedImportance;
// Sensitive info detected by the notification assistant
private boolean mSensitiveContent;
private static final int PARCEL_VERSION = 2;
public Ranking() { }
public Ranking() {
}
// You can parcel it, but it's not Parcelable
/** @hide */
@@ -1770,6 +1773,7 @@ public abstract class NotificationListenerService extends Service {
out.writeInt(mRankingAdjustment);
out.writeBoolean(mIsBubble);
out.writeInt(mProposedImportance);
out.writeBoolean(mSensitiveContent);
}
/** @hide */
@@ -1809,6 +1813,7 @@ public abstract class NotificationListenerService extends Service {
mRankingAdjustment = in.readInt();
mIsBubble = in.readBoolean();
mProposedImportance = in.readInt();
mSensitiveContent = in.readBoolean();
}
@@ -1917,6 +1922,17 @@ public abstract class NotificationListenerService extends Service {
return mProposedImportance;
}
/**
* Returns true if the notification text is sensitive (e.g. containing an OTP).
*
* @return whether the notification contains sensitive content
* @hide
*/
@SystemApi
public boolean hasSensitiveContent() {
return mSensitiveContent;
}
/**
* If the system has overridden the group key, then this will be non-null, and this
* key should be used to bundle notifications.
@@ -2081,7 +2097,8 @@ public abstract class NotificationListenerService extends Service {
boolean noisy, ArrayList<Notification.Action> smartActions,
ArrayList<CharSequence> smartReplies, boolean canBubble,
boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo,
int rankingAdjustment, boolean isBubble, int proposedImportance) {
int rankingAdjustment, boolean isBubble, int proposedImportance,
boolean sensitiveContent) {
mKey = key;
mRank = rank;
mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW;
@@ -2108,6 +2125,7 @@ public abstract class NotificationListenerService extends Service {
mRankingAdjustment = rankingAdjustment;
mIsBubble = isBubble;
mProposedImportance = proposedImportance;
mSensitiveContent = sensitiveContent;
}
/**
@@ -2149,7 +2167,8 @@ public abstract class NotificationListenerService extends Service {
other.mShortcutInfo,
other.mRankingAdjustment,
other.mIsBubble,
other.mProposedImportance);
other.mProposedImportance,
other.mSensitiveContent);
}
/**
@@ -2209,7 +2228,8 @@ public abstract class NotificationListenerService extends Service {
(other.mShortcutInfo == null ? 0 : other.mShortcutInfo.getId()))
&& Objects.equals(mRankingAdjustment, other.mRankingAdjustment)
&& Objects.equals(mIsBubble, other.mIsBubble)
&& Objects.equals(mProposedImportance, other.mProposedImportance);
&& Objects.equals(mProposedImportance, other.mProposedImportance)
&& Objects.equals(mSensitiveContent, other.mSensitiveContent);
}
}

View File

@@ -268,30 +268,31 @@ public class NotificationListener extends NotificationListenerWithPlugins implem
if (!rankingMap.getRanking(key, ranking)) {
ranking.populate(
key,
0,
false,
0,
0,
0,
null,
null,
null,
new ArrayList<>(),
new ArrayList<>(),
false,
0,
false,
0,
false,
new ArrayList<>(),
new ArrayList<>(),
false,
false,
false,
null,
0,
false,
0
/* rank= */ 0,
/* matchesInterruptionFilter= */ false,
/* visibilityOverride= */ 0,
/* suppressedVisualEffects= */ 0,
/* importance= */ 0,
/* explanation= */ null,
/* overrideGroupKey= */ null,
/* channel= */ null,
/* overridePeople= */ new ArrayList<>(),
/* snoozeCriteria= */ new ArrayList<>(),
/* showBadge= */ false,
/* userSentiment= */ 0,
/* hidden= */ false,
/* lastAudiblyAlertedMs= */ 0,
/* noisy= */ false,
/* smartActions= */ new ArrayList<>(),
/* smartReplies= */ new ArrayList<>(),
/* canBubble= */ false,
/* isTextChanged= */ false,
/* isConversation= */ false,
/* shortcutInfo= */ null,
/* rankingAdjustment= */ 0,
/* isBubble= */ false,
/* proposedImportance= */ 0,
/* sensitiveContent= */ false
);
}
return ranking;

View File

@@ -120,13 +120,31 @@ class TargetSdkResolverTest : SysuiTestCase() {
private fun createRanking(key: String) = Ranking().apply {
populate(
key,
0,
false,
0,
0,
NotificationManager.IMPORTANCE_DEFAULT,
null, null,
null, null, null, true, 0, false, -1, false, null, null, false, false,
false, null, 0, false, 0)
/* rank = */ 0,
/* matchesInterruptionFilter = */ false,
/* visibilityOverride = */ 0,
/* suppressedVisualEffects = */ 0,
/* importance = */ NotificationManager.IMPORTANCE_DEFAULT,
/* explanation = */ null,
/* overrideGroupKey = */ null,
/* channel = */ null,
/* overridePeople = */ null,
/* snoozeCriteria = */ null,
/* showBadge = */ true,
/* userSentiment = */ 0,
/* hidden = */ false,
/* lastAudiblyAlertedMs = */ -1,
/* noisy = */ false,
/* smartActions = */ null,
/* smartReplies = */ null,
/* canBubble = */ false,
/* isTextChanged = */ false,
/* isConversation = */ false,
/* shortcutInfo = */ null,
/* rankingAdjustment = */ 0,
/* isBubble = */ false,
/* proposedImportance = */ 0,
/* sensitiveContent = */ false
)
}
}

View File

@@ -60,6 +60,7 @@ public class RankingBuilder {
private int mRankingAdjustment = 0;
private boolean mIsBubble = false;
private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
private boolean mSensitiveContent = false;
public RankingBuilder() {
}
@@ -90,6 +91,7 @@ public class RankingBuilder {
mRankingAdjustment = ranking.getRankingAdjustment();
mIsBubble = ranking.isBubble();
mProposedImportance = ranking.getProposedImportance();
mSensitiveContent = ranking.hasSensitiveContent();
}
public Ranking build() {
@@ -119,7 +121,8 @@ public class RankingBuilder {
mShortcutInfo,
mRankingAdjustment,
mIsBubble,
mProposedImportance);
mProposedImportance,
mSensitiveContent);
return ranking;
}
@@ -224,6 +227,11 @@ public class RankingBuilder {
return this;
}
public RankingBuilder setSensitiveContent(boolean sensitiveContent) {
mSensitiveContent = sensitiveContent;
return this;
}
public RankingBuilder setUserSentiment(int userSentiment) {
mUserSentiment = userSentiment;
return this;

View File

@@ -8619,7 +8619,8 @@ public class NotificationManagerService extends SystemService {
r.getImportance(),
r.getRankingScore(),
r.isConversation(),
r.getProposedImportance());
r.getProposedImportance(),
r.hasSensitiveContent());
extractorDataBefore.put(r.getKey(), extractorData);
mRankingHelper.extractSignals(r);
}
@@ -9915,7 +9916,8 @@ public class NotificationManagerService extends SystemService {
? RANKING_UNCHANGED
: (record.getRankingScore() > 0 ? RANKING_PROMOTED : RANKING_DEMOTED),
record.getNotification().isBubbleNotification(),
record.getProposedImportance()
record.getProposedImportance(),
record.hasSensitiveContent()
);
rankings.add(ranking);
}

View File

@@ -211,6 +211,7 @@ public final class NotificationRecord {
// are sorted.
private boolean mPendingLogUpdate = false;
private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
private boolean mSensitiveContent = false;
public NotificationRecord(Context context, StatusBarNotification sbn,
NotificationChannel channel) {
@@ -503,6 +504,7 @@ public final class NotificationRecord {
pw.println(prefix + "mProposedImportance="
+ NotificationListenerService.Ranking.importanceToString(mProposedImportance));
pw.println(prefix + "mIsAppImportanceLocked=" + mIsAppImportanceLocked);
pw.println(prefix + "mSensitiveContent=" + mSensitiveContent);
pw.println(prefix + "mIntercept=" + mIntercept);
pw.println(prefix + "mHidden==" + mHidden);
pw.println(prefix + "mGlobalSortKey=" + mGlobalSortKey);
@@ -747,6 +749,12 @@ public final class NotificationRecord {
Adjustment.KEY_IMPORTANCE_PROPOSAL,
Integer.toString(mProposedImportance));
}
if (signals.containsKey(Adjustment.KEY_SENSITIVE_CONTENT)) {
mSensitiveContent = signals.getBoolean(Adjustment.KEY_SENSITIVE_CONTENT);
EventLogTags.writeNotificationAdjusted(getKey(),
Adjustment.KEY_SENSITIVE_CONTENT,
Boolean.toString(mSensitiveContent));
}
if (!signals.isEmpty() && adjustment.getIssuer() != null) {
mAdjustmentIssuer = adjustment.getIssuer();
}
@@ -883,6 +891,13 @@ public final class NotificationRecord {
return mProposedImportance;
}
/**
* @return true if the notification contains sensitive content detected by the assistant.
*/
public boolean hasSensitiveContent() {
return mSensitiveContent;
}
public float getRankingScore() {
return mRankingScore;
}

View File

@@ -46,6 +46,7 @@ public final class NotificationRecordExtractorData {
private final float mRankingScore;
private final boolean mIsConversation;
private final int mProposedImportance;
private final boolean mSensitiveContent;
NotificationRecordExtractorData(int position, int visibility, boolean showBadge,
boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey,
@@ -53,7 +54,7 @@ public final class NotificationRecordExtractorData {
Integer userSentiment, Integer suppressVisually,
ArrayList<Notification.Action> systemSmartActions,
ArrayList<CharSequence> smartReplies, int importance, float rankingScore,
boolean isConversation, int proposedImportance) {
boolean isConversation, int proposedImportance, boolean sensitiveContent) {
mPosition = position;
mVisibility = visibility;
mShowBadge = showBadge;
@@ -71,6 +72,7 @@ public final class NotificationRecordExtractorData {
mRankingScore = rankingScore;
mIsConversation = isConversation;
mProposedImportance = proposedImportance;
mSensitiveContent = sensitiveContent;
}
// Returns whether the provided NotificationRecord differs from the cached data in any way.
@@ -90,7 +92,8 @@ public final class NotificationRecordExtractorData {
|| !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
|| !Objects.equals(mSmartReplies, r.getSmartReplies())
|| mImportance != r.getImportance()
|| mProposedImportance != r.getProposedImportance();
|| mProposedImportance != r.getProposedImportance()
|| mSensitiveContent != r.hasSensitiveContent();
}
// Returns whether the NotificationRecord has a change from this data for which we should
@@ -113,6 +116,7 @@ public final class NotificationRecordExtractorData {
|| mImportance != r.getImportance()
|| !r.rankingScoreMatches(mRankingScore)
|| mIsConversation != r.isConversation()
|| mProposedImportance != r.getProposedImportance();
|| mProposedImportance != r.getProposedImportance()
|| mSensitiveContent != r.hasSensitiveContent();
}
}

View File

@@ -1737,15 +1737,33 @@ public final class DataManagerTest {
NotificationListenerService.Ranking ranking = (NotificationListenerService.Ranking)
invocationOnMock.getArguments()[1];
ranking.populate(
(String) invocationOnMock.getArguments()[0],
0,
false,
0,
0,
/* key= */ (String) invocationOnMock.getArguments()[0],
/* rank= */ 0,
/* matchesInterruptionFilter= */ false,
/* visibilityOverride= */ 0,
/* suppressedVisualEffects= */ 0,
mParentNotificationChannel.getImportance(),
null, null,
mParentNotificationChannel, null, null, true, 0, false, -1, false, null, null,
false, false, false, null, 0, false, 0);
/* explanation= */ null,
/* overrideGroupKey= */ null,
mParentNotificationChannel,
/* overridePeople= */ null,
/* snoozeCriteria= */ null,
/* showBadge= */ true,
/* userSentiment= */ 0,
/* hidden= */ false,
/* lastAudiblyAlertedMs= */ -1,
/* noisy= */ false,
/* smartActions= */ null,
/* smartReplies= */ null,
/* canBubble= */ false,
/* isTextChanged= */ false,
/* isConversation= */ false,
/* shortcutInfo= */ null,
/* rankingAdjustment= */ 0,
/* isBubble= */ false,
/* proposedImportance= */ 0,
/* sensitiveContent= */ false
);
return true;
}).when(mRankingMap).getRanking(eq(key),
any(NotificationListenerService.Ranking.class));
@@ -1763,15 +1781,33 @@ public final class DataManagerTest {
NotificationListenerService.Ranking ranking = (NotificationListenerService.Ranking)
invocationOnMock.getArguments()[1];
ranking.populate(
(String) invocationOnMock.getArguments()[0],
0,
false,
0,
0,
mNotificationChannel.getImportance(),
null, null,
mNotificationChannel, null, null, true, 0, false, -1, false, null, null, false,
false, false, null, 0, false, 0);
/* key= */ (String) invocationOnMock.getArguments()[0],
/* rank= */ 0,
/* matchesInterruptionFilter= */ false,
/* visibilityOverride= */ 0,
/* suppressedVisualEffects= */ 0,
mParentNotificationChannel.getImportance(),
/* explanation= */ null,
/* overrideGroupKey= */ null,
mParentNotificationChannel,
/* overridePeople= */ null,
/* snoozeCriteria= */ null,
/* showBadge= */ true,
/* userSentiment= */ 0,
/* hidden= */ false,
/* lastAudiblyAlertedMs= */ -1,
/* noisy= */ false,
/* smartActions= */ null,
/* smartReplies= */ null,
/* canBubble= */ false,
/* isTextChanged= */ false,
/* isConversation= */ false,
/* shortcutInfo= */ null,
/* rankingAdjustment= */ 0,
/* isBubble= */ false,
/* proposedImportance= */ 0,
/* sensitiveContent= */ false
);
return true;
}).when(mRankingMap).getRanking(eq(CUSTOM_KEY),
any(NotificationListenerService.Ranking.class));

View File

@@ -194,7 +194,8 @@ public class NotificationListenerServiceTest extends UiServiceTestCase {
tweak.getConversationShortcutInfo(),
tweak.getRankingAdjustment(),
tweak.isBubble(),
tweak.getProposedImportance()
tweak.getProposedImportance(),
tweak.hasSensitiveContent()
);
assertNotEquals(nru, nru2);
}
@@ -276,7 +277,8 @@ public class NotificationListenerServiceTest extends UiServiceTestCase {
getShortcutInfo(i),
getRankingAdjustment(i),
isBubble(i),
getProposedImportance(i)
getProposedImportance(i),
hasSensitiveContent(i)
);
rankings[i] = ranking;
}
@@ -408,6 +410,10 @@ public class NotificationListenerServiceTest extends UiServiceTestCase {
return index % 5 - 1;
}
private boolean hasSensitiveContent(int index) {
return index % 3 == 0;
}
private boolean isBubble(int index) {
return index % 4 == 0;
}
@@ -450,6 +456,7 @@ public class NotificationListenerServiceTest extends UiServiceTestCase {
b.getConversationShortcutInfo().getId());
assertActionsEqual(a.getSmartActions(), b.getSmartActions());
assertEquals(a.getProposedImportance(), b.getProposedImportance());
assertEquals(a.hasSensitiveContent(), b.hasSensitiveContent());
}
private void detailedAssertEquals(RankingMap a, RankingMap b) {

View File

@@ -19,29 +19,20 @@ package com.android.server.notification;
import static android.app.NotificationManager.IMPORTANCE_HIGH;
import static android.app.NotificationManager.IMPORTANCE_LOW;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.UserHandle;
import android.service.notification.Adjustment;
import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
import com.android.server.UiServiceTestCase;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Objects;
public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
@Test
@@ -65,7 +56,8 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
r.getImportance(),
r.getRankingScore(),
r.isConversation(),
r.getProposedImportance());
r.getProposedImportance(),
r.hasSensitiveContent());
assertFalse(extractorData.hasDiffForRankingLocked(r, 1));
assertFalse(extractorData.hasDiffForLoggingLocked(r, 1));
@@ -92,7 +84,8 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
r.getImportance(),
r.getRankingScore(),
r.isConversation(),
r.getProposedImportance());
r.getProposedImportance(),
r.hasSensitiveContent());
Bundle signals = new Bundle();
signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_HIGH);
@@ -104,6 +97,40 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
assertTrue(extractorData.hasDiffForLoggingLocked(r, 1));
}
@Test
public void testHasDiffs_sensitiveContentChange() {
NotificationRecord r = generateRecord();
NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData(
1,
r.getPackageVisibilityOverride(),
r.canShowBadge(),
r.canBubble(),
r.getNotification().isBubbleNotification(),
r.getChannel(),
r.getGroupKey(),
r.getPeopleOverride(),
r.getSnoozeCriteria(),
r.getUserSentiment(),
r.getSuppressedVisualEffects(),
r.getSystemGeneratedSmartActions(),
r.getSmartReplies(),
r.getImportance(),
r.getRankingScore(),
r.isConversation(),
r.getProposedImportance(),
r.hasSensitiveContent());
Bundle signals = new Bundle();
signals.putBoolean(Adjustment.KEY_SENSITIVE_CONTENT, true);
Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0);
r.addAdjustment(adjustment);
r.applyAdjustments();
assertTrue(extractorData.hasDiffForRankingLocked(r, 1));
assertTrue(extractorData.hasDiffForLoggingLocked(r, 1));
}
private NotificationRecord generateRecord() {
NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
final Notification.Builder builder = new Notification.Builder(getContext())

View File

@@ -787,6 +787,24 @@ public class NotificationRecordTest extends UiServiceTestCase {
assertFalse(record.getIsAppImportanceLocked());
}
@Test
public void testSensitiveContent() {
StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
false /* lights */, false /* defaultLights */, groupId /* group */);
NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
assertFalse(record.hasSensitiveContent());
Bundle signals = new Bundle();
signals.putBoolean(Adjustment.KEY_SENSITIVE_CONTENT, true);
record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId()));
record.applyAdjustments();
assertTrue(record.hasSensitiveContent());
}
@Test
public void testIsInterruptive_textChanged_notSeen() {
StatusBarNotification sbn = getNotification(PKG_O, false /* noisy */,