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; 18 19 import static androidx.test.espresso.Espresso.onView; 20 import static androidx.test.espresso.action.ViewActions.click; 21 import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; 22 import static androidx.test.espresso.assertion.ViewAssertions.matches; 23 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; 24 import static androidx.test.espresso.matcher.ViewMatchers.withId; 25 import static androidx.test.espresso.matcher.ViewMatchers.withText; 26 27 import static org.hamcrest.CoreMatchers.not; 28 29 import android.Manifest; 30 import android.app.Activity; 31 import android.content.Context; 32 import android.database.Cursor; 33 import android.database.MatrixCursor; 34 import android.net.Uri; 35 import android.os.Bundle; 36 import android.os.CancellationSignal; 37 import android.provider.CalendarContract; 38 import android.telephony.TelephonyManager; 39 import android.test.mock.MockContentProvider; 40 import android.test.mock.MockContentResolver; 41 42 import androidx.lifecycle.Observer; 43 import androidx.lifecycle.ViewModelProvider; 44 import androidx.test.core.app.ActivityScenario; 45 import androidx.test.ext.junit.runners.AndroidJUnit4; 46 import androidx.test.filters.LargeTest; 47 import androidx.test.platform.app.InstrumentationRegistry; 48 import androidx.test.rule.GrantPermissionRule; 49 import androidx.test.runner.lifecycle.ActivityLifecycleCallback; 50 import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; 51 import androidx.test.runner.lifecycle.Stage; 52 53 import com.android.car.calendar.common.Event; 54 import com.android.car.calendar.common.EventsLiveData; 55 56 import com.google.common.collect.ImmutableList; 57 58 import org.junit.After; 59 import org.junit.Before; 60 import org.junit.Rule; 61 import org.junit.Test; 62 import org.junit.runner.RunWith; 63 64 import java.time.Clock; 65 import java.time.LocalDateTime; 66 import java.time.ZoneId; 67 import java.time.ZonedDateTime; 68 import java.time.temporal.ChronoUnit; 69 import java.util.ArrayList; 70 import java.util.List; 71 import java.util.Locale; 72 import java.util.concurrent.CountDownLatch; 73 import java.util.concurrent.TimeUnit; 74 75 @LargeTest 76 @RunWith(AndroidJUnit4.class) 77 public class CarCalendarUiTest { 78 private static final ZoneId BERLIN_ZONE_ID = ZoneId.of("Europe/Berlin"); 79 private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC"); 80 private static final Locale LOCALE = Locale.ENGLISH; 81 private static final ZonedDateTime CURRENT_DATE_TIME = 82 LocalDateTime.of(2019, 12, 10, 10, 10, 10, 500500).atZone(BERLIN_ZONE_ID); 83 private static final ZonedDateTime START_DATE_TIME = 84 CURRENT_DATE_TIME.truncatedTo(ChronoUnit.HOURS); 85 private static final String EVENT_TITLE = "the title"; 86 private static final String EVENT_LOCATION = "the location"; 87 private static final String EVENT_DESCRIPTION = "the description"; 88 private static final String CALENDAR_NAME = "the calendar name"; 89 private static final int CALENDAR_COLOR = 0xCAFEBABE; 90 private static final int EVENT_ATTENDEE_STATUS = 91 CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED; 92 93 private final ActivityLifecycleCallback mLifecycleCallback = this::onActivityLifecycleChanged; 94 95 @Rule 96 public final GrantPermissionRule permissionRule = 97 GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR); 98 99 private List<Object[]> mTestEventRows; 100 101 // If set to true fake dependencies will not be set and the real provider will be used. 102 private boolean mDoNotSetFakeDependencies; 103 104 // These can be set in the test thread and read on the main thread. 105 private volatile CountDownLatch mEventChangesLatch; 106 107 @Before setUp()108 public void setUp() { 109 ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(mLifecycleCallback); 110 mTestEventRows = new ArrayList<>(); 111 mDoNotSetFakeDependencies = false; 112 } 113 onActivityLifecycleChanged(Activity activity, Stage stage)114 private void onActivityLifecycleChanged(Activity activity, Stage stage) { 115 if (mDoNotSetFakeDependencies) return; 116 117 if (stage.equals(Stage.PRE_ON_CREATE)) { 118 setActivityDependencies((CarCalendarActivity) activity); 119 } else if (stage.equals(Stage.CREATED)) { 120 observeEventsLiveData((CarCalendarActivity) activity); 121 } 122 } 123 setActivityDependencies(CarCalendarActivity activity)124 private void setActivityDependencies(CarCalendarActivity activity) { 125 Clock fixedTimeClock = Clock.fixed(CURRENT_DATE_TIME.toInstant(), BERLIN_ZONE_ID); 126 Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 127 MockContentResolver mockContentResolver = new MockContentResolver(context); 128 TestCalendarContentProvider testCalendarContentProvider = 129 new TestCalendarContentProvider(context); 130 mockContentResolver.addProvider(CalendarContract.AUTHORITY, testCalendarContentProvider); 131 activity.mDependencies = 132 new CarCalendarActivity.Dependencies(LOCALE, fixedTimeClock, mockContentResolver, 133 activity.getSystemService(TelephonyManager.class)); 134 } 135 observeEventsLiveData(CarCalendarActivity activity)136 private void observeEventsLiveData(CarCalendarActivity activity) { 137 CarCalendarViewModel carCalendarViewModel = 138 new ViewModelProvider(activity).get(CarCalendarViewModel.class); 139 EventsLiveData eventsLiveData = carCalendarViewModel.getEventsLiveData(); 140 mEventChangesLatch = new CountDownLatch(1); 141 142 // Notifications occur on the main thread. 143 eventsLiveData.observeForever( 144 new Observer<ImmutableList<Event>>() { 145 // Ignore the first change event triggered on registration with default value. 146 boolean mIgnoredFirstChange; 147 148 @Override 149 public void onChanged(ImmutableList<Event> events) { 150 if (mIgnoredFirstChange) { 151 // Signal that the events were changed and notified on main thread. 152 mEventChangesLatch.countDown(); 153 } 154 mIgnoredFirstChange = true; 155 } 156 }); 157 } 158 159 @After tearDown()160 public void tearDown() { 161 ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(mLifecycleCallback); 162 } 163 164 @Test withFakeDependencies_titleShows()165 public void withFakeDependencies_titleShows() { 166 try (ActivityScenario<CarCalendarActivity> ignored = 167 ActivityScenario.launch(CarCalendarActivity.class)) { 168 onView(withText(R.string.app_name)).check(matches(isDisplayed())); 169 } 170 } 171 172 @Test withoutFakeDependencies_titleShows()173 public void withoutFakeDependencies_titleShows() { 174 mDoNotSetFakeDependencies = true; 175 try (ActivityScenario<CarCalendarActivity> ignored = 176 ActivityScenario.launch(CarCalendarActivity.class)) { 177 onView(withText(R.string.app_name)).check(matches(isDisplayed())); 178 } 179 } 180 181 @Test event_displayed()182 public void event_displayed() { 183 mTestEventRows.add(buildTestRow(START_DATE_TIME, 1, EVENT_TITLE, false)); 184 try (ActivityScenario<CarCalendarActivity> ignored = 185 ActivityScenario.launch(CarCalendarActivity.class)) { 186 waitForEventsChange(); 187 188 // Wait for the UI to be updated with changed events. 189 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 190 191 onView(withText(EVENT_TITLE)).check(matches(isDisplayed())); 192 } 193 } 194 195 @Test singleAllDayEvent_notCollapsed()196 public void singleAllDayEvent_notCollapsed() { 197 // All day events are stored in UTC time. 198 ZonedDateTime utcDayStartTime = 199 START_DATE_TIME.withZoneSameInstant(UTC_ZONE_ID).truncatedTo(ChronoUnit.DAYS); 200 201 mTestEventRows.add(buildTestRow(utcDayStartTime, 24, EVENT_TITLE, true)); 202 203 try (ActivityScenario<CarCalendarActivity> ignored = 204 ActivityScenario.launch(CarCalendarActivity.class)) { 205 waitForEventsChange(); 206 207 // Wait for the UI to be updated with changed events. 208 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 209 210 // A single all-day event should not be collapsible. 211 onView(withId(R.id.expand_collapse_icon)).check(doesNotExist()); 212 onView(withText(EVENT_TITLE)).check(matches(isDisplayed())); 213 } 214 } 215 216 @Test multipleAllDayEvents_collapsed()217 public void multipleAllDayEvents_collapsed() { 218 mTestEventRows.add(buildTestRowAllDay(EVENT_TITLE)); 219 mTestEventRows.add(buildTestRowAllDay("Another all day event")); 220 221 try (ActivityScenario<CarCalendarActivity> ignored = 222 ActivityScenario.launch(CarCalendarActivity.class)) { 223 waitForEventsChange(); 224 225 // Wait for the UI to be updated with changed events. 226 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 227 228 // Multiple all-day events should be collapsed. 229 onView(withId(R.id.expand_collapse_icon)).check(matches(isDisplayed())); 230 onView(withText(EVENT_TITLE)).check(matches(not(isDisplayed()))); 231 } 232 } 233 234 @Test multipleAllDayEvents_expands()235 public void multipleAllDayEvents_expands() { 236 mTestEventRows.add(buildTestRowAllDay(EVENT_TITLE)); 237 mTestEventRows.add(buildTestRowAllDay("Another all day event")); 238 239 try (ActivityScenario<CarCalendarActivity> ignored = 240 ActivityScenario.launch(CarCalendarActivity.class)) { 241 waitForEventsChange(); 242 243 // Wait for the UI to be updated with changed events. 244 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 245 246 // Multiple all-day events should be collapsed. 247 onView(withId(R.id.expand_collapse_icon)).perform(click()); 248 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 249 onView(withText(EVENT_TITLE)).check(matches(isDisplayed())); 250 } 251 } 252 waitForEventsChange()253 private void waitForEventsChange() { 254 try { 255 mEventChangesLatch.await(10, TimeUnit.SECONDS); 256 } catch (InterruptedException e) { 257 throw new RuntimeException(e); 258 } 259 } 260 261 private class TestCalendarContentProvider extends MockContentProvider { TestCalendarContentProvider(Context context)262 TestCalendarContentProvider(Context context) { 263 super(context); 264 } 265 266 @Override query( Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal)267 public Cursor query( 268 Uri uri, 269 String[] projection, 270 Bundle queryArgs, 271 CancellationSignal cancellationSignal) { 272 if (uri.toString().startsWith(CalendarContract.Instances.CONTENT_URI.toString())) { 273 MatrixCursor cursor = 274 new MatrixCursor( 275 new String[] { 276 CalendarContract.Instances.TITLE, 277 CalendarContract.Instances.ALL_DAY, 278 CalendarContract.Instances.BEGIN, 279 CalendarContract.Instances.END, 280 CalendarContract.Instances.DESCRIPTION, 281 CalendarContract.Instances.EVENT_LOCATION, 282 CalendarContract.Instances.SELF_ATTENDEE_STATUS, 283 CalendarContract.Instances.CALENDAR_COLOR, 284 CalendarContract.Instances.CALENDAR_DISPLAY_NAME, 285 }); 286 for (Object[] row : mTestEventRows) { 287 cursor.addRow(row); 288 } 289 return cursor; 290 } else if (uri.equals(CalendarContract.Calendars.CONTENT_URI)) { 291 MatrixCursor cursor = new MatrixCursor(new String[] {" Test name"}); 292 cursor.addRow(new String[] {"Test value"}); 293 return cursor; 294 } 295 throw new IllegalStateException("Unexpected query uri " + uri); 296 } 297 } 298 buildTestRowAllDay(String title)299 private Object[] buildTestRowAllDay(String title) { 300 // All day events are stored in UTC time. 301 ZonedDateTime utcDayStartTime = 302 START_DATE_TIME.withZoneSameInstant(UTC_ZONE_ID).truncatedTo(ChronoUnit.DAYS); 303 return buildTestRow(utcDayStartTime, 24, title, true); 304 } 305 buildTestRow( ZonedDateTime startDateTime, int eventDurationHours, String title, boolean allDay)306 private static Object[] buildTestRow( 307 ZonedDateTime startDateTime, int eventDurationHours, String title, boolean allDay) { 308 return new Object[] { 309 title, 310 allDay ? 1 : 0, 311 startDateTime.toInstant().toEpochMilli(), 312 startDateTime.plusHours(eventDurationHours).toInstant().toEpochMilli(), 313 EVENT_DESCRIPTION, 314 EVENT_LOCATION, 315 EVENT_ATTENDEE_STATUS, 316 CALENDAR_COLOR, 317 CALENDAR_NAME 318 }; 319 } 320 } 321