| /* |
| * 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 |
| }; |
| } |
| } |