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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user