aboutsummaryrefslogtreecommitdiff
path: root/tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java')
-rw-r--r--tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java609
1 files changed, 609 insertions, 0 deletions
diff --git a/tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java b/tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java
new file mode 100644
index 0000000..ff00e8d
--- /dev/null
+++ b/tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java
@@ -0,0 +1,609 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.calendar.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import static java.time.temporal.ChronoUnit.HOURS;
+
+import android.Manifest;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.provider.CalendarContract;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+
+import androidx.lifecycle.Observer;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.GrantPermissionRule;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoField;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class EventsLiveDataTest {
+ private static final ZoneId BERLIN_ZONE_ID = ZoneId.of("Europe/Berlin");
+ private static final ZonedDateTime CURRENT_DATE_TIME =
+ LocalDateTime.of(2019, 12, 10, 10, 10, 10, 500500).atZone(BERLIN_ZONE_ID);
+ private static final Dialer.NumberAndAccess EVENT_NUMBER_PIN =
+ new Dialer.NumberAndAccess("the number", "the pin");
+ private static final String EVENT_TITLE = "the title";
+ private static final boolean EVENT_ALL_DAY = false;
+ private static final String EVENT_LOCATION = "the location";
+ private static final String EVENT_DESCRIPTION = "the description";
+ private static final String CALENDAR_NAME = "the calendar name";
+ private static final int CALENDAR_COLOR = 0xCAFEBABE;
+ private static final int EVENT_ATTENDEE_STATUS =
+ CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED;
+
+ @Rule
+ public final GrantPermissionRule permissionRule =
+ GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR);
+
+ private EventsLiveData mEventsLiveData;
+ private TestContentProvider mTestContentProvider;
+ private TestHandler mTestHandler;
+ private TestClock mTestClock;
+
+ @Before
+ public void setUp() {
+ mTestClock = new TestClock(BERLIN_ZONE_ID);
+ mTestClock.setTime(CURRENT_DATE_TIME);
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ // Create a fake result for the calendar content provider.
+ MockContentResolver mockContentResolver = new MockContentResolver(context);
+
+ mTestContentProvider = new TestContentProvider(context);
+ mockContentResolver.addProvider(CalendarContract.AUTHORITY, mTestContentProvider);
+
+ EventDescriptions mockEventDescriptions = mock(EventDescriptions.class);
+ when(mockEventDescriptions.extractNumberAndPins(any()))
+ .thenReturn(ImmutableList.of(EVENT_NUMBER_PIN));
+
+ EventLocations mockEventLocations = mock(EventLocations.class);
+ when(mockEventLocations.isValidLocation(anyString())).thenReturn(true);
+ mTestHandler = TestHandler.create();
+ mEventsLiveData =
+ new EventsLiveData(
+ mTestClock,
+ mTestHandler,
+ mockContentResolver,
+ mockEventDescriptions,
+ mockEventLocations);
+ }
+
+ @After
+ public void tearDown() {
+ if (mTestHandler != null) {
+ mTestHandler.stop();
+ }
+ }
+
+ @Test
+ public void noObserver_noQueryMade() {
+ // No query should be made because there are no observers.
+ assertThat(mTestContentProvider.mTestEventCursor).isNull();
+ }
+
+ @Test
+ @UiThreadTest
+ public void addObserver_queryMade() throws InterruptedException {
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+ mEventsLiveData.observeForever((value) -> latch.countDown());
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ assertThat(mTestContentProvider.mTestEventCursor).isNotNull();
+ }
+
+ @Test
+ @UiThreadTest
+ public void addObserver_contentObserved() throws InterruptedException {
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+ mEventsLiveData.observeForever((value) -> latch.countDown());
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ assertThat(mTestContentProvider.mTestEventCursor.mLastContentObserver).isNotNull();
+ }
+
+ @Test
+ @UiThreadTest
+ public void removeObserver_contentNotObserved() throws InterruptedException {
+ // Expect onChanged when we observe, when the data is read, and when we stop observing.
+ final CountDownLatch latch = new CountDownLatch(2);
+ Observer<ImmutableList<Event>> observer = (value) -> latch.countDown();
+ mEventsLiveData.observeForever(observer);
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ final CountDownLatch latch2 = new CountDownLatch(1);
+ mEventsLiveData.removeObserver(observer);
+
+ // Wait for the observer to be unregistered on the background thread.
+ latch2.await(5, TimeUnit.SECONDS);
+
+ assertThat(mTestContentProvider.mTestEventCursor.mLastContentObserver).isNull();
+ }
+
+ @Test
+ public void addObserver_oneEventResult() throws InterruptedException {
+
+ mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, 1));
+
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+
+ // Must add observer on main thread.
+ runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ ImmutableList<Event> events = mEventsLiveData.getValue();
+ assertThat(events).isNotNull();
+ assertThat(events).hasSize(1);
+ Event event = events.get(0);
+
+ long eventStartMillis = addHoursAndTruncate(CURRENT_DATE_TIME, 0);
+ long eventEndMillis = addHoursAndTruncate(CURRENT_DATE_TIME, 1);
+
+ assertThat(event.getTitle()).isEqualTo(EVENT_TITLE);
+ assertThat(event.getCalendarDetails().getColor()).isEqualTo(CALENDAR_COLOR);
+ assertThat(event.getLocation()).isEqualTo(EVENT_LOCATION);
+ assertThat(event.getStartInstant().toEpochMilli()).isEqualTo(eventStartMillis);
+ assertThat(event.getEndInstant().toEpochMilli()).isEqualTo(eventEndMillis);
+ assertThat(event.getStatus()).isEqualTo(Event.Status.ACCEPTED);
+ assertThat(event.getNumberAndAccess()).isEqualTo(EVENT_NUMBER_PIN);
+ }
+
+ @Test
+ public void changeCursorData_onChangedCalled() throws InterruptedException {
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch initializeCountdownLatch = new CountDownLatch(2);
+
+ // Expect the same init callbacks as above but with an extra when the data is updated.
+ CountDownLatch changeCountdownLatch = new CountDownLatch(3);
+
+ // Must add observer on main thread.
+ runOnMain(
+ () ->
+ mEventsLiveData.observeForever(
+ // Count down both latches when data is changed.
+ (value) -> {
+ initializeCountdownLatch.countDown();
+ changeCountdownLatch.countDown();
+ }));
+
+ // Wait for the data to be read on the background thread.
+ initializeCountdownLatch.await(5, TimeUnit.SECONDS);
+
+ // Signal that the content has changed.
+ mTestContentProvider.mTestEventCursor.signalDataChanged();
+
+ // Wait for the changed data to be read on the background thread.
+ changeCountdownLatch.await(5, TimeUnit.SECONDS);
+ }
+
+ private void runOnMain(Runnable runnable) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+ }
+
+ @Test
+ public void addObserver_updateScheduled() throws InterruptedException {
+ mTestHandler.setExpectedMessageCount(2);
+
+ // Must add observer on main thread.
+ runOnMain(
+ () ->
+ mEventsLiveData.observeForever(
+ (value) -> {
+ /* Do nothing */
+ }));
+
+ mTestHandler.awaitExpectedMessages(5);
+
+ // Show that a message was scheduled for the future.
+ assertThat(mTestHandler.mLastUptimeMillis).isAtLeast(SystemClock.uptimeMillis());
+ }
+
+ @Test
+ public void noCalendars_valueNull() throws InterruptedException {
+ mTestContentProvider.mAddFakeCalendar = false;
+
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+ runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ assertThat(mEventsLiveData.getValue()).isNull();
+ }
+
+ @Test
+ @UiThreadTest
+ public void noCalendars_contentObserved() throws InterruptedException {
+ mTestContentProvider.mAddFakeCalendar = false;
+
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+ mEventsLiveData.observeForever((value) -> latch.countDown());
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ assertThat(mTestContentProvider.mTestEventCursor.mLastContentObserver).isNotNull();
+ }
+
+ @Test
+ public void multiDayEvent_createsMultipleEvents() throws InterruptedException {
+ // Replace the default event with one that lasts 24 hours.
+ mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, 24));
+
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+
+ runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ // Expect an event for the 2 parts of the split event instance.
+ assertThat(mEventsLiveData.getValue()).hasSize(2);
+ }
+
+ @Test
+ public void multiDayEvent_keepsOriginalTimes() throws InterruptedException {
+ // Replace the default event with one that lasts 24 hours.
+ int hours = 48;
+ mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, hours));
+
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+
+ runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ Event middlePartEvent = mEventsLiveData.getValue().get(1);
+
+ // The start and end times should remain the original times.
+ ZonedDateTime expectedStartTime = CURRENT_DATE_TIME.truncatedTo(HOURS);
+ assertThat(middlePartEvent.getStartInstant()).isEqualTo(expectedStartTime.toInstant());
+ ZonedDateTime expectedEndTime = expectedStartTime.plus(hours, HOURS);
+ assertThat(middlePartEvent.getEndInstant()).isEqualTo(expectedEndTime.toInstant());
+ }
+
+ @Test
+ public void multipleEvents_resultsSortedStart() throws InterruptedException {
+ // Replace the default event with two that are out of time order.
+ ZonedDateTime twoHoursAfterCurrentTime = CURRENT_DATE_TIME.plus(Duration.ofHours(2));
+ mTestContentProvider.addRow(buildTestRowWithDuration(twoHoursAfterCurrentTime, 1));
+ mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, 1));
+
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+
+ runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ ImmutableList<Event> events = mEventsLiveData.getValue();
+
+ assertThat(events.get(0).getStartInstant().toEpochMilli())
+ .isEqualTo(addHoursAndTruncate(CURRENT_DATE_TIME, 0));
+ assertThat(events.get(1).getStartInstant().toEpochMilli())
+ .isEqualTo(addHoursAndTruncate(CURRENT_DATE_TIME, 2));
+ }
+
+ @Test
+ public void multipleEvents_resultsSortedTitle() throws InterruptedException {
+ // Replace the default event with two that are out of time order.
+ mTestContentProvider.addRow(buildTestRowWithTitle(CURRENT_DATE_TIME, "Title B"));
+ mTestContentProvider.addRow(buildTestRowWithTitle(CURRENT_DATE_TIME, "Title A"));
+ mTestContentProvider.addRow(buildTestRowWithTitle(CURRENT_DATE_TIME, "Title C"));
+
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+
+ runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ ImmutableList<Event> events = mEventsLiveData.getValue();
+
+ assertThat(events.get(0).getTitle()).isEqualTo("Title A");
+ assertThat(events.get(1).getTitle()).isEqualTo("Title B");
+ assertThat(events.get(2).getTitle()).isEqualTo("Title C");
+ }
+
+ @Test
+ public void allDayEvent_timesSetToLocal() throws InterruptedException {
+ // All-day events always start at UTC midnight.
+ ZonedDateTime utcMidnightStart =
+ CURRENT_DATE_TIME.withZoneSameLocal(ZoneId.of("UTC")).truncatedTo(ChronoUnit.DAYS);
+ mTestContentProvider.addRow(buildTestRowAllDay(utcMidnightStart));
+
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+
+ runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ ImmutableList<Event> events = mEventsLiveData.getValue();
+
+ Instant localMidnightStart = CURRENT_DATE_TIME.truncatedTo(ChronoUnit.DAYS).toInstant();
+ assertThat(events.get(0).getStartInstant()).isEqualTo(localMidnightStart);
+ }
+
+ @Test
+ public void allDayEvent_queryCoversLocalDayStart() throws InterruptedException {
+ // All-day events always start at UTC midnight.
+ ZonedDateTime utcMidnightStart =
+ CURRENT_DATE_TIME.withZoneSameLocal(ZoneId.of("UTC")).truncatedTo(ChronoUnit.DAYS);
+ mTestContentProvider.addRow(buildTestRowAllDay(utcMidnightStart));
+
+ // Set the time to 23:XX in the BERLIN_ZONE_ID which will be after the event end time.
+ mTestClock.setTime(CURRENT_DATE_TIME.with(ChronoField.HOUR_OF_DAY, 23));
+
+ // Expect onChanged to be called for when we start to observe and when the data is read.
+ CountDownLatch latch = new CountDownLatch(2);
+
+ runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
+
+ // Wait for the data to be read on the background thread.
+ latch.await(5, TimeUnit.SECONDS);
+
+ // Show that the event is included even though its end time is before the current time.
+ assertThat(mEventsLiveData.getValue()).isNotEmpty();
+ }
+
+ private static class TestContentProvider extends MockContentProvider {
+ TestEventCursor mTestEventCursor;
+ boolean mAddFakeCalendar = true;
+ List<Object[]> mEventRows = new ArrayList<>();
+
+ TestContentProvider(Context context) {
+ super(context);
+ }
+
+ private void addRow(Object[] row) {
+ mEventRows.add(row);
+ }
+
+ @Override
+ public Cursor query(
+ Uri uri,
+ String[] projection,
+ Bundle queryArgs,
+ CancellationSignal cancellationSignal) {
+ if (uri.toString().startsWith(CalendarContract.Instances.CONTENT_URI.toString())) {
+ mTestEventCursor = new TestEventCursor(uri);
+ for (Object[] row : mEventRows) {
+ mTestEventCursor.addRow(row);
+ }
+ return mTestEventCursor;
+ } else if (uri.equals(CalendarContract.Calendars.CONTENT_URI)) {
+ MatrixCursor calendarsCursor = new MatrixCursor(new String[] {" Test name"});
+ if (mAddFakeCalendar) {
+ calendarsCursor.addRow(new String[] {"Test value"});
+ }
+ return calendarsCursor;
+ }
+ throw new IllegalStateException("Unexpected query uri " + uri);
+ }
+
+ static class TestEventCursor extends MatrixCursor {
+ final Uri mUri;
+ ContentObserver mLastContentObserver;
+
+ TestEventCursor(Uri uri) {
+ super(
+ new String[] {
+ CalendarContract.Instances.TITLE,
+ CalendarContract.Instances.ALL_DAY,
+ CalendarContract.Instances.BEGIN,
+ CalendarContract.Instances.END,
+ CalendarContract.Instances.DESCRIPTION,
+ CalendarContract.Instances.EVENT_LOCATION,
+ CalendarContract.Instances.SELF_ATTENDEE_STATUS,
+ CalendarContract.Instances.CALENDAR_COLOR,
+ CalendarContract.Instances.CALENDAR_DISPLAY_NAME,
+ });
+ mUri = uri;
+ }
+
+ @Override
+ public void registerContentObserver(ContentObserver observer) {
+ super.registerContentObserver(observer);
+ mLastContentObserver = observer;
+ }
+
+ @Override
+ public void unregisterContentObserver(ContentObserver observer) {
+ super.unregisterContentObserver(observer);
+ mLastContentObserver = null;
+ }
+
+ void signalDataChanged() {
+ super.onChange(true);
+ }
+ }
+ }
+
+ private static class TestHandler extends Handler {
+ final HandlerThread mThread;
+ long mLastUptimeMillis;
+ CountDownLatch mCountDownLatch;
+
+ static TestHandler create() {
+ HandlerThread thread =
+ new HandlerThread(
+ EventsLiveDataTest.class.getSimpleName(),
+ Process.THREAD_PRIORITY_FOREGROUND);
+ thread.start();
+ return new TestHandler(thread);
+ }
+
+ TestHandler(HandlerThread thread) {
+ super(thread.getLooper());
+ mThread = thread;
+ }
+
+ void stop() {
+ mThread.quit();
+ }
+
+ void setExpectedMessageCount(int expectedMessageCount) {
+ mCountDownLatch = new CountDownLatch(expectedMessageCount);
+ }
+
+ void awaitExpectedMessages(int seconds) throws InterruptedException {
+ mCountDownLatch.await(seconds, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
+ mLastUptimeMillis = uptimeMillis;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ return super.sendMessageAtTime(msg, uptimeMillis);
+ }
+ }
+
+ // Similar to {@link android.os.SimpleClock} but without @hide and with mutable millis.
+ static class TestClock extends Clock {
+ private final ZoneId mZone;
+ private long mTimeMs;
+
+ TestClock(ZoneId zone) {
+ mZone = zone;
+ }
+
+ void setTime(ZonedDateTime time) {
+ mTimeMs = time.toInstant().toEpochMilli();
+ }
+
+ @Override
+ public ZoneId getZone() {
+ return mZone;
+ }
+
+ @Override
+ public Clock withZone(ZoneId zone) {
+ return new TestClock(zone) {
+ @Override
+ public long millis() {
+ return TestClock.this.millis();
+ }
+ };
+ }
+
+ @Override
+ public long millis() {
+ return mTimeMs;
+ }
+
+ @Override
+ public Instant instant() {
+ return Instant.ofEpochMilli(millis());
+ }
+ }
+
+ static long addHoursAndTruncate(ZonedDateTime dateTime, int hours) {
+ return dateTime.truncatedTo(HOURS)
+ .plus(Duration.ofHours(hours))
+ .toInstant()
+ .toEpochMilli();
+ }
+
+ static Object[] buildTestRowWithDuration(ZonedDateTime startDateTime, int eventDurationHours) {
+ return buildTestRowWithDuration(
+ startDateTime, eventDurationHours, EVENT_TITLE, EVENT_ALL_DAY);
+ }
+
+ static Object[] buildTestRowAllDay(ZonedDateTime startDateTime) {
+ return buildTestRowWithDuration(startDateTime, 24, EVENT_TITLE, true);
+ }
+
+ static Object[] buildTestRowWithTitle(ZonedDateTime startDateTime, String title) {
+ return buildTestRowWithDuration(startDateTime, 1, title, EVENT_ALL_DAY);
+ }
+
+ static Object[] buildTestRowWithDuration(
+ ZonedDateTime currentDateTime, int eventDurationHours, String title, boolean allDay) {
+ return new Object[] {
+ title,
+ allDay ? 1 : 0,
+ addHoursAndTruncate(currentDateTime, 0),
+ addHoursAndTruncate(currentDateTime, eventDurationHours),
+ EVENT_DESCRIPTION,
+ EVENT_LOCATION,
+ EVENT_ATTENDEE_STATUS,
+ CALENDAR_COLOR,
+ CALENDAR_NAME
+ };
+ }
+}