aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorJohn Patterson <jdp@google.com>2020-07-01 12:45:04 +0200
committerJohn Patterson <jdp@google.com>2020-07-08 13:13:57 +0000
commita66655fd373cc9ef9ff06516a19141c60d2c2422 (patch)
tree1be2e60d2bcb1b3b37b3c24655a5ab8c0433f759 /tests
parent41f8651ee86669c7607622b1207acb8a07bd66db (diff)
downloadCalendar-a66655fd373cc9ef9ff06516a19141c60d2c2422.tar.gz
Add CarCalendarAppandroid-vts-11.0_r9android-vts-11.0_r8android-vts-11.0_r7android-vts-11.0_r6android-vts-11.0_r5android-vts-11.0_r4android-vts-11.0_r3android-vts-11.0_r2android-vts-11.0_r16android-vts-11.0_r15android-vts-11.0_r14android-vts-11.0_r13android-vts-11.0_r12android-vts-11.0_r11android-vts-11.0_r10android-vts-11.0_r1android-security-11.0.0_r76android-security-11.0.0_r75android-security-11.0.0_r74android-security-11.0.0_r73android-security-11.0.0_r72android-security-11.0.0_r71android-security-11.0.0_r70android-security-11.0.0_r69android-security-11.0.0_r68android-security-11.0.0_r67android-security-11.0.0_r66android-security-11.0.0_r65android-security-11.0.0_r64android-security-11.0.0_r63android-security-11.0.0_r62android-security-11.0.0_r61android-security-11.0.0_r60android-security-11.0.0_r59android-security-11.0.0_r58android-security-11.0.0_r57android-security-11.0.0_r56android-security-11.0.0_r55android-security-11.0.0_r54android-security-11.0.0_r53android-security-11.0.0_r52android-security-11.0.0_r51android-security-11.0.0_r50android-security-11.0.0_r49android-security-11.0.0_r1android-cts-11.0_r9android-cts-11.0_r8android-cts-11.0_r7android-cts-11.0_r6android-cts-11.0_r5android-cts-11.0_r4android-cts-11.0_r3android-cts-11.0_r2android-cts-11.0_r16android-cts-11.0_r15android-cts-11.0_r14android-cts-11.0_r13android-cts-11.0_r12android-cts-11.0_r11android-cts-11.0_r10android-cts-11.0_r1android-11.0.0_r5android-11.0.0_r4android-11.0.0_r3android-11.0.0_r25android-11.0.0_r2android-11.0.0_r17android-11.0.0_r1android11-tests-releaseandroid11-security-releaseandroid11-s1-releaseandroid11-releaseandroid11-dev
This was moved from vendor/auto/embedded/apps/Calendar Header licence was updated in all files Added OWNERS file Bug: 140862425 Bug: 160135021 Merged-In: I64cca6684ba132f47308911bdc73f35fe2176502 Test: clean build from root still works Change-Id: I64cca6684ba132f47308911bdc73f35fe2176502
Diffstat (limited to 'tests')
-rw-r--r--tests/ui/Android.bp38
-rw-r--r--tests/ui/AndroidManifest.xml34
-rw-r--r--tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java303
-rw-r--r--tests/unit/Android.bp37
-rw-r--r--tests/unit/AndroidManifest.xml34
-rw-r--r--tests/unit/src/com/android/car/calendar/common/CalendarFormatterTest.java98
-rw-r--r--tests/unit/src/com/android/car/calendar/common/EventDescriptionsTest.java131
-rw-r--r--tests/unit/src/com/android/car/calendar/common/EventLocationsTest.java60
-rw-r--r--tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java609
9 files changed, 1344 insertions, 0 deletions
diff --git a/tests/ui/Android.bp b/tests/ui/Android.bp
new file mode 100644
index 0000000..829e312
--- /dev/null
+++ b/tests/ui/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 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.
+//
+
+android_test {
+ name: "CarCalendarUiTests",
+ srcs: ["src/**/*.java"],
+ instrumentation_for: "CarCalendarApp",
+ optimize: {
+ enabled: false,
+ },
+ sdk_version: "system_current",
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.espresso.core",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "androidx.test.runner",
+ "mockito-target",
+ "truth-prebuilt",
+ ],
+ libs: [
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
+ "android.test.runner.stubs",
+ ],
+}
diff --git a/tests/ui/AndroidManifest.xml b/tests/ui/AndroidManifest.xml
new file mode 100644
index 0000000..21b126e
--- /dev/null
+++ b/tests/ui/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.calendar.tests.ui" >
+
+ <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23" />
+
+ <uses-permission android:name="android.permission.READ_CALENDAR" />
+ <uses-permission android:name="android.permission.CALL_PHONE" />
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:label="Car Calendar UI Tests"
+ android:targetPackage="com.android.car.calendar" />
+
+ <application android:label="CarCalendarUiTests" >
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+</manifest>
diff --git a/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java b/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java
new file mode 100644
index 0000000..5c7883c
--- /dev/null
+++ b/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.CoreMatchers.not;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.provider.CalendarContract;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.GrantPermissionRule;
+import androidx.test.runner.lifecycle.ActivityLifecycleCallback;
+import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
+import androidx.test.runner.lifecycle.Stage;
+
+import com.android.car.calendar.common.Event;
+import com.android.car.calendar.common.EventsLiveData;
+
+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.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class CarCalendarUiTest {
+ private static final ZoneId BERLIN_ZONE_ID = ZoneId.of("Europe/Berlin");
+ private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC");
+ private static final Locale LOCALE = Locale.ENGLISH;
+ private static final ZonedDateTime CURRENT_DATE_TIME =
+ LocalDateTime.of(2019, 12, 10, 10, 10, 10, 500500).atZone(BERLIN_ZONE_ID);
+ private static final ZonedDateTime START_DATE_TIME =
+ CURRENT_DATE_TIME.truncatedTo(ChronoUnit.HOURS);
+ private static final String EVENT_TITLE = "the title";
+ 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;
+
+ private final ActivityLifecycleCallback mLifecycleCallback = this::onActivityLifecycleChanged;
+
+ @Rule
+ public final GrantPermissionRule permissionRule =
+ GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR);
+
+ private List<Object[]> mTestEventRows;
+
+ // These can be set in the test thread and read on the main thread.
+ private volatile CountDownLatch mEventChangesLatch;
+
+ @Before
+ public void setUp() {
+ ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(mLifecycleCallback);
+ mTestEventRows = new ArrayList<>();
+ }
+
+ private void onActivityLifecycleChanged(Activity activity, Stage stage) {
+ if (stage.equals(Stage.PRE_ON_CREATE)) {
+ setActivityDependencies((CarCalendarActivity) activity);
+ } else if (stage.equals(Stage.CREATED)) {
+ observeEventsLiveData((CarCalendarActivity) activity);
+ }
+ }
+
+ private void setActivityDependencies(CarCalendarActivity activity) {
+ Clock fixedTimeClock = Clock.fixed(CURRENT_DATE_TIME.toInstant(), BERLIN_ZONE_ID);
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ MockContentResolver mockContentResolver = new MockContentResolver(context);
+ TestCalendarContentProvider testCalendarContentProvider =
+ new TestCalendarContentProvider(context);
+ mockContentResolver.addProvider(CalendarContract.AUTHORITY, testCalendarContentProvider);
+ activity.mDependencies =
+ new CarCalendarActivity.Dependencies(LOCALE, fixedTimeClock, mockContentResolver);
+ }
+
+ private void observeEventsLiveData(CarCalendarActivity activity) {
+ CarCalendarViewModel carCalendarViewModel =
+ new ViewModelProvider(activity).get(CarCalendarViewModel.class);
+ EventsLiveData eventsLiveData = carCalendarViewModel.getEventsLiveData();
+ mEventChangesLatch = new CountDownLatch(1);
+
+ // Notifications occur on the main thread.
+ eventsLiveData.observeForever(
+ new Observer<ImmutableList<Event>>() {
+ // Ignore the first change event triggered on registration with default value.
+ boolean mIgnoredFirstChange;
+
+ @Override
+ public void onChanged(ImmutableList<Event> events) {
+ if (mIgnoredFirstChange) {
+ // Signal that the events were changed and notified on main thread.
+ mEventChangesLatch.countDown();
+ }
+ mIgnoredFirstChange = true;
+ }
+ });
+ }
+
+ @After
+ public void tearDown() {
+ ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(mLifecycleCallback);
+ }
+
+ @Test
+ public void calendar_titleShows() {
+ try (ActivityScenario<CarCalendarActivity> ignored =
+ ActivityScenario.launch(CarCalendarActivity.class)) {
+ onView(withText(R.string.app_name)).check(matches(isDisplayed()));
+ }
+ }
+
+ @Test
+ public void event_displayed() {
+ mTestEventRows.add(buildTestRow(START_DATE_TIME, 1, EVENT_TITLE, false));
+ try (ActivityScenario<CarCalendarActivity> ignored =
+ ActivityScenario.launch(CarCalendarActivity.class)) {
+ waitForEventsChange();
+
+ // Wait for the UI to be updated with changed events.
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ onView(withText(EVENT_TITLE)).check(matches(isDisplayed()));
+ }
+ }
+
+ @Test
+ public void singleAllDayEvent_notCollapsed() {
+ // All day events are stored in UTC time.
+ ZonedDateTime utcDayStartTime =
+ START_DATE_TIME.withZoneSameInstant(UTC_ZONE_ID).truncatedTo(ChronoUnit.DAYS);
+
+ mTestEventRows.add(buildTestRow(utcDayStartTime, 24, EVENT_TITLE, true));
+
+ try (ActivityScenario<CarCalendarActivity> ignored =
+ ActivityScenario.launch(CarCalendarActivity.class)) {
+ waitForEventsChange();
+
+ // Wait for the UI to be updated with changed events.
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // A single all-day event should not be collapsible.
+ onView(withId(R.id.expand_collapse_icon)).check(doesNotExist());
+ onView(withText(EVENT_TITLE)).check(matches(isDisplayed()));
+ }
+ }
+
+ @Test
+ public void multipleAllDayEvents_collapsed() {
+ mTestEventRows.add(buildTestRowAllDay(EVENT_TITLE));
+ mTestEventRows.add(buildTestRowAllDay("Another all day event"));
+
+ try (ActivityScenario<CarCalendarActivity> ignored =
+ ActivityScenario.launch(CarCalendarActivity.class)) {
+ waitForEventsChange();
+
+ // Wait for the UI to be updated with changed events.
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // Multiple all-day events should be collapsed.
+ onView(withId(R.id.expand_collapse_icon)).check(matches(isDisplayed()));
+ onView(withText(EVENT_TITLE)).check(matches(not(isDisplayed())));
+ }
+ }
+
+ @Test
+ public void multipleAllDayEvents_expands() {
+ mTestEventRows.add(buildTestRowAllDay(EVENT_TITLE));
+ mTestEventRows.add(buildTestRowAllDay("Another all day event"));
+
+ try (ActivityScenario<CarCalendarActivity> ignored =
+ ActivityScenario.launch(CarCalendarActivity.class)) {
+ waitForEventsChange();
+
+ // Wait for the UI to be updated with changed events.
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // Multiple all-day events should be collapsed.
+ onView(withId(R.id.expand_collapse_icon)).perform(click());
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ onView(withText(EVENT_TITLE)).check(matches(isDisplayed()));
+ }
+ }
+
+ private void waitForEventsChange() {
+ try {
+ mEventChangesLatch.await(10, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private class TestCalendarContentProvider extends MockContentProvider {
+ TestCalendarContentProvider(Context context) {
+ super(context);
+ }
+
+ @Override
+ public Cursor query(
+ Uri uri,
+ String[] projection,
+ Bundle queryArgs,
+ CancellationSignal cancellationSignal) {
+ if (uri.toString().startsWith(CalendarContract.Instances.CONTENT_URI.toString())) {
+ MatrixCursor cursor =
+ new MatrixCursor(
+ 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,
+ });
+ for (Object[] row : mTestEventRows) {
+ cursor.addRow(row);
+ }
+ return cursor;
+ } else if (uri.equals(CalendarContract.Calendars.CONTENT_URI)) {
+ MatrixCursor cursor = new MatrixCursor(new String[] {" Test name"});
+ cursor.addRow(new String[] {"Test value"});
+ return cursor;
+ }
+ throw new IllegalStateException("Unexpected query uri " + uri);
+ }
+ }
+
+ private Object[] buildTestRowAllDay(String title) {
+ // All day events are stored in UTC time.
+ ZonedDateTime utcDayStartTime =
+ START_DATE_TIME.withZoneSameInstant(UTC_ZONE_ID).truncatedTo(ChronoUnit.DAYS);
+ return buildTestRow(utcDayStartTime, 24, title, true);
+ }
+
+ private static Object[] buildTestRow(
+ ZonedDateTime startDateTime, int eventDurationHours, String title, boolean allDay) {
+ return new Object[] {
+ title,
+ allDay ? 1 : 0,
+ startDateTime.toInstant().toEpochMilli(),
+ startDateTime.plusHours(eventDurationHours).toInstant().toEpochMilli(),
+ EVENT_DESCRIPTION,
+ EVENT_LOCATION,
+ EVENT_ATTENDEE_STATUS,
+ CALENDAR_COLOR,
+ CALENDAR_NAME
+ };
+ }
+}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
new file mode 100644
index 0000000..42c7ee0
--- /dev/null
+++ b/tests/unit/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 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.
+//
+
+android_test {
+ name: "CarCalendarUnitTests",
+ srcs: ["src/**/*.java"],
+ instrumentation_for: "CarCalendarApp",
+ optimize: {
+ enabled: false,
+ },
+ sdk_version: "system_current",
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "androidx.test.runner",
+ "mockito-target",
+ "truth-prebuilt",
+ ],
+ libs: [
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
+ "android.test.runner.stubs",
+ ],
+}
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..08e66c1
--- /dev/null
+++ b/tests/unit/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.calendar.tests.unit" >
+
+ <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23" />
+
+ <uses-permission android:name="android.permission.READ_CALENDAR" />
+ <uses-permission android:name="android.permission.CALL_PHONE" />
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:label="Car Calendar Unit Tests"
+ android:targetPackage="com.android.car.calendar" />
+
+ <application android:label="CarCalendarUnitTests" >
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+</manifest>
diff --git a/tests/unit/src/com/android/car/calendar/common/CalendarFormatterTest.java b/tests/unit/src/com/android/car/calendar/common/CalendarFormatterTest.java
new file mode 100644
index 0000000..132504e
--- /dev/null
+++ b/tests/unit/src/com/android/car/calendar/common/CalendarFormatterTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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 android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Locale;
+
+public class CalendarFormatterTest {
+
+ 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 Locale LOCALE = Locale.ENGLISH;
+ private CalendarFormatter mFormatter;
+
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ Clock clock = Clock.fixed(CURRENT_DATE_TIME.toInstant(), BERLIN_ZONE_ID);
+ mFormatter = new CalendarFormatter(context, LOCALE, clock);
+ }
+
+ @Test
+ public void getDateText_today() {
+ String dateText = mFormatter.getDateText(CURRENT_DATE_TIME.toLocalDate());
+
+ assertThat(dateText).startsWith("Today");
+ assertThat(dateText).endsWith("Tue, Dec 10");
+ }
+
+ @Test
+ public void getDateText_tomorrow() {
+ String dateText = mFormatter.getDateText(CURRENT_DATE_TIME.plusDays(1).toLocalDate());
+
+ assertThat(dateText).startsWith("Tomorrow");
+ assertThat(dateText).endsWith("Wed, Dec 11");
+ }
+
+ @Test
+ public void getDateText_nextWeek_onlyShowsDate() {
+ String dateText = mFormatter.getDateText(CURRENT_DATE_TIME.plusDays(7).toLocalDate());
+
+ assertThat(dateText).isEqualTo("Tue, Dec 17");
+ }
+
+ @Test
+ public void getTimeRangeText_sameAmPm() {
+ String dateText =
+ mFormatter.getTimeRangeText(
+ CURRENT_DATE_TIME.toInstant(), CURRENT_DATE_TIME.plusHours(1).toInstant());
+
+ assertThat(dateText).isEqualTo("10:10 – 11:10 AM");
+ }
+
+ @Test
+ public void getTimeRangeText_differentAmPm() {
+ String dateText =
+ mFormatter.getTimeRangeText(
+ CURRENT_DATE_TIME.toInstant(), CURRENT_DATE_TIME.plusHours(3).toInstant());
+
+ assertThat(dateText).isEqualTo("10:10 AM – 1:10 PM");
+ }
+
+ @Test
+ public void getTimeRangeText_differentDays() {
+ String dateText =
+ mFormatter.getTimeRangeText(
+ CURRENT_DATE_TIME.toInstant(), CURRENT_DATE_TIME.plusDays(1).toInstant());
+
+ assertThat(dateText).isEqualTo("Dec 10, 10:10 AM – Dec 11, 10:10 AM");
+ }
+}
diff --git a/tests/unit/src/com/android/car/calendar/common/EventDescriptionsTest.java b/tests/unit/src/com/android/car/calendar/common/EventDescriptionsTest.java
new file mode 100644
index 0000000..358e9cf
--- /dev/null
+++ b/tests/unit/src/com/android/car/calendar/common/EventDescriptionsTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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 android.net.Uri;
+
+import com.google.common.collect.Iterables;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Locale;
+
+public class EventDescriptionsTest {
+
+ private static final String BASE_NUMBER = "30 303986300";
+ private static final String LOCAL_NUMBER = "0" + BASE_NUMBER;
+ private static final String INTERNATIONAL_NUMBER = "+49 " + BASE_NUMBER;
+ private static final String ACCESS = ",,12;3*45#";
+ private EventDescriptions mEventDescriptions;
+
+ @Before
+ public void setUp() {
+ mEventDescriptions = new EventDescriptions(Locale.GERMANY);
+ }
+
+ @Test
+ public void extractNumberAndPin_localNumber_resultIsLocal() {
+ List<Dialer.NumberAndAccess> numberAndAccesses =
+ mEventDescriptions.extractNumberAndPins(LOCAL_NUMBER);
+ assertThat(numberAndAccesses).isNotEmpty();
+ Dialer.NumberAndAccess numberAndAccess = Iterables.getFirst(numberAndAccesses, null);
+ assertThat(numberAndAccess.getNumber()).isEqualTo(INTERNATIONAL_NUMBER);
+ }
+
+ @Test
+ public void extractNumberAndPin_internationalNumber_resultIsLocal() {
+ List<Dialer.NumberAndAccess> numberAndAccesses =
+ mEventDescriptions.extractNumberAndPins(INTERNATIONAL_NUMBER);
+ assertThat(numberAndAccesses).isNotEmpty();
+ Dialer.NumberAndAccess numberAndAccess = Iterables.getFirst(numberAndAccesses, null);
+ assertThat(numberAndAccess.getNumber()).isEqualTo(INTERNATIONAL_NUMBER);
+ }
+
+ @Test
+ public void extractNumberAndPin_internationalNumberWithDifferentLocale_resultIsInternational() {
+ mEventDescriptions = new EventDescriptions(Locale.FRANCE);
+ List<Dialer.NumberAndAccess> numberAndAccesses =
+ mEventDescriptions.extractNumberAndPins(INTERNATIONAL_NUMBER);
+ assertThat(numberAndAccesses).isNotEmpty();
+ Dialer.NumberAndAccess numberAndAccess = Iterables.getFirst(numberAndAccesses, null);
+ assertThat(numberAndAccess.getNumber()).isEqualTo(INTERNATIONAL_NUMBER);
+ }
+
+ @Test
+ public void extractNumberAndPin_internationalNumberAndPin() {
+ String input = INTERNATIONAL_NUMBER + " PIN: " + ACCESS;
+ List<Dialer.NumberAndAccess> numberAndAccesses =
+ mEventDescriptions.extractNumberAndPins(input);
+ assertThat(numberAndAccesses).isNotEmpty();
+ Dialer.NumberAndAccess numberAndAccess = Iterables.getFirst(numberAndAccesses, null);
+ assertThat(numberAndAccess.getNumber()).isEqualTo(INTERNATIONAL_NUMBER);
+ assertThat(numberAndAccess.getAccess()).isEqualTo(ACCESS);
+ }
+
+ @Test
+ public void extractNumberAndPin_internationalNumberAndCode() {
+ String input = INTERNATIONAL_NUMBER + " with access code " + ACCESS;
+ List<Dialer.NumberAndAccess> numberAndAccesses =
+ mEventDescriptions.extractNumberAndPins(input);
+ assertThat(numberAndAccesses).isNotEmpty();
+ Dialer.NumberAndAccess numberAndAccess = Iterables.getFirst(numberAndAccesses, null);
+ assertThat(numberAndAccess.getNumber()).isEqualTo(INTERNATIONAL_NUMBER);
+ assertThat(numberAndAccess.getAccess()).isEqualTo(ACCESS);
+ }
+
+ @Test
+ public void extractNumberAndPin_multipleNumbers() {
+ String input =
+ INTERNATIONAL_NUMBER
+ + " PIN: "
+ + ACCESS
+ + "\n an invalid one is "
+ + BASE_NUMBER
+ + " but a local one is "
+ + LOCAL_NUMBER;
+ List<Dialer.NumberAndAccess> numberAndAccesses =
+ mEventDescriptions.extractNumberAndPins(input);
+
+ // The local number is valid but repeated so only included once.
+ assertThat(numberAndAccesses).hasSize(1);
+ }
+
+ @Test
+ public void extractNumberAndPin_encodedTelFormat() throws UnsupportedEncodingException {
+ String encoded = Uri.encode(INTERNATIONAL_NUMBER + ACCESS);
+ String input = "blah blah <tel:" + encoded + "> blah blah";
+ List<Dialer.NumberAndAccess> numberAndAccesses =
+ mEventDescriptions.extractNumberAndPins(input);
+ assertThat(numberAndAccesses).hasSize(1);
+ Dialer.NumberAndAccess numberAndAccess = Iterables.getFirst(numberAndAccesses, null);
+ assertThat(numberAndAccess.getNumber()).isEqualTo(INTERNATIONAL_NUMBER);
+ assertThat(numberAndAccess.getAccess()).isEqualTo(ACCESS);
+ }
+
+ @Test
+ public void extractNumberAndPin_smallNumber_returnsNull() throws UnsupportedEncodingException {
+ String input = "blah blah 345 - blah blah";
+ List<Dialer.NumberAndAccess> numberAndAccesses =
+ mEventDescriptions.extractNumberAndPins(input);
+ assertThat(numberAndAccesses).isEmpty();
+ }
+}
diff --git a/tests/unit/src/com/android/car/calendar/common/EventLocationsTest.java b/tests/unit/src/com/android/car/calendar/common/EventLocationsTest.java
new file mode 100644
index 0000000..23f833a
--- /dev/null
+++ b/tests/unit/src/com/android/car/calendar/common/EventLocationsTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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 org.junit.Before;
+import org.junit.Test;
+
+public class EventLocationsTest {
+
+ private static final String BASE_NUMBER = "30 303986300";
+
+ private EventLocations mEventLocations;
+
+ @Before
+ public void setUp() {
+ mEventLocations = new EventLocations();
+ }
+
+ @Test
+ public void isValidLocation_meetingRooms_isFalse() {
+ assertThat(mEventLocations.isValidLocation("MUC-ARP-6Z3-Radln (5) [GVC]")).isFalse();
+ assertThat(mEventLocations.isValidLocation("SFO-SPE-3-Anchor Brewing Co. (1) [GVC]"))
+ .isFalse();
+ assertThat(mEventLocations.isValidLocation("SFO-SPE-3-Speakeasy Ales & Lagers (10) [GVC]"))
+ .isFalse();
+ assertThat(
+ mEventLocations.isValidLocation(
+ "MTV-900-1-Good Charlotte (13) [GVC, No External Guests]"))
+ .isFalse();
+ assertThat(
+ mEventLocations.isValidLocation(
+ "MTV-900-2-Panic! at the Disco (5) [GVC, Jamboard]"))
+ .isFalse();
+ assertThat(mEventLocations.isValidLocation("US-MTV-900-1-1F2 (collaboration area)"))
+ .isFalse();
+ }
+
+ @Test
+ public void isValidLocation_notMeetingRooms_isTrue() {
+ assertThat(mEventLocations.isValidLocation("My place")).isTrue();
+ assertThat(mEventLocations.isValidLocation("At JDP-1974-09")).isTrue();
+ assertThat(mEventLocations.isValidLocation("178.3454, 234.345")).isTrue();
+ }
+}
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
+ };
+ }
+}