1 /* 2 * Copyright (C) 2022 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 android.photopicker.cts; 18 19 import static android.Manifest.permission.READ_DEVICE_CONFIG; 20 import static android.Manifest.permission.WRITE_DEVICE_CONFIG; 21 import static android.photopicker.cts.PhotoPickerBaseTest.INVALID_CLOUD_PROVIDER; 22 import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider; 23 import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton; 24 import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList; 25 26 import static com.google.common.truth.Truth.assertThat; 27 import static com.google.common.truth.Truth.assertWithMessage; 28 29 import android.app.UiAutomation; 30 import android.content.ClipData; 31 import android.content.Context; 32 import android.os.UserHandle; 33 import android.provider.DeviceConfig; 34 import android.provider.MediaStore; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.util.Pair; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.test.InstrumentationRegistry; 42 import androidx.test.uiautomator.UiDevice; 43 import androidx.test.uiautomator.UiObject; 44 45 import com.android.modules.utils.build.SdkLevel; 46 47 import java.io.IOException; 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.List; 51 52 public class PhotoPickerCloudUtils { 53 public static final String NAMESPACE_MEDIAPROVIDER = "mediaprovider"; 54 public static final String NAMESPACE_STORAGE_NATIVE_BOOT = "storage_native_boot"; 55 private static final String KEY_ALLOWED_CLOUD_PROVIDERS = "allowed_cloud_providers"; 56 private static final String KEY_CLOUD_MEDIA_FEATURE_ENABLED = "cloud_media_feature_enabled"; 57 private static final String KEY_PICKER_PICK_IMAGES_PRELOAD = 58 "picker_pick_images_preload_selected"; 59 private static final String DISABLE_DEVICE_CONFIG_SYNC = 60 "cmd device_config set_sync_disabled_for_tests %s"; 61 private static final String DISABLE_DEVICE_CONFIG_SYNC_MODE = "until_reboot"; 62 private static final String TAG = "PickerCloudUtils"; 63 64 /** 65 * Device config is reset from the server periodically. When tests override device config, it is 66 * not a sticky change. The device config may be reset to server values at any point - even 67 * while a test is running. In order to prevent unwanted device config resets, this method 68 * disables device config syncs until device reboot. 69 */ disableDeviceConfigSync()70 static void disableDeviceConfigSync() { 71 try { 72 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 73 .executeShellCommand(String.format(DISABLE_DEVICE_CONFIG_SYNC, 74 DISABLE_DEVICE_CONFIG_SYNC_MODE)); 75 } catch (IOException e) { 76 Log.e(TAG, "Could not disable device_config sync. " 77 + "Device config may reset to server values at any point during test runs.", e); 78 } 79 } extractMediaIds(ClipData clipData, int minCount)80 public static List<String> extractMediaIds(ClipData clipData, int minCount) { 81 final int count = clipData.getItemCount(); 82 assertThat(count).isAtLeast(minCount); 83 84 final List<String> mediaIds = new ArrayList<>(); 85 for (int i = 0; i < count; i++) { 86 mediaIds.add(clipData.getItemAt(i).getUri().getLastPathSegment()); 87 } 88 89 return mediaIds; 90 } 91 selectAndAddPickerMedia(UiDevice uiDevice, int maxCount)92 public static void selectAndAddPickerMedia(UiDevice uiDevice, int maxCount) 93 throws Exception { 94 final List<UiObject> itemList = findItemList(maxCount); 95 for (int i = 0; i < itemList.size(); i++) { 96 final UiObject item = itemList.get(i); 97 item.click(); 98 uiDevice.waitForIdle(); 99 } 100 101 final UiObject addButton = findAddButton(); 102 addButton.click(); 103 uiDevice.waitForIdle(); 104 } 105 fetchPickerMedia(GetResultActivity activity, UiDevice uiDevice, int maxCount)106 public static ClipData fetchPickerMedia(GetResultActivity activity, UiDevice uiDevice, 107 int maxCount) throws Exception { 108 selectAndAddPickerMedia(uiDevice, maxCount); 109 110 return activity.getResult().data.getClipData(); 111 } 112 initCloudProviderWithImage( Context context, PickerProviderMediaGenerator.MediaGenerator mediaGenerator, String authority, Pair<String, String>... mediaPairs)113 public static void initCloudProviderWithImage( 114 Context context, PickerProviderMediaGenerator.MediaGenerator mediaGenerator, 115 String authority, Pair<String, String>... mediaPairs) throws Exception { 116 PhotoPickerBaseTest.setCloudProvider(authority); 117 assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(context.getContentResolver(), 118 authority)).isTrue(); 119 120 for (Pair<String, String> pair : mediaPairs) { 121 addImage(mediaGenerator, pair.first, pair.second); 122 } 123 124 syncCloudProvider(context); 125 } 126 addImage(PickerProviderMediaGenerator.MediaGenerator generator, String localId, String cloudId)127 public static void addImage(PickerProviderMediaGenerator.MediaGenerator generator, 128 String localId, String cloudId) 129 throws Exception { 130 final long imageSizeBytes = 107684; 131 generator.addMedia(localId, cloudId, /* albumId */ null, "image/jpeg", 132 /* mimeTypeExtension */ 0, imageSizeBytes, /* isFavorite */ false, 133 R.raw.lg_g4_iso_800_jpg); 134 } 135 containsExcept(List<String> mediaIds, String contained, String notContained)136 public static void containsExcept(List<String> mediaIds, String contained, 137 String notContained) { 138 assertThat(mediaIds).contains(contained); 139 assertThat(mediaIds).containsNoneIn(Collections.singletonList(notContained)); 140 } 141 142 /** 143 * @return true if cloud is enabled in the device config of the given namespace, 144 * otherwise false. 145 */ isCloudMediaEnabled(@onNull String namespace)146 public static boolean isCloudMediaEnabled(@NonNull String namespace) { 147 return Boolean.parseBoolean(readDeviceConfigProp(namespace, 148 KEY_CLOUD_MEDIA_FEATURE_ENABLED)); 149 } 150 151 /** 152 * @return the allowed providers in the device config of the given namespace. 153 */ 154 @Nullable getAllowedProvidersDeviceConfig(@onNull String namespace)155 static String getAllowedProvidersDeviceConfig(@NonNull String namespace) { 156 return readDeviceConfigProp(namespace, KEY_ALLOWED_CLOUD_PROVIDERS); 157 } 158 159 /** 160 * Enables cloud media and sets the allowed cloud provider in the namespace storage_native_boot 161 * and mediaprovider. 162 */ enableCloudMediaAndSetAllowedCloudProviders(@onNull String allowedPackagesJoined)163 static void enableCloudMediaAndSetAllowedCloudProviders(@NonNull String allowedPackagesJoined) { 164 enableCloudMediaAndSetAllowedCloudProviders(NAMESPACE_MEDIAPROVIDER, allowedPackagesJoined); 165 enableCloudMediaAndSetAllowedCloudProviders( 166 NAMESPACE_STORAGE_NATIVE_BOOT, allowedPackagesJoined); 167 } 168 169 /** 170 * Enables cloud media and sets the allowed cloud provider in the given namespace. 171 */ enableCloudMediaAndSetAllowedCloudProviders(@onNull String namespace, @NonNull String allowedPackagesJoined)172 static void enableCloudMediaAndSetAllowedCloudProviders(@NonNull String namespace, 173 @NonNull String allowedPackagesJoined) { 174 writeDeviceConfigProp(namespace, KEY_ALLOWED_CLOUD_PROVIDERS, allowedPackagesJoined); 175 assertWithMessage("Failed to update the allowed cloud providers device config") 176 .that(getAllowedProvidersDeviceConfig(namespace)) 177 .isEqualTo(allowedPackagesJoined); 178 179 writeDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_FEATURE_ENABLED, true); 180 assertWithMessage("Failed to update the cloud media feature device config") 181 .that(isCloudMediaEnabled(namespace)) 182 .isTrue(); 183 } 184 185 /** 186 * Disable cloud media in the given namespace. 187 */ disableCloudMediaAndClearAllowedCloudProviders(@onNull String namespace)188 static void disableCloudMediaAndClearAllowedCloudProviders(@NonNull String namespace) { 189 writeDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_FEATURE_ENABLED, false); 190 assertWithMessage("Failed to update the cloud media feature device config") 191 .that(isCloudMediaEnabled(namespace)) 192 .isFalse(); 193 194 deleteDeviceConfigProp(namespace, KEY_ALLOWED_CLOUD_PROVIDERS); 195 assertWithMessage("Failed to delete the allowed cloud providers device config") 196 .that(getAllowedProvidersDeviceConfig(namespace)) 197 .isNull(); 198 } 199 enablePickImagesPreload()200 static void enablePickImagesPreload() { 201 writeDeviceConfigProp(NAMESPACE_MEDIAPROVIDER, KEY_PICKER_PICK_IMAGES_PRELOAD, true); 202 } 203 disablePickImagesPreload()204 static void disablePickImagesPreload() { 205 writeDeviceConfigProp(NAMESPACE_MEDIAPROVIDER, KEY_PICKER_PICK_IMAGES_PRELOAD, false); 206 } 207 isPickImagesPreloadEnabled()208 static boolean isPickImagesPreloadEnabled() { 209 return Boolean.parseBoolean( 210 readDeviceConfigProp(NAMESPACE_MEDIAPROVIDER, KEY_PICKER_PICK_IMAGES_PRELOAD) 211 ); 212 } 213 214 @NonNull getUiAutomation()215 private static UiAutomation getUiAutomation() { 216 return InstrumentationRegistry.getInstrumentation().getUiAutomation(); 217 } 218 219 @Nullable readDeviceConfigProp(@onNull String namespace, @NonNull String name)220 private static String readDeviceConfigProp(@NonNull String namespace, @NonNull String name) { 221 getUiAutomation().adoptShellPermissionIdentity(READ_DEVICE_CONFIG); 222 try { 223 return DeviceConfig.getProperty(namespace, name); 224 } finally { 225 getUiAutomation().dropShellPermissionIdentity(); 226 } 227 } 228 writeDeviceConfigProp(@onNull String namespace, @NonNull String key, boolean value)229 private static void writeDeviceConfigProp(@NonNull String namespace, 230 @NonNull String key, boolean value) { 231 writeDeviceConfigProp(namespace, key, Boolean.toString(value)); 232 } 233 writeDeviceConfigProp(@onNull String namespace, @NonNull String name, @NonNull String value)234 private static void writeDeviceConfigProp(@NonNull String namespace, @NonNull String name, 235 @NonNull String value) { 236 getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG); 237 238 try { 239 DeviceConfig.setProperty(namespace, name, value, 240 /* makeDefault*/ false); 241 } finally { 242 getUiAutomation().dropShellPermissionIdentity(); 243 } 244 } 245 deleteDeviceConfigProp(@onNull String namespace, @NonNull String name)246 private static void deleteDeviceConfigProp(@NonNull String namespace, @NonNull String name) { 247 try { 248 getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG); 249 if (SdkLevel.isAtLeastU()) { 250 DeviceConfig.deleteProperty(namespace, name); 251 } else { 252 // DeviceConfig.deleteProperty API is only available from T onwards. 253 try { 254 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 255 .executeShellCommand( 256 String.format("device_config delete %s %s", 257 namespace, name)); 258 } catch (IOException e) { 259 Log.e(TAG, String.format("Could not delete device_config %s / %s", 260 namespace, name), e); 261 } 262 } 263 } finally { 264 getUiAutomation().dropShellPermissionIdentity(); 265 } 266 } 267 268 /** 269 * Set cloud provider in the device to the provided authority. 270 * If the provided cloud authority equals {@link PhotoPickerBaseTest#INVALID_CLOUD_PROVIDER}, 271 * the cloud provider will not be updated. 272 */ setCloudProvider(@ullable UiDevice sDevice, @Nullable String authority)273 public static void setCloudProvider(@Nullable UiDevice sDevice, 274 @Nullable String authority) throws IOException { 275 if (INVALID_CLOUD_PROVIDER.equals(authority)) { 276 Log.w(TAG, "Cloud provider is invalid. " 277 + "Ignoring the request to set the cloud provider to invalid"); 278 return; 279 } 280 if (authority == null) { 281 sDevice.executeShellCommand( 282 "content call" 283 + " --user " + UserHandle.myUserId() 284 + " --uri content://media/ --method set_cloud_provider --extra" 285 + " cloud_provider:n:null"); 286 } else { 287 sDevice.executeShellCommand( 288 "content call" 289 + " --user " + UserHandle.myUserId() 290 + " --uri content://media/ --method set_cloud_provider --extra" 291 + " cloud_provider:s:" 292 + authority); 293 } 294 } 295 296 /** 297 * @return the current cloud provider. 298 */ getCurrentCloudProvider(UiDevice sDevice)299 public static String getCurrentCloudProvider(UiDevice sDevice) throws IOException { 300 final String out = 301 sDevice.executeShellCommand( 302 "content call" 303 + " --user " + UserHandle.myUserId() 304 + " --uri content://media/ --method get_cloud_provider"); 305 return extractCloudProvider(out); 306 } 307 extractCloudProvider(String out)308 private static String extractCloudProvider(String out) { 309 String[] splitOutput; 310 if (TextUtils.isEmpty(out) || ((splitOutput = out.split("=")).length < 2)) { 311 throw new RuntimeException("Could not get current cloud provider. Output: " + out); 312 } 313 String cloudprovider = splitOutput[1]; 314 cloudprovider = cloudprovider.substring(0, cloudprovider.length() - 3); 315 if (cloudprovider.equals("null")) { 316 return null; 317 } 318 return cloudprovider; 319 } 320 } 321