Merge "Add onSuggestedReplySent in NotificationAssistantService"

This commit is contained in:
TreeHugger Robot
2018-11-27 19:51:55 +00:00
committed by Android (Google) Code Review
17 changed files with 309 additions and 122 deletions

View File

@@ -5082,8 +5082,11 @@ package android.service.notification {
method public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap, android.service.notification.NotificationStats, int);
method public abstract void onNotificationSnoozedUntilContext(android.service.notification.StatusBarNotification, java.lang.String);
method public void onNotificationsSeen(java.util.List<java.lang.String>);
method public void onSuggestedReplySent(java.lang.String, java.lang.CharSequence, int);
method public final void unsnoozeNotification(java.lang.String);
field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService";
field public static final int SOURCE_FROM_APP = 0; // 0x0
field public static final int SOURCE_FROM_ASSISTANT = 1; // 0x1
}
public final class NotificationStats implements android.os.Parcelable {

View File

@@ -1162,8 +1162,11 @@ package android.service.notification {
method public void onNotificationExpansionChanged(java.lang.String, boolean, boolean);
method public abstract void onNotificationSnoozedUntilContext(android.service.notification.StatusBarNotification, java.lang.String);
method public void onNotificationsSeen(java.util.List<java.lang.String>);
method public void onSuggestedReplySent(java.lang.String, java.lang.CharSequence, int);
method public final void unsnoozeNotification(java.lang.String);
field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService";
field public static final int SOURCE_FROM_APP = 0; // 0x0
field public static final int SOURCE_FROM_ASSISTANT = 1; // 0x1
}
public abstract class NotificationListenerService extends android.app.Service {

View File

@@ -49,4 +49,5 @@ oneway interface INotificationListener
void onNotificationsSeen(in List<String> keys);
void onNotificationExpansionChanged(String key, boolean userAction, boolean expanded);
void onNotificationDirectReply(String key);
void onSuggestedReplySent(String key, in CharSequence reply, int source);
}

View File

@@ -16,6 +16,9 @@
package android.service.notification;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.IntDef;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
import android.annotation.TestApi;
@@ -33,6 +36,7 @@ import android.util.Log;
import com.android.internal.os.SomeArgs;
import java.lang.annotation.Retention;
import java.util.List;
/**
@@ -63,6 +67,13 @@ import java.util.List;
public abstract class NotificationAssistantService extends NotificationListenerService {
private static final String TAG = "NotificationAssistants";
/** @hide */
@Retention(SOURCE)
@IntDef({SOURCE_FROM_APP, SOURCE_FROM_ASSISTANT})
public @interface Source {}
public static final int SOURCE_FROM_APP = 0;
public static final int SOURCE_FROM_ASSISTANT = 1;
/**
* The {@link Intent} that must be declared as handled by the service.
*/
@@ -174,6 +185,14 @@ public abstract class NotificationAssistantService extends NotificationListenerS
*/
public void onNotificationDirectReply(String key) {}
/**
* Implement this to know when a suggested reply is sent.
* @param key the notification key
* @param reply the reply that is just sent
* @param source the source of the reply, e.g. SOURCE_FROM_APP
*/
public void onSuggestedReplySent(String key, CharSequence reply, @Source int source) {}
/**
* Updates a notification. N.B. this wont cause
* an existing notification to alert, but might allow a future update to
@@ -289,6 +308,15 @@ public abstract class NotificationAssistantService extends NotificationListenerS
mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT, args)
.sendToTarget();
}
@Override
public void onSuggestedReplySent(String key, CharSequence reply, int source) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = key;
args.arg2 = reply;
args.argi2 = source;
mHandler.obtainMessage(MyHandler.MSG_ON_SUGGESTED_REPLY_SENT, args).sendToTarget();
}
}
private final class MyHandler extends Handler {
@@ -297,6 +325,7 @@ public abstract class NotificationAssistantService extends NotificationListenerS
public static final int MSG_ON_NOTIFICATIONS_SEEN = 3;
public static final int MSG_ON_NOTIFICATION_EXPANSION_CHANGED = 4;
public static final int MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT = 5;
public static final int MSG_ON_SUGGESTED_REPLY_SENT = 6;
public MyHandler(Looper looper) {
super(looper, null, false);
@@ -357,6 +386,15 @@ public abstract class NotificationAssistantService extends NotificationListenerS
onNotificationDirectReply(key);
break;
}
case MSG_ON_SUGGESTED_REPLY_SENT: {
SomeArgs args = (SomeArgs) msg.obj;
String key = (String) args.arg1;
CharSequence reply = (CharSequence) args.arg2;
int source = args.argi2;
args.recycle();
onSuggestedReplySent(key, reply, source);
break;
}
}
}
}

View File

@@ -1376,6 +1376,11 @@ public abstract class NotificationListenerService extends Service {
// no-op in the listener
}
@Override
public void onSuggestedReplySent(String key, CharSequence reply, int source) {
// no-op in the listener
}
@Override
public void onNotificationChannelModification(String pkgName, UserHandle user,
NotificationChannel channel,

View File

@@ -66,7 +66,7 @@ interface IStatusBarService
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 onNotificationSmartReplySent(in String key, in int replyIndex, in CharSequence reply, boolean generatedByAssistant);
void onNotificationSettingsViewed(String key);
void setSystemUiVisibility(int vis, int mask, String cause);

View File

@@ -369,6 +369,14 @@ public class Assistant extends NotificationAssistantService {
if (DEBUG) Log.i(TAG, "onNotificationDirectReply " + key);
}
@Override
public void onSuggestedReplySent(String key, CharSequence reply, int source) {
if (DEBUG) {
Log.d(TAG, "onSuggestedReplySent() called with: key = [" + key + "], reply = [" + reply
+ "], source = [" + source + "]");
}
}
@Override
public void onListenerConnected() {
if (DEBUG) Log.i(TAG, "CONNECTED");

View File

@@ -41,12 +41,16 @@ public class SmartReplyController {
mCallback = callback;
}
public void smartReplySent(NotificationData.Entry entry, int replyIndex, CharSequence reply) {
/**
* Notifies StatusBarService a smart reply is sent.
*/
public void smartReplySent(NotificationData.Entry entry, int replyIndex, CharSequence reply,
boolean generatedByAssistant) {
mCallback.onSmartReplySent(entry, reply);
mSendingKeys.add(entry.key);
try {
mBarService.onNotificationSmartReplySent(entry.notification.getKey(),
replyIndex);
mBarService.onNotificationSmartReplySent(
entry.notification.getKey(), replyIndex, reply, generatedByAssistant);
} catch (RemoteException e) {
// Nothing to do, system going down
}

View File

@@ -1290,10 +1290,10 @@ public class NotificationContentView extends FrameLayout {
return;
}
SmartRepliesAndActions smartRepliesAndActions = chooseSmartRepliesAndActions(
mSmartReplyConstants, entry);
SmartRepliesAndActions smartRepliesAndActions =
chooseSmartRepliesAndActions(mSmartReplyConstants, entry);
applyRemoteInput(entry, smartRepliesAndActions.freeformRemoteInputActionPair != null);
applyRemoteInput(entry, smartRepliesAndActions.hasFreeformRemoteInput);
applySmartReplyView(smartRepliesAndActions, entry);
}
@@ -1320,62 +1320,47 @@ public class NotificationContentView extends FrameLayout {
boolean appGeneratedSmartRepliesExist =
enableAppGeneratedSmartReplies
&& remoteInputActionPair != null
&& !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices());
&& !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())
&& remoteInputActionPair.second.actionIntent != null;
List<Notification.Action> appGeneratedSmartActions = notification.getContextualActions();
boolean appGeneratedSmartActionsExist = !appGeneratedSmartActions.isEmpty();
List<Notification.Action> sysGeneratedSmartActions =
notification.getAllowSystemGeneratedContextualActions()
? entry.systemGeneratedSmartActions : Collections.emptyList();
SmartReplyView.SmartReplies smartReplies = null;
SmartReplyView.SmartActions smartActions = null;
if (appGeneratedSmartRepliesExist) {
return new SmartRepliesAndActions(remoteInputActionPair.first,
remoteInputActionPair.second.actionIntent,
smartReplies = new SmartReplyView.SmartReplies(
remoteInputActionPair.first.getChoices(),
appGeneratedSmartActions,
freeformRemoteInputActionPair);
} else if (appGeneratedSmartActionsExist) {
return new SmartRepliesAndActions(null, null, null, appGeneratedSmartActions,
freeformRemoteInputActionPair);
} else if (!ArrayUtils.isEmpty(entry.smartReplies)
&& freeformRemoteInputActionPair != null
&& freeformRemoteInputActionPair.second.getAllowGeneratedReplies()) {
// App didn't generate anything, use NAS-generated replies and actions
return new SmartRepliesAndActions(freeformRemoteInputActionPair.first,
freeformRemoteInputActionPair.second.actionIntent,
entry.smartReplies,
sysGeneratedSmartActions,
freeformRemoteInputActionPair);
remoteInputActionPair.first,
remoteInputActionPair.second.actionIntent,
false /* fromAssistant */);
}
// App didn't generate anything, and there are no NAS-generated smart replies.
return new SmartRepliesAndActions(null, null, null, sysGeneratedSmartActions,
freeformRemoteInputActionPair);
}
@VisibleForTesting
static class SmartRepliesAndActions {
public final RemoteInput remoteInputWithChoices;
public final PendingIntent pendingIntentForSmartReplies;
public final CharSequence[] smartReplies;
public final List<Notification.Action> smartActions;
public final Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair;
SmartRepliesAndActions(RemoteInput remoteInput, PendingIntent pendingIntent,
CharSequence[] choices, List<Notification.Action> smartActions,
Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair) {
this.remoteInputWithChoices = remoteInput;
this.pendingIntentForSmartReplies = pendingIntent;
this.smartReplies = choices;
this.smartActions = smartActions;
this.freeformRemoteInputActionPair = freeformRemoteInputActionPair;
if (appGeneratedSmartActionsExist) {
smartActions = new SmartReplyView.SmartActions(appGeneratedSmartActions,
false /* fromAssistant */);
}
boolean smartRepliesExist() {
return remoteInputWithChoices != null
&& pendingIntentForSmartReplies != null
&& !ArrayUtils.isEmpty(smartReplies);
// Apps didn't provide any smart replies / actions, use those from NAS (if any).
if (!appGeneratedSmartRepliesExist && !appGeneratedSmartActionsExist) {
boolean useGeneratedReplies = !ArrayUtils.isEmpty(entry.smartReplies)
&& freeformRemoteInputActionPair != null
&& freeformRemoteInputActionPair.second.getAllowGeneratedReplies()
&& freeformRemoteInputActionPair.second.actionIntent != null;
if (useGeneratedReplies) {
smartReplies = new SmartReplyView.SmartReplies(
entry.smartReplies,
freeformRemoteInputActionPair.first,
freeformRemoteInputActionPair.second.actionIntent,
true /* fromAssistant */);
}
boolean useSmartActions = !ArrayUtils.isEmpty(entry.systemGeneratedSmartActions)
&& notification.getAllowSystemGeneratedContextualActions();
if (useSmartActions) {
smartActions = new SmartReplyView.SmartActions(
entry.systemGeneratedSmartActions, true /* fromAssistant */);
}
}
return new SmartRepliesAndActions(
smartReplies, smartActions, freeformRemoteInputActionPair != null);
}
private void applyRemoteInput(NotificationData.Entry entry, boolean hasFreeformRemoteInput) {
@@ -1482,12 +1467,9 @@ public class NotificationContentView extends FrameLayout {
if (mExpandedChild != null) {
mExpandedSmartReplyView =
applySmartReplyView(mExpandedChild, smartRepliesAndActions, entry);
if (mExpandedSmartReplyView != null
&& smartRepliesAndActions.remoteInputWithChoices != null
&& smartRepliesAndActions.smartReplies != null
&& smartRepliesAndActions.smartReplies.length > 0) {
mSmartReplyController.smartRepliesAdded(entry,
smartRepliesAndActions.smartReplies.length);
if (mExpandedSmartReplyView != null && smartRepliesAndActions.smartReplies != null) {
mSmartReplyController.smartRepliesAdded(
entry, smartRepliesAndActions.smartReplies.choices.length);
}
}
}
@@ -1501,8 +1483,8 @@ public class NotificationContentView extends FrameLayout {
}
LinearLayout smartReplyContainer = (LinearLayout) smartReplyContainerCandidate;
// If there are no smart replies and no smart actions - early out.
if (!smartRepliesAndActions.smartRepliesExist()
&& smartRepliesAndActions.smartActions.isEmpty()) {
if (smartRepliesAndActions.smartReplies == null
&& smartRepliesAndActions.smartActions == null) {
smartReplyContainer.setVisibility(View.GONE);
return null;
}
@@ -1532,10 +1514,13 @@ public class NotificationContentView extends FrameLayout {
}
if (smartReplyView != null) {
smartReplyView.resetSmartSuggestions(smartReplyContainer);
smartReplyView.addRepliesFromRemoteInput(smartRepliesAndActions.remoteInputWithChoices,
smartRepliesAndActions.pendingIntentForSmartReplies, mSmartReplyController,
entry, smartRepliesAndActions.smartReplies);
smartReplyView.addSmartActions(smartRepliesAndActions.smartActions);
if (smartRepliesAndActions.smartReplies != null) {
smartReplyView.addRepliesFromRemoteInput(
smartRepliesAndActions.smartReplies, mSmartReplyController, entry);
}
if (smartRepliesAndActions.smartActions != null) {
smartReplyView.addSmartActions(smartRepliesAndActions.smartActions);
}
smartReplyContainer.setVisibility(View.VISIBLE);
}
return smartReplyView;
@@ -1954,4 +1939,22 @@ public class NotificationContentView extends FrameLayout {
}
pw.println();
}
@VisibleForTesting
static class SmartRepliesAndActions {
@Nullable
public final SmartReplyView.SmartReplies smartReplies;
@Nullable
public final SmartReplyView.SmartActions smartActions;
public final boolean hasFreeformRemoteInput;
SmartRepliesAndActions(
@Nullable SmartReplyView.SmartReplies smartReplies,
@Nullable SmartReplyView.SmartActions smartActions,
boolean hasFreeformRemoteInput) {
this.smartReplies = smartReplies;
this.smartActions = smartActions;
this.hasFreeformRemoteInput = hasFreeformRemoteInput;
}
}
}

View File

@@ -1,6 +1,7 @@
package com.android.systemui.statusbar.policy;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
@@ -189,15 +190,13 @@ public class SmartReplyView extends ViewGroup {
* into the notification are shown.
*/
public void addRepliesFromRemoteInput(
RemoteInput remoteInput, PendingIntent pendingIntent,
SmartReplyController smartReplyController, NotificationData.Entry entry,
CharSequence[] choices) {
if (remoteInput != null && pendingIntent != null) {
if (choices != null) {
for (int i = 0; i < choices.length; ++i) {
SmartReplies smartReplies,
SmartReplyController smartReplyController, NotificationData.Entry entry) {
if (smartReplies.remoteInput != null && smartReplies.pendingIntent != null) {
if (smartReplies.choices != null) {
for (int i = 0; i < smartReplies.choices.length; ++i) {
Button replyButton = inflateReplyButton(
getContext(), this, i, choices[i], remoteInput, pendingIntent,
smartReplyController, entry);
getContext(), this, i, smartReplies, smartReplyController, entry);
addView(replyButton);
}
}
@@ -209,10 +208,10 @@ public class SmartReplyView extends ViewGroup {
* Add smart actions to be shown next to smart replies. Only the actions that fit into the
* notification are shown.
*/
public void addSmartActions(List<Notification.Action> smartActions) {
int numSmartActions = smartActions.size();
public void addSmartActions(SmartActions smartActions) {
int numSmartActions = smartActions.actions.size();
for (int n = 0; n < numSmartActions; n++) {
Notification.Action action = smartActions.get(n);
Notification.Action action = smartActions.actions.get(n);
if (action.actionIntent != null) {
Button actionButton = inflateActionButton(getContext(), this, action);
addView(actionButton);
@@ -228,22 +227,25 @@ public class SmartReplyView extends ViewGroup {
@VisibleForTesting
Button inflateReplyButton(Context context, ViewGroup root, int replyIndex,
CharSequence choice, RemoteInput remoteInput, PendingIntent pendingIntent,
SmartReplyController smartReplyController, NotificationData.Entry entry) {
SmartReplies smartReplies, SmartReplyController smartReplyController,
NotificationData.Entry entry) {
Button b = (Button) LayoutInflater.from(context).inflate(
R.layout.smart_reply_button, root, false);
CharSequence choice = smartReplies.choices[replyIndex];
b.setText(choice);
OnDismissAction action = () -> {
smartReplyController.smartReplySent(entry, replyIndex, b.getText());
smartReplyController.smartReplySent(
entry, replyIndex, b.getText(), smartReplies.fromAssistant);
Bundle results = new Bundle();
results.putString(remoteInput.getResultKey(), choice.toString());
results.putString(smartReplies.remoteInput.getResultKey(), choice.toString());
Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
RemoteInput.addResultsToIntent(new RemoteInput[]{remoteInput}, intent, results);
RemoteInput.addResultsToIntent(new RemoteInput[]{smartReplies.remoteInput}, intent,
results);
RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE);
entry.setHasSentReply();
try {
pendingIntent.send(context, 0, intent);
smartReplies.pendingIntent.send(context, 0, intent);
} catch (PendingIntent.CanceledException e) {
Log.w(TAG, "Unable to send smart reply", e);
}
@@ -741,4 +743,40 @@ public class SmartReplyView extends ViewGroup {
return show;
}
}
/**
* Data class for smart replies.
*/
public static class SmartReplies {
@NonNull
public final RemoteInput remoteInput;
@NonNull
public final PendingIntent pendingIntent;
@NonNull
public final CharSequence[] choices;
public final boolean fromAssistant;
public SmartReplies(CharSequence[] choices, RemoteInput remoteInput,
PendingIntent pendingIntent, boolean fromAssistant) {
this.choices = choices;
this.remoteInput = remoteInput;
this.pendingIntent = pendingIntent;
this.fromAssistant = fromAssistant;
}
}
/**
* Data class for smart actions.
*/
public static class SmartActions {
@NonNull
public final List<Notification.Action> actions;
public final boolean fromAssistant;
public SmartActions(List<Notification.Action> actions, boolean fromAssistant) {
this.actions = actions;
this.fromAssistant = fromAssistant;
}
}
}

View File

@@ -16,18 +16,15 @@ package com.android.systemui.statusbar;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.app.Notification;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.support.test.filters.SmallTest;
import android.testing.AndroidTestingRunner;
@@ -38,7 +35,6 @@ import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import org.junit.Before;
import org.junit.Test;
@@ -93,7 +89,7 @@ public class SmartReplyControllerTest extends SysuiTestCase {
@Test
public void testSendSmartReply_updatesRemoteInput() {
mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT, false);
// Sending smart reply should make calls to NotificationEntryManager
// to update the notification with reply and spinner.
@@ -103,11 +99,21 @@ public class SmartReplyControllerTest extends SysuiTestCase {
@Test
public void testSendSmartReply_logsToStatusBar() throws RemoteException {
mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT, false);
// Check we log the result to the status bar service.
verify(mIStatusBarService).onNotificationSmartReplySent(mSbn.getKey(),
TEST_CHOICE_INDEX);
TEST_CHOICE_INDEX, TEST_CHOICE_TEXT, false);
}
@Test
public void testSendSmartReply_logsToStatusBar_generatedByAssistant() throws RemoteException {
mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT, true);
// Check we log the result to the status bar service.
verify(mIStatusBarService).onNotificationSmartReplySent(mSbn.getKey(),
TEST_CHOICE_INDEX, TEST_CHOICE_TEXT, true);
}
@Test
@@ -121,14 +127,14 @@ public class SmartReplyControllerTest extends SysuiTestCase {
@Test
public void testSendSmartReply_reportsSending() {
mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT, false);
assertTrue(mSmartReplyController.isSendingSmartReply(mSbn.getKey()));
}
@Test
public void testSendingSmartReply_afterRemove_shouldReturnFalse() {
mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT, false);
mSmartReplyController.stopSending(mEntry);
assertFalse(mSmartReplyController.isSendingSmartReply(mSbn.getKey()));

View File

@@ -16,11 +16,10 @@
package com.android.systemui.statusbar.notification.row;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.Mockito.doNothing;
@@ -170,8 +169,10 @@ public class NotificationContentViewTest extends SysuiTestCase {
private void setupAppGeneratedReplies(
CharSequence[] smartReplyTitles,
Notification.Action freeFormRemoteInputAction) {
PendingIntent pendingIntent =
PendingIntent.getBroadcast(mContext, 0, new Intent(TEST_ACTION), 0);
Notification.Action action =
new Notification.Action.Builder(null, "Test Action", null).build();
new Notification.Action.Builder(null, "Test Action", pendingIntent).build();
when(mRemoteInput.getChoices()).thenReturn(smartReplyTitles);
Pair<RemoteInput, Notification.Action> remoteInputActionPair =
Pair.create(mRemoteInput, action);
@@ -191,7 +192,7 @@ public class NotificationContentViewTest extends SysuiTestCase {
NotificationContentView.SmartRepliesAndActions repliesAndActions =
NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
assertFalse(repliesAndActions.smartRepliesExist());
assertThat(repliesAndActions.smartReplies).isNull();
}
@Test
@@ -203,7 +204,9 @@ public class NotificationContentViewTest extends SysuiTestCase {
NotificationContentView.SmartRepliesAndActions repliesAndActions =
NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
assertThat(repliesAndActions.smartReplies, equalTo(smartReplies));
assertThat(repliesAndActions.smartReplies.choices).isEqualTo(smartReplies);
assertThat(repliesAndActions.smartReplies.fromAssistant).isFalse();
assertThat(repliesAndActions.smartActions).isNull();
}
@Test
@@ -219,8 +222,10 @@ public class NotificationContentViewTest extends SysuiTestCase {
NotificationContentView.SmartRepliesAndActions repliesAndActions =
NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
assertThat(repliesAndActions.smartReplies, equalTo(smartReplies));
assertThat(repliesAndActions.smartActions, equalTo(smartActions));
assertThat(repliesAndActions.smartReplies.choices).isEqualTo(smartReplies);
assertThat(repliesAndActions.smartReplies.fromAssistant).isFalse();
assertThat(repliesAndActions.smartActions.actions).isEqualTo(smartActions);
assertThat(repliesAndActions.smartActions.fromAssistant).isFalse();
}
@Test
@@ -237,8 +242,9 @@ public class NotificationContentViewTest extends SysuiTestCase {
NotificationContentView.SmartRepliesAndActions repliesAndActions =
NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
assertThat(repliesAndActions.smartReplies, equalTo(mEntry.smartReplies));
assertThat(repliesAndActions.smartActions, is(empty()));
assertThat(repliesAndActions.smartReplies.choices).isEqualTo(mEntry.smartReplies);
assertThat(repliesAndActions.smartReplies.fromAssistant).isTrue();
assertThat(repliesAndActions.smartActions).isNull();
}
@Test
@@ -255,8 +261,8 @@ public class NotificationContentViewTest extends SysuiTestCase {
NotificationContentView.SmartRepliesAndActions repliesAndActions =
NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
assertThat(repliesAndActions.smartReplies, equalTo(null));
assertThat(repliesAndActions.smartActions, is(empty()));
assertThat(repliesAndActions.smartReplies).isNull();
assertThat(repliesAndActions.smartActions).isNull();
}
@Test
@@ -270,8 +276,10 @@ public class NotificationContentViewTest extends SysuiTestCase {
NotificationContentView.SmartRepliesAndActions repliesAndActions =
NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
assertThat(repliesAndActions.smartReplies, equalTo(null));
assertThat(repliesAndActions.smartActions, equalTo(mEntry.systemGeneratedSmartActions));
assertThat(repliesAndActions.smartReplies).isNull();
assertThat(repliesAndActions.smartActions.actions)
.isEqualTo(mEntry.systemGeneratedSmartActions);
assertThat(repliesAndActions.smartActions.fromAssistant).isTrue();
}
@Test
@@ -296,8 +304,10 @@ public class NotificationContentViewTest extends SysuiTestCase {
NotificationContentView.SmartRepliesAndActions repliesAndActions =
NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
assertThat(repliesAndActions.smartReplies, equalTo(appGenSmartReplies));
assertThat(repliesAndActions.smartActions, equalTo(appGenSmartActions));
assertThat(repliesAndActions.smartReplies.choices).isEqualTo(appGenSmartReplies);
assertThat(repliesAndActions.smartReplies.fromAssistant).isFalse();
assertThat(repliesAndActions.smartActions.actions).isEqualTo(appGenSmartActions);
assertThat(repliesAndActions.smartActions.fromAssistant).isFalse();
}
@Test
@@ -313,8 +323,8 @@ public class NotificationContentViewTest extends SysuiTestCase {
NotificationContentView.SmartRepliesAndActions repliesAndActions =
NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
assertThat(repliesAndActions.smartReplies, equalTo(null));
assertThat(repliesAndActions.smartActions, is(empty()));
assertThat(repliesAndActions.smartActions).isNull();
assertThat(repliesAndActions.smartReplies).isNull();
}
private Notification.Action.Builder createActionBuilder(String actionTitle) {

View File

@@ -181,7 +181,16 @@ public class SmartReplyViewTest extends SysuiTestCase {
public void testSendSmartReply_controllerCalled() {
setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
verify(mLogger).smartReplySent(mEntry, 2, TEST_CHOICES[2]);
verify(mLogger).smartReplySent(mEntry, 2, TEST_CHOICES[2],
false /* generatedByAsssitant */);
}
@Test
public void testSendSmartReply_controllerCalled_generatedByAssistant() {
setSmartReplies(TEST_CHOICES, true);
mView.getChildAt(2).performClick();
verify(mLogger).smartReplySent(mEntry, 2, TEST_CHOICES[2],
true /* generatedByAsssitant */);
}
@Test
@@ -392,11 +401,17 @@ public class SmartReplyViewTest extends SysuiTestCase {
}
private void setSmartReplies(CharSequence[] choices) {
setSmartReplies(choices, false);
}
private void setSmartReplies(CharSequence[] choices, boolean fromAssistant) {
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0,
new Intent(TEST_ACTION), 0);
RemoteInput input = new RemoteInput.Builder(TEST_RESULT_KEY).setChoices(choices).build();
SmartReplyView.SmartReplies smartReplies =
new SmartReplyView.SmartReplies(choices, input, pendingIntent, fromAssistant);
mView.resetSmartSuggestions(mContainer);
mView.addRepliesFromRemoteInput(input, pendingIntent, mLogger, mEntry, choices);
mView.addRepliesFromRemoteInput(smartReplies, mLogger, mEntry);
}
private Notification.Action createAction(String actionTitle) {
@@ -415,12 +430,12 @@ public class SmartReplyViewTest extends SysuiTestCase {
private void setSmartActions(String[] actionTitles) {
mView.resetSmartSuggestions(mContainer);
mView.addSmartActions(createActions(actionTitles));
mView.addSmartActions(new SmartReplyView.SmartActions(createActions(actionTitles), false));
}
private void setSmartRepliesAndActions(CharSequence[] choices, String[] actionTitles) {
setSmartReplies(choices);
mView.addSmartActions(createActions(actionTitles));
mView.addSmartActions(new SmartReplyView.SmartActions(createActions(actionTitles), false));
}
private ViewGroup buildExpectedView(CharSequence[] choices, int lineCount) {
@@ -453,9 +468,11 @@ public class SmartReplyViewTest extends SysuiTestCase {
// Add smart replies
Button previous = null;
SmartReplyView.SmartReplies smartReplies =
new SmartReplyView.SmartReplies(choices, null, null, false);
for (int i = 0; i < choices.length; ++i) {
Button current = mView.inflateReplyButton(mContext, mView, i, choices[i],
null, null, null, null);
Button current = mView.inflateReplyButton(mContext, mView, i, smartReplies,
null, null);
current.setPadding(paddingHorizontal, current.getPaddingTop(), paddingHorizontal,
current.getPaddingBottom());
if (previous != null) {

View File

@@ -45,5 +45,15 @@ public interface NotificationDelegate {
void onNotificationDirectReplied(String key);
void onNotificationSettingsViewed(String key);
void onNotificationSmartRepliesAdded(String key, int replyCount);
void onNotificationSmartReplySent(String key, int replyIndex);
/**
* Notifies a smart reply is sent.
*
* @param key the notification key
* @param clickedIndex the index of clicked reply
* @param reply the reply that is sent
* @param generatedByAssistant specifies is the reply generated by NAS
*/
void onNotificationSmartReplySent(String key, int clickedIndex, CharSequence reply,
boolean generatedByAssistant);
}

View File

@@ -920,7 +920,8 @@ public class NotificationManagerService extends SystemService {
}
@Override
public void onNotificationSmartReplySent(String key, int replyIndex) {
public void onNotificationSmartReplySent(String key, int replyIndex, CharSequence reply,
boolean generatedByAssistant) {
synchronized (mNotificationLock) {
NotificationRecord r = mNotificationsByKey.get(key);
if (r != null) {
@@ -930,6 +931,8 @@ public class NotificationManagerService extends SystemService {
mMetricsLogger.write(logMaker);
// Treat clicking on a smart reply as a user interaction.
reportUserInteraction(r);
mAssistants.notifyAssistantSuggestedReplySent(
r.sbn, reply, generatedByAssistant);
}
}
}
@@ -6899,6 +6902,27 @@ public class NotificationManagerService extends SystemService {
});
}
@GuardedBy("mNotificationLock")
void notifyAssistantSuggestedReplySent(
final StatusBarNotification sbn, CharSequence reply, boolean generatedByAssistant) {
final String key = sbn.getKey();
notifyAssistantLocked(
sbn,
false /* sameUserOnly */,
(assistant, sbnHolder) -> {
try {
assistant.onSuggestedReplySent(
key,
reply,
generatedByAssistant
? NotificationAssistantService.SOURCE_FROM_ASSISTANT
: NotificationAssistantService.SOURCE_FROM_APP);
} catch (RemoteException ex) {
Log.e(TAG, "unable to notify assistant (snoozed): " + assistant, ex);
}
});
}
/**
* asynchronously notify the assistant that a notification has been snoozed until a

View File

@@ -1177,12 +1177,14 @@ public class StatusBarManagerService extends IStatusBarService.Stub {
}
@Override
public void onNotificationSmartReplySent(String key, int replyIndex)
public void onNotificationSmartReplySent(
String key, int replyIndex, CharSequence reply, boolean generatedByAssistant)
throws RemoteException {
enforceStatusBarService();
long identity = Binder.clearCallingIdentity();
try {
mNotificationDelegate.onNotificationSmartReplySent(key, replyIndex);
mNotificationDelegate.onNotificationSmartReplySent(key, replyIndex, reply,
generatedByAssistant);
} finally {
Binder.restoreCallingIdentity(identity);
}

View File

@@ -3696,4 +3696,19 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
new TestableToastCallback(), 2000, 0);
assertEquals(1, mService.mToastQueue.size());
}
@Test
public void testOnNotificationSmartReplySent() {
final int replyIndex = 2;
final String reply = "Hello";
final boolean generatedByAssistant = true;
NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
mService.addNotification(r);
mService.mNotificationDelegate.onNotificationSmartReplySent(
r.getKey(), replyIndex, reply, generatedByAssistant);
verify(mAssistants).notifyAssistantSuggestedReplySent(
eq(r.sbn), eq(reply), eq(generatedByAssistant));
}
}