Add logging for smart replies in notifications.

Log the first time a notification with smart
replies is visible.
Log each click on a smart reply.

Test: atest SystemUITests
Bug: 72153458
Change-Id: I6dc498871000dbb9af978567db3d258b20978781
This commit is contained in:
Kenny Guy
2018-04-05 21:18:38 +01:00
parent 8b0b733a2b
commit 23991105bd
11 changed files with 215 additions and 15 deletions

View File

@@ -64,6 +64,8 @@ interface IStatusBarService
in NotificationVisibility[] noLongerVisibleKeys);
void onNotificationExpansionChanged(in String key, in boolean userAction, in boolean expanded);
void onNotificationDirectReplied(String key);
void onNotificationSmartRepliesAdded(in String key, in int replyCount);
void onNotificationSmartReplySent(in String key, in int replyIndex);
void onNotificationSettingsViewed(String key);
void setSystemUiVisibility(int vis, int mask, String cause);

View File

@@ -42,6 +42,7 @@ import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationViewHierarchyManager;
import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.SmartReplyLogger;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBouncer;
@@ -146,5 +147,6 @@ public class SystemUIFactory {
() -> new NotificationViewHierarchyManager(context));
providers.put(NotificationEntryManager.class, () -> new NotificationEntryManager(context));
providers.put(KeyguardDismissUtil.class, KeyguardDismissUtil::new);
providers.put(SmartReplyLogger.class, () -> new SmartReplyLogger(context));
}
}

View File

@@ -80,7 +80,7 @@ public class NotificationContentView extends FrameLayout {
private RemoteInputView mHeadsUpRemoteInput;
private SmartReplyConstants mSmartReplyConstants;
private SmartReplyView mExpandedSmartReplyView;
private SmartReplyLogger mSmartReplyLogger;
private NotificationViewWrapper mContractedWrapper;
private NotificationViewWrapper mExpandedWrapper;
@@ -153,6 +153,7 @@ public class NotificationContentView extends FrameLayout {
super(context, attrs);
mHybridGroupManager = new HybridGroupManager(getContext(), this);
mSmartReplyConstants = Dependency.get(SmartReplyConstants.class);
mSmartReplyLogger = Dependency.get(SmartReplyLogger.class);
initView();
}
@@ -1243,7 +1244,7 @@ public class NotificationContentView extends FrameLayout {
}
applyRemoteInput(entry, hasRemoteInput);
applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices);
applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices, entry);
}
private void applyRemoteInput(NotificationData.Entry entry, boolean hasRemoteInput) {
@@ -1344,13 +1345,21 @@ public class NotificationContentView extends FrameLayout {
return null;
}
private void applySmartReplyView(RemoteInput remoteInput, PendingIntent pendingIntent) {
mExpandedSmartReplyView = mExpandedChild == null ?
null : applySmartReplyView(mExpandedChild, remoteInput, pendingIntent);
private void applySmartReplyView(RemoteInput remoteInput, PendingIntent pendingIntent,
NotificationData.Entry entry) {
if (mExpandedChild != null) {
SmartReplyView view =
applySmartReplyView(mExpandedChild, remoteInput, pendingIntent, entry);
if (view != null && remoteInput != null && remoteInput.getChoices() != null
&& remoteInput.getChoices().length > 0) {
mSmartReplyLogger.smartRepliesAdded(entry, remoteInput.getChoices().length);
}
}
}
private SmartReplyView applySmartReplyView(
View view, RemoteInput remoteInput, PendingIntent pendingIntent) {
View view, RemoteInput remoteInput, PendingIntent pendingIntent,
NotificationData.Entry entry) {
View smartReplyContainerCandidate = view.findViewById(
com.android.internal.R.id.smart_reply_container);
if (!(smartReplyContainerCandidate instanceof LinearLayout)) {
@@ -1372,7 +1381,8 @@ public class NotificationContentView extends FrameLayout {
}
}
if (smartReplyView != null) {
smartReplyView.setRepliesFromRemoteInput(remoteInput, pendingIntent);
smartReplyView.setRepliesFromRemoteInput(remoteInput, pendingIntent,
mSmartReplyLogger, entry);
smartReplyContainer.setVisibility(View.VISIBLE);
}
return smartReplyView;

View File

@@ -0,0 +1,52 @@
/*
* 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.systemui.statusbar;
import android.content.Context;
import android.os.RemoteException;
import android.os.ServiceManager;
import com.android.internal.statusbar.IStatusBarService;
/**
* Handles reporting when smart replies are added to a notification
* and clicked upon.
*/
public class SmartReplyLogger {
protected IStatusBarService mBarService;
public SmartReplyLogger(Context context) {
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
}
public void smartReplySent(NotificationData.Entry entry, int replyIndex) {
try {
mBarService.onNotificationSmartReplySent(entry.notification.getKey(),
replyIndex);
} catch (RemoteException e) {
// Nothing to do, system going down
}
}
public void smartRepliesAdded(final NotificationData.Entry entry, int replyCount) {
try {
mBarService.onNotificationSmartRepliesAdded(entry.notification.getKey(),
replyCount);
} catch (RemoteException e) {
// Nothing to do, system going down
}
}
}

View File

@@ -23,6 +23,8 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.KeyguardHostView.OnDismissAction;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.SmartReplyLogger;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import java.text.BreakIterator;
@@ -109,14 +111,16 @@ public class SmartReplyView extends ViewGroup {
Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
}
public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent) {
public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent,
SmartReplyLogger smartReplyLogger, NotificationData.Entry entry) {
removeAllViews();
if (remoteInput != null && pendingIntent != null) {
CharSequence[] choices = remoteInput.getChoices();
if (choices != null) {
for (CharSequence choice : choices) {
for (int i = 0; i < choices.length; ++i) {
Button replyButton = inflateReplyButton(
getContext(), this, choice, remoteInput, pendingIntent);
getContext(), this, i, choices[i], remoteInput, pendingIntent,
smartReplyLogger, entry);
addView(replyButton);
}
}
@@ -130,8 +134,9 @@ public class SmartReplyView extends ViewGroup {
}
@VisibleForTesting
Button inflateReplyButton(Context context, ViewGroup root, CharSequence choice,
RemoteInput remoteInput, PendingIntent pendingIntent) {
Button inflateReplyButton(Context context, ViewGroup root, int replyIndex,
CharSequence choice, RemoteInput remoteInput, PendingIntent pendingIntent,
SmartReplyLogger smartReplyLogger, NotificationData.Entry entry) {
Button b = (Button) LayoutInflater.from(context).inflate(
R.layout.smart_reply_button, root, false);
b.setText(choice);
@@ -147,6 +152,7 @@ public class SmartReplyView extends ViewGroup {
} catch (PendingIntent.CanceledException e) {
Log.w(TAG, "Unable to send smart reply", e);
}
smartReplyLogger.smartReplySent(entry, replyIndex);
return false; // do not defer
};

View File

@@ -22,11 +22,16 @@ import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.service.notification.StatusBarNotification;
import android.support.test.filters.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -38,6 +43,8 @@ import android.widget.LinearLayout;
import com.android.keyguard.KeyguardHostView.OnDismissAction;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.SmartReplyLogger;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import java.util.concurrent.atomic.AtomicReference;
@@ -46,6 +53,8 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@@ -66,8 +75,12 @@ public class SmartReplyViewTest extends SysuiTestCase {
private int mDoubleLinePaddingHorizontal;
private int mSpacing;
@Mock private SmartReplyLogger mLogger;
private NotificationData.Entry mEntry;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mReceiver = new BlockingQueueIntentReceiver();
mContext.registerReceiver(mReceiver, new IntentFilter(TEST_ACTION));
mDependency.get(KeyguardDismissUtil.class).setDismissHandler(
@@ -82,6 +95,10 @@ public class SmartReplyViewTest extends SysuiTestCase {
mDoubleLinePaddingHorizontal = res.getDimensionPixelSize(
R.dimen.smart_reply_button_padding_horizontal_double_line);
mSpacing = res.getDimensionPixelSize(R.dimen.smart_reply_button_spacing);
StatusBarNotification notification = mock(StatusBarNotification.class);
when(notification.getKey()).thenReturn("akey");
mEntry = new NotificationData.Entry(notification);
}
@After
@@ -137,6 +154,13 @@ public class SmartReplyViewTest extends SysuiTestCase {
assertEquals(RemoteInput.SOURCE_CHOICE, RemoteInput.getResultsSource(resultIntent));
}
@Test
public void testSendSmartReply_LoggerCall() {
setRepliesFromRemoteInput(TEST_CHOICES);
mView.getChildAt(2).performClick();
verify(mLogger).smartReplySent(mEntry, 2);
}
@Test
public void testMeasure_empty() {
mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
@@ -316,7 +340,7 @@ public class SmartReplyViewTest extends SysuiTestCase {
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0,
new Intent(TEST_ACTION), 0);
RemoteInput input = new RemoteInput.Builder(TEST_RESULT_KEY).setChoices(choices).build();
mView.setRepliesFromRemoteInput(input, pendingIntent);
mView.setRepliesFromRemoteInput(input, pendingIntent, mLogger, mEntry);
}
/** Builds a {@link ViewGroup} whose measures and layout mirror a {@link SmartReplyView}. */
@@ -343,8 +367,9 @@ public class SmartReplyViewTest extends SysuiTestCase {
}
Button previous = null;
for (CharSequence choice : choices) {
Button current = mView.inflateReplyButton(mContext, mView, choice, null, null);
for (int i = 0; i < choices.length; ++i) {
Button current = mView.inflateReplyButton(mContext, mView, i, choices[i],
null, null, null, null);
current.setPadding(paddingHorizontal, current.getPaddingTop(), paddingHorizontal,
current.getPaddingBottom());
if (previous != null) {

View File

@@ -5618,6 +5618,24 @@ message MetricsEvent {
// OS: P
SETTINGS_AUTO_BRIGHTNESS = 1381;
// OPEN: Smart replies in a notification seen at least once
// CATEGORY: NOTIFICATION
// PACKAGE: App that posted the notification
// SUBTYPE: Number of smart replies.
// OS: P
SMART_REPLY_VISIBLE = 1382;
// ACTION: Smart reply in a notification clicked.
// CATEGORY: NOTIFICATION
// PACKAGE: App that posted the notification
// SUBTYPE: Index of smart reply clicked.
// OS: P
SMART_REPLY_ACTION = 1383;
// Tagged data for SMART_REPLY_VISIBLE. Count of number of smart replies.
// OS: P
NOTIFICATION_SMART_REPLY_COUNT = 1384;
// ---- End P Constants, all P constants go above this line ----
// Add new aosp constants above this line.
// END OF AOSP CONSTANTS

View File

@@ -40,4 +40,6 @@ public interface NotificationDelegate {
void onNotificationExpansionChanged(String key, boolean userAction, boolean expanded);
void onNotificationDirectReplied(String key);
void onNotificationSettingsViewed(String key);
void onNotificationSmartRepliesAdded(String key, int replyCount);
void onNotificationSmartReplySent(String key, int replyIndex);
}

View File

@@ -121,6 +121,7 @@ import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioManagerInternal;
import android.media.IRingtonePlayer;
import android.metrics.LogMaker;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
@@ -395,6 +396,8 @@ public class NotificationManagerService extends SystemService {
private GroupHelper mGroupHelper;
private boolean mIsTelevision;
private MetricsLogger mMetricsLogger;
private static class Archive {
final int mBufferSize;
final ArrayDeque<StatusBarNotification> mBuffer;
@@ -801,6 +804,18 @@ public class NotificationManagerService extends SystemService {
// Report to usage stats that notification was made visible
if (DBG) Slog.d(TAG, "Marking notification as visible " + nv.key);
reportSeen(r);
// If the newly visible notification has smart replies
// then log that the user has seen them.
if (r.getNumSmartRepliesAdded() > 0
&& !r.hasSeenSmartReplies()) {
r.setSeenSmartReplies(true);
LogMaker logMaker = r.getLogMaker()
.setCategory(MetricsEvent.SMART_REPLY_VISIBLE)
.addTaggedData(MetricsEvent.NOTIFICATION_SMART_REPLY_COUNT,
r.getNumSmartRepliesAdded());
mMetricsLogger.write(logMaker);
}
}
r.setVisibility(true, nv.rank);
nv.recycle();
@@ -854,6 +869,31 @@ public class NotificationManagerService extends SystemService {
}
}
@Override
public void onNotificationSmartRepliesAdded(String key, int replyCount) {
synchronized (mNotificationLock) {
NotificationRecord r = mNotificationsByKey.get(key);
if (r != null) {
r.setNumSmartRepliesAdded(replyCount);
}
}
}
@Override
public void onNotificationSmartReplySent(String key, int replyIndex) {
synchronized (mNotificationLock) {
NotificationRecord r = mNotificationsByKey.get(key);
if (r != null) {
LogMaker logMaker = r.getLogMaker()
.setCategory(MetricsEvent.SMART_REPLY_ACTION)
.setSubtype(replyIndex);
mMetricsLogger.write(logMaker);
// Treat clicking on a smart reply as a user interaction.
reportUserInteraction(r);
}
}
}
@Override
public void onNotificationSettingsViewed(String key) {
synchronized (mNotificationLock) {
@@ -1349,6 +1389,7 @@ public class NotificationManagerService extends SystemService {
extractorNames = new String[0];
}
mUsageStats = usageStats;
mMetricsLogger = new MetricsLogger();
mRankingHandler = new RankingHandlerWorker(mRankingThread.getLooper());
mConditionProviders = conditionProviders;
mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), mConditionProviders);

View File

@@ -149,6 +149,8 @@ public final class NotificationRecord {
private final NotificationStats mStats;
private int mUserSentiment;
private boolean mIsInterruptive;
private int mNumberOfSmartRepliesAdded;
private boolean mHasSeenSmartReplies;
@VisibleForTesting
public NotificationRecord(Context context, StatusBarNotification sbn,
@@ -962,6 +964,22 @@ public final class NotificationRecord {
mStats.setViewedSettings();
}
public void setNumSmartRepliesAdded(int noReplies) {
mNumberOfSmartRepliesAdded = noReplies;
}
public int getNumSmartRepliesAdded() {
return mNumberOfSmartRepliesAdded;
}
public boolean hasSeenSmartReplies() {
return mHasSeenSmartReplies;
}
public void setSeenSmartReplies(boolean hasSeenSmartReplies) {
mHasSeenSmartReplies = hasSeenSmartReplies;
}
public Set<Uri> getNotificationUris() {
Notification notification = getNotification();
Set<Uri> uris = new ArraySet<>();

View File

@@ -1095,6 +1095,30 @@ public class StatusBarManagerService extends IStatusBarService.Stub {
}
}
@Override
public void onNotificationSmartRepliesAdded(String key, int replyCount)
throws RemoteException {
enforceStatusBarService();
long identity = Binder.clearCallingIdentity();
try {
mNotificationDelegate.onNotificationSmartRepliesAdded(key, replyCount);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onNotificationSmartReplySent(String key, int replyIndex)
throws RemoteException {
enforceStatusBarService();
long identity = Binder.clearCallingIdentity();
try {
mNotificationDelegate.onNotificationSmartReplySent(key, replyIndex);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onNotificationSettingsViewed(String key) throws RemoteException {
enforceStatusBarService();