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 com.android.providers.media; 18 19 import static android.os.Build.VERSION_CODES.TIRAMISU; 20 import static android.provider.DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT; 21 22 import static java.util.Objects.requireNonNull; 23 24 import android.content.res.Resources; 25 import android.os.Binder; 26 import android.os.Build; 27 import android.os.SystemProperties; 28 import android.provider.DeviceConfig; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import androidx.annotation.VisibleForTesting; 33 import androidx.core.util.Supplier; 34 35 import com.android.modules.utils.build.SdkLevel; 36 import com.android.providers.media.util.StringUtils; 37 38 import java.util.Arrays; 39 import java.util.Collections; 40 import java.util.List; 41 import java.util.concurrent.Executor; 42 43 /** 44 * Interface for retrieving MediaProvider configs. 45 * The configs are usually stored to and read from {@link android.provider.DeviceConfig} provider, 46 * however having this interface provides an easy way to mock out configs in tests (which don't 47 * always have permissions for accessing the {@link android.provider.DeviceConfig}). 48 */ 49 public interface ConfigStore { 50 boolean DEFAULT_TAKE_OVER_GET_CONTENT = false; 51 boolean DEFAULT_USER_SELECT_FOR_APP = true; 52 boolean DEFAULT_STABILISE_VOLUME_INTERNAL = false; 53 boolean DEFAULT_STABILIZE_VOLUME_EXTERNAL = false; 54 55 boolean DEFAULT_TRANSCODE_ENABLED = true; 56 boolean DEFAULT_TRANSCODE_OPT_OUT_STRATEGY_ENABLED = false; 57 int DEFAULT_TRANSCODE_MAX_DURATION = 60 * 1000; // 1 minute 58 59 int DEFAULT_PICKER_SYNC_DELAY = 5000; // 5 seconds 60 61 boolean DEFAULT_PICKER_GET_CONTENT_PRELOAD = true; 62 boolean DEFAULT_PICKER_PICK_IMAGES_PRELOAD = true; 63 boolean DEFAULT_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG = false; 64 65 boolean DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED = false; 66 boolean DEFAULT_ENFORCE_CLOUD_PROVIDER_ALLOWLIST = true; 67 68 /** 69 * @return if the Cloud-Media-in-Photo-Picker enabled (e.g. platform will recognize and 70 * "plug-in" {@link android.provider.CloudMediaProvider}s. 71 */ isCloudMediaInPhotoPickerEnabled()72 default boolean isCloudMediaInPhotoPickerEnabled() { 73 return DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED; 74 } 75 76 /** 77 * @return package name of the pre-configured "system default" 78 * {@link android.provider.CloudMediaProvider}. 79 * @see #isCloudMediaInPhotoPickerEnabled() 80 */ 81 @Nullable getDefaultCloudProviderPackage()82 default String getDefaultCloudProviderPackage() { 83 return null; 84 } 85 86 /** 87 * @return a non-null list of names of packages that are allowed to serve as the system 88 * Cloud Media Provider. 89 * @see #isCloudMediaInPhotoPickerEnabled() 90 * @see #shouldEnforceCloudProviderAllowlist() 91 */ 92 @NonNull getAllowedCloudProviderPackages()93 default List<String> getAllowedCloudProviderPackages() { 94 return Collections.emptyList(); 95 } 96 97 /** 98 * @return if the Cloud Media Provider allowlist should be enforced. 99 * @see #isCloudMediaInPhotoPickerEnabled() 100 * @see #getAllowedCloudProviderPackages() 101 */ shouldEnforceCloudProviderAllowlist()102 default boolean shouldEnforceCloudProviderAllowlist() { 103 return DEFAULT_ENFORCE_CLOUD_PROVIDER_ALLOWLIST; 104 } 105 106 /** 107 * @return a delay (in milliseconds) before executing PhotoPicker media sync on media events 108 * like inserts/updates/deletes to artificially throttle the burst notifications. 109 */ getPickerSyncDelayMs()110 default int getPickerSyncDelayMs() { 111 return DEFAULT_PICKER_SYNC_DELAY; 112 } 113 114 /** 115 * @return if {@link com.android.providers.media.photopicker.PhotoPickerActivity} should preload 116 * selected media items before "returning" 117 * ({@link com.android.providers.media.photopicker.PhotoPickerActivity#setResultAndFinishSelf()}) 118 * back to the calling application, in the case when the PhotoPicker was launched via 119 * {@link android.content.Intent#ACTION_GET_CONTENT ACTION_GET_CONTENT}. 120 * @see com.android.providers.media.photopicker.PhotoPickerActivity#shouldPreloadSelectedItems() 121 * @see com.android.providers.media.photopicker.SelectedMediaPreloader 122 */ shouldPickerPreloadForGetContent()123 default boolean shouldPickerPreloadForGetContent() { 124 return DEFAULT_PICKER_GET_CONTENT_PRELOAD; 125 } 126 127 /** 128 * @return if {@link com.android.providers.media.photopicker.PhotoPickerActivity} should preload 129 * selected media items before "returning" 130 * ({@link com.android.providers.media.photopicker.PhotoPickerActivity#setResultAndFinishSelf()}) 131 * back to the calling application, in the case when the PhotoPicker was launched via 132 * {@link android.provider.MediaStore#ACTION_PICK_IMAGES ACTION_PICK_IMAGES}. 133 * @see com.android.providers.media.photopicker.PhotoPickerActivity#shouldPreloadSelectedItems() 134 * @see com.android.providers.media.photopicker.SelectedMediaPreloader 135 */ shouldPickerPreloadForPickImages()136 default boolean shouldPickerPreloadForPickImages() { 137 return DEFAULT_PICKER_PICK_IMAGES_PRELOAD; 138 } 139 140 /** 141 * @return if {@link com.android.providers.media.photopicker.PhotoPickerActivity} should respect 142 * {@code EXTRA_PRELOAD_SELECTED} {@code Intent} "argument" when making a 143 * decision whether to preload selected media items before "returning" 144 * ({@link com.android.providers.media.photopicker.PhotoPickerActivity#setResultAndFinishSelf()}) 145 * back to the calling application, in the case when the PhotoPicker was launched via 146 * {@link android.provider.MediaStore#ACTION_PICK_IMAGES ACTION_PICK_IMAGES}. 147 * @see com.android.providers.media.photopicker.PhotoPickerActivity#shouldPreloadSelectedItems() 148 * @see com.android.providers.media.photopicker.SelectedMediaPreloader 149 */ shouldPickerRespectPreloadArgumentForPickImages()150 default boolean shouldPickerRespectPreloadArgumentForPickImages() { 151 return DEFAULT_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG; 152 } 153 154 /** 155 * @return if PhotoPicker should handle {@link android.content.Intent#ACTION_GET_CONTENT}. 156 */ isGetContentTakeOverEnabled()157 default boolean isGetContentTakeOverEnabled() { 158 return DEFAULT_TAKE_OVER_GET_CONTENT; 159 } 160 161 /** 162 * @return if PhotoPickerUserSelectActivity should be enabled 163 */ isUserSelectForAppEnabled()164 default boolean isUserSelectForAppEnabled() { 165 return DEFAULT_USER_SELECT_FOR_APP; 166 } 167 168 /** 169 * @return if stable URI are enabled for the internal volume. 170 */ isStableUrisForInternalVolumeEnabled()171 default boolean isStableUrisForInternalVolumeEnabled() { 172 return DEFAULT_STABILISE_VOLUME_INTERNAL; 173 } 174 175 /** 176 * @return if stable URI are enabled for the external volume. 177 */ isStableUrisForExternalVolumeEnabled()178 default boolean isStableUrisForExternalVolumeEnabled() { 179 return DEFAULT_STABILIZE_VOLUME_EXTERNAL; 180 } 181 182 /** 183 * @return if transcoding is enabled. 184 */ isTranscodeEnabled()185 default boolean isTranscodeEnabled() { 186 return DEFAULT_TRANSCODE_ENABLED; 187 } 188 189 /** 190 * @return if transcoding is the default option. 191 */ shouldTranscodeDefault()192 default boolean shouldTranscodeDefault() { 193 return DEFAULT_TRANSCODE_OPT_OUT_STRATEGY_ENABLED; 194 } 195 196 /** 197 * @return max transcode duration (in milliseconds). 198 */ getTranscodeMaxDurationMs()199 default int getTranscodeMaxDurationMs() { 200 return DEFAULT_TRANSCODE_MAX_DURATION; 201 } 202 203 @NonNull getTranscodeCompatManifest()204 List<String> getTranscodeCompatManifest(); 205 206 @NonNull getTranscodeCompatStale()207 List<String> getTranscodeCompatStale(); 208 209 /** 210 * Add a listener for changes. 211 */ addOnChangeListener(@onNull Executor executor, @NonNull Runnable listener)212 void addOnChangeListener(@NonNull Executor executor, @NonNull Runnable listener); 213 214 /** 215 * Implementation of the {@link ConfigStore} that reads "real" configs from 216 * {@link android.provider.DeviceConfig}. Meant to be used by the "production" code. 217 */ 218 class ConfigStoreImpl implements ConfigStore { 219 private static final String KEY_TAKE_OVER_GET_CONTENT = "take_over_get_content"; 220 private static final String KEY_USER_SELECT_FOR_APP = "user_select_for_app"; 221 222 @VisibleForTesting 223 public static final String KEY_STABILISE_VOLUME_INTERNAL = "stablise_volume_internal"; 224 private static final String KEY_STABILIZE_VOLUME_EXTERNAL = "stabilize_volume_external"; 225 226 private static final String KEY_TRANSCODE_ENABLED = "transcode_enabled"; 227 private static final String KEY_TRANSCODE_OPT_OUT_STRATEGY_ENABLED = "transcode_default"; 228 private static final String KEY_TRANSCODE_MAX_DURATION = "transcode_max_duration_ms"; 229 private static final String KEY_TRANSCODE_COMPAT_MANIFEST = "transcode_compat_manifest"; 230 private static final String KEY_TRANSCODE_COMPAT_STALE = "transcode_compat_stale"; 231 232 private static final String SYSPROP_TRANSCODE_MAX_DURATION = 233 "persist.sys.fuse.transcode_max_file_duration_ms"; 234 private static final int TRANSCODE_MAX_DURATION_INVALID = 0; 235 private static final String KEY_PICKER_SYNC_DELAY = "default_sync_delay_ms"; 236 private static final String KEY_PICKER_GET_CONTENT_PRELOAD = 237 "picker_get_content_preload_selected"; 238 private static final String KEY_PICKER_PICK_IMAGES_PRELOAD = 239 "picker_pick_images_preload_selected"; 240 private static final String KEY_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG = 241 "picker_pick_images_respect_preload_selected_arg"; 242 243 private static final String KEY_CLOUD_MEDIA_FEATURE_ENABLED = "cloud_media_feature_enabled"; 244 private static final String KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST = "allowed_cloud_providers"; 245 private static final String KEY_CLOUD_MEDIA_ENFORCE_PROVIDER_ALLOWLIST = 246 "cloud_media_enforce_provider_allowlist"; 247 248 private static final boolean sCanReadDeviceConfig = SdkLevel.isAtLeastS(); 249 250 @NonNull 251 private final Resources mResources; 252 ConfigStoreImpl(@onNull Resources resources)253 ConfigStoreImpl(@NonNull Resources resources) { 254 mResources = requireNonNull(resources); 255 } 256 257 @Override isCloudMediaInPhotoPickerEnabled()258 public boolean isCloudMediaInPhotoPickerEnabled() { 259 return getBooleanDeviceConfig(KEY_CLOUD_MEDIA_FEATURE_ENABLED, 260 DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED); 261 } 262 263 @Nullable 264 @Override getDefaultCloudProviderPackage()265 public String getDefaultCloudProviderPackage() { 266 String pkg = mResources.getString(R.string.config_default_cloud_media_provider_package); 267 if (pkg == null && Build.VERSION.SDK_INT <= TIRAMISU) { 268 // We are on Android T or below and do not have 269 // config_default_cloud_media_provider_package: let's see if we have now deprecated 270 // config_default_cloud_provider_authority. 271 final String authority = 272 mResources.getString(R.string.config_default_cloud_provider_authority); 273 if (authority != null) { 274 pkg = maybeExtractPackageNameFromCloudProviderAuthority(authority); 275 } 276 } 277 return pkg; 278 } 279 280 @NonNull 281 @Override getAllowedCloudProviderPackages()282 public List<String> getAllowedCloudProviderPackages() { 283 final List<String> allowlist = 284 getStringArrayDeviceConfig(KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST); 285 286 // BACKWARD COMPATIBILITY WORKAROUND. 287 // See javadoc to maybeExtractPackageNameFromCloudProviderAuthority() below for more 288 // details. 289 for (int i = 0; i < allowlist.size(); i++) { 290 final String pkg = 291 maybeExtractPackageNameFromCloudProviderAuthority(allowlist.get(i)); 292 if (pkg != null) { 293 allowlist.set(i, pkg); 294 } 295 } 296 297 return allowlist; 298 } 299 300 @Override shouldEnforceCloudProviderAllowlist()301 public boolean shouldEnforceCloudProviderAllowlist() { 302 return getBooleanDeviceConfig(KEY_CLOUD_MEDIA_ENFORCE_PROVIDER_ALLOWLIST, 303 DEFAULT_ENFORCE_CLOUD_PROVIDER_ALLOWLIST); 304 } 305 306 @Override getPickerSyncDelayMs()307 public int getPickerSyncDelayMs() { 308 return getIntDeviceConfig(KEY_PICKER_SYNC_DELAY, DEFAULT_PICKER_SYNC_DELAY); 309 } 310 311 @Override shouldPickerPreloadForGetContent()312 public boolean shouldPickerPreloadForGetContent() { 313 return getBooleanDeviceConfig(KEY_PICKER_GET_CONTENT_PRELOAD, 314 DEFAULT_PICKER_GET_CONTENT_PRELOAD); 315 } 316 317 @Override shouldPickerPreloadForPickImages()318 public boolean shouldPickerPreloadForPickImages() { 319 return getBooleanDeviceConfig(KEY_PICKER_PICK_IMAGES_PRELOAD, 320 DEFAULT_PICKER_PICK_IMAGES_PRELOAD); 321 } 322 323 @Override shouldPickerRespectPreloadArgumentForPickImages()324 public boolean shouldPickerRespectPreloadArgumentForPickImages() { 325 return getBooleanDeviceConfig(KEY_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG, 326 DEFAULT_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG); 327 } 328 329 @Override isGetContentTakeOverEnabled()330 public boolean isGetContentTakeOverEnabled() { 331 return getBooleanDeviceConfig(KEY_TAKE_OVER_GET_CONTENT, DEFAULT_TAKE_OVER_GET_CONTENT); 332 } 333 334 @Override isUserSelectForAppEnabled()335 public boolean isUserSelectForAppEnabled() { 336 return getBooleanDeviceConfig(KEY_USER_SELECT_FOR_APP, DEFAULT_USER_SELECT_FOR_APP); 337 } 338 339 @Override isStableUrisForInternalVolumeEnabled()340 public boolean isStableUrisForInternalVolumeEnabled() { 341 return getBooleanDeviceConfig( 342 KEY_STABILISE_VOLUME_INTERNAL, DEFAULT_STABILISE_VOLUME_INTERNAL); 343 } 344 345 @Override isStableUrisForExternalVolumeEnabled()346 public boolean isStableUrisForExternalVolumeEnabled() { 347 return getBooleanDeviceConfig( 348 KEY_STABILIZE_VOLUME_EXTERNAL, DEFAULT_STABILIZE_VOLUME_EXTERNAL); 349 } 350 351 @Override isTranscodeEnabled()352 public boolean isTranscodeEnabled() { 353 return getBooleanDeviceConfig( 354 KEY_TRANSCODE_ENABLED, DEFAULT_TRANSCODE_ENABLED); 355 } 356 357 @Override shouldTranscodeDefault()358 public boolean shouldTranscodeDefault() { 359 return getBooleanDeviceConfig(KEY_TRANSCODE_OPT_OUT_STRATEGY_ENABLED, 360 DEFAULT_TRANSCODE_OPT_OUT_STRATEGY_ENABLED); 361 } 362 363 @Override getTranscodeMaxDurationMs()364 public int getTranscodeMaxDurationMs() { 365 // First check if OEMs overwrite default duration via system property. 366 int maxDurationMs = SystemProperties.getInt( 367 SYSPROP_TRANSCODE_MAX_DURATION, TRANSCODE_MAX_DURATION_INVALID); 368 369 // Give priority to OEM value if set. Only accept larger values, which can be desired 370 // for more performant devices. Lower values may result in unexpected behaviour 371 // (a value of 0 would mean transcoding is actually disabled) or break CTS tests (a 372 // value small enough to prevent transcoding the videos under test). 373 // Otherwise, fallback to device config / default values. 374 if (maxDurationMs != TRANSCODE_MAX_DURATION_INVALID 375 && maxDurationMs > DEFAULT_TRANSCODE_MAX_DURATION) { 376 return maxDurationMs; 377 } 378 return getIntDeviceConfig(KEY_TRANSCODE_MAX_DURATION, DEFAULT_TRANSCODE_MAX_DURATION); 379 } 380 381 @Override 382 @NonNull getTranscodeCompatManifest()383 public List<String> getTranscodeCompatManifest() { 384 return getStringArrayDeviceConfig(KEY_TRANSCODE_COMPAT_MANIFEST); 385 } 386 387 @Override 388 @NonNull getTranscodeCompatStale()389 public List<String> getTranscodeCompatStale() { 390 return getStringArrayDeviceConfig(KEY_TRANSCODE_COMPAT_STALE); 391 } 392 393 @Override addOnChangeListener(@onNull Executor executor, @NonNull Runnable listener)394 public void addOnChangeListener(@NonNull Executor executor, @NonNull Runnable listener) { 395 if (!sCanReadDeviceConfig) { 396 return; 397 } 398 399 // TODO(b/246590468): Follow best naming practices for namespaces of device config flags 400 // that make changes to this package independent of reboot 401 DeviceConfig.addOnPropertiesChangedListener( 402 NAMESPACE_STORAGE_NATIVE_BOOT, executor, unused -> listener.run()); 403 } 404 getBooleanDeviceConfig(@onNull String key, boolean defaultValue)405 private static boolean getBooleanDeviceConfig(@NonNull String key, boolean defaultValue) { 406 if (!sCanReadDeviceConfig) { 407 return defaultValue; 408 } 409 return withCleanCallingIdentity(() -> 410 DeviceConfig.getBoolean(NAMESPACE_STORAGE_NATIVE_BOOT, key, defaultValue)); 411 } 412 getIntDeviceConfig(@onNull String key, int defaultValue)413 private static int getIntDeviceConfig(@NonNull String key, int defaultValue) { 414 if (!sCanReadDeviceConfig) { 415 return defaultValue; 416 } 417 return withCleanCallingIdentity(() -> 418 DeviceConfig.getInt(NAMESPACE_STORAGE_NATIVE_BOOT, key, defaultValue)); 419 } 420 getStringDeviceConfig(@onNull String key)421 private static String getStringDeviceConfig(@NonNull String key) { 422 if (!sCanReadDeviceConfig) { 423 return null; 424 } 425 return withCleanCallingIdentity(() -> 426 DeviceConfig.getString(NAMESPACE_STORAGE_NATIVE_BOOT, key, null)); 427 } 428 getStringArrayDeviceConfig(@onNull String key)429 private static List<String> getStringArrayDeviceConfig(@NonNull String key) { 430 final String items = getStringDeviceConfig(key); 431 if (StringUtils.isNullOrEmpty(items)) { 432 return Collections.emptyList(); 433 } 434 return Arrays.asList(items.split(",")); 435 } 436 withCleanCallingIdentity(@onNull Supplier<T> action)437 private static <T> T withCleanCallingIdentity(@NonNull Supplier<T> action) { 438 final long callingIdentity = Binder.clearCallingIdentity(); 439 try { 440 return action.get(); 441 } finally { 442 Binder.restoreCallingIdentity(callingIdentity); 443 } 444 } 445 446 /** 447 * BACKWARD COMPATIBILITY WORKAROUND 448 * Initially, instead of using package names when allow-listing and setting the system 449 * default CloudMediaProviders we used authorities. 450 * This, however, introduced a vulnerability, so we switched to using package names. 451 * But, by then, we had been allow-listing and setting default CMPs using authorities. 452 * Luckily for us, all of those CMPs had authorities in one the following formats: 453 * "${package-name}.cloudprovider" or "${package-name}.picker", 454 * e.g. "com.hooli.android.photos" package would implement a CMP with 455 * "com.hooli.android.photos.cloudpicker" authority. 456 * So in order for the old allow-listings and defaults to work now, we try to extract 457 * package names from authorities by removing the ".cloudprovider" and ".cloudpicker" 458 * suffixes. 459 */ 460 @Nullable maybeExtractPackageNameFromCloudProviderAuthority( @onNull String authority)461 private static String maybeExtractPackageNameFromCloudProviderAuthority( 462 @NonNull String authority) { 463 if (authority.endsWith(".cloudprovider")) { 464 return authority.substring(0, authority.length() - ".cloudprovider".length()); 465 } else if (authority.endsWith(".cloudpicker")) { 466 return authority.substring(0, authority.length() - ".cloudpicker".length()); 467 } else { 468 return null; 469 } 470 } 471 } 472 } 473