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