1 /**
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.ext.services.notification;
17 
18 import static com.google.common.truth.Truth.assertAbout;
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.ArgumentMatchers.any;
22 import static org.mockito.ArgumentMatchers.argThat;
23 import static org.mockito.Mockito.never;
24 import static org.mockito.Mockito.times;
25 import static org.mockito.Mockito.verify;
26 import static org.mockito.Mockito.when;
27 
28 import android.annotation.NonNull;
29 import android.app.Notification;
30 import android.app.NotificationChannel;
31 import android.app.NotificationManager;
32 import android.app.PendingIntent;
33 import android.app.Person;
34 import android.app.RemoteInput;
35 import android.content.Context;
36 import android.content.Intent;
37 import android.content.pm.IPackageManager;
38 import android.graphics.drawable.Icon;
39 import android.os.Bundle;
40 import android.os.Process;
41 import android.service.notification.NotificationAssistantService;
42 import android.service.notification.StatusBarNotification;
43 import android.view.textclassifier.ConversationAction;
44 import android.view.textclassifier.ConversationActions;
45 import android.view.textclassifier.TextClassificationManager;
46 import android.view.textclassifier.TextClassifier;
47 import android.view.textclassifier.TextClassifierEvent;
48 
49 import androidx.test.InstrumentationRegistry;
50 import androidx.test.runner.AndroidJUnit4;
51 
52 import com.google.common.truth.FailureMetadata;
53 import com.google.common.truth.Subject;
54 
55 import org.junit.Before;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 import org.mockito.ArgumentCaptor;
59 import org.mockito.ArgumentMatcher;
60 import org.mockito.Mock;
61 import org.mockito.MockitoAnnotations;
62 
63 import java.time.Instant;
64 import java.time.ZoneOffset;
65 import java.time.ZonedDateTime;
66 import java.util.Arrays;
67 import java.util.Collections;
68 import java.util.List;
69 import java.util.Objects;
70 
71 import javax.annotation.Nullable;
72 
73 import androidx.test.InstrumentationRegistry;
74 import androidx.test.runner.AndroidJUnit4;
75 
76 @RunWith(AndroidJUnit4.class)
77 public class SmartActionsHelperTest {
78     private static final String RESULT_ID = "id";
79     private static final float SCORE = 0.7f;
80     private static final CharSequence SMART_REPLY = "Home";
81     private static final ConversationAction REPLY_ACTION =
82             new ConversationAction.Builder(ConversationAction.TYPE_TEXT_REPLY)
83                     .setTextReply(SMART_REPLY)
84                     .setConfidenceScore(SCORE)
85                     .build();
86     private static final String MESSAGE = "Where are you?";
87 
88     @Mock
89     IPackageManager mIPackageManager;
90     @Mock
91     private TextClassifier mTextClassifier;
92     private StatusBarNotification mStatusBarNotification;
93     @Mock
94     private SmsHelper mSmsHelper;
95 
96     private SmartActionsHelper mSmartActionsHelper;
97     private Context mContext;
98     private Notification.Builder mNotificationBuilder;
99     private AssistantSettings mSettings;
100 
101     @Before
setup()102     public void setup() {
103         MockitoAnnotations.initMocks(this);
104         mContext = InstrumentationRegistry.getTargetContext();
105 
106         mContext.getSystemService(TextClassificationManager.class)
107                 .setTextClassifier(mTextClassifier);
108         when(mTextClassifier.suggestConversationActions(any(ConversationActions.Request.class)))
109                 .thenReturn(new ConversationActions(Arrays.asList(REPLY_ACTION), RESULT_ID));
110 
111         mNotificationBuilder = new Notification.Builder(mContext, "channel");
112         mSettings = AssistantSettings.createForTesting(
113                 null, null, Process.myUserHandle().getIdentifier(), null);
114         mSettings.mGenerateActions = true;
115         mSettings.mGenerateReplies = true;
116         mSmartActionsHelper = new SmartActionsHelper(mContext, mSettings);
117     }
118 
setStatusBarNotification(Notification n)119     private void setStatusBarNotification(Notification n) {
120         mStatusBarNotification = new StatusBarNotification("random.app", "random.app", 0,
121         "tag", Process.myUid(), Process.myPid(), n, Process.myUserHandle(), null, 0);
122     }
123 
124     @Test
testSuggest_notMessageNotification()125     public void testSuggest_notMessageNotification() {
126         Notification notification = mNotificationBuilder.setContentText(MESSAGE).build();
127         setStatusBarNotification(notification);
128 
129         mSmartActionsHelper.suggest(createNotificationEntry());
130 
131         verify(mTextClassifier, never())
132                 .suggestConversationActions(any(ConversationActions.Request.class));
133     }
134 
135     @Test
testSuggest_noInlineReply()136     public void testSuggest_noInlineReply() {
137         Notification notification =
138                 mNotificationBuilder
139                         .setContentText(MESSAGE)
140                         .setCategory(Notification.CATEGORY_MESSAGE)
141                         .build();
142         setStatusBarNotification(notification);
143 
144         ConversationActions.Request request = runSuggestAndCaptureRequest();
145 
146         // actions are enabled, but replies are not.
147         assertThat(
148                 request.getTypeConfig().resolveEntityListModifications(
149                         Arrays.asList(ConversationAction.TYPE_TEXT_REPLY,
150                                 ConversationAction.TYPE_OPEN_URL)))
151                 .containsExactly(ConversationAction.TYPE_OPEN_URL);
152     }
153 
154     @Test
testSuggest_settingsOff()155     public void testSuggest_settingsOff() {
156         mSettings.mGenerateActions = false;
157         mSettings.mGenerateReplies = false;
158         Notification notification = createMessageNotification();
159         setStatusBarNotification(notification);
160 
161         mSmartActionsHelper.suggest(createNotificationEntry());
162 
163         verify(mTextClassifier, never())
164                 .suggestConversationActions(any(ConversationActions.Request.class));
165     }
166 
167     @Test
testSuggest_settings_repliesOnActionsOff()168     public void testSuggest_settings_repliesOnActionsOff() {
169         mSettings.mGenerateReplies = true;
170         mSettings.mGenerateActions = false;
171         Notification notification = createMessageNotification();
172         setStatusBarNotification(notification);
173 
174         ConversationActions.Request request = runSuggestAndCaptureRequest();
175 
176         // replies are enabled, but actions are not.
177         assertThat(
178                 request.getTypeConfig().resolveEntityListModifications(
179                         Arrays.asList(ConversationAction.TYPE_TEXT_REPLY,
180                                 ConversationAction.TYPE_OPEN_URL)))
181                 .containsExactly(ConversationAction.TYPE_TEXT_REPLY);
182     }
183 
184     @Test
testSuggest_settings_repliesOffActionsOn()185     public void testSuggest_settings_repliesOffActionsOn() {
186         mSettings.mGenerateReplies = false;
187         mSettings.mGenerateActions = true;
188         Notification notification = createMessageNotification();
189         setStatusBarNotification(notification);
190 
191         ConversationActions.Request request = runSuggestAndCaptureRequest();
192 
193         // actions are enabled, but replies are not.
194         assertThat(
195                 request.getTypeConfig().resolveEntityListModifications(
196                         Arrays.asList(ConversationAction.TYPE_TEXT_REPLY,
197                                 ConversationAction.TYPE_OPEN_URL)))
198                 .containsExactly(ConversationAction.TYPE_OPEN_URL);
199     }
200 
201 
202     @Test
testSuggest_nonMessageStyleMessageNotification()203     public void testSuggest_nonMessageStyleMessageNotification() {
204         Notification notification = createMessageNotification();
205         setStatusBarNotification(notification);
206 
207         List<ConversationActions.Message> messages =
208                 runSuggestAndCaptureRequest().getConversation();
209 
210         assertThat(messages).hasSize(1);
211         MessageSubject.assertThat(messages.get(0)).hasText(MESSAGE);
212         ArgumentCaptor<TextClassifierEvent> argumentCaptor =
213                 ArgumentCaptor.forClass(TextClassifierEvent.class);
214         verify(mTextClassifier).onTextClassifierEvent(argumentCaptor.capture());
215         TextClassifierEvent textClassifierEvent = argumentCaptor.getValue();
216         assertTextClassifierEvent(textClassifierEvent, TextClassifierEvent.TYPE_ACTIONS_GENERATED);
217         assertThat(textClassifierEvent.getEntityTypes()).asList()
218                 .containsExactly(ConversationAction.TYPE_TEXT_REPLY);
219     }
220 
221     @Test
testSuggest_messageStyle()222     public void testSuggest_messageStyle() {
223         Person me = new Person.Builder().setName("Me").build();
224         Person userA = new Person.Builder().setName("A").build();
225         Person userB = new Person.Builder().setName("B").build();
226         Notification.MessagingStyle style =
227                 new Notification.MessagingStyle(me)
228                         .addMessage("firstMessage", 1000, (Person) null)
229                         .addMessage("secondMessage", 2000, me)
230                         .addMessage("thirdMessage", 3000, userA)
231                         .addMessage("fourthMessage", 4000, userB);
232         Notification notification =
233                 mNotificationBuilder
234                         .setContentText("You have three new messages")
235                         .setStyle(style)
236                         .setActions(createReplyAction())
237                         .build();
238         setStatusBarNotification(notification);
239 
240         List<ConversationActions.Message> messages =
241                 runSuggestAndCaptureRequest().getConversation();
242         assertThat(messages).hasSize(4);
243 
244         ConversationActions.Message firstMessage = messages.get(0);
245         MessageSubject.assertThat(firstMessage).hasText("firstMessage");
246         MessageSubject.assertThat(firstMessage)
247                 .hasPerson(ConversationActions.Message.PERSON_USER_SELF);
248         MessageSubject.assertThat(firstMessage)
249                 .hasReferenceTime(createZonedDateTimeFromMsUtc(1000));
250 
251         ConversationActions.Message secondMessage = messages.get(1);
252         MessageSubject.assertThat(secondMessage).hasText("secondMessage");
253         MessageSubject.assertThat(secondMessage)
254                 .hasPerson(ConversationActions.Message.PERSON_USER_SELF);
255         MessageSubject.assertThat(secondMessage)
256                 .hasReferenceTime(createZonedDateTimeFromMsUtc(2000));
257 
258         ConversationActions.Message thirdMessage = messages.get(2);
259         MessageSubject.assertThat(thirdMessage).hasText("thirdMessage");
260         MessageSubject.assertThat(thirdMessage).hasPerson(userA);
261         MessageSubject.assertThat(thirdMessage)
262                 .hasReferenceTime(createZonedDateTimeFromMsUtc(3000));
263 
264         ConversationActions.Message fourthMessage = messages.get(3);
265         MessageSubject.assertThat(fourthMessage).hasText("fourthMessage");
266         MessageSubject.assertThat(fourthMessage).hasPerson(userB);
267         MessageSubject.assertThat(fourthMessage)
268                 .hasReferenceTime(createZonedDateTimeFromMsUtc(4000));
269 
270         ArgumentCaptor<TextClassifierEvent> argumentCaptor =
271                 ArgumentCaptor.forClass(TextClassifierEvent.class);
272         verify(mTextClassifier).onTextClassifierEvent(argumentCaptor.capture());
273         TextClassifierEvent textClassifierEvent = argumentCaptor.getValue();
274         assertTextClassifierEvent(textClassifierEvent, TextClassifierEvent.TYPE_ACTIONS_GENERATED);
275         assertThat(textClassifierEvent.getEntityTypes()).asList()
276                 .containsExactly(ConversationAction.TYPE_TEXT_REPLY);
277     }
278 
279     @Test
testSuggest_lastMessageLocalUser()280     public void testSuggest_lastMessageLocalUser() {
281         Person me = new Person.Builder().setName("Me").build();
282         Person userA = new Person.Builder().setName("A").build();
283         Notification.MessagingStyle style =
284                 new Notification.MessagingStyle(me)
285                         .addMessage("firstMessage", 1000, userA)
286                         .addMessage("secondMessage", 2000, me);
287         Notification notification =
288                 mNotificationBuilder
289                         .setContentText("You have two new messages")
290                         .setStyle(style)
291                         .setActions(createReplyAction())
292                         .build();
293         setStatusBarNotification(notification);
294 
295         mSmartActionsHelper.suggest(createNotificationEntry());
296 
297         verify(mTextClassifier, never())
298                 .suggestConversationActions(any(ConversationActions.Request.class));
299     }
300 
301     @Test
testSuggest_messageStyle_noPerson()302     public void testSuggest_messageStyle_noPerson() {
303         Person me = new Person.Builder().setName("Me").build();
304         Notification.MessagingStyle style =
305                 new Notification.MessagingStyle(me).addMessage("message", 1000, (Person) null);
306         Notification notification =
307                 mNotificationBuilder
308                         .setContentText("You have one new message")
309                         .setStyle(style)
310                         .setActions(createReplyAction())
311                         .build();
312         setStatusBarNotification(notification);
313 
314         mSmartActionsHelper.suggest(createNotificationEntry());
315 
316         verify(mTextClassifier, never())
317                 .suggestConversationActions(any(ConversationActions.Request.class));
318     }
319 
320     @Test
testOnSuggestedReplySent()321     public void testOnSuggestedReplySent() {
322         Notification notification = createMessageNotification();
323         setStatusBarNotification(notification);
324 
325         mSmartActionsHelper.suggest(createNotificationEntry());
326         mSmartActionsHelper.onSuggestedReplySent(mStatusBarNotification.getKey(), SMART_REPLY,
327                 NotificationAssistantService.SOURCE_FROM_ASSISTANT);
328 
329         ArgumentCaptor<TextClassifierEvent> argumentCaptor =
330                 ArgumentCaptor.forClass(TextClassifierEvent.class);
331         verify(mTextClassifier, times(2)).onTextClassifierEvent(argumentCaptor.capture());
332         List<TextClassifierEvent> events = argumentCaptor.getAllValues();
333         assertTextClassifierEvent(events.get(0), TextClassifierEvent.TYPE_ACTIONS_GENERATED);
334         assertTextClassifierEvent(events.get(1), TextClassifierEvent.TYPE_SMART_ACTION);
335         float[] scores = events.get(1).getScores();
336         assertThat(scores).hasLength(1);
337         assertThat(scores[0]).isEqualTo(SCORE);
338     }
339 
340     @Test
testOnSuggestedReplySent_anotherNotification()341     public void testOnSuggestedReplySent_anotherNotification() {
342         Notification notification = createMessageNotification();
343         setStatusBarNotification(notification);
344 
345         mSmartActionsHelper.suggest(createNotificationEntry());
346         mSmartActionsHelper.onSuggestedReplySent(
347                 "something_else", MESSAGE, NotificationAssistantService.SOURCE_FROM_ASSISTANT);
348 
349         verify(mTextClassifier, never()).onTextClassifierEvent(
350                 argThat(new TextClassifierEventMatcher(TextClassifierEvent.TYPE_SMART_ACTION)));
351     }
352 
353     @Test
testOnSuggestedReplySent_missingResultId()354     public void testOnSuggestedReplySent_missingResultId() {
355         when(mTextClassifier.suggestConversationActions(any(ConversationActions.Request.class)))
356                 .thenReturn(new ConversationActions(Collections.singletonList(REPLY_ACTION), null));
357         Notification notification = createMessageNotification();
358         setStatusBarNotification(notification);
359 
360         mSmartActionsHelper.suggest(createNotificationEntry());
361         mSmartActionsHelper.onSuggestedReplySent(mStatusBarNotification.getKey(), SMART_REPLY,
362                 NotificationAssistantService.SOURCE_FROM_ASSISTANT);
363 
364         verify(mTextClassifier, never()).onTextClassifierEvent(any(TextClassifierEvent.class));
365     }
366 
367     @Test
testOnNotificationDirectReply()368     public void testOnNotificationDirectReply() {
369         Notification notification = createMessageNotification();
370         setStatusBarNotification(notification);
371 
372         mSmartActionsHelper.suggest(createNotificationEntry());
373         mSmartActionsHelper.onNotificationDirectReplied(mStatusBarNotification.getKey());
374 
375         ArgumentCaptor<TextClassifierEvent> argumentCaptor =
376                 ArgumentCaptor.forClass(TextClassifierEvent.class);
377         verify(mTextClassifier, times(2)).onTextClassifierEvent(argumentCaptor.capture());
378         List<TextClassifierEvent> events = argumentCaptor.getAllValues();
379         assertTextClassifierEvent(events.get(0), TextClassifierEvent.TYPE_ACTIONS_GENERATED);
380         assertTextClassifierEvent(events.get(1), TextClassifierEvent.TYPE_MANUAL_REPLY);
381     }
382 
383     @Test
testOnNotificationExpansionChanged()384     public void testOnNotificationExpansionChanged() {
385         Notification notification = createMessageNotification();
386         setStatusBarNotification(notification);
387 
388         mSmartActionsHelper.suggest(createNotificationEntry());
389         mSmartActionsHelper.onNotificationExpansionChanged(createNotificationEntry(), true);
390 
391         ArgumentCaptor<TextClassifierEvent> argumentCaptor =
392                 ArgumentCaptor.forClass(TextClassifierEvent.class);
393         verify(mTextClassifier, times(2)).onTextClassifierEvent(argumentCaptor.capture());
394         List<TextClassifierEvent> events = argumentCaptor.getAllValues();
395         assertTextClassifierEvent(events.get(0), TextClassifierEvent.TYPE_ACTIONS_GENERATED);
396         assertTextClassifierEvent(events.get(1), TextClassifierEvent.TYPE_ACTIONS_SHOWN);
397     }
398 
399     @Test
testOnNotificationsSeen_notExpanded()400     public void testOnNotificationsSeen_notExpanded() {
401         Notification notification = createMessageNotification();
402         setStatusBarNotification(notification);
403 
404         mSmartActionsHelper.suggest(createNotificationEntry());
405         mSmartActionsHelper.onNotificationExpansionChanged(createNotificationEntry(), false);
406 
407         verify(mTextClassifier, never()).onTextClassifierEvent(
408                 argThat(new TextClassifierEventMatcher(TextClassifierEvent.TYPE_ACTIONS_SHOWN)));
409     }
410 
411     @Test
testOnNotifications_expanded()412     public void testOnNotifications_expanded() {
413         Notification notification = createMessageNotification();
414         setStatusBarNotification(notification);
415 
416         mSmartActionsHelper.suggest(createNotificationEntry());
417         mSmartActionsHelper.onNotificationExpansionChanged(createNotificationEntry(), true);
418 
419         ArgumentCaptor<TextClassifierEvent> argumentCaptor =
420                 ArgumentCaptor.forClass(TextClassifierEvent.class);
421         verify(mTextClassifier, times(2)).onTextClassifierEvent(argumentCaptor.capture());
422         List<TextClassifierEvent> events = argumentCaptor.getAllValues();
423         assertTextClassifierEvent(events.get(0), TextClassifierEvent.TYPE_ACTIONS_GENERATED);
424         assertTextClassifierEvent(events.get(1), TextClassifierEvent.TYPE_ACTIONS_SHOWN);
425     }
426 
427     @Test
testCopyAction()428     public void testCopyAction() {
429         Bundle extras = new Bundle();
430         Bundle entitiesExtras = new Bundle();
431         entitiesExtras.putString(SmartActionsHelper.KEY_TEXT, "12345");
432         extras.putParcelable(SmartActionsHelper.ENTITIES_EXTRAS, entitiesExtras);
433         ConversationAction conversationAction =
434                 new ConversationAction.Builder(ConversationAction.TYPE_COPY)
435                         .setExtras(extras)
436                         .build();
437         when(mTextClassifier.suggestConversationActions(any(ConversationActions.Request.class)))
438                 .thenReturn(
439                         new ConversationActions(
440                                 Collections.singletonList(conversationAction), null));
441 
442         Notification notification = createMessageNotification();
443         setStatusBarNotification(notification);
444         SmartActionsHelper.SmartSuggestions suggestions =
445                 mSmartActionsHelper.suggest(createNotificationEntry());
446 
447         assertThat(suggestions.actions).hasSize(1);
448         Notification.Action action = suggestions.actions.get(0);
449         assertThat(action.title).isEqualTo("12345");
450     }
451 
createZonedDateTimeFromMsUtc(long msUtc)452     private ZonedDateTime createZonedDateTimeFromMsUtc(long msUtc) {
453         return ZonedDateTime.ofInstant(Instant.ofEpochMilli(msUtc), ZoneOffset.systemDefault());
454     }
455 
runSuggestAndCaptureRequest()456     private ConversationActions.Request runSuggestAndCaptureRequest() {
457         mSmartActionsHelper.suggest(createNotificationEntry());
458 
459         ArgumentCaptor<ConversationActions.Request> argumentCaptor =
460                 ArgumentCaptor.forClass(ConversationActions.Request.class);
461         verify(mTextClassifier).suggestConversationActions(argumentCaptor.capture());
462         return argumentCaptor.getValue();
463     }
464 
createReplyAction()465     private Notification.Action createReplyAction() {
466         PendingIntent pendingIntent =
467                 PendingIntent.getActivity(mContext, 0, new Intent(mContext, this.getClass()), 0);
468         RemoteInput remoteInput = new RemoteInput.Builder("result")
469                 .setAllowFreeFormInput(true)
470                 .build();
471         return new Notification.Action.Builder(
472                 Icon.createWithResource(mContext.getResources(),
473                         android.R.drawable.stat_sys_warning),
474                 "Reply", pendingIntent)
475                 .addRemoteInput(remoteInput)
476                 .build();
477     }
478 
createNotificationEntry()479     private NotificationEntry createNotificationEntry() {
480         NotificationChannel channel =
481                 new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_DEFAULT);
482         return new NotificationEntry(
483                 mContext, mIPackageManager, mStatusBarNotification, channel, mSmsHelper);
484     }
485 
createMessageNotification()486     private Notification createMessageNotification() {
487         return mNotificationBuilder
488                 .setContentText(MESSAGE)
489                 .setCategory(Notification.CATEGORY_MESSAGE)
490                 .setActions(createReplyAction())
491                 .build();
492     }
493 
assertTextClassifierEvent( TextClassifierEvent textClassifierEvent, int expectedEventType)494     private void assertTextClassifierEvent(
495             TextClassifierEvent textClassifierEvent, int expectedEventType) {
496         assertThat(textClassifierEvent.getEventCategory())
497                 .isEqualTo(TextClassifierEvent.CATEGORY_CONVERSATION_ACTIONS);
498         assertThat(textClassifierEvent.getEventContext().getPackageName())
499                 .isEqualTo(InstrumentationRegistry.getTargetContext().getPackageName());
500         assertThat(textClassifierEvent.getEventContext().getWidgetType())
501                 .isEqualTo(TextClassifier.WIDGET_TYPE_NOTIFICATION);
502         assertThat(textClassifierEvent.getEventType()).isEqualTo(expectedEventType);
503     }
504 
505     private static final class MessageSubject
506             extends Subject<MessageSubject, ConversationActions.Message> {
507 
508         private static final Subject.Factory<MessageSubject, ConversationActions.Message> FACTORY =
509                 new Subject.Factory<MessageSubject, ConversationActions.Message>() {
510                     @Override
511                     public MessageSubject createSubject(
512                             @NonNull FailureMetadata failureMetadata,
513                             @NonNull ConversationActions.Message subject) {
514                         return new MessageSubject(failureMetadata, subject);
515                     }
516                 };
517 
MessageSubject( FailureMetadata failureMetadata, @Nullable ConversationActions.Message subject)518         private MessageSubject(
519                 FailureMetadata failureMetadata, @Nullable ConversationActions.Message subject) {
520             super(failureMetadata, subject);
521         }
522 
hasText(String text)523         private void hasText(String text) {
524             if (!Objects.equals(text, getSubject().getText().toString())) {
525                 failWithBadResults("has text", text, "has", getSubject().getText());
526             }
527         }
528 
hasPerson(Person person)529         private void hasPerson(Person person) {
530             if (!Objects.equals(person, getSubject().getAuthor())) {
531                 failWithBadResults("has author", person, "has", getSubject().getAuthor());
532             }
533         }
534 
hasReferenceTime(ZonedDateTime referenceTime)535         private void hasReferenceTime(ZonedDateTime referenceTime) {
536             if (!Objects.equals(referenceTime, getSubject().getReferenceTime())) {
537                 failWithBadResults(
538                         "has reference time",
539                         referenceTime,
540                         "has",
541                         getSubject().getReferenceTime());
542             }
543         }
544 
assertThat(ConversationActions.Message message)545         private static MessageSubject assertThat(ConversationActions.Message message) {
546             return assertAbout(FACTORY).that(message);
547         }
548     }
549 
550     private final class TextClassifierEventMatcher implements ArgumentMatcher<TextClassifierEvent> {
551 
552         private int mType;
553 
TextClassifierEventMatcher(int type)554         private TextClassifierEventMatcher(int type) {
555             mType = type;
556         }
557 
558         @Override
matches(TextClassifierEvent textClassifierEvent)559         public boolean matches(TextClassifierEvent textClassifierEvent) {
560             if (textClassifierEvent == null) {
561                 return false;
562             }
563             return mType == textClassifierEvent.getEventType();
564         }
565     }
566 }
567