• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.providers.media.photopicker.espresso;
18 
19 import static androidx.test.InstrumentationRegistry.getTargetContext;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static org.mockito.Mockito.doAnswer;
24 import static org.mockito.Mockito.mock;
25 import static org.mockito.Mockito.when;
26 
27 import android.Manifest;
28 import android.content.Intent;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Environment;
32 import android.provider.MediaStore;
33 import android.system.ErrnoException;
34 import android.system.Os;
35 
36 import androidx.core.util.Supplier;
37 import androidx.test.InstrumentationRegistry;
38 
39 import com.android.providers.media.IsolatedContext;
40 import com.android.providers.media.R;
41 import com.android.providers.media.photopicker.data.UserIdManager;
42 import com.android.providers.media.photopicker.data.model.UserId;
43 
44 import org.junit.AfterClass;
45 import org.junit.BeforeClass;
46 
47 import java.io.File;
48 import java.io.FileOutputStream;
49 import java.io.IOException;
50 import java.nio.file.Files;
51 import java.nio.file.attribute.FileTime;
52 import java.util.concurrent.TimeUnit;
53 import java.util.concurrent.TimeoutException;
54 
55 public class PhotoPickerBaseTest {
56     protected static final int PICKER_TAB_RECYCLERVIEW_ID = R.id.picker_tab_recyclerview;
57     protected static final int TAB_LAYOUT_ID = R.id.tab_layout;
58     protected static final int PICKER_PHOTOS_STRING_ID = R.string.picker_photos;
59     protected static final int PICKER_ALBUMS_STRING_ID = R.string.picker_albums;
60     protected static final int PREVIEW_VIEW_PAGER_ID = R.id.preview_viewPager;
61     protected static final int ICON_CHECK_ID = R.id.icon_check;
62     protected static final int ICON_THUMBNAIL_ID = R.id.icon_thumbnail;
63     protected static final int VIEW_SELECTED_BUTTON_ID = R.id.button_view_selected;
64     protected static final int PREVIEW_IMAGE_VIEW_ID = R.id.preview_imageView;
65     protected static final int DRAG_BAR_ID = R.id.drag_bar;
66     protected static final int PREVIEW_GIF_ID = R.id.preview_gif;
67     protected static final int PREVIEW_MOTION_PHOTO_ID = R.id.preview_motion_photo;
68     protected static final int PREVIEW_ADD_OR_SELECT_BUTTON_ID = R.id.preview_add_or_select_button;
69     protected static final int PRIVACY_TEXT_ID = R.id.privacy_text;
70 
71     protected static final int DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH
72             = R.dimen.preview_add_or_select_width;
73 
74     /**
75      * The position of the first image item in the grid on the Photos tab
76      */
77     protected static final int IMAGE_1_POSITION = 1;
78 
79     /**
80      * The position of the second item in the grid on the Photos tab
81      */
82     protected static final int IMAGE_2_POSITION = 2;
83 
84     /**
85      * The position of the video item in the grid on the Photos tab
86      */
87     protected static final int VIDEO_POSITION = 3;
88 
89     /**
90      * The default position of a banner in the Photos & Albums tab recycler view adapters
91      */
92     static final int DEFAULT_BANNER_POSITION = 0;
93 
94 
95     private static final Intent sSingleSelectIntent;
96     static {
97         sSingleSelectIntent = new Intent(MediaStore.ACTION_PICK_IMAGES);
98         sSingleSelectIntent.addCategory(Intent.CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST);
99     }
100 
101     private static final Intent sMultiSelectionIntent;
102     static {
103         sMultiSelectionIntent = new Intent(MediaStore.ACTION_PICK_IMAGES);
104         Bundle extras = new Bundle();
extras.putInt(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit())105         extras.putInt(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
106         sMultiSelectionIntent.addCategory(Intent.CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST);
107         sMultiSelectionIntent.putExtras(extras);
108     }
109 
110     private static final Intent sUserSelectImagesForAppIntent;
111     static {
112         sUserSelectImagesForAppIntent = new Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP);
113         sUserSelectImagesForAppIntent.addCategory(Intent.CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST);
114         Bundle extras = new Bundle();
extras.putInt(Intent.EXTRA_UID, 1234)115         extras.putInt(Intent.EXTRA_UID, 1234);
116         sUserSelectImagesForAppIntent.putExtras(extras);
117     }
118 
119     private static final File IMAGE_1_FILE = new File(Environment.getExternalStorageDirectory(),
120             Environment.DIRECTORY_DCIM + "/Camera"
121                     + "/image_" + System.currentTimeMillis() + ".jpeg");
122     private static final File IMAGE_2_FILE = new File(Environment.getExternalStorageDirectory(),
123             Environment.DIRECTORY_DOWNLOADS + "/image_" + System.currentTimeMillis() + ".jpeg");
124     private static final File VIDEO_FILE = new File(Environment.getExternalStorageDirectory(),
125             Environment.DIRECTORY_MOVIES + "/video_" + System.currentTimeMillis() + ".mp4");
126 
127     private static final long POLLING_TIMEOUT_MILLIS_LONG = TimeUnit.SECONDS.toMillis(2);
128     private static final long POLLING_SLEEP_MILLIS = 200;
129 
130     private static IsolatedContext sIsolatedContext;
131     private static UserIdManager sUserIdManager;
132 
getSingleSelectMimeTypeFilterIntent(String mimeTypeFilter)133     public static Intent getSingleSelectMimeTypeFilterIntent(String mimeTypeFilter) {
134         final Intent intent = new Intent(sSingleSelectIntent);
135         intent.setType(mimeTypeFilter);
136         return intent;
137     }
138 
getSingleSelectionIntent()139     public static Intent getSingleSelectionIntent() {
140         return sSingleSelectIntent;
141     }
142 
getMultiSelectionIntent()143     public static Intent getMultiSelectionIntent() {
144         return sMultiSelectionIntent;
145     }
146 
getUserSelectImagesForAppIntent()147     public static Intent getUserSelectImagesForAppIntent() {
148         return sUserSelectImagesForAppIntent;
149     }
150 
getMultiSelectionIntent(int max)151     public static Intent getMultiSelectionIntent(int max) {
152         final Intent intent = new Intent(sMultiSelectionIntent);
153         Bundle extras = new Bundle();
154         extras.putInt(MediaStore.EXTRA_PICK_IMAGES_MAX, max);
155         intent.putExtras(extras);
156         return intent;
157     }
158 
getIsolatedContext()159     public static IsolatedContext getIsolatedContext() {
160         return sIsolatedContext;
161     }
162 
getMockUserIdManager()163     public static UserIdManager getMockUserIdManager() {
164         return sUserIdManager;
165     }
166 
167     @BeforeClass
setupClass()168     public static void setupClass() throws Exception {
169         MediaStore.waitForIdle(getTargetContext().getContentResolver());
170         pollForCondition(() -> isExternalStorageStateMounted(), "Timed out while"
171                 + " waiting for ExternalStorageState to be MEDIA_MOUNTED");
172 
173         InstrumentationRegistry.getInstrumentation().getUiAutomation()
174                 .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
175                         Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
176                         Manifest.permission.INTERACT_ACROSS_USERS,
177                         Manifest.permission.READ_DEVICE_CONFIG);
178 
179         sIsolatedContext = new IsolatedContext(getTargetContext(), "modern",
180                 /* asFuseThread */ false);
181 
182         sUserIdManager = mock(UserIdManager.class);
183         when(sUserIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
184 
185         createFiles();
186     }
187 
188     @AfterClass
destroyClass()189     public static void destroyClass() {
190         deleteFiles(/* invalidateMediaStore */ false);
191 
192         InstrumentationRegistry.getInstrumentation()
193                 .getUiAutomation().dropShellPermissionIdentity();
194     }
195 
deleteFiles(boolean invalidateMediaStore)196     protected static void deleteFiles(boolean invalidateMediaStore) {
197         deleteFile(IMAGE_1_FILE, invalidateMediaStore);
198         deleteFile(IMAGE_2_FILE, invalidateMediaStore);
199         deleteFile(VIDEO_FILE, invalidateMediaStore);
200     }
201 
deleteFile(File file, boolean invalidateMediaStore)202     private static void deleteFile(File file, boolean invalidateMediaStore) {
203         file.delete();
204         if (invalidateMediaStore) {
205             final Uri uri = MediaStore.scanFile(getIsolatedContext().getContentResolver(), file);
206             assertThat(uri).isNull();
207             // Force picker db sync for that db operation
208             MediaStore.waitForIdle(getIsolatedContext().getContentResolver());
209         }
210     }
211 
createFiles()212     private static void createFiles() throws Exception {
213         long timeNow = System.currentTimeMillis();
214         // Create files and change dateModified so that we can predict the recyclerView item
215         // position. Set modified date ahead of time, so that even if other files are created,
216         // the below files always have positions 1, 2 and 3.
217         createFile(IMAGE_1_FILE, timeNow + 30000);
218         createFile(IMAGE_2_FILE, timeNow + 20000);
219         createFile(VIDEO_FILE, timeNow + 10000);
220     }
221 
pollForCondition(Supplier<Boolean> condition, String errorMessage)222     private static void pollForCondition(Supplier<Boolean> condition, String errorMessage)
223             throws Exception {
224         for (int i = 0; i < POLLING_TIMEOUT_MILLIS_LONG / POLLING_SLEEP_MILLIS; i++) {
225             if (condition.get()) {
226                 return;
227             }
228             Thread.sleep(POLLING_SLEEP_MILLIS);
229         }
230         throw new TimeoutException(errorMessage);
231     }
232 
isExternalStorageStateMounted()233     private static boolean isExternalStorageStateMounted() {
234         final File target = Environment.getExternalStorageDirectory();
235         try {
236             return (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState(target))
237                     && Os.statvfs(target.getAbsolutePath()).f_blocks > 0);
238         } catch (ErrnoException ignored) {
239         }
240         return false;
241     }
242 
createFile(File file, long dateModified)243     private static void createFile(File file, long dateModified) throws IOException {
244         File parentFile = file.getParentFile();
245         parentFile.mkdirs();
246 
247         assertThat(parentFile.exists()).isTrue();
248         assertThat(file.createNewFile()).isTrue();
249         // Write 1 byte because 0byte files are not valid in the picker db
250         try (FileOutputStream fos = new FileOutputStream(file)) {
251             fos.write(1);
252         }
253 
254         // Change dateModified so that we can predict the recyclerView item position
255         Files.setLastModifiedTime(file.toPath(), FileTime.fromMillis(dateModified));
256 
257         final Uri uri = MediaStore.scanFile(getIsolatedContext().getContentResolver(), file);
258         MediaStore.waitForIdle(getIsolatedContext().getContentResolver());
259         assertThat(uri).isNotNull();
260     }
261 
262     /**
263      * Mock UserIdManager class such that the profile button is active and the user is in personal
264      * profile.
265      */
setUpActiveProfileButton()266     static void setUpActiveProfileButton() {
267         when(sUserIdManager.isMultiUserProfiles()).thenReturn(true);
268         when(sUserIdManager.isBlockedByAdmin()).thenReturn(false);
269         when(sUserIdManager.isWorkProfileOff()).thenReturn(false);
270         when(sUserIdManager.isCrossProfileAllowed()).thenReturn(true);
271         when(sUserIdManager.isManagedUserSelected()).thenReturn(false);
272 
273         // setPersonalAsCurrentUserProfile() is called onClick of Active Profile Button to change
274         // profiles
275         doAnswer(invocation -> {
276             updateIsManagedUserSelected(/* isManagedUserSelected */ false);
277             return null;
278         }).when(sUserIdManager).setPersonalAsCurrentUserProfile();
279 
280         // setManagedAsCurrentUserProfile() is called onClick of Active Profile Button to change
281         // profiles
282         doAnswer(invocation -> {
283             updateIsManagedUserSelected(/* isManagedUserSelected */ true);
284             return null;
285         }).when(sUserIdManager).setManagedAsCurrentUserProfile();
286     }
287 
288     /**
289      * Mock UserIdManager class such that the user is in personal profile and work apps are
290      * turned off
291      */
setUpWorkAppsOffProfileButton()292     static void setUpWorkAppsOffProfileButton() {
293         when(sUserIdManager.isMultiUserProfiles()).thenReturn(true);
294         when(sUserIdManager.isBlockedByAdmin()).thenReturn(false);
295         when(sUserIdManager.isWorkProfileOff()).thenReturn(true);
296         when(sUserIdManager.isCrossProfileAllowed()).thenReturn(false);
297         when(sUserIdManager.isManagedUserSelected()).thenReturn(false);
298     }
299 
300     /**
301      * Mock UserIdManager class such that the user is in work profile and accessing personal
302      * profile content is blocked by admin
303      */
setUpBlockedByAdminProfileButton()304     static void setUpBlockedByAdminProfileButton() {
305         when(sUserIdManager.isMultiUserProfiles()).thenReturn(true);
306         when(sUserIdManager.isBlockedByAdmin()).thenReturn(true);
307         when(sUserIdManager.isWorkProfileOff()).thenReturn(false);
308         when(sUserIdManager.isCrossProfileAllowed()).thenReturn(false);
309         when(sUserIdManager.isManagedUserSelected()).thenReturn(true);
310     }
311 
updateIsManagedUserSelected(boolean isManagedUserSelected)312     private static void updateIsManagedUserSelected(boolean isManagedUserSelected) {
313         when(sUserIdManager.isManagedUserSelected()).thenReturn(isManagedUserSelected);
314     }
315 }
316