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