1 /* 2 * Copyright 2020 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 17 package com.android.car.calendar.common; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.mockito.ArgumentMatchers.any; 22 import static org.mockito.ArgumentMatchers.anyString; 23 import static org.mockito.Mockito.mock; 24 import static org.mockito.Mockito.timeout; 25 import static org.mockito.Mockito.verify; 26 import static org.mockito.Mockito.when; 27 28 import static java.time.temporal.ChronoUnit.HOURS; 29 30 import android.Manifest; 31 import android.content.Context; 32 import android.database.ContentObserver; 33 import android.database.Cursor; 34 import android.database.MatrixCursor; 35 import android.net.Uri; 36 import android.os.Bundle; 37 import android.os.CancellationSignal; 38 import android.os.Handler; 39 import android.os.HandlerThread; 40 import android.os.Message; 41 import android.os.Process; 42 import android.os.SystemClock; 43 import android.provider.CalendarContract; 44 import android.test.mock.MockContentProvider; 45 import android.test.mock.MockContentResolver; 46 47 import androidx.lifecycle.Observer; 48 import androidx.test.annotation.UiThreadTest; 49 import androidx.test.ext.junit.runners.AndroidJUnit4; 50 import androidx.test.platform.app.InstrumentationRegistry; 51 import androidx.test.rule.GrantPermissionRule; 52 53 import com.google.common.collect.ImmutableList; 54 55 import org.junit.After; 56 import org.junit.Before; 57 import org.junit.Rule; 58 import org.junit.Test; 59 import org.junit.runner.RunWith; 60 61 import java.time.Clock; 62 import java.time.Duration; 63 import java.time.Instant; 64 import java.time.LocalDateTime; 65 import java.time.ZoneId; 66 import java.time.ZonedDateTime; 67 import java.time.temporal.ChronoField; 68 import java.time.temporal.ChronoUnit; 69 import java.util.ArrayList; 70 import java.util.List; 71 import java.util.concurrent.CountDownLatch; 72 import java.util.concurrent.TimeUnit; 73 74 @RunWith(AndroidJUnit4.class) 75 public class EventsLiveDataTest { 76 private static final ZoneId BERLIN_ZONE_ID = ZoneId.of("Europe/Berlin"); 77 private static final ZonedDateTime CURRENT_DATE_TIME = 78 LocalDateTime.of(2019, 12, 10, 10, 10, 10, 500500).atZone(BERLIN_ZONE_ID); 79 private static final Dialer.NumberAndAccess EVENT_NUMBER_PIN = 80 new Dialer.NumberAndAccess("the number", "the pin"); 81 private static final String EVENT_TITLE = "the title"; 82 private static final boolean EVENT_ALL_DAY = false; 83 private static final String EVENT_LOCATION = "the location"; 84 private static final String EVENT_DESCRIPTION = "the description"; 85 private static final String CALENDAR_NAME = "the calendar name"; 86 private static final int CALENDAR_COLOR = 0xCAFEBABE; 87 private static final int EVENT_ATTENDEE_STATUS = 88 CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED; 89 90 @Rule 91 public final GrantPermissionRule permissionRule = 92 GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR); 93 94 private EventsLiveData mEventsLiveData; 95 private TestContentProvider mTestContentProvider; 96 private TestHandler mTestHandler; 97 private TestClock mTestClock; 98 99 @Before setUp()100 public void setUp() { 101 mTestClock = new TestClock(BERLIN_ZONE_ID); 102 mTestClock.setTime(CURRENT_DATE_TIME); 103 Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 104 105 // Create a fake result for the calendar content provider. 106 MockContentResolver mockContentResolver = new MockContentResolver(context); 107 108 mTestContentProvider = new TestContentProvider(context); 109 mockContentResolver.addProvider(CalendarContract.AUTHORITY, mTestContentProvider); 110 111 EventDescriptions mockEventDescriptions = mock(EventDescriptions.class); 112 when(mockEventDescriptions.extractNumberAndPins(any())) 113 .thenReturn(ImmutableList.of(EVENT_NUMBER_PIN)); 114 115 EventLocations mockEventLocations = mock(EventLocations.class); 116 when(mockEventLocations.isValidLocation(anyString())).thenReturn(true); 117 mTestHandler = TestHandler.create(); 118 mEventsLiveData = 119 new EventsLiveData( 120 mTestClock, 121 mTestHandler, 122 mockContentResolver, 123 mockEventDescriptions, 124 mockEventLocations); 125 } 126 127 @After tearDown()128 public void tearDown() { 129 if (mTestHandler != null) { 130 mTestHandler.stop(); 131 } 132 } 133 134 @Test noObserver_noQueryMade()135 public void noObserver_noQueryMade() { 136 // No query should be made because there are no observers. 137 assertThat(mTestContentProvider.mTestEventCursor).isNull(); 138 } 139 140 @Test 141 @UiThreadTest addObserver_queryMade()142 public void addObserver_queryMade() throws InterruptedException { 143 // Observing triggers content to be read. 144 mEventsLiveData.observeForever((unused) -> { /* Do nothing */ }); 145 146 // Wait for the data to be read on the background thread. 147 mTestContentProvider.awaitCalendarQuery(); 148 149 assertThat(mTestContentProvider.mTestEventCursor).isNotNull(); 150 } 151 152 @Test 153 @UiThreadTest addObserver_contentObserved()154 public void addObserver_contentObserved() throws InterruptedException { 155 // Observing triggers content to be read. 156 mEventsLiveData.observeForever((unused) -> { /* Do nothing */ }); 157 158 // Wait for the data to be read on the background thread. 159 mTestContentProvider.awaitCalendarQuery(); 160 161 awaitAndAssertDone(mTestContentProvider.mTestEventCursor.mRegisterContentObserverLatch); 162 } 163 164 @Test addObserver_observerCalled()165 public void addObserver_observerCalled() throws InterruptedException { 166 // Observing triggers content to be read. 167 Observer<ImmutableList<Event>> mockObserver = mock(Observer.class); 168 runOnMain(() -> mEventsLiveData.observeForever(mockObserver)); 169 170 // TODO(jdp) This method of verifying an async behaviour is easier to read. 171 verify(mockObserver, timeout(1000).times(1)).onChanged(any()); 172 } 173 174 @Test addTwoObservers_bothObserversCalled()175 public void addTwoObservers_bothObserversCalled() throws InterruptedException { 176 // Observing triggers content to be read. 177 Observer<ImmutableList<Event>> mockObserver1 = mock(Observer.class); 178 runOnMain(() -> mEventsLiveData.observeForever(mockObserver1)); 179 Observer<ImmutableList<Event>> mockObserver2 = mock(Observer.class); 180 runOnMain(() -> mEventsLiveData.observeForever(mockObserver2)); 181 182 verify(mockObserver1, timeout(1000).times(1)).onChanged(any()); 183 verify(mockObserver2, timeout(1000).times(1)).onChanged(any()); 184 } 185 186 @Test removeObserver_contentNotObserved()187 public void removeObserver_contentNotObserved() throws InterruptedException { 188 // Observing triggers content to be read. 189 Observer<ImmutableList<Event>> observer = (unused) -> { /* Do nothing */ }; 190 runOnMain(() -> mEventsLiveData.observeForever(observer)); 191 192 // Wait for the data to be read on the background thread. 193 mTestContentProvider.awaitCalendarQuery(); 194 195 awaitAndAssertDone(mTestContentProvider.mTestEventCursor.mRegisterContentObserverLatch); 196 runOnMain(() -> mEventsLiveData.removeObserver(observer)); 197 awaitAndAssertDone(mTestContentProvider.mTestEventCursor.mUnregisterContentObserverLatch); 198 } 199 200 @Test addObserver_oneEventResult()201 public void addObserver_oneEventResult() throws InterruptedException { 202 mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, 1)); 203 204 // Expect onChanged to be called for when the data is read. 205 CountDownLatch latch = new CountDownLatch(1); 206 207 // Must add observer on main thread. 208 runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown())); 209 210 // Wait for the data to be read on the background thread. 211 awaitAndAssertDone(latch); 212 213 ImmutableList<Event> events = mEventsLiveData.getValue(); 214 assertThat(events).isNotNull(); 215 assertThat(events).hasSize(1); 216 Event event = events.get(0); 217 218 long eventStartMillis = addHoursAndTruncate(CURRENT_DATE_TIME, 0); 219 long eventEndMillis = addHoursAndTruncate(CURRENT_DATE_TIME, 1); 220 221 assertThat(event.getTitle()).isEqualTo(EVENT_TITLE); 222 assertThat(event.getCalendarDetails().getColor()).isEqualTo(CALENDAR_COLOR); 223 assertThat(event.getLocation()).isEqualTo(EVENT_LOCATION); 224 assertThat(event.getStartInstant().toEpochMilli()).isEqualTo(eventStartMillis); 225 assertThat(event.getEndInstant().toEpochMilli()).isEqualTo(eventEndMillis); 226 assertThat(event.getStatus()).isEqualTo(Event.Status.ACCEPTED); 227 assertThat(event.getNumberAndAccess()).isEqualTo(EVENT_NUMBER_PIN); 228 } 229 230 @Test notifyDataChange_dataNotChanged_onChangedNotCalled()231 public void notifyDataChange_dataNotChanged_onChangedNotCalled() throws InterruptedException { 232 mTestContentProvider.addRow(buildTestRow()); 233 234 // Expect onChanged to be called for when the data is read. 235 CountDownLatch initializeCountdownLatch = new CountDownLatch(1); 236 237 // Expect the same callback as above but with an extra when the data is updated. 238 CountDownLatch changeCountdownLatch = new CountDownLatch(2); 239 240 // Must add observer on main thread. 241 runOnMain( 242 () -> 243 mEventsLiveData.observeForever( 244 // Count down both latches when data is changed. 245 (value) -> { 246 initializeCountdownLatch.countDown(); 247 changeCountdownLatch.countDown(); 248 })); 249 250 // Wait for the data to be read on the background thread. 251 awaitAndAssertDone(initializeCountdownLatch); 252 253 // Signal that the content has changed but do not update the data. 254 mTestContentProvider.mTestEventCursor.signalDataChanged(); 255 256 // Wait for the changed data to be read on the background thread. 257 awaitAndAssertNotDone(changeCountdownLatch); 258 } 259 260 @Test notifyDataChange_dataChanged_onChangedCalled()261 public void notifyDataChange_dataChanged_onChangedCalled() throws InterruptedException { 262 mTestContentProvider.addRow(buildTestRow()); 263 264 // Expect onChanged to be called for when the data is read. 265 CountDownLatch initializeCountdownLatch = new CountDownLatch(1); 266 267 // Expect the same callback as above but with an extra when the data is updated. 268 CountDownLatch changeCountdownLatch = new CountDownLatch(2); 269 270 // Must add observer on main thread. 271 runOnMain( 272 () -> 273 mEventsLiveData.observeForever( 274 // Count down both latches when data is changed. 275 (value) -> { 276 initializeCountdownLatch.countDown(); 277 changeCountdownLatch.countDown(); 278 })); 279 280 // Wait for the data to be read on the background thread. 281 awaitAndAssertDone(initializeCountdownLatch); 282 283 // Change the data and signal that the content has changed. 284 mTestContentProvider.addRow(buildTestRowWithTitle("Another event")); 285 mTestContentProvider.mTestEventCursor.signalDataChanged(); 286 287 // Wait for the changed data to be read on the background thread. 288 awaitAndAssertDone(changeCountdownLatch); 289 } 290 291 @Test addObserver_updateScheduled()292 public void addObserver_updateScheduled() throws InterruptedException { 293 mTestHandler.setExpectedMessageCount(2); 294 295 // Must add observer on main thread. 296 runOnMain(() -> mEventsLiveData.observeForever((unused) -> { /* Do nothing */ })); 297 298 mTestHandler.awaitExpectedMessages(); 299 300 // Show that a message was scheduled for the future. 301 assertThat(mTestHandler.mLastUptimeMillis).isAtLeast(SystemClock.uptimeMillis()); 302 } 303 304 @Test noCalendars_valueNull()305 public void noCalendars_valueNull() throws InterruptedException { 306 mTestContentProvider.mAddFakeCalendar = false; 307 mTestContentProvider.addRow(buildTestRow()); 308 309 // Expect onChanged to be called for when the data is read. 310 CountDownLatch latch = new CountDownLatch(1); 311 runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown())); 312 313 // Wait for the data to be read on the background thread. 314 awaitAndAssertDone(latch); 315 316 assertThat(mEventsLiveData.getValue()).isNull(); 317 } 318 319 @Test 320 @UiThreadTest noCalendars_contentObserved()321 public void noCalendars_contentObserved() throws InterruptedException { 322 mTestContentProvider.mAddFakeCalendar = false; 323 mEventsLiveData.observeForever((unused) -> { /* Do nothing */ }); 324 mTestContentProvider.awaitCalendarQuery(); 325 awaitAndAssertDone(mTestContentProvider.mTestEventCursor.mRegisterContentObserverLatch); 326 } 327 328 @Test multiDayEvent_createsMultipleEvents()329 public void multiDayEvent_createsMultipleEvents() throws InterruptedException { 330 // Replace the default event with one that lasts 24 hours. 331 mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, 24)); 332 333 CountDownLatch latch = new CountDownLatch(1); 334 335 runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown())); 336 337 // Wait for the data to be read on the background thread. 338 awaitAndAssertDone(latch); 339 340 // Expect an event for the 2 parts of the split event instance. 341 assertThat(mEventsLiveData.getValue()).hasSize(2); 342 } 343 344 @Test multiDayEvent_keepsOriginalTimes()345 public void multiDayEvent_keepsOriginalTimes() throws InterruptedException { 346 // Replace the default event with one that lasts 24 hours. 347 int hours = 48; 348 mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, hours)); 349 350 CountDownLatch latch = new CountDownLatch(1); 351 352 runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown())); 353 354 // Wait for the data to be read on the background thread. 355 awaitAndAssertDone(latch); 356 357 Event middlePartEvent = mEventsLiveData.getValue().get(1); 358 359 // The start and end times should remain the original times. 360 ZonedDateTime expectedStartTime = CURRENT_DATE_TIME.truncatedTo(HOURS); 361 assertThat(middlePartEvent.getStartInstant()).isEqualTo(expectedStartTime.toInstant()); 362 ZonedDateTime expectedEndTime = expectedStartTime.plus(hours, HOURS); 363 assertThat(middlePartEvent.getEndInstant()).isEqualTo(expectedEndTime.toInstant()); 364 } 365 366 @Test multipleEvents_resultsSortedStart()367 public void multipleEvents_resultsSortedStart() throws InterruptedException { 368 // Replace the default event with two that are out of time order. 369 ZonedDateTime twoHoursAfterCurrentTime = CURRENT_DATE_TIME.plus(Duration.ofHours(2)); 370 mTestContentProvider.addRow(buildTestRowWithDuration(twoHoursAfterCurrentTime, 1)); 371 mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, 1)); 372 373 CountDownLatch latch = new CountDownLatch(1); 374 375 runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown())); 376 377 // Wait for the data to be read on the background thread. 378 awaitAndAssertDone(latch); 379 380 ImmutableList<Event> events = mEventsLiveData.getValue(); 381 382 assertThat(events.get(0).getStartInstant().toEpochMilli()) 383 .isEqualTo(addHoursAndTruncate(CURRENT_DATE_TIME, 0)); 384 assertThat(events.get(1).getStartInstant().toEpochMilli()) 385 .isEqualTo(addHoursAndTruncate(CURRENT_DATE_TIME, 2)); 386 } 387 388 @Test multipleEvents_resultsSortedTitle()389 public void multipleEvents_resultsSortedTitle() throws InterruptedException { 390 // Replace the default event with two that are out of time order. 391 mTestContentProvider.addRow(buildTestRowWithTitle("Title B")); 392 mTestContentProvider.addRow(buildTestRowWithTitle("Title A")); 393 mTestContentProvider.addRow(buildTestRowWithTitle("Title C")); 394 395 // Expect onChanged to be called for when the data is read. 396 CountDownLatch latch = new CountDownLatch(1); 397 398 runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown())); 399 400 // Wait for the data to be read on the background thread. 401 awaitAndAssertDone(latch); 402 403 ImmutableList<Event> events = mEventsLiveData.getValue(); 404 405 assertThat(events.get(0).getTitle()).isEqualTo("Title A"); 406 assertThat(events.get(1).getTitle()).isEqualTo("Title B"); 407 assertThat(events.get(2).getTitle()).isEqualTo("Title C"); 408 } 409 410 @Test allDayEvent_timesSetToLocal()411 public void allDayEvent_timesSetToLocal() throws InterruptedException { 412 // All-day events always start at UTC midnight. 413 ZonedDateTime utcMidnightStart = 414 CURRENT_DATE_TIME.withZoneSameLocal(ZoneId.of("UTC")).truncatedTo(ChronoUnit.DAYS); 415 mTestContentProvider.addRow(buildTestRowAllDay(utcMidnightStart)); 416 417 // Expect onChanged to be called when the data is read. 418 CountDownLatch latch = new CountDownLatch(1); 419 420 runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown())); 421 422 // Wait for the data to be read on the background thread. 423 awaitAndAssertDone(latch); 424 425 ImmutableList<Event> events = mEventsLiveData.getValue(); 426 427 Instant localMidnightStart = CURRENT_DATE_TIME.truncatedTo(ChronoUnit.DAYS).toInstant(); 428 assertThat(events.get(0).getStartInstant()).isEqualTo(localMidnightStart); 429 } 430 431 @Test allDayEvent_queryCoversLocalDayStart()432 public void allDayEvent_queryCoversLocalDayStart() throws InterruptedException { 433 // All-day events always start at UTC midnight. 434 ZonedDateTime utcMidnightStart = 435 CURRENT_DATE_TIME.withZoneSameLocal(ZoneId.of("UTC")).truncatedTo(ChronoUnit.DAYS); 436 mTestContentProvider.addRow(buildTestRowAllDay(utcMidnightStart)); 437 438 // Set the time to 23:XX in the BERLIN_ZONE_ID which will be after the event end time. 439 mTestClock.setTime(CURRENT_DATE_TIME.with(ChronoField.HOUR_OF_DAY, 23)); 440 441 // Expect onChanged to be called for when the data is read. 442 CountDownLatch latch = new CountDownLatch(1); 443 444 runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown())); 445 446 // Wait for the data to be read on the background thread. 447 awaitAndAssertDone(latch); 448 449 // Show that the event is included even though its end time is before the current time. 450 assertThat(mEventsLiveData.getValue()).isNotEmpty(); 451 } 452 runOnMain(Runnable runnable)453 private void runOnMain(Runnable runnable) { 454 InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); 455 } 456 awaitAndAssertDone(CountDownLatch latch)457 private static void awaitAndAssertDone(CountDownLatch latch) throws InterruptedException { 458 assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue(); 459 } 460 awaitAndAssertNotDone(CountDownLatch latch)461 private static void awaitAndAssertNotDone(CountDownLatch latch) throws InterruptedException { 462 assertThat(latch.await(2, TimeUnit.SECONDS)).isFalse(); 463 } 464 465 private static class TestContentProvider extends MockContentProvider { 466 TestEventCursor mTestEventCursor; 467 boolean mAddFakeCalendar = true; 468 List<Object[]> mEventRows = new ArrayList<>(); 469 CountDownLatch mCalendarQueryLatch = new CountDownLatch(1); 470 TestContentProvider(Context context)471 TestContentProvider(Context context) { 472 super(context); 473 } 474 addRow(Object[] row)475 private void addRow(Object[] row) { 476 mEventRows.add(row); 477 } 478 479 @Override query( Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal)480 public Cursor query( 481 Uri uri, 482 String[] projection, 483 Bundle queryArgs, 484 CancellationSignal cancellationSignal) { 485 if (uri.toString().startsWith(CalendarContract.Instances.CONTENT_URI.toString())) { 486 mTestEventCursor = new TestEventCursor(uri); 487 for (Object[] row : mEventRows) { 488 mTestEventCursor.addRow(row); 489 } 490 return mTestEventCursor; 491 } else if (uri.equals(CalendarContract.Calendars.CONTENT_URI)) { 492 MatrixCursor calendarsCursor = new MatrixCursor(new String[] {" Test name"}); 493 if (mAddFakeCalendar) { 494 calendarsCursor.addRow(new String[] {"Test value"}); 495 } 496 mCalendarQueryLatch.countDown(); 497 return calendarsCursor; 498 } 499 throw new IllegalStateException("Unexpected query uri " + uri); 500 } 501 awaitCalendarQuery()502 void awaitCalendarQuery() throws InterruptedException { 503 awaitAndAssertDone(mCalendarQueryLatch); 504 } 505 506 static class TestEventCursor extends MatrixCursor { 507 final Uri mUri; 508 CountDownLatch mRegisterContentObserverLatch = new CountDownLatch(1); 509 CountDownLatch mUnregisterContentObserverLatch = new CountDownLatch(1); 510 TestEventCursor(Uri uri)511 TestEventCursor(Uri uri) { 512 super( 513 new String[] { 514 CalendarContract.Instances.TITLE, 515 CalendarContract.Instances.ALL_DAY, 516 CalendarContract.Instances.BEGIN, 517 CalendarContract.Instances.END, 518 CalendarContract.Instances.DESCRIPTION, 519 CalendarContract.Instances.EVENT_LOCATION, 520 CalendarContract.Instances.SELF_ATTENDEE_STATUS, 521 CalendarContract.Instances.CALENDAR_COLOR, 522 CalendarContract.Instances.CALENDAR_DISPLAY_NAME, 523 }); 524 mUri = uri; 525 } 526 527 @Override registerContentObserver(ContentObserver observer)528 public void registerContentObserver(ContentObserver observer) { 529 super.registerContentObserver(observer); 530 mRegisterContentObserverLatch.countDown(); 531 } 532 533 @Override unregisterContentObserver(ContentObserver observer)534 public void unregisterContentObserver(ContentObserver observer) { 535 super.unregisterContentObserver(observer); 536 mUnregisterContentObserverLatch.countDown(); 537 } 538 signalDataChanged()539 void signalDataChanged() { 540 super.onChange(true); 541 } 542 } 543 } 544 545 private static class TestHandler extends Handler { 546 final HandlerThread mThread; 547 long mLastUptimeMillis; 548 CountDownLatch mCountDownLatch; 549 create()550 static TestHandler create() { 551 HandlerThread thread = 552 new HandlerThread( 553 EventsLiveDataTest.class.getSimpleName(), 554 Process.THREAD_PRIORITY_FOREGROUND); 555 thread.start(); 556 return new TestHandler(thread); 557 } 558 TestHandler(HandlerThread thread)559 TestHandler(HandlerThread thread) { 560 super(thread.getLooper()); 561 mThread = thread; 562 } 563 stop()564 void stop() { 565 mThread.quit(); 566 } 567 setExpectedMessageCount(int expectedMessageCount)568 void setExpectedMessageCount(int expectedMessageCount) { 569 mCountDownLatch = new CountDownLatch(expectedMessageCount); 570 } 571 awaitExpectedMessages()572 void awaitExpectedMessages() throws InterruptedException { 573 awaitAndAssertDone(mCountDownLatch); 574 } 575 576 @Override sendMessageAtTime(Message msg, long uptimeMillis)577 public boolean sendMessageAtTime(Message msg, long uptimeMillis) { 578 mLastUptimeMillis = uptimeMillis; 579 if (mCountDownLatch != null) { 580 mCountDownLatch.countDown(); 581 } 582 return super.sendMessageAtTime(msg, uptimeMillis); 583 } 584 } 585 586 // Similar to {@link android.os.SimpleClock} but without @hide and with mutable millis. 587 static class TestClock extends Clock { 588 private final ZoneId mZone; 589 private long mTimeMs; 590 TestClock(ZoneId zone)591 TestClock(ZoneId zone) { 592 mZone = zone; 593 } 594 setTime(ZonedDateTime time)595 void setTime(ZonedDateTime time) { 596 mTimeMs = time.toInstant().toEpochMilli(); 597 } 598 599 @Override getZone()600 public ZoneId getZone() { 601 return mZone; 602 } 603 604 @Override withZone(ZoneId zone)605 public Clock withZone(ZoneId zone) { 606 return new TestClock(zone) { 607 @Override 608 public long millis() { 609 return TestClock.this.millis(); 610 } 611 }; 612 } 613 614 @Override millis()615 public long millis() { 616 return mTimeMs; 617 } 618 619 @Override instant()620 public Instant instant() { 621 return Instant.ofEpochMilli(millis()); 622 } 623 } 624 625 static long addHoursAndTruncate(ZonedDateTime dateTime, int hours) { 626 return dateTime.truncatedTo(HOURS).plus(Duration.ofHours(hours)).toInstant().toEpochMilli(); 627 } 628 629 static Object[] buildTestRowWithDuration(ZonedDateTime startDateTime, int eventDurationHours) { 630 return buildTestRowWithDuration( 631 startDateTime, eventDurationHours, EVENT_TITLE, EVENT_ALL_DAY); 632 } 633 634 static Object[] buildTestRowAllDay(ZonedDateTime startDateTime) { 635 return buildTestRowWithDuration(startDateTime, 24, EVENT_TITLE, true); 636 } 637 638 static Object[] buildTestRowWithTitle(String title) { 639 return buildTestRowWithDuration(CURRENT_DATE_TIME, 1, title, EVENT_ALL_DAY); 640 } 641 642 static Object[] buildTestRow() { 643 return buildTestRowWithDuration(CURRENT_DATE_TIME, 1, EVENT_TITLE, EVENT_ALL_DAY); 644 } 645 646 static Object[] buildTestRowWithDuration( 647 ZonedDateTime currentDateTime, int eventDurationHours, String title, boolean allDay) { 648 return new Object[] { 649 title, 650 allDay ? 1 : 0, 651 addHoursAndTruncate(currentDateTime, 0), 652 addHoursAndTruncate(currentDateTime, eventDurationHours), 653 EVENT_DESCRIPTION, 654 EVENT_LOCATION, 655 EVENT_ATTENDEE_STATUS, 656 CALENDAR_COLOR, 657 CALENDAR_NAME 658 }; 659 } 660 } 661