| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.providers.media; |
| |
| import static android.os.Build.VERSION_CODES.TIRAMISU; |
| import static android.provider.DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| import android.content.res.Resources; |
| import android.os.Binder; |
| import android.os.Build; |
| import android.os.SystemProperties; |
| import android.provider.DeviceConfig; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.core.util.Supplier; |
| |
| import com.android.modules.utils.build.SdkLevel; |
| import com.android.providers.media.util.StringUtils; |
| |
| import java.io.PrintWriter; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * Interface for retrieving MediaProvider configs. |
| * The configs are usually stored to and read from {@link android.provider.DeviceConfig} provider, |
| * however having this interface provides an easy way to mock out configs in tests (which don't |
| * always have permissions for accessing the {@link android.provider.DeviceConfig}). |
| */ |
| public interface ConfigStore { |
| |
| // TODO(b/288066342): Remove and replace after new constant definition in |
| // {@link android.provider.DeviceConfig}. |
| String NAMESPACE_MEDIAPROVIDER = "mediaprovider"; |
| boolean DEFAULT_TAKE_OVER_GET_CONTENT = false; |
| boolean DEFAULT_USER_SELECT_FOR_APP = true; |
| boolean DEFAULT_STABILISE_VOLUME_INTERNAL = false; |
| boolean DEFAULT_STABILIZE_VOLUME_EXTERNAL = false; |
| |
| boolean DEFAULT_TRANSCODE_ENABLED = true; |
| boolean DEFAULT_TRANSCODE_OPT_OUT_STRATEGY_ENABLED = false; |
| int DEFAULT_TRANSCODE_MAX_DURATION = 60 * 1000; // 1 minute |
| |
| boolean DEFAULT_PICKER_GET_CONTENT_PRELOAD = true; |
| boolean DEFAULT_PICKER_PICK_IMAGES_PRELOAD = true; |
| boolean DEFAULT_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG = false; |
| |
| boolean DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED = false; |
| boolean DEFAULT_ENFORCE_CLOUD_PROVIDER_ALLOWLIST = true; |
| |
| /** |
| * @return if the Cloud-Media-in-Photo-Picker enabled (e.g. platform will recognize and |
| * "plug-in" {@link android.provider.CloudMediaProvider}s. |
| */ |
| default boolean isCloudMediaInPhotoPickerEnabled() { |
| return DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED; |
| } |
| |
| /** |
| * @return package name of the pre-configured "system default" |
| * {@link android.provider.CloudMediaProvider}. |
| * @see #isCloudMediaInPhotoPickerEnabled() |
| */ |
| @Nullable |
| default String getDefaultCloudProviderPackage() { |
| return null; |
| } |
| |
| /** |
| * @return a non-null list of names of packages that are allowed to serve as the system |
| * Cloud Media Provider. |
| * @see #isCloudMediaInPhotoPickerEnabled() |
| * @see #shouldEnforceCloudProviderAllowlist() |
| */ |
| @NonNull |
| default List<String> getAllowedCloudProviderPackages() { |
| return Collections.emptyList(); |
| } |
| |
| /** |
| * @return if the Cloud Media Provider allowlist should be enforced. |
| * @see #isCloudMediaInPhotoPickerEnabled() |
| * @see #getAllowedCloudProviderPackages() |
| */ |
| default boolean shouldEnforceCloudProviderAllowlist() { |
| return DEFAULT_ENFORCE_CLOUD_PROVIDER_ALLOWLIST; |
| } |
| |
| /** |
| * @return if {@link com.android.providers.media.photopicker.PhotoPickerActivity} should preload |
| * selected media items before "returning" |
| * ({@link com.android.providers.media.photopicker.PhotoPickerActivity#setResultAndFinishSelf()}) |
| * back to the calling application, in the case when the PhotoPicker was launched via |
| * {@link android.content.Intent#ACTION_GET_CONTENT ACTION_GET_CONTENT}. |
| * @see com.android.providers.media.photopicker.PhotoPickerActivity#shouldPreloadSelectedItems() |
| * @see com.android.providers.media.photopicker.SelectedMediaPreloader |
| */ |
| default boolean shouldPickerPreloadForGetContent() { |
| return DEFAULT_PICKER_GET_CONTENT_PRELOAD; |
| } |
| |
| /** |
| * @return if {@link com.android.providers.media.photopicker.PhotoPickerActivity} should preload |
| * selected media items before "returning" |
| * ({@link com.android.providers.media.photopicker.PhotoPickerActivity#setResultAndFinishSelf()}) |
| * back to the calling application, in the case when the PhotoPicker was launched via |
| * {@link android.provider.MediaStore#ACTION_PICK_IMAGES ACTION_PICK_IMAGES}. |
| * @see com.android.providers.media.photopicker.PhotoPickerActivity#shouldPreloadSelectedItems() |
| * @see com.android.providers.media.photopicker.SelectedMediaPreloader |
| */ |
| default boolean shouldPickerPreloadForPickImages() { |
| return DEFAULT_PICKER_PICK_IMAGES_PRELOAD; |
| } |
| |
| /** |
| * @return if {@link com.android.providers.media.photopicker.PhotoPickerActivity} should respect |
| * {@code EXTRA_PRELOAD_SELECTED} {@code Intent} "argument" when making a |
| * decision whether to preload selected media items before "returning" |
| * ({@link com.android.providers.media.photopicker.PhotoPickerActivity#setResultAndFinishSelf()}) |
| * back to the calling application, in the case when the PhotoPicker was launched via |
| * {@link android.provider.MediaStore#ACTION_PICK_IMAGES ACTION_PICK_IMAGES}. |
| * @see com.android.providers.media.photopicker.PhotoPickerActivity#shouldPreloadSelectedItems() |
| * @see com.android.providers.media.photopicker.SelectedMediaPreloader |
| */ |
| default boolean shouldPickerRespectPreloadArgumentForPickImages() { |
| return DEFAULT_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG; |
| } |
| |
| /** |
| * @return if PhotoPicker should handle {@link android.content.Intent#ACTION_GET_CONTENT}. |
| */ |
| default boolean isGetContentTakeOverEnabled() { |
| return DEFAULT_TAKE_OVER_GET_CONTENT; |
| } |
| |
| /** |
| * @return if PhotoPickerUserSelectActivity should be enabled |
| */ |
| default boolean isUserSelectForAppEnabled() { |
| return DEFAULT_USER_SELECT_FOR_APP; |
| } |
| |
| /** |
| * @return if stable URI are enabled for the internal volume. |
| */ |
| default boolean isStableUrisForInternalVolumeEnabled() { |
| return DEFAULT_STABILISE_VOLUME_INTERNAL; |
| } |
| |
| /** |
| * @return if stable URI are enabled for the external volume. |
| */ |
| default boolean isStableUrisForExternalVolumeEnabled() { |
| return DEFAULT_STABILIZE_VOLUME_EXTERNAL; |
| } |
| |
| /** |
| * @return if transcoding is enabled. |
| */ |
| default boolean isTranscodeEnabled() { |
| return DEFAULT_TRANSCODE_ENABLED; |
| } |
| |
| /** |
| * @return if transcoding is the default option. |
| */ |
| default boolean shouldTranscodeDefault() { |
| return DEFAULT_TRANSCODE_OPT_OUT_STRATEGY_ENABLED; |
| } |
| |
| /** |
| * @return max transcode duration (in milliseconds). |
| */ |
| default int getTranscodeMaxDurationMs() { |
| return DEFAULT_TRANSCODE_MAX_DURATION; |
| } |
| |
| @NonNull |
| List<String> getTranscodeCompatManifest(); |
| |
| @NonNull |
| List<String> getTranscodeCompatStale(); |
| |
| /** |
| * Add a listener for changes. |
| */ |
| void addOnChangeListener(@NonNull Executor executor, @NonNull Runnable listener); |
| |
| /** |
| * Print the {@link ConfigStore} state into the given stream. |
| */ |
| default void dump(PrintWriter writer) { |
| writer.println("Config store state:"); |
| writer.println(" isCloudMediaInPhotoPickerEnabled=" + isCloudMediaInPhotoPickerEnabled()); |
| writer.println(" defaultCloudProviderPackage=" + getDefaultCloudProviderPackage()); |
| writer.println(" allowedCloudProviderPackages=" + getAllowedCloudProviderPackages()); |
| writer.println(" shouldEnforceCloudProviderAllowlist=" |
| + shouldEnforceCloudProviderAllowlist()); |
| writer.println(" shouldPickerPreloadForGetContent=" + shouldPickerPreloadForGetContent()); |
| writer.println(" shouldPickerPreloadForPickImages=" + shouldPickerPreloadForPickImages()); |
| writer.println(" shouldPickerRespectPreloadArgumentForPickImages=" |
| + shouldPickerRespectPreloadArgumentForPickImages()); |
| writer.println(" isGetContentTakeOverEnabled=" + isGetContentTakeOverEnabled()); |
| writer.println(" isUserSelectForAppEnabled=" + isUserSelectForAppEnabled()); |
| writer.println(" isStableUrisForInternalVolumeEnabled=" |
| + isStableUrisForInternalVolumeEnabled()); |
| writer.println(" isStableUrisForExternalVolumeEnabled=" |
| + isStableUrisForExternalVolumeEnabled()); |
| writer.println(" isTranscodeEnabled=" + isTranscodeEnabled()); |
| writer.println(" shouldTranscodeDefault=" + shouldTranscodeDefault()); |
| writer.println(" transcodeMaxDurationMs=" + getTranscodeMaxDurationMs()); |
| writer.println(" transcodeCompatManifest=" + getTranscodeCompatManifest()); |
| writer.println(" transcodeCompatStale=" + getTranscodeCompatStale()); |
| } |
| |
| /** |
| * Implementation of the {@link ConfigStore} that reads "real" configs from |
| * {@link android.provider.DeviceConfig}. Meant to be used by the "production" code. |
| */ |
| class ConfigStoreImpl implements ConfigStore { |
| private static final String KEY_TAKE_OVER_GET_CONTENT = "take_over_get_content"; |
| private static final String KEY_USER_SELECT_FOR_APP = "user_select_for_app"; |
| |
| @VisibleForTesting |
| public static final String KEY_STABILIZE_VOLUME_INTERNAL = "stabilize_volume_internal"; |
| @VisibleForTesting |
| public static final String KEY_STABILIZE_VOLUME_EXTERNAL = "stabilize_volume_external"; |
| |
| private static final String KEY_TRANSCODE_ENABLED = "transcode_enabled"; |
| private static final String KEY_TRANSCODE_OPT_OUT_STRATEGY_ENABLED = "transcode_default"; |
| private static final String KEY_TRANSCODE_MAX_DURATION = "transcode_max_duration_ms"; |
| private static final String KEY_TRANSCODE_COMPAT_MANIFEST = "transcode_compat_manifest"; |
| private static final String KEY_TRANSCODE_COMPAT_STALE = "transcode_compat_stale"; |
| |
| private static final String SYSPROP_TRANSCODE_MAX_DURATION = |
| "persist.sys.fuse.transcode_max_file_duration_ms"; |
| private static final int TRANSCODE_MAX_DURATION_INVALID = 0; |
| |
| private static final String KEY_PICKER_GET_CONTENT_PRELOAD = |
| "picker_get_content_preload_selected"; |
| private static final String KEY_PICKER_PICK_IMAGES_PRELOAD = |
| "picker_pick_images_preload_selected"; |
| private static final String KEY_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG = |
| "picker_pick_images_respect_preload_selected_arg"; |
| |
| private static final String KEY_CLOUD_MEDIA_FEATURE_ENABLED = "cloud_media_feature_enabled"; |
| private static final String KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST = "allowed_cloud_providers"; |
| private static final String KEY_CLOUD_MEDIA_ENFORCE_PROVIDER_ALLOWLIST = |
| "cloud_media_enforce_provider_allowlist"; |
| |
| private static final boolean sCanReadDeviceConfig = SdkLevel.isAtLeastS(); |
| |
| @NonNull |
| private final Resources mResources; |
| |
| ConfigStoreImpl(@NonNull Resources resources) { |
| mResources = requireNonNull(resources); |
| } |
| |
| @Override |
| public boolean isCloudMediaInPhotoPickerEnabled() { |
| Boolean isEnabled = |
| getBooleanDeviceConfig( |
| NAMESPACE_MEDIAPROVIDER, |
| KEY_CLOUD_MEDIA_FEATURE_ENABLED, |
| DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED); |
| |
| List<String> allowList = |
| getStringArrayDeviceConfig( |
| NAMESPACE_MEDIAPROVIDER, KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST); |
| |
| // Only consider the feature enabled when the enabled flag is on AND when the allowlist |
| // of permitted cloud media providers is not empty. |
| return isEnabled && !allowList.isEmpty(); |
| } |
| |
| @Nullable |
| @Override |
| public String getDefaultCloudProviderPackage() { |
| String pkg = mResources.getString(R.string.config_default_cloud_media_provider_package); |
| if (pkg == null && Build.VERSION.SDK_INT <= TIRAMISU) { |
| // We are on Android T or below and do not have |
| // config_default_cloud_media_provider_package: let's see if we have now deprecated |
| // config_default_cloud_provider_authority. |
| final String authority = |
| mResources.getString(R.string.config_default_cloud_provider_authority); |
| if (authority != null) { |
| pkg = maybeExtractPackageNameFromCloudProviderAuthority(authority); |
| } |
| } |
| return pkg; |
| } |
| |
| @NonNull |
| @Override |
| public List<String> getAllowedCloudProviderPackages() { |
| final List<String> allowlist = |
| getStringArrayDeviceConfig(NAMESPACE_MEDIAPROVIDER, |
| KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST); |
| |
| // BACKWARD COMPATIBILITY WORKAROUND. |
| // See javadoc to maybeExtractPackageNameFromCloudProviderAuthority() below for more |
| // details. |
| for (int i = 0; i < allowlist.size(); i++) { |
| final String pkg = |
| maybeExtractPackageNameFromCloudProviderAuthority(allowlist.get(i)); |
| if (pkg != null) { |
| allowlist.set(i, pkg); |
| } |
| } |
| |
| return allowlist; |
| } |
| |
| @Override |
| public boolean shouldEnforceCloudProviderAllowlist() { |
| return getBooleanDeviceConfig( |
| NAMESPACE_MEDIAPROVIDER, |
| KEY_CLOUD_MEDIA_ENFORCE_PROVIDER_ALLOWLIST, |
| DEFAULT_ENFORCE_CLOUD_PROVIDER_ALLOWLIST); |
| } |
| |
| @Override |
| public boolean shouldPickerPreloadForGetContent() { |
| return getBooleanDeviceConfig(KEY_PICKER_GET_CONTENT_PRELOAD, |
| DEFAULT_PICKER_GET_CONTENT_PRELOAD); |
| } |
| |
| @Override |
| public boolean shouldPickerPreloadForPickImages() { |
| return getBooleanDeviceConfig(KEY_PICKER_PICK_IMAGES_PRELOAD, |
| DEFAULT_PICKER_PICK_IMAGES_PRELOAD); |
| } |
| |
| @Override |
| public boolean shouldPickerRespectPreloadArgumentForPickImages() { |
| return getBooleanDeviceConfig(KEY_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG, |
| DEFAULT_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG); |
| } |
| |
| @Override |
| public boolean isGetContentTakeOverEnabled() { |
| return getBooleanDeviceConfig(KEY_TAKE_OVER_GET_CONTENT, DEFAULT_TAKE_OVER_GET_CONTENT); |
| } |
| |
| @Override |
| public boolean isUserSelectForAppEnabled() { |
| return getBooleanDeviceConfig(KEY_USER_SELECT_FOR_APP, DEFAULT_USER_SELECT_FOR_APP); |
| } |
| |
| @Override |
| public boolean isStableUrisForInternalVolumeEnabled() { |
| return getBooleanDeviceConfig(NAMESPACE_MEDIAPROVIDER, KEY_STABILIZE_VOLUME_INTERNAL, |
| DEFAULT_STABILISE_VOLUME_INTERNAL); |
| } |
| |
| @Override |
| public boolean isStableUrisForExternalVolumeEnabled() { |
| return getBooleanDeviceConfig(NAMESPACE_MEDIAPROVIDER, KEY_STABILIZE_VOLUME_EXTERNAL, |
| DEFAULT_STABILIZE_VOLUME_EXTERNAL); |
| } |
| |
| @Override |
| public boolean isTranscodeEnabled() { |
| return getBooleanDeviceConfig( |
| KEY_TRANSCODE_ENABLED, DEFAULT_TRANSCODE_ENABLED); |
| } |
| |
| @Override |
| public boolean shouldTranscodeDefault() { |
| return getBooleanDeviceConfig(KEY_TRANSCODE_OPT_OUT_STRATEGY_ENABLED, |
| DEFAULT_TRANSCODE_OPT_OUT_STRATEGY_ENABLED); |
| } |
| |
| @Override |
| public int getTranscodeMaxDurationMs() { |
| // First check if OEMs overwrite default duration via system property. |
| int maxDurationMs = SystemProperties.getInt( |
| SYSPROP_TRANSCODE_MAX_DURATION, TRANSCODE_MAX_DURATION_INVALID); |
| |
| // Give priority to OEM value if set. Only accept larger values, which can be desired |
| // for more performant devices. Lower values may result in unexpected behaviour |
| // (a value of 0 would mean transcoding is actually disabled) or break CTS tests (a |
| // value small enough to prevent transcoding the videos under test). |
| // Otherwise, fallback to device config / default values. |
| if (maxDurationMs != TRANSCODE_MAX_DURATION_INVALID |
| && maxDurationMs > DEFAULT_TRANSCODE_MAX_DURATION) { |
| return maxDurationMs; |
| } |
| return getIntDeviceConfig(KEY_TRANSCODE_MAX_DURATION, DEFAULT_TRANSCODE_MAX_DURATION); |
| } |
| |
| @Override |
| @NonNull |
| public List<String> getTranscodeCompatManifest() { |
| return getStringArrayDeviceConfig(KEY_TRANSCODE_COMPAT_MANIFEST); |
| } |
| |
| @Override |
| @NonNull |
| public List<String> getTranscodeCompatStale() { |
| return getStringArrayDeviceConfig(KEY_TRANSCODE_COMPAT_STALE); |
| } |
| |
| @Override |
| public void addOnChangeListener(@NonNull Executor executor, @NonNull Runnable listener) { |
| if (!sCanReadDeviceConfig) { |
| return; |
| } |
| |
| // TODO(b/246590468): Follow best naming practices for namespaces of device config flags |
| // that make changes to this package independent of reboot |
| DeviceConfig.addOnPropertiesChangedListener( |
| NAMESPACE_STORAGE_NATIVE_BOOT, executor, unused -> listener.run()); |
| DeviceConfig.addOnPropertiesChangedListener( |
| NAMESPACE_MEDIAPROVIDER, executor, unused -> listener.run()); |
| } |
| |
| private static boolean getBooleanDeviceConfig(@NonNull String key, boolean defaultValue) { |
| if (!sCanReadDeviceConfig) { |
| return defaultValue; |
| } |
| return withCleanCallingIdentity(() -> |
| DeviceConfig.getBoolean(NAMESPACE_STORAGE_NATIVE_BOOT, key, defaultValue)); |
| } |
| |
| private static boolean getBooleanDeviceConfig(@NonNull String namespace, |
| @NonNull String key, boolean defaultValue) { |
| if (!sCanReadDeviceConfig) { |
| return defaultValue; |
| } |
| return withCleanCallingIdentity( |
| () -> DeviceConfig.getBoolean(namespace, key, defaultValue)); |
| } |
| |
| private static int getIntDeviceConfig(@NonNull String key, int defaultValue) { |
| if (!sCanReadDeviceConfig) { |
| return defaultValue; |
| } |
| return withCleanCallingIdentity(() -> |
| DeviceConfig.getInt(NAMESPACE_STORAGE_NATIVE_BOOT, key, defaultValue)); |
| } |
| |
| private static String getStringDeviceConfig(@NonNull String key) { |
| if (!sCanReadDeviceConfig) { |
| return null; |
| } |
| return withCleanCallingIdentity(() -> |
| DeviceConfig.getString(NAMESPACE_STORAGE_NATIVE_BOOT, key, null)); |
| } |
| |
| private static String getStringDeviceConfig(@NonNull String namespace, |
| @NonNull String key) { |
| if (!sCanReadDeviceConfig) { |
| return null; |
| } |
| return withCleanCallingIdentity(() -> |
| DeviceConfig.getString(namespace, key, null)); |
| } |
| |
| private static List<String> getStringArrayDeviceConfig(@NonNull String key) { |
| final String items = getStringDeviceConfig(key); |
| if (StringUtils.isNullOrEmpty(items)) { |
| return Collections.emptyList(); |
| } |
| return Arrays.asList(items.split(",")); |
| } |
| |
| private static List<String> getStringArrayDeviceConfig(@NonNull String namespace, |
| @NonNull String key) { |
| final String items = getStringDeviceConfig(namespace, key); |
| if (StringUtils.isNullOrEmpty(items)) { |
| return Collections.emptyList(); |
| } |
| return Arrays.asList(items.split(",")); |
| } |
| |
| private static <T> T withCleanCallingIdentity(@NonNull Supplier<T> action) { |
| final long callingIdentity = Binder.clearCallingIdentity(); |
| try { |
| return action.get(); |
| } finally { |
| Binder.restoreCallingIdentity(callingIdentity); |
| } |
| } |
| |
| /** |
| * BACKWARD COMPATIBILITY WORKAROUND |
| * Initially, instead of using package names when allow-listing and setting the system |
| * default CloudMediaProviders we used authorities. |
| * This, however, introduced a vulnerability, so we switched to using package names. |
| * But, by then, we had been allow-listing and setting default CMPs using authorities. |
| * Luckily for us, all of those CMPs had authorities in one the following formats: |
| * "${package-name}.cloudprovider" or "${package-name}.picker", |
| * e.g. "com.hooli.android.photos" package would implement a CMP with |
| * "com.hooli.android.photos.cloudpicker" authority. |
| * So in order for the old allow-listings and defaults to work now, we try to extract |
| * package names from authorities by removing the ".cloudprovider" and ".cloudpicker" |
| * suffixes. |
| */ |
| @Nullable |
| private static String maybeExtractPackageNameFromCloudProviderAuthority( |
| @NonNull String authority) { |
| if (authority.endsWith(".cloudprovider")) { |
| return authority.substring(0, authority.length() - ".cloudprovider".length()); |
| } else if (authority.endsWith(".cloudpicker")) { |
| return authority.substring(0, authority.length() - ".cloudpicker".length()); |
| } else { |
| return null; |
| } |
| } |
| } |
| } |