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