• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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