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