method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR;
field public static final java.lang.String KEY_PEOPLE = "key_people";
+ field public static final java.lang.String KEY_SMART_ACTIONS = "key_smart_actions";
field public static final java.lang.String KEY_SNOOZE_CRITERIA = "key_snooze_criteria";
field public static final java.lang.String KEY_USER_SENTIMENT = "key_user_sentiment";
}
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR;
field public static final java.lang.String KEY_PEOPLE = "key_people";
+ field public static final java.lang.String KEY_SMART_ACTIONS = "key_smart_actions";
field public static final java.lang.String KEY_SNOOZE_CRITERIA = "key_snooze_criteria";
field public static final java.lang.String KEY_USER_SENTIMENT = "key_user_sentiment";
}
public static final String KEY_USER_SENTIMENT = "key_user_sentiment";
/**
+ * Data type: ArrayList of {@link android.app.Notification.Action}.
+ * Used to suggest extra actions for a notification.
+ */
+ public static final String KEY_SMART_ACTIONS = "key_smart_actions";
+
+ /**
* Create a notification adjustment.
*
* @param pkg The package of the notification.
private boolean mShowBadge;
private @UserSentiment int mUserSentiment = USER_SENTIMENT_NEUTRAL;
private boolean mHidden;
+ private ArrayList<Notification.Action> mSmartActions;
public Ranking() {}
}
/**
+ * @hide
+ */
+ public List<Notification.Action> getSmartActions() {
+ return mSmartActions;
+ }
+
+ /**
* Returns whether this notification can be displayed as a badge.
*
* @return true if the notification can be displayed as a badge, false otherwise.
CharSequence explanation, String overrideGroupKey,
NotificationChannel channel, ArrayList<String> overridePeople,
ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge,
- int userSentiment, boolean hidden) {
+ int userSentiment, boolean hidden, ArrayList<Notification.Action> smartActions) {
mKey = key;
mRank = rank;
mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW;
mShowBadge = showBadge;
mUserSentiment = userSentiment;
mHidden = hidden;
+ mSmartActions = smartActions;
}
/**
private ArrayMap<String, Boolean> mShowBadge;
private ArrayMap<String, Integer> mUserSentiment;
private ArrayMap<String, Boolean> mHidden;
+ private ArrayMap<String, ArrayList<Notification.Action>> mSmartActions;
private RankingMap(NotificationRankingUpdate rankingUpdate) {
mRankingUpdate = rankingUpdate;
getVisibilityOverride(key), getSuppressedVisualEffects(key),
getImportance(key), getImportanceExplanation(key), getOverrideGroupKey(key),
getChannel(key), getOverridePeople(key), getSnoozeCriteria(key),
- getShowBadge(key), getUserSentiment(key), getHidden(key));
+ getShowBadge(key), getUserSentiment(key), getHidden(key), getSmartActions(key));
return rank >= 0;
}
return hidden == null ? false : hidden.booleanValue();
}
+ private ArrayList<Notification.Action> getSmartActions(String key) {
+ synchronized (this) {
+ if (mSmartActions == null) {
+ buildSmartActions();
+ }
+ }
+ return mSmartActions.get(key);
+ }
+
// Locked by 'this'
private void buildRanksLocked() {
String[] orderedKeys = mRankingUpdate.getOrderedKeys();
}
}
+ // Locked by 'this'
+ private void buildSmartActions() {
+ Bundle smartActions = mRankingUpdate.getSmartActions();
+ mSmartActions = new ArrayMap<>(smartActions.size());
+ for (String key : smartActions.keySet()) {
+ mSmartActions.put(key, smartActions.getParcelableArrayList(key));
+ }
+ }
+
// ----------- Parcelable
@Override
private final Bundle mShowBadge;
private final Bundle mUserSentiment;
private final Bundle mHidden;
+ private final Bundle mSmartActions;
public NotificationRankingUpdate(String[] keys, String[] interceptedKeys,
Bundle visibilityOverrides, Bundle suppressedVisualEffects,
int[] importance, Bundle explanation, Bundle overrideGroupKeys,
Bundle channels, Bundle overridePeople, Bundle snoozeCriteria,
- Bundle showBadge, Bundle userSentiment, Bundle hidden) {
+ Bundle showBadge, Bundle userSentiment, Bundle hidden, Bundle smartActions) {
mKeys = keys;
mInterceptedKeys = interceptedKeys;
mVisibilityOverrides = visibilityOverrides;
mShowBadge = showBadge;
mUserSentiment = userSentiment;
mHidden = hidden;
+ mSmartActions = smartActions;
}
public NotificationRankingUpdate(Parcel in) {
mShowBadge = in.readBundle();
mUserSentiment = in.readBundle();
mHidden = in.readBundle();
+ mSmartActions = in.readBundle();
}
@Override
out.writeBundle(mShowBadge);
out.writeBundle(mUserSentiment);
out.writeBundle(mHidden);
+ out.writeBundle(mSmartActions);
}
public static final Parcelable.Creator<NotificationRankingUpdate> CREATOR
public Bundle getHidden() {
return mHidden;
}
+
+ public Bundle getSmartActions() {
+ return mSmartActions;
+ }
}
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.Nullable;
+import android.app.Notification;
import android.app.NotificationChannel;
import android.content.Context;
import android.content.pm.PackageInfo;
mNotificationInflater.setUsesIncreasedHeight(use);
}
+ public void setSmartActions(List<Notification.Action> smartActions) {
+ mNotificationInflater.setSmartActions(smartActions);
+ }
+
public void setUseIncreasedHeadsUpHeight(boolean use) {
mUseIncreasedHeadsUpHeight = use;
mNotificationInflater.setUsesIncreasedHeadsUpHeight(use);
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
import android.Manifest;
+import android.annotation.NonNull;
import android.app.AppGlobals;
import android.app.Notification;
import android.app.NotificationChannel;
import android.widget.ImageView;
import android.widget.RemoteViews;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.util.ArrayUtils;
public CharSequence remoteInputText;
public List<SnoozeCriterion> snoozeCriteria;
public int userSentiment = Ranking.USER_SENTIMENT_NEUTRAL;
+ @NonNull
+ public List<Notification.Action> smartActions = Collections.emptyList();
private int mCachedContrastColor = COLOR_INVALID;
private int mCachedContrastColorIsFor = COLOR_INVALID;
private boolean hasSentReply;
public Entry(StatusBarNotification n) {
+ this(n, null);
+ }
+
+ public Entry(StatusBarNotification n, @Nullable Ranking ranking) {
this.key = n.getKey();
this.notification = n;
+ if (ranking != null) {
+ populateFromRanking(ranking);
+ }
+ }
+
+ public void populateFromRanking(@NonNull Ranking ranking) {
+ channel = ranking.getChannel();
+ snoozeCriteria = ranking.getSnoozeCriteria();
+ userSentiment = ranking.getUserSentiment();
+ smartActions = ranking.getSmartActions() == null
+ ? Collections.emptyList() : ranking.getSmartActions();
}
public void setInterruption() {
/**
* Update the notification icons.
+ *
* @param context the context to create the icons with.
* @param sbn the notification to read the icon from.
* @throws InflationException
}
public void onInflationTaskFinished() {
- mRunningTask = null;
+ mRunningTask = null;
}
@VisibleForTesting
getRanking(key, mTmpRanking);
return mTmpRanking.getOverrideGroupKey();
}
- return null;
+ return null;
}
public List<SnoozeCriterion> getSnoozeCriteria(String key) {
entry.notification.setOverrideGroupKey(overrideGroupKey);
mGroupManager.onEntryUpdated(entry, oldSbn);
}
- entry.channel = getChannel(entry.key);
- entry.snoozeCriteria = getSnoozeCriteria(entry.key);
- entry.userSentiment = mTmpRanking.getUserSentiment();
+ entry.populateFromRanking(mTmpRanking);
}
}
}
public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
public String getCurrentMediaNotificationKey();
public NotificationGroupManager getGroupManager();
+
/**
* @return true iff the device is dozing
*/
import static com.android.systemui.statusbar.NotificationRemoteInputManager
.FORCE_REMOTE_INPUT_HISTORY;
+import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.EventLog;
import android.util.Log;
&& !mPresenter.isPresenterFullyCollapsed();
row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
+ row.setSmartActions(entry.smartActions);
row.updateNotification(entry);
}
-
protected void addNotificationViews(NotificationData.Entry entry) {
if (entry == null) {
return;
updateNotifications();
}
- protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn)
+ protected NotificationData.Entry createNotificationViews(
+ StatusBarNotification sbn, NotificationListenerService.Ranking ranking)
throws InflationException {
if (DEBUG) {
- Log.d(TAG, "createNotificationViews(notification=" + sbn);
+ Log.d(TAG, "createNotificationViews(notification=" + sbn + " " + ranking);
}
- NotificationData.Entry entry = new NotificationData.Entry(sbn);
+ NotificationData.Entry entry = new NotificationData.Entry(sbn, ranking);
Dependency.get(LeakDetector.class).trackInstance(entry);
entry.createIcons(mContext, sbn);
// Construct the expanded view.
}
private void addNotificationInternal(StatusBarNotification notification,
- NotificationListenerService.RankingMap ranking) throws InflationException {
+ NotificationListenerService.RankingMap rankingMap) throws InflationException {
String key = notification.getKey();
if (DEBUG) Log.d(TAG, "addNotification key=" + key);
- mNotificationData.updateRanking(ranking);
- NotificationData.Entry shadeEntry = createNotificationViews(notification);
+ mNotificationData.updateRanking(rankingMap);
+ NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
+ rankingMap.getRanking(key, ranking);
+ NotificationData.Entry shadeEntry = createNotificationViews(notification, ranking);
boolean isHeadsUped = shouldPeek(shadeEntry);
if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) {
if (shouldSuppressFullScreenIntent(shadeEntry)) {
mPresenter.updateNotificationViews();
}
- public void updateNotificationRanking(NotificationListenerService.RankingMap ranking) {
- mNotificationData.updateRanking(ranking);
+ public void updateNotificationRanking(NotificationListenerService.RankingMap rankingMap) {
+ List<NotificationData.Entry> entries = new ArrayList<>();
+ entries.addAll(mNotificationData.getActiveNotifications());
+ entries.addAll(mPendingNotifications.values());
+
+ // Has a copy of the current UI adjustments.
+ ArrayMap<String, NotificationUiAdjustment> oldAdjustments = new ArrayMap<>();
+ for (NotificationData.Entry entry : entries) {
+ NotificationUiAdjustment adjustment =
+ NotificationUiAdjustment.extractFromNotificationEntry(entry);
+ oldAdjustments.put(entry.key, adjustment);
+ }
+
+ // Populate notification entries from the new rankings.
+ mNotificationData.updateRanking(rankingMap);
+ updateRankingOfPendingNotifications(rankingMap);
+
+ // By comparing the old and new UI adjustments, reinflate the view accordingly.
+ for (NotificationData.Entry entry : entries) {
+ NotificationUiAdjustment newAdjustment =
+ NotificationUiAdjustment.extractFromNotificationEntry(entry);
+
+ if (NotificationUiAdjustment.needReinflate(
+ oldAdjustments.get(entry.key), newAdjustment)) {
+ if (entry.row != null) {
+ entry.reset();
+ PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,
+ entry.notification.getUser().getIdentifier());
+ updateNotification(entry, pmUser, entry.notification, entry.row);
+ } else {
+ // Once the RowInflaterTask is done, it will pick up the updated entry, so
+ // no-op here.
+ }
+ }
+ }
+
updateNotifications();
}
+ private void updateRankingOfPendingNotifications(
+ @Nullable NotificationListenerService.RankingMap rankingMap) {
+ if (rankingMap == null) {
+ return;
+ }
+ NotificationListenerService.Ranking tmpRanking = new NotificationListenerService.Ranking();
+ for (NotificationData.Entry pendingNotification : mPendingNotifications.values()) {
+ rankingMap.getRanking(pendingNotification.key, tmpRanking);
+ pendingNotification.populateFromRanking(tmpRanking);
+ }
+ }
+
protected boolean shouldPeek(NotificationData.Entry entry) {
return shouldPeek(entry, entry.notification);
}
--- /dev/null
+/*
+ * Copyright 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.systemui.statusbar;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.RemoteInput;
+import android.graphics.drawable.Icon;
+import android.text.TextUtils;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * By diffing two entries, determines is view reinflation needed.
+ */
+public class NotificationUiAdjustment {
+
+ public final String key;
+ public final List<Notification.Action> smartActions;
+
+ @VisibleForTesting
+ NotificationUiAdjustment(String key, List<Notification.Action> smartActions) {
+ this.key = key;
+ this.smartActions = smartActions == null
+ ? Collections.emptyList()
+ : new ArrayList<>(smartActions);
+ }
+
+ public static NotificationUiAdjustment extractFromNotificationEntry(
+ NotificationData.Entry entry) {
+ return new NotificationUiAdjustment(entry.key, entry.smartActions);
+ }
+
+ public static boolean needReinflate(
+ @NonNull NotificationUiAdjustment oldAdjustment,
+ @NonNull NotificationUiAdjustment newAdjustment) {
+ if (oldAdjustment == newAdjustment) {
+ return false;
+ }
+ return areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions);
+ }
+
+ public static boolean areDifferent(
+ @NonNull List<Notification.Action> first, @NonNull List<Notification.Action> second) {
+ if (first == second) {
+ return false;
+ }
+ if (first == null || second == null) {
+ return true;
+ }
+ if (first.size() != second.size()) {
+ return true;
+ }
+ for (int i = 0; i < first.size(); i++) {
+ Notification.Action firstAction = first.get(i);
+ Notification.Action secondAction = second.get(i);
+
+ if (!TextUtils.equals(firstAction.title, secondAction.title)) {
+ return true;
+ }
+
+ if (areDifferent(firstAction.getIcon(), secondAction.getIcon())) {
+ return true;
+ }
+
+ if (!Objects.equals(firstAction.actionIntent, secondAction.actionIntent)) {
+ return true;
+ }
+
+ if (areDifferent(firstAction.getRemoteInputs(), secondAction.getRemoteInputs())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean areDifferent(@Nullable Icon first, @Nullable Icon second) {
+ if (first == second) {
+ return false;
+ }
+ if (first == null || second == null) {
+ return true;
+ }
+ return !first.sameAs(second);
+ }
+
+ private static boolean areDifferent(
+ @Nullable RemoteInput[] first, @Nullable RemoteInput[] second) {
+ if (first == second) {
+ return false;
+ }
+ if (first == null || second == null) {
+ return true;
+ }
+ if (first.length != second.length) {
+ return true;
+ }
+ for (int i = 0; i < first.length; i++) {
+ RemoteInput firstRemoteInput = first[i];
+ RemoteInput secondRemoteInput = second[i];
+
+ if (!TextUtils.equals(firstRemoteInput.getLabel(), secondRemoteInput.getLabel())) {
+ return true;
+ }
+ if (areDifferent(firstRemoteInput.getChoices(), secondRemoteInput.getChoices())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean areDifferent(
+ @Nullable CharSequence[] first, @Nullable CharSequence[] second) {
+ if (first == second) {
+ return false;
+ }
+ if (first == null || second == null) {
+ return true;
+ }
+ if (first.length != second.length) {
+ return true;
+ }
+ for (int i = 0; i < first.length; i++) {
+ CharSequence firstCharSequence = first[i];
+ CharSequence secondCharSequence = second[i];
+ if (!TextUtils.equals(firstCharSequence, secondCharSequence)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
import android.widget.RemoteViews;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.R;
-import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.NotificationContentView;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.util.Assert;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
private boolean mIsChildInGroup;
private InflationCallback mCallback;
private boolean mRedactAmbient;
+ private List<Notification.Action> mSmartActions;
public NotificationInflater(ExpandableNotificationRow row) {
mRow = row;
mUsesIncreasedHeight = usesIncreasedHeight;
}
+ public void setSmartActions(List<Notification.Action> smartActions) {
+ mSmartActions = smartActions;
+ }
+
public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) {
mUsesIncreasedHeadsUpHeight = usesIncreasedHeight;
}
AsyncInflationTask task = new AsyncInflationTask(sbn, reInflateFlags, mRow,
mIsLowPriority,
mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
- mCallback, mRemoteViewClickHandler);
+ mCallback, mRemoteViewClickHandler, mSmartActions);
if (mCallback != null && mCallback.doInflateSynchronous()) {
task.onPostExecute(task.doInBackground());
} else {
private Exception mError;
private RemoteViews.OnClickHandler mRemoteViewClickHandler;
private CancellationSignal mCancellationSignal;
+ private List<Notification.Action> mSmartActions;
private AsyncInflationTask(StatusBarNotification notification,
int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority,
boolean isChildInGroup, boolean usesIncreasedHeight,
boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
InflationCallback callback,
- RemoteViews.OnClickHandler remoteViewClickHandler) {
+ RemoteViews.OnClickHandler remoteViewClickHandler,
+ List<Notification.Action> smartActions) {
mRow = row;
mSbn = notification;
mReInflateFlags = reInflateFlags;
mRedactAmbient = redactAmbient;
mRemoteViewClickHandler = remoteViewClickHandler;
mCallback = callback;
+ mSmartActions = smartActions == null
+ ? Collections.emptyList()
+ : new ArrayList<>(smartActions);
NotificationData.Entry entry = row.getEntry();
entry.setInflationTask(this);
}
final Notification.Builder recoveredBuilder
= Notification.Builder.recoverBuilder(mContext,
mSbn.getNotification());
+
+ applyChanges(recoveredBuilder);
+
Context packageContext = mSbn.getPackageContext(mContext);
Notification notification = mSbn.getNotification();
if (notification.isMediaNotification()) {
}
}
+ /**
+ * Apply changes to the given notification builder, like adding smart actions suggested by
+ * a {@link android.service.notification.NotificationAssistantService}.
+ */
+ private void applyChanges(Notification.Builder builder) {
+ if (mSmartActions != null) {
+ for (Notification.Action smartAction : mSmartActions) {
+ builder.addAction(smartAction);
+ }
+ }
+ }
+
private void handleError(Exception e) {
mRow.getEntry().onInflationTaskFinished();
StatusBarNotification sbn = mRow.getStatusBarNotification();
import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.Intent;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
import android.media.session.MediaSession;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationListenerService.Ranking;
+import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.SmallTest;
private static final int UID_ALLOW_DURING_SETUP = 456;
private static final String TEST_HIDDEN_NOTIFICATION_KEY = "testHiddenNotificationKey";
private static final String TEST_EXEMPT_DND_VISUAL_SUPPRESSION_KEY = "exempt";
+ private static final NotificationChannel NOTIFICATION_CHANNEL =
+ new NotificationChannel("id", "name", NotificationChannel.USER_LOCKED_IMPORTANCE);
private final StatusBarNotification mMockStatusBarNotification =
mock(StatusBarNotification.class);
@Test
public void testChannelSetWhenAdded() {
mNotificationData.add(mRow.getEntry());
- Assert.assertTrue(mRow.getEntry().channel != null);
+ assertEquals(NOTIFICATION_CHANNEL, mRow.getEntry().channel);
}
-
-
@Test
public void testAllRelevantNotisTaggedWithAppOps() throws Exception {
mNotificationData.add(mRow.getEntry());
assertFalse(mNotificationData.isExemptFromDndVisualSuppression(entry));
}
+ @Test
+ public void testCreateNotificationDataEntry_RankingUpdate() {
+ Ranking ranking = mock(Ranking.class);
+
+ ArrayList<Notification.Action> smartActions = new ArrayList<>();
+ smartActions.add(createAction());
+ when(ranking.getSmartActions()).thenReturn(smartActions);
+
+ when(ranking.getChannel()).thenReturn(NOTIFICATION_CHANNEL);
+
+ when(ranking.getUserSentiment()).thenReturn(Ranking.USER_SENTIMENT_NEGATIVE);
+
+ SnoozeCriterion snoozeCriterion = new SnoozeCriterion("id", "explanation", "confirmation");
+ ArrayList<SnoozeCriterion> snoozeCriterions = new ArrayList<>();
+ snoozeCriterions.add(snoozeCriterion);
+ when(ranking.getSnoozeCriteria()).thenReturn(snoozeCriterions);
+
+ NotificationData.Entry entry =
+ new NotificationData.Entry(mMockStatusBarNotification, ranking);
+
+ assertEquals(smartActions, entry.smartActions);
+ assertEquals(NOTIFICATION_CHANNEL, entry.channel);
+ assertEquals(Ranking.USER_SENTIMENT_NEGATIVE, entry.userSentiment);
+ assertEquals(snoozeCriterions, entry.snoozeCriteria);
+ }
+
private void initStatusBarNotification(boolean allowDuringSetup) {
Bundle bundle = new Bundle();
bundle.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, allowDuringSetup);
}
@Override
- public NotificationChannel getChannel(String key) {
- return new NotificationChannel(null, null, 0);
- }
-
- @Override
- protected boolean getRanking(String key, NotificationListenerService.Ranking outRanking) {
+ protected boolean getRanking(String key, Ranking outRanking) {
super.getRanking(key, outRanking);
if (key.equals(TEST_HIDDEN_NOTIFICATION_KEY)) {
outRanking.populate(key, outRanking.getRank(),
outRanking.getVisibilityOverride(), outRanking.getSuppressedVisualEffects(),
outRanking.getImportance(), outRanking.getImportanceExplanation(),
outRanking.getOverrideGroupKey(), outRanking.getChannel(), null, null,
- outRanking.canShowBadge(), outRanking.getUserSentiment(), true);
+ outRanking.canShowBadge(), outRanking.getUserSentiment(), true,
+ null);
} else if (key.equals(TEST_EXEMPT_DND_VISUAL_SUPPRESSION_KEY)) {
outRanking.populate(key, outRanking.getRank(),
outRanking.matchesInterruptionFilter(),
outRanking.getVisibilityOverride(), 255,
outRanking.getImportance(), outRanking.getImportanceExplanation(),
outRanking.getOverrideGroupKey(), outRanking.getChannel(), null, null,
- outRanking.canShowBadge(), outRanking.getUserSentiment(), true);
+ outRanking.canShowBadge(), outRanking.getUserSentiment(), true, null);
} else {
outRanking.populate(key, outRanking.getRank(),
outRanking.matchesInterruptionFilter(),
outRanking.getVisibilityOverride(), outRanking.getSuppressedVisualEffects(),
outRanking.getImportance(), outRanking.getImportanceExplanation(),
- outRanking.getOverrideGroupKey(), outRanking.getChannel(), null, null,
- outRanking.canShowBadge(), outRanking.getUserSentiment(), false);
+ outRanking.getOverrideGroupKey(), NOTIFICATION_CHANNEL, null, null,
+ outRanking.canShowBadge(), outRanking.getUserSentiment(), false, null);
}
return true;
}
}
+
+ private Notification.Action createAction() {
+ return new Notification.Action.Builder(
+ Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon),
+ "action",
+ PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 0)).build();
+ }
}
import android.app.AppOpsManager;
import android.app.Notification;
import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import com.android.systemui.ForegroundServiceController;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.NotificationInflater;
+import com.android.systemui.statusbar.notification.RowInflaterTask;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Mock private VisualStabilityManager mVisualStabilityManager;
@Mock private MetricsLogger mMetricsLogger;
@Mock private SmartReplyController mSmartReplyController;
+ @Mock private RowInflaterTask mAsyncInflationTask;
private NotificationData.Entry mEntry;
private StatusBarNotification mSbn;
0,
NotificationManager.IMPORTANCE_DEFAULT,
null, null,
- null, null, null, true, sentiment, false);
+ null, null, null, true, sentiment, false, null);
+ return true;
+ }).when(mRankingMap).getRanking(eq(key), any(NotificationListenerService.Ranking.class));
+ }
+
+ private void setSmartActions(String key, ArrayList<Notification.Action> smartActions) {
+ doAnswer(invocationOnMock -> {
+ NotificationListenerService.Ranking ranking = (NotificationListenerService.Ranking)
+ invocationOnMock.getArguments()[1];
+ ranking.populate(
+ key,
+ 0,
+ false,
+ 0,
+ 0,
+ NotificationManager.IMPORTANCE_DEFAULT,
+ null, null,
+ null, null, null, true,
+ NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL, false,
+ smartActions);
return true;
}).when(mRankingMap).getRanking(eq(key), any(NotificationListenerService.Ranking.class));
}
Assert.assertTrue(newSbn.getNotification().extras
.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
}
+
+ @Test
+ public void testUpdateNotificationRanking() {
+ when(mPresenter.isDeviceProvisioned()).thenReturn(true);
+ when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true);
+
+ mEntry.row = mRow;
+ mEntry.setInflationTask(mAsyncInflationTask);
+ mEntryManager.getNotificationData().add(mEntry);
+ setSmartActions(mEntry.key, new ArrayList<>(Arrays.asList(createAction())));
+
+ mEntryManager.updateNotificationRanking(mRankingMap);
+ verify(mRow).updateNotification(eq(mEntry));
+ assertEquals(1, mEntry.smartActions.size());
+ assertEquals("action", mEntry.smartActions.get(0).title);
+ }
+
+ @Test
+ public void testUpdateNotificationRanking_noChange() {
+ when(mPresenter.isDeviceProvisioned()).thenReturn(true);
+ when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true);
+
+ mEntry.row = mRow;
+ mEntryManager.getNotificationData().add(mEntry);
+ setSmartActions(mEntry.key, null);
+
+ mEntryManager.updateNotificationRanking(mRankingMap);
+ verify(mRow, never()).updateNotification(eq(mEntry));
+ assertEquals(0, mEntry.smartActions.size());
+ }
+
+ @Test
+ public void testUpdateNotificationRanking_rowNotInflatedYet() {
+ when(mPresenter.isDeviceProvisioned()).thenReturn(true);
+ when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true);
+
+ mEntry.row = null;
+ mEntryManager.getNotificationData().add(mEntry);
+ setSmartActions(mEntry.key, new ArrayList<>(Arrays.asList(createAction())));
+
+ mEntryManager.updateNotificationRanking(mRankingMap);
+ verify(mRow, never()).updateNotification(eq(mEntry));
+ assertEquals(1, mEntry.smartActions.size());
+ assertEquals("action", mEntry.smartActions.get(0).title);
+ }
+
+ @Test
+ public void testUpdateNotificationRanking_pendingNotification() {
+ when(mPresenter.isDeviceProvisioned()).thenReturn(true);
+ when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true);
+
+ mEntry.row = null;
+ mEntryManager.mPendingNotifications.put(mEntry.key, mEntry);
+ setSmartActions(mEntry.key, new ArrayList<>(Arrays.asList(createAction())));
+
+ mEntryManager.updateNotificationRanking(mRankingMap);
+ verify(mRow, never()).updateNotification(eq(mEntry));
+ assertEquals(1, mEntry.smartActions.size());
+ assertEquals("action", mEntry.smartActions.get(0).title);
+ }
+
+ private Notification.Action createAction() {
+ return new Notification.Action.Builder(
+ Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon),
+ "action",
+ PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 0)).build();
+ }
}
--- /dev/null
+/*
+ * Copyright 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.systemui.statusbar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.support.test.filters.SmallTest;
+
+import com.android.internal.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Test;
+
+import java.util.Collections;
+
+@SmallTest
+public class NotificationUiAdjustmentTest extends SysuiTestCase {
+
+ @Test
+ public void needReinflate_differentLength() {
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+ Notification.Action action =
+ createActionBuilder("first", R.drawable.ic_corp_icon, pendingIntent).build();
+ assertThat(NotificationUiAdjustment.needReinflate(
+ new NotificationUiAdjustment("first", Collections.emptyList()),
+ new NotificationUiAdjustment("second", Collections.singletonList(action))))
+ .isTrue();
+ }
+
+ @Test
+ public void needReinflate_differentLabels() {
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+ Notification.Action firstAction =
+ createActionBuilder("first", R.drawable.ic_corp_icon, pendingIntent).build();
+ Notification.Action secondAction =
+ createActionBuilder("second", R.drawable.ic_corp_icon, pendingIntent).build();
+
+ assertThat(NotificationUiAdjustment.needReinflate(
+ new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+ new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+ .isTrue();
+ }
+
+ @Test
+ public void needReinflate_differentIcons() {
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+ Notification.Action firstAction =
+ createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent).build();
+ Notification.Action secondAction =
+ createActionBuilder("same", R.drawable.ic_account_circle, pendingIntent)
+ .build();
+
+ assertThat(NotificationUiAdjustment.needReinflate(
+ new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+ new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+ .isTrue();
+ }
+
+ @Test
+ public void needReinflate_differentPendingIntent() {
+ PendingIntent firstPendingIntent =
+ PendingIntent.getActivity(mContext, 0, new Intent(Intent.ACTION_VIEW), 0);
+ PendingIntent secondPendingIntent =
+ PendingIntent.getActivity(mContext, 0, new Intent(Intent.ACTION_PROCESS_TEXT), 0);
+ Notification.Action firstAction =
+ createActionBuilder("same", R.drawable.ic_corp_icon, firstPendingIntent)
+ .build();
+ Notification.Action secondAction =
+ createActionBuilder("same", R.drawable.ic_corp_icon, secondPendingIntent)
+ .build();
+
+ assertThat(NotificationUiAdjustment.needReinflate(
+ new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+ new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+ .isTrue();
+ }
+
+ @Test
+ public void needReinflate_differentChoices() {
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+
+ RemoteInput firstRemoteInput =
+ createRemoteInput("same", "same", new CharSequence[] {"first"});
+ RemoteInput secondRemoteInput =
+ createRemoteInput("same", "same", new CharSequence[] {"second"});
+
+ Notification.Action firstAction =
+ createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+ .addRemoteInput(firstRemoteInput)
+ .build();
+ Notification.Action secondAction =
+ createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+ .addRemoteInput(secondRemoteInput)
+ .build();
+
+ assertThat(NotificationUiAdjustment.needReinflate(
+ new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+ new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+ .isTrue();
+ }
+
+ @Test
+ public void needReinflate_differentRemoteInputLabel() {
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+
+ RemoteInput firstRemoteInput =
+ createRemoteInput("same", "first", new CharSequence[] {"same"});
+ RemoteInput secondRemoteInput =
+ createRemoteInput("same", "second", new CharSequence[] {"same"});
+
+ Notification.Action firstAction =
+ createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+ .addRemoteInput(firstRemoteInput)
+ .build();
+ Notification.Action secondAction =
+ createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+ .addRemoteInput(secondRemoteInput)
+ .build();
+
+ assertThat(NotificationUiAdjustment.needReinflate(
+ new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+ new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+ .isTrue();
+ }
+
+ @Test
+ public void needReinflate_negative() {
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+ RemoteInput firstRemoteInput =
+ createRemoteInput("same", "same", new CharSequence[] {"same"});
+ RemoteInput secondRemoteInput =
+ createRemoteInput("same", "same", new CharSequence[] {"same"});
+
+ Notification.Action firstAction =
+ createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+ .addRemoteInput(firstRemoteInput).build();
+ Notification.Action secondAction =
+ createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+ .addRemoteInput(secondRemoteInput).build();
+
+ assertThat(NotificationUiAdjustment.needReinflate(
+ new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+ new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+ .isFalse();
+ }
+
+ private Notification.Action.Builder createActionBuilder(
+ String title, int drawableRes, PendingIntent pendingIntent) {
+ return new Notification.Action.Builder(
+ Icon.createWithResource(mContext, drawableRes), title, pendingIntent);
+ }
+
+ private RemoteInput createRemoteInput(String resultKey, String label, CharSequence[] choices) {
+ return new RemoteInput.Builder(resultKey).setLabel(label).setChoices(choices).build();
+ }
+}
ArrayList<ArrayList<SnoozeCriterion>> snoozeCriteriaBefore = new ArrayList<>(N);
ArrayList<Integer> userSentimentBefore = new ArrayList<>(N);
ArrayList<Integer> suppressVisuallyBefore = new ArrayList<>(N);
+ ArrayList<ArrayList<Notification.Action>> smartActionsBefore = new ArrayList<>(N);
for (int i = 0; i < N; i++) {
final NotificationRecord r = mNotificationList.get(i);
orderBefore.add(r.getKey());
snoozeCriteriaBefore.add(r.getSnoozeCriteria());
userSentimentBefore.add(r.getUserSentiment());
suppressVisuallyBefore.add(r.getSuppressedVisualEffects());
+ smartActionsBefore.add(r.getSmartActions());
mRankingHelper.extractSignals(r);
}
mRankingHelper.sort(mNotificationList);
|| !Objects.equals(snoozeCriteriaBefore.get(i), r.getSnoozeCriteria())
|| !Objects.equals(userSentimentBefore.get(i), r.getUserSentiment())
|| !Objects.equals(suppressVisuallyBefore.get(i),
- r.getSuppressedVisualEffects())) {
+ r.getSuppressedVisualEffects())
+ || !Objects.equals(smartActionsBefore.get(i), r.getSmartActions())) {
mHandler.scheduleSendRankingUpdate();
return;
}
Bundle showBadge = new Bundle();
Bundle userSentiment = new Bundle();
Bundle hidden = new Bundle();
+ Bundle smartActions = new Bundle();
for (int i = 0; i < N; i++) {
NotificationRecord record = mNotificationList.get(i);
if (!isVisibleToListener(record.sbn, info)) {
showBadge.putBoolean(key, record.canShowBadge());
userSentiment.putInt(key, record.getUserSentiment());
hidden.putBoolean(key, record.isHidden());
+ smartActions.putParcelableArrayList(key, record.getSmartActions());
}
final int M = keys.size();
String[] keysAr = keys.toArray(new String[M]);
}
return new NotificationRankingUpdate(keysAr, interceptedKeysAr, visibilityOverrides,
suppressedVisualEffects, importanceAr, explanation, overrideGroupKeys,
- channels, overridePeople, snoozeCriteria, showBadge, userSentiment, hidden);
+ channels, overridePeople, snoozeCriteria, showBadge, userSentiment, hidden,
+ smartActions);
}
boolean hasCompanionDevice(ManagedServiceInfo info) {
private Light mLight;
private String mGroupLogTag;
private String mChannelIdLogTag;
+ private ArrayList<Notification.Action> mSmartActions;
private final List<Adjustment> mAdjustments;
private final NotificationStats mStats;
Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEUTRAL));
}
}
+ if (signals.containsKey(Adjustment.KEY_SMART_ACTIONS)) {
+ setSmartActions(signals.getParcelableArrayList(Adjustment.KEY_SMART_ACTIONS));
+ }
}
}
}
mHasSeenSmartReplies = hasSeenSmartReplies;
}
+ public void setSmartActions(ArrayList<Notification.Action> smartActions) {
+ mSmartActions = smartActions;
+ }
+
+ public ArrayList<Notification.Action> getSmartActions() {
+ return mSmartActions;
+ }
+
/**
* @return all {@link Uri} that should have permission granted to whoever
* will be rendering it. This list has already been vetted to only
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;
ArrayList<String> people = new ArrayList<>();
people.add("you");
signals.putStringArrayList(Adjustment.KEY_PEOPLE, people);
+ ArrayList<Notification.Action> smartActions = new ArrayList<>();
+ smartActions.add(createAction());
+ signals.putParcelableArrayList(Adjustment.KEY_SMART_ACTIONS, smartActions);
Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0);
r.addAdjustment(adjustment);
assertTrue(r.getGroupKey().contains(GroupHelper.AUTOGROUP_KEY));
assertEquals(people, r.getPeopleOverride());
assertEquals(snoozeCriteria, r.getSnoozeCriteria());
+ assertEquals(smartActions, r.getSmartActions());
}
@Test
0, n, UserHandle.ALL, null, System.currentTimeMillis());
return new NotificationRecord(getContext(), sbn, channel);
}
+
+ private Notification.Action createAction() {
+ return new Notification.Action.Builder(
+ Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon),
+ "action",
+ PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 0)).build();
+ }
}
import static org.mockito.Mockito.when;
import android.app.INotificationManager;
+import android.app.Notification;
import android.app.NotificationChannel;
+import android.app.PendingIntent;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.Parcel;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.NotificationRankingUpdate;
assertEquals(getShowBadge(i), ranking.canShowBadge());
assertEquals(getUserSentiment(i), ranking.getUserSentiment());
assertEquals(getHidden(i), ranking.isSuspended());
+ assertActionsEqual(getSmartActions(key, i), ranking.getSmartActions());
}
}
int[] importance = new int[mKeys.length];
Bundle userSentiment = new Bundle();
Bundle mHidden = new Bundle();
+ Bundle smartActions = new Bundle();
for (int i = 0; i < mKeys.length; i++) {
String key = mKeys[i];
showBadge.putBoolean(key, getShowBadge(i));
userSentiment.putInt(key, getUserSentiment(i));
mHidden.putBoolean(key, getHidden(i));
+ smartActions.putParcelableArrayList(key, getSmartActions(key, i));
}
NotificationRankingUpdate update = new NotificationRankingUpdate(mKeys,
interceptedKeys.toArray(new String[0]), visibilityOverrides,
suppressedVisualEffects, importance, explanation, overrideGroupKeys,
- channels, overridePeople, snoozeCriteria, showBadge, userSentiment, mHidden);
+ channels, overridePeople, snoozeCriteria, showBadge, userSentiment, mHidden,
+ smartActions);
return update;
}
return snooze;
}
+ private ArrayList<Notification.Action> getSmartActions(String key, int index) {
+ ArrayList<Notification.Action> actions = new ArrayList<>();
+ for (int i = 0; i < index; i++) {
+ PendingIntent intent = PendingIntent.getBroadcast(
+ getContext(),
+ index /*requestCode*/,
+ new Intent("ACTION_" + key),
+ 0 /*flags*/);
+ actions.add(new Notification.Action.Builder(null /*icon*/, key, intent).build());
+ }
+ return actions;
+ }
+
+ private void assertActionsEqual(
+ List<Notification.Action> expecteds, List<Notification.Action> actuals) {
+ assertEquals(expecteds.size(), actuals.size());
+ for (int i = 0; i < expecteds.size(); i++) {
+ Notification.Action expected = expecteds.get(i);
+ Notification.Action actual = actuals.get(i);
+ assertEquals(expected.title, actual.title);
+ }
+ }
+
public static class TestListenerService extends NotificationListenerService {
private final IBinder binder = new LocalBinder();
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
+import android.graphics.drawable.Icon;
import android.media.AudioAttributes;
import android.metrics.LogMaker;
import android.net.Uri;
import android.service.notification.StatusBarNotification;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
-import android.util.Slog;
+import com.android.internal.R;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.server.UiServiceTestCase;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
import java.util.Objects;
@SmallTest
record.calculateGrantableUris();
// should not throw
}
+
+ @Test
+ public void testSmartActions() {
+ 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);
+ assertNull(record.getSmartActions());
+
+ ArrayList<Notification.Action> smartActions = new ArrayList<>();
+ smartActions.add(new Notification.Action.Builder(
+ Icon.createWithResource(getContext(), R.drawable.btn_default),
+ "text", null).build());
+ record.setSmartActions(smartActions);
+ assertEquals(smartActions, record.getSmartActions());
+ }
}