• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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