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