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