• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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