[automerger skipped] Import translations. DO NOT MERGE ANYWHERE am: 09879990b5 -s ours
am skip reason: subject contains skip directive
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/providers/MediaProvider/+/18779817
Change-Id: Ie745acd1c9f362c5af1727c050a00c872affaaae
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 27e9f85..a542d34 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,19 +1,6 @@
-
package {
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
-}
-
-// Added automatically by a large-scale-change
-// See: http://go/android-license-faq
-license {
- name: "packages_providers_MediaProvider_license",
- visibility: [":__subpackages__"],
- license_kinds: [
- "SPDX-license-identifier-Apache-2.0",
- ],
- license_text: [
- "NOTICE",
- ],
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
android_app {
@@ -74,10 +61,9 @@
"error_prone_mediaprovider",
"glide-annotation-processor",
],
-
+ jarjar_rules: "jarjar-rules.txt",
sdk_version: "module_current",
min_sdk_version: "30",
- target_sdk_version: "31",
certificate: "media",
privileged: true,
@@ -96,7 +82,10 @@
],
},
- required: ["preinstalled-packages-com.android.providers.media.module.xml"],
+ required: [
+ "preinstalled-packages-com.android.providers.media.module.xml",
+ "privapp_allowlist_com.android.providers.media.module.xml",
+ ],
lint: {
strict_updatability_linting: true,
@@ -127,6 +116,7 @@
"src/com/android/providers/media/util/MimeUtils.java",
"src/com/android/providers/media/util/StringUtils.java",
"src/com/android/providers/media/playlist/*.java",
+ "src/com/android/providers/media/dao/*.java",
],
sdk_version: "module_current",
min_sdk_version: "30",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2f71d1c..788df5d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -26,6 +26,7 @@
<uses-permission android:name="android.permission.USE_RESERVED_DISK" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS" />
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Permissions required for reading and logging compat changes -->
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
diff --git a/NOTICE b/NOTICE
deleted file mode 100644
index c5b1efa..0000000
--- a/NOTICE
+++ /dev/null
@@ -1,190 +0,0 @@
-
- Copyright (c) 2005-2008, 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.
-
- 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.
-
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 5b80e36..0e2dd2f 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -6,3 +6,5 @@
[Hook Scripts]
hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT}
+
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 980119c..2fc29df 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,24 +1,25 @@
{
- "mainline-presubmit": [
- {
- "name": "MediaProviderTests[com.google.android.mediaprovider.apex]"
- },
- {
- "name": "CtsScopedStorageCoreHostTest[com.google.android.mediaprovider.apex]"
- },
- {
- "name": "CtsScopedStorageHostTest[com.google.android.mediaprovider.apex]"
- },
- {
- "name": "CtsScopedStorageDeviceOnlyTest[com.google.android.mediaprovider.apex]"
- },
- {
- "name": "CtsMediaProviderTranscodeTests[com.google.android.mediaprovider.apex]"
- },
- {
- "name": "CtsPhotoPickerTest[com.google.android.mediaprovider.apex]"
- }
- ],
+ // TODO(b/204107787): Re-enable this once MP from master can be installed on R and S devices
+ // "mainline-presubmit": [
+ // {
+ // "name": "MediaProviderTests[com.google.android.mediaprovider.apex]"
+ // }
+ // {
+ // "name": "MediaProviderTests[com.google.android.mediaprovider.apex]"
+ // },
+ // {
+ // "name": "CtsScopedStorageCoreHostTest"
+ // },
+ // {
+ // "name": "CtsScopedStorageHostTest"
+ // },
+ // {
+ // "name": "CtsScopedStorageDeviceOnlyTest"
+ // },
+ // {
+ // "name": "CtsMediaProviderTranscodeTests[com.google.android.mediaprovider.apex]"
+ // }
+ // ],
"presubmit": [
{
"name": "MediaProviderTests"
diff --git a/apex/Android.bp b/apex/Android.bp
index 668ff1c..c735880 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -1,10 +1,6 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
apex {
@@ -18,15 +14,17 @@
apex_defaults {
name: "com.android.mediaprovider-defaults",
bootclasspath_fragments: ["com.android.mediaprovider-bootclasspath-fragment"],
- prebuilts: ["current_sdkinfo"],
+ prebuilts: [
+ "current_sdkinfo",
+ "privapp_allowlist_com.android.providers.media.module.xml"
+ ],
key: "com.android.mediaprovider.key",
certificate: ":com.android.mediaprovider.certificate",
file_contexts: ":com.android.mediaprovider-file_contexts",
- min_sdk_version: "30",
+ defaults: ["r-launched-apex-module"],
// Indicates that pre-installed version of this apex can be compressed.
// Whether it actually will be compressed is controlled on per-device basis.
compressible: true,
- updatable: true,
}
apex_key {
@@ -69,5 +67,15 @@
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+
+ // The following packages contain classes from other modules on the
+ // bootclasspath. That means that the hidden API flags for this module
+ // has to explicitly list every single class this module provides in
+ // that package to differentiate them from the classes provided by other
+ // modules. That can include private classes that are not part of the
+ // API.
+ split_packages: [
+ "android.provider",
+ ],
},
}
diff --git a/apex/apex_manifest.json b/apex/apex_manifest.json
index f056b53..fe2ed11 100644
--- a/apex/apex_manifest.json
+++ b/apex/apex_manifest.json
@@ -1,4 +1,4 @@
{
"name": "com.android.mediaprovider",
- "version": 339999910
+ "version": 339990000
}
diff --git a/apex/framework/Android.bp b/apex/framework/Android.bp
index ed56b54..a48bd75 100644
--- a/apex/framework/Android.bp
+++ b/apex/framework/Android.bp
@@ -14,11 +14,7 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
java_sdk_library {
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt
index 3ea467e..8e1691a 100644
--- a/apex/framework/api/current.txt
+++ b/apex/framework/api/current.txt
@@ -1,6 +1,100 @@
// Signature format: 2.0
package android.provider {
+ public abstract class CloudMediaProvider extends android.content.ContentProvider {
+ ctor public CloudMediaProvider();
+ method public final void attachInfo(@NonNull android.content.Context, @NonNull android.content.pm.ProviderInfo);
+ method @NonNull public final android.os.Bundle call(@NonNull String, @Nullable String, @Nullable android.os.Bundle);
+ method @NonNull public final android.net.Uri canonicalize(@NonNull android.net.Uri);
+ method public final int delete(@NonNull android.net.Uri, @Nullable String, @Nullable String[]);
+ method @NonNull public final String getType(@NonNull android.net.Uri);
+ method @NonNull public final android.net.Uri insert(@NonNull android.net.Uri, @NonNull android.content.ContentValues);
+ method @Nullable public android.provider.CloudMediaProvider.CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull android.os.Bundle, @NonNull android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback);
+ method @NonNull public abstract android.os.Bundle onGetMediaCollectionInfo(@NonNull android.os.Bundle);
+ method @NonNull public abstract android.os.ParcelFileDescriptor onOpenMedia(@NonNull String, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method @NonNull public abstract android.content.res.AssetFileDescriptor onOpenPreview(@NonNull String, @NonNull android.graphics.Point, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method @NonNull public android.database.Cursor onQueryAlbums(@NonNull android.os.Bundle);
+ method @NonNull public abstract android.database.Cursor onQueryDeletedMedia(@NonNull android.os.Bundle);
+ method @NonNull public abstract android.database.Cursor onQueryMedia(@NonNull android.os.Bundle);
+ method @NonNull public final android.os.ParcelFileDescriptor openFile(@NonNull android.net.Uri, @NonNull String) throws java.io.FileNotFoundException;
+ method @NonNull public final android.os.ParcelFileDescriptor openFile(@NonNull android.net.Uri, @NonNull String, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method @NonNull public final android.content.res.AssetFileDescriptor openTypedAssetFile(@NonNull android.net.Uri, @NonNull String, @Nullable android.os.Bundle) throws java.io.FileNotFoundException;
+ method @NonNull public final android.content.res.AssetFileDescriptor openTypedAssetFile(@NonNull android.net.Uri, @NonNull String, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method @NonNull public final android.database.Cursor query(@NonNull android.net.Uri, @Nullable String[], @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal);
+ method @NonNull public final android.database.Cursor query(@NonNull android.net.Uri, @Nullable String[], @Nullable String, @Nullable String[], @Nullable String);
+ method @NonNull public final android.database.Cursor query(@NonNull android.net.Uri, @Nullable String[], @Nullable String, @Nullable String[], @Nullable String, @Nullable android.os.CancellationSignal);
+ method public final int update(@NonNull android.net.Uri, @NonNull android.content.ContentValues, @Nullable String, @Nullable String[]);
+ }
+
+ public abstract static class CloudMediaProvider.CloudMediaSurfaceController {
+ ctor public CloudMediaProvider.CloudMediaSurfaceController();
+ method public abstract void onConfigChange(@NonNull android.os.Bundle);
+ method public abstract void onDestroy();
+ method public abstract void onMediaPause(int);
+ method public abstract void onMediaPlay(int);
+ method public abstract void onMediaSeekTo(int, long);
+ method public abstract void onPlayerCreate();
+ method public abstract void onPlayerRelease();
+ method public abstract void onSurfaceChanged(int, int, int, int);
+ method public abstract void onSurfaceCreated(int, @NonNull android.view.Surface, @NonNull String);
+ method public abstract void onSurfaceDestroyed(int);
+ }
+
+ public static final class CloudMediaProvider.CloudMediaSurfaceStateChangedCallback {
+ method public void setPlaybackState(int, int, @Nullable android.os.Bundle);
+ field public static final int PLAYBACK_STATE_BUFFERING = 1; // 0x1
+ field public static final int PLAYBACK_STATE_COMPLETED = 5; // 0x5
+ field public static final int PLAYBACK_STATE_ERROR_PERMANENT_FAILURE = 7; // 0x7
+ field public static final int PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE = 6; // 0x6
+ field public static final int PLAYBACK_STATE_MEDIA_SIZE_CHANGED = 8; // 0x8
+ field public static final int PLAYBACK_STATE_PAUSED = 4; // 0x4
+ field public static final int PLAYBACK_STATE_READY = 2; // 0x2
+ field public static final int PLAYBACK_STATE_STARTED = 3; // 0x3
+ }
+
+ public final class CloudMediaProviderContract {
+ field public static final String EXTRA_ALBUM_ID = "android.provider.extra.ALBUM_ID";
+ field public static final String EXTRA_LOOPING_PLAYBACK_ENABLED = "android.provider.extra.LOOPING_PLAYBACK_ENABLED";
+ field public static final String EXTRA_MEDIA_COLLECTION_ID = "android.provider.extra.MEDIA_COLLECTION_ID";
+ field public static final String EXTRA_PAGE_TOKEN = "android.provider.extra.PAGE_TOKEN";
+ field public static final String EXTRA_PREVIEW_THUMBNAIL = "android.provider.extra.PREVIEW_THUMBNAIL";
+ field public static final String EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = "android.provider.extra.SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED";
+ field public static final String EXTRA_SYNC_GENERATION = "android.provider.extra.SYNC_GENERATION";
+ field public static final String MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION = "com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS";
+ field public static final String PROVIDER_INTERFACE = "android.content.action.CLOUD_MEDIA_PROVIDER";
+ }
+
+ public static final class CloudMediaProviderContract.AlbumColumns {
+ field public static final String DATE_TAKEN_MILLIS = "date_taken_millis";
+ field public static final String DISPLAY_NAME = "display_name";
+ field public static final String ID = "id";
+ field public static final String MEDIA_COUNT = "album_media_count";
+ field public static final String MEDIA_COVER_ID = "album_media_cover_id";
+ }
+
+ public static final class CloudMediaProviderContract.MediaCollectionInfo {
+ field public static final String ACCOUNT_CONFIGURATION_INTENT = "account_configuration_intent";
+ field public static final String ACCOUNT_NAME = "account_name";
+ field public static final String LAST_MEDIA_SYNC_GENERATION = "last_media_sync_generation";
+ field public static final String MEDIA_COLLECTION_ID = "media_collection_id";
+ }
+
+ public static final class CloudMediaProviderContract.MediaColumns {
+ field public static final String DATE_TAKEN_MILLIS = "date_taken_millis";
+ field public static final String DURATION_MILLIS = "duration_millis";
+ field public static final String ID = "id";
+ field public static final String IS_FAVORITE = "is_favorite";
+ field public static final String MEDIA_STORE_URI = "media_store_uri";
+ field public static final String MIME_TYPE = "mime_type";
+ field public static final String SIZE_BYTES = "size_bytes";
+ field public static final String STANDARD_MIME_TYPE_EXTENSION = "standard_mime_type_extension";
+ field public static final int STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP = 3; // 0x3
+ field public static final int STANDARD_MIME_TYPE_EXTENSION_GIF = 1; // 0x1
+ field public static final int STANDARD_MIME_TYPE_EXTENSION_MOTION_PHOTO = 2; // 0x2
+ field public static final int STANDARD_MIME_TYPE_EXTENSION_NONE = 0; // 0x0
+ field public static final String SYNC_GENERATION = "sync_generation";
+ }
+
public final class MediaStore {
ctor public MediaStore();
method public static boolean canManageMedia(@NonNull android.content.Context);
@@ -22,12 +116,16 @@
method @NonNull public static String getVersion(@NonNull android.content.Context);
method @NonNull public static String getVersion(@NonNull android.content.Context, @NonNull String);
method @NonNull public static String getVolumeName(@NonNull android.net.Uri);
+ method public static boolean isCurrentCloudMediaProviderAuthority(@NonNull android.content.ContentResolver, @NonNull String);
method public static boolean isCurrentSystemGallery(@NonNull android.content.ContentResolver, int, @NonNull String);
+ method public static boolean isSupportedCloudMediaProviderAuthority(@NonNull android.content.ContentResolver, @NonNull String);
+ method public static void notifyCloudMediaChangedEvent(@NonNull android.content.ContentResolver, @NonNull String, @NonNull String) throws java.lang.SecurityException;
method @Deprecated @NonNull public static android.net.Uri setIncludePending(@NonNull android.net.Uri);
method @NonNull public static android.net.Uri setRequireOriginal(@NonNull android.net.Uri);
field public static final String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
field public static final String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE";
field public static final String ACTION_PICK_IMAGES = "android.provider.action.PICK_IMAGES";
+ field public static final String ACTION_PICK_IMAGES_SETTINGS = "android.provider.action.PICK_IMAGES_SETTINGS";
field public static final String ACTION_REVIEW = "android.provider.action.REVIEW";
field public static final String ACTION_REVIEW_SECURE = "android.provider.action.REVIEW_SECURE";
field public static final String ACTION_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE";
diff --git a/apex/framework/java/android/provider/CloudMediaProvider.java b/apex/framework/java/android/provider/CloudMediaProvider.java
index 2ef671a..6f3b246 100644
--- a/apex/framework/java/android/provider/CloudMediaProvider.java
+++ b/apex/framework/java/android/provider/CloudMediaProvider.java
@@ -17,9 +17,11 @@
package android.provider;
import static android.provider.CloudMediaProviderContract.EXTRA_ASYNC_CONTENT_PROVIDER;
+import static android.provider.CloudMediaProviderContract.EXTRA_AUTHORITY;
import static android.provider.CloudMediaProviderContract.EXTRA_ERROR_MESSAGE;
import static android.provider.CloudMediaProviderContract.EXTRA_FILE_DESCRIPTOR;
import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
+import static android.provider.CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB;
import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER;
import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK;
@@ -54,6 +56,7 @@
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteCallback;
+import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
@@ -105,8 +108,6 @@
* {@link #onGetMediaCollectionInfo}.
*
* @see MediaStore#ACTION_PICK_IMAGES
- *
- * @hide
*/
public abstract class CloudMediaProvider extends ContentProvider {
private static final String TAG = "CloudMediaProvider";
@@ -379,12 +380,14 @@
DEFAULT_LOOPING_PLAYBACK_ENABLED);
final boolean muteAudio = extras.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED);
+ final String authority = extras.getString(EXTRA_AUTHORITY);
final CloudMediaSurfaceStateChangedCallback callback =
new CloudMediaSurfaceStateChangedCallback(
ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder));
final Bundle config = new Bundle();
config.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, enableLoop);
config.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, muteAudio);
+ config.putString(EXTRA_AUTHORITY, authority);
final CloudMediaSurfaceController controller =
onCreateCloudMediaSurfaceController(config, callback);
if (controller == null) {
@@ -448,15 +451,29 @@
public final AssetFileDescriptor openTypedAssetFile(
@NonNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts,
@Nullable CancellationSignal signal) throws FileNotFoundException {
- String mediaId = uri.getLastPathSegment();
- final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE)
- && mimeTypeFilter.startsWith("image/");
- if (wantsThumb) {
- Point point = (Point) opts.getParcelable(ContentResolver.EXTRA_SIZE);
- return onOpenPreview(mediaId, point, opts, signal);
+ final String mediaId = uri.getLastPathSegment();
+ final Bundle bundle = new Bundle();
+ Point previewSize = null;
+
+ final DisplayMetrics screenMetrics = getContext().getResources().getDisplayMetrics();
+ int minPreviewLength = Math.min(screenMetrics.widthPixels, screenMetrics.heightPixels);
+
+ if (opts != null) {
+ bundle.putBoolean(EXTRA_MEDIASTORE_THUMB, opts.getBoolean(EXTRA_MEDIASTORE_THUMB));
+
+ if (opts.containsKey(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL)) {
+ bundle.putBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL, true);
+ minPreviewLength = minPreviewLength / 2;
+ }
+
+ previewSize = opts.getParcelable(ContentResolver.EXTRA_SIZE);
}
- return new AssetFileDescriptor(onOpenMedia(mediaId, opts, signal), 0 /* startOffset */,
- AssetFileDescriptor.UNKNOWN_LENGTH);
+
+ if (previewSize == null) {
+ previewSize = new Point(minPreviewLength, minPreviewLength);
+ }
+
+ return onOpenPreview(mediaId, previewSize, bundle, signal);
}
/**
diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java
index 8dfd5c6..9e35058 100644
--- a/apex/framework/java/android/provider/CloudMediaProviderContract.java
+++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java
@@ -31,8 +31,6 @@
* provides a foundational implementation of this contract.
*
* @see CloudMediaProvider
- *
- * @hide
*/
public final class CloudMediaProviderContract {
private static final String TAG = "CloudMediaProviderContract";
@@ -458,7 +456,6 @@
* @see CloudMediaProvider#onQueryAlbums
* <p>
* Type: STRING
- * @hide
*/
public static final String EXTRA_MEDIA_COLLECTION_ID =
"android.provider.extra.MEDIA_COLLECTION_ID";
@@ -558,6 +555,16 @@
"android.provider.extra.PREVIEW_THUMBNAIL";
/**
+ * A boolean to indicate {@link com.android.providers.media.photopicker.PhotoPickerProvider}
+ * this request is requesting a cached thumbnail file from MediaStore.
+ *
+ * Type: BOOLEAN
+ *
+ * {@hide}
+ */
+ public static final String EXTRA_MEDIASTORE_THUMB = "android.provider.extra.MEDIASTORE_THUMB";
+
+ /**
* Constant used to execute {@link CloudMediaProvider#onGetMediaCollectionInfo} via
* {@link ContentProvider#call}.
*
@@ -649,6 +656,13 @@
public static final String EXTRA_ERROR_MESSAGE = "android.provider.extra.error_message";
/**
+ * Constant used to get/set the {@link CloudMediaProvider} authority.
+ *
+ * {@hide}
+ */
+ public static final String EXTRA_AUTHORITY = "android.provider.extra.authority";
+
+ /**
* URI path for {@link CloudMediaProvider#onQueryMedia}
*
* {@hide}
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 47ef4ec..ca67b8a 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -261,6 +261,11 @@
public static final String CREATE_SURFACE_CONTROLLER = "create_surface_controller";
/** {@hide} */
+ public static final String USES_FUSE_PASSTHROUGH = "uses_fuse_passthrough";
+ /** {@hide} */
+ public static final String USES_FUSE_PASSTHROUGH_RESULT = "uses_fuse_passthrough_result";
+
+ /** {@hide} */
public static final String QUERY_ARG_LIMIT = ContentResolver.QUERY_ARG_LIMIT;
/** {@hide} */
public static final String QUERY_ARG_MIME_TYPE = "android:query-arg-mime_type";
@@ -706,9 +711,9 @@
* expose a limited set of read-only operations. Specifically, picker URIs
* can only be opened for read and queried for columns in {@link PickerMediaColumns}.
* <p>
- * Before this API, apps could use {@link Intent#ACTION_GET_CONTENT}. However, this
- * new action is recommended for images and videos use-cases, since it ofers a
- * better user experience.
+ * Before this API, apps could use {@link Intent#ACTION_GET_CONTENT}. However,
+ * {@link #ACTION_PICK_IMAGES} is now the recommended option for images and videos,
+ * since it ofers a better user experience.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_PICK_IMAGES = "android.provider.action.PICK_IMAGES";
@@ -722,7 +727,6 @@
*
* @see #ACTION_PICK_IMAGES
* @see #isCurrentCloudMediaProviderAuthority(ContentResolver, String)
- * @hide
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_PICK_IMAGES_SETTINGS =
@@ -4682,7 +4686,6 @@
*
* @see android.provider.CloudMediaProvider
* @see #isSupportedCloudMediaProviderAuthority(ContentResolver, String)
- * @hide
*/
public static boolean isCurrentCloudMediaProviderAuthority(@NonNull ContentResolver resolver,
@NonNull String authority) {
@@ -4696,7 +4699,6 @@
*
* @see android.provider.CloudMediaProvider
* @see #isCurrentCloudMediaProviderAuthority(ContentResolver, String)
- * @hide
*/
public static boolean isSupportedCloudMediaProviderAuthority(@NonNull ContentResolver resolver,
@NonNull String authority) {
@@ -4715,8 +4717,6 @@
* unsuccessful.
*
* @return {@code true} if the notification was successful, {@code false} otherwise
- *
- * @hide
*/
public static void notifyCloudMediaChangedEvent(@NonNull ContentResolver resolver,
@NonNull String authority, @NonNull String currentMediaCollectionId)
diff --git a/apex/permissions/Android.bp b/apex/permissions/Android.bp
new file mode 100644
index 0000000..e7330a8
--- /dev/null
+++ b/apex/permissions/Android.bp
@@ -0,0 +1,26 @@
+
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+ default_visibility: ["//packages/providers/MediaProvider:__subpackages__"],
+}
+
+prebuilt_etc {
+ name: "privapp_allowlist_com.android.providers.media.module.xml",
+ sub_dir: "permissions",
+ src: "com.android.providers.media.module.xml",
+}
\ No newline at end of file
diff --git a/apex/permissions/OWNERS b/apex/permissions/OWNERS
new file mode 100644
index 0000000..8b7e2e5
--- /dev/null
+++ b/apex/permissions/OWNERS
@@ -0,0 +1,2 @@
+per-file *.xml,OWNERS = set noparent
+per-file *.xml,OWNERS = file:platform/frameworks/base:/data/etc/OWNERS
diff --git a/apex/permissions/com.android.providers.media.module.xml b/apex/permissions/com.android.providers.media.module.xml
new file mode 100644
index 0000000..86da4d5
--- /dev/null
+++ b/apex/permissions/com.android.providers.media.module.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<permissions>
+ <privapp-permissions package="com.android.providers.media.module">
+ <permission name="android.permission.INTERACT_ACROSS_USERS"/>
+ <permission name="android.permission.MANAGE_USERS"/>
+ <permission name="android.permission.USE_RESERVED_DISK"/>
+ <permission name="android.permission.WRITE_MEDIA_STORAGE"/>
+ <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+ <permission name="android.permission.WATCH_APPOPS"/>
+ <permission name="android.permission.UPDATE_APP_OPS_STATS"/>
+ <permission name="android.permission.UPDATE_DEVICE_STATS"/>
+ <!-- Permissions required for reading and logging compat changes -->
+ <permission name="android.permission.LOG_COMPAT_CHANGE" />
+ <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
+ <permission name="android.permission.REGISTER_STATS_PULL_ATOM" />
+ <!-- Permissions required for reading DeviceConfig -->
+ <permission name="android.permission.READ_DEVICE_CONFIG" />
+ <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"/>
+ <permission name="android.permission.MODIFY_QUIET_MODE"/>
+ <!-- Permissions required to check if an app is in the foreground or not during IO -->
+ <permission name="android.permission.PACKAGE_USAGE_STATS"/>
+ </privapp-permissions>
+</permissions>
\ No newline at end of file
diff --git a/apex/testing/Android.bp b/apex/testing/Android.bp
index 41612ba..d42a561 100644
--- a/apex/testing/Android.bp
+++ b/apex/testing/Android.bp
@@ -1,10 +1,6 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
apex_test {
diff --git a/deploy.sh b/deploy.sh
index cc1be8e..5666be8 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -1,7 +1,7 @@
set -e
# Build both our APK and APEX combined together
-./build/soong/soong_ui.bash --make-mode -j64 MediaProviderLegacy com.google.android.mediaprovider
+MODULE_BUILD_FROM_SOURCE=true ./build/soong/soong_ui.bash --make-mode -j64 MediaProviderLegacy com.google.android.mediaprovider
# Push our updated APEX to device, then force apexd to remount it
adb shell stop
diff --git a/errorprone/Android.bp b/errorprone/Android.bp
index 3f11f91..fc3d67f 100644
--- a/errorprone/Android.bp
+++ b/errorprone/Android.bp
@@ -1,11 +1,6 @@
-
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
java_plugin {
diff --git a/jni/Android.bp b/jni/Android.bp
index 758bee1..b94f1b9 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -16,11 +16,7 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
cc_library_shared {
@@ -38,6 +34,7 @@
],
header_libs: [
+ "bpf_syscall_wrappers",
"libnativehelper_header_only",
],
@@ -67,6 +64,7 @@
tidy: true,
tidy_checks: [
"-google-runtime-int",
+ "-performance-no-int-to-ptr",
],
sdk_version: "current",
@@ -103,6 +101,9 @@
],
tidy: true,
+ tidy_checks: [
+ "-performance-no-int-to-ptr",
+ ],
sdk_version: "current",
stl: "c++_static",
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
old mode 100755
new mode 100644
index 34ea39c..b2b8ab3
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -17,10 +17,10 @@
#define LIBFUSE_LOG_TAG "libfuse"
#include "FuseDaemon.h"
-#include "android-base/strings.h"
#include <android-base/logging.h>
#include <android-base/properties.h>
+#include <android-base/strings.h>
#include <android/log.h>
#include <android/trace.h>
#include <ctype.h>
@@ -28,11 +28,11 @@
#include <errno.h>
#include <fcntl.h>
#include <fuse_i.h>
+#include <fuse_kernel.h>
#include <fuse_log.h>
#include <fuse_lowlevel.h>
#include <inttypes.h>
#include <limits.h>
-#include <linux/fuse.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
@@ -51,7 +51,6 @@
#include <unistd.h>
#include <iostream>
-#include <list>
#include <map>
#include <mutex>
#include <queue>
@@ -61,6 +60,8 @@
#include <unordered_set>
#include <vector>
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
#include "MediaProviderWrapper.h"
#include "libfuse_jni/FuseUtils.h"
#include "libfuse_jni/ReaddirHelper.h"
@@ -71,7 +72,6 @@
using mediaprovider::fuse::handle;
using mediaprovider::fuse::node;
using mediaprovider::fuse::RedactionInfo;
-using std::list;
using std::string;
using std::vector;
@@ -116,11 +116,17 @@
const std::regex PATTERN_OWNED_PATH(
"^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb)/([^/]+)(/?.*)?",
std::regex_constants::icase);
+const std::regex PATTERN_BPF_BACKING_PATH("^/storage/[^/]+/[0-9]+/Android/(data|obb)$",
+ std::regex_constants::icase);
static constexpr char TRANSFORM_SYNTHETIC_DIR[] = "synthetic";
static constexpr char TRANSFORM_TRANSCODE_DIR[] = "transcode";
static constexpr char PRIMARY_VOLUME_PREFIX[] = "/storage/emulated";
+static constexpr char FUSE_BPF_PROG_PATH[] = "/sys/fs/bpf/prog_fuse_media_fuse_media";
+
+enum class BpfFd { REMOVE = -1 };
+
/*
* In order to avoid double caching with fuse, call fadvise on the file handles
* in the underlying file system. However, if this is done on every read/write,
@@ -247,16 +253,22 @@
/* Single FUSE mount */
struct fuse {
- explicit fuse(const std::string& _path, const ino_t _ino,
- const std::vector<string>& _supported_transcoding_relative_paths)
+ explicit fuse(const std::string& _path, const ino_t _ino, const bool _uncached_mode,
+ const bool _bpf, const int _bpf_fd,
+ const std::vector<string>& _supported_transcoding_relative_paths,
+ const std::vector<string>& _supported_uncached_relative_paths)
: path(_path),
tracker(mediaprovider::fuse::NodeTracker(&lock)),
root(node::CreateRoot(_path, &lock, _ino, &tracker)),
+ uncached_mode(_uncached_mode),
mp(0),
zero_addr(0),
disable_dentry_cache(false),
passthrough(false),
- supported_transcoding_relative_paths(_supported_transcoding_relative_paths) {}
+ bpf(_bpf),
+ bpf_fd(_bpf_fd),
+ supported_transcoding_relative_paths(_supported_transcoding_relative_paths),
+ supported_uncached_relative_paths(_supported_uncached_relative_paths) {}
inline bool IsRoot(const node* node) const { return node == root; }
@@ -281,6 +293,14 @@
return node::FromInode(inode, &tracker);
}
+ inline node* FromInodeNoThrow(__u64 inode) {
+ if (inode == FUSE_ROOT_ID) {
+ return root;
+ }
+
+ return node::FromInodeNoThrow(inode, &tracker);
+ }
+
inline __u64 ToInode(node* node) const {
if (IsRoot(node)) {
return FUSE_ROOT_ID;
@@ -305,6 +325,41 @@
return false;
}
+ inline bool IsUncachedPath(const std::string& path) {
+ const std::string base_path = GetEffectiveRootPath() + "/";
+ for (const std::string& relative_path : supported_uncached_relative_paths) {
+ if (android::base::StartsWithIgnoreCase(path, base_path + relative_path)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ inline bool ShouldNotCache(const std::string& path) {
+ if (uncached_mode) {
+ // Cache is disabled for the entire volume.
+ return true;
+ }
+
+ if (supported_uncached_relative_paths.empty()) {
+ // By default there is no supported uncached path. Just return early in this case.
+ return false;
+ }
+
+ if (!android::base::StartsWithIgnoreCase(path, PRIMARY_VOLUME_PREFIX)) {
+ // Uncached path config applies only to primary volumes.
+ return false;
+ }
+
+ if (android::base::EndsWith(path, "/")) {
+ return IsUncachedPath(path);
+ } else {
+ // Append a slash at the end to make sure that the exact match is picked up.
+ return IsUncachedPath(path + "/");
+ }
+ }
+
std::recursive_mutex lock;
const string path;
// The Inode tracker associated with this FUSE instance.
@@ -312,6 +367,8 @@
node* const root;
struct fuse_session* se;
+ const bool uncached_mode;
+
/*
* Used to make JNI calls to MediaProvider.
* Responsibility of freeing this object falls on corresponding
@@ -330,9 +387,14 @@
std::atomic_bool* active;
std::atomic_bool disable_dentry_cache;
std::atomic_bool passthrough;
+ std::atomic_bool bpf;
+
+ const int bpf_fd;
+
// FUSE device id.
std::atomic_uint dev;
const std::vector<string> supported_transcoding_relative_paths;
+ const std::vector<string> supported_uncached_relative_paths;
};
struct OpenInfo {
@@ -413,6 +475,10 @@
return std::regex_match(path, PATTERN_OWNED_PATH);
}
+static bool is_bpf_backing_path(const string& path) {
+ return std::regex_match(path, PATTERN_BPF_BACKING_PATH);
+}
+
// See fuse_lowlevel.h fuse_lowlevel_notify_inval_entry for how to call this safetly without
// deadlocking the kernel
static void fuse_inval(fuse_session* se, fuse_ino_t parent_ino, fuse_ino_t child_ino,
@@ -429,13 +495,12 @@
}
}
-static double get_entry_timeout(const string& path, node* node, struct fuse* fuse) {
+static double get_entry_timeout(const string& path, bool should_inval, struct fuse* fuse) {
string media_path = fuse->GetEffectiveRootPath() + "/Android/media";
- if (fuse->disable_dentry_cache || node->ShouldInvalidate() ||
- is_package_owned_path(path, fuse->path) ||
- android::base::StartsWithIgnoreCase(path, media_path)) {
+ if (fuse->disable_dentry_cache || should_inval || is_package_owned_path(path, fuse->path) ||
+ android::base::StartsWithIgnoreCase(path, media_path) || fuse->ShouldNotCache(path)) {
// We set dentry timeout to 0 for the following reasons:
- // 1. The dentry cache was completely disabled
+ // 1. The dentry cache was completely disabled for the entire volume.
// 2.1 Case-insensitive lookups need to invalidate other case-insensitive dentry matches
// 2.2 Nodes supporting transforms need to be invalidated, so that subsequent lookups by a
// uid requiring a transform is guaranteed to come to the FUSE daemon.
@@ -445,6 +510,7 @@
// 4. Installd might delete Android/media/<package> dirs when app data is cleared.
// This can leave a stale entry in the kernel dcache, and break subsequent creation of the
// dir via FUSE.
+ // 5. The dentry cache was completely disabled for the given path.
return 0;
}
return std::numeric_limits<double>::max();
@@ -544,32 +610,34 @@
return nullptr;
}
- const bool should_invalidate = file_lookup_result->transforms_supported;
+ bool should_invalidate = file_lookup_result->transforms_supported;
const bool transforms_complete = file_lookup_result->transforms_complete;
const int transforms = file_lookup_result->transforms;
const int transforms_reason = file_lookup_result->transforms_reason;
const string& io_path = file_lookup_result->io_path;
+ if (transforms) {
+ // If the node requires transforms, we MUST never cache it in the VFS
+ CHECK(should_invalidate);
+ }
node = parent->LookupChildByName(name, true /* acquire */, transforms);
if (!node) {
ino_t ino = e->attr.st_ino;
- node = ::node::Create(parent, name, io_path, should_invalidate, transforms_complete,
- transforms, transforms_reason, &fuse->lock, ino, &fuse->tracker);
+ node = ::node::Create(parent, name, io_path, transforms_complete, transforms,
+ transforms_reason, &fuse->lock, ino, &fuse->tracker);
} else if (!mediaprovider::fuse::containsMount(path)) {
- // Only invalidate a path if it does not contain mount.
+ // Only invalidate a path if it does not contain mount and |name| != node->GetName.
// Invalidate both names to ensure there's no dentry left in the kernel after the following
// operations:
// 1) touch foo, touch FOO, unlink *foo*
// 2) touch foo, touch FOO, unlink *FOO*
// Invalidating lookup_name fixes (1) and invalidating node_name fixes (2)
- // SetShouldInvalidate invalidates lookup_name by using 0 timeout below and we explicitly
- // invalidate node_name if different case
- // Note that we invalidate async otherwise we will deadlock the kernel
+ // -Set |should_invalidate| to true to invalidate lookup_name by using 0 timeout below
+ // -Explicitly invalidate node_name. Note that we invalidate async otherwise we will
+ // deadlock the kernel
if (name != node->GetName()) {
- // Record that we have made a case insensitive lookup, this allows us invalidate nodes
- // correctly on subsequent lookups for the case of |node|
- node->SetShouldInvalidate();
-
+ // Force node invalidation to fix the kernel dentry cache for case (1) above
+ should_invalidate = true;
// Make copies of the node name and path so we're not attempting to acquire
// any node locks from the invalidation thread. Depending on timing, we may end
// up invalidating the wrong inode but that shouldn't result in correctness issues.
@@ -578,6 +646,9 @@
const std::string& node_name = node->GetName();
std::thread t([=]() { fuse_inval(fuse->se, parent_ino, child_ino, node_name, path); });
t.detach();
+ // Update the name after |node_name| reference above has been captured in lambda
+ // This avoids invalidating the node again on subsequent accesses with |name|
+ node->SetName(name);
}
// This updated value allows us correctly decide if to keep_cache and use direct_io during
@@ -609,8 +680,19 @@
// reuse inode numbers.
e->generation = 0;
e->ino = fuse->ToInode(node);
- e->entry_timeout = get_entry_timeout(path, node, fuse);
- e->attr_timeout = std::numeric_limits<double>::max();
+
+ // When FUSE BPF is used, the caching of node attributes and lookups is
+ // disabled to avoid possible inconsistencies between the FUSE cache and
+ // the lower file system state.
+ // With FUSE BPF the file system requests are forwarded to the lower file
+ // system bypassing the FUSE daemon, so dropping the caching does not
+ // introduce a performance regression.
+ // Currently FUSE BPF is limited to the Android/data and Android/obb
+ // directories.
+ if (!fuse->bpf || !is_bpf_backing_path(path)) {
+ e->entry_timeout = get_entry_timeout(path, should_invalidate, fuse);
+ e->attr_timeout = std::numeric_limits<double>::max();
+ }
return node;
}
@@ -679,7 +761,9 @@
}
// Return true if the path is accessible for that uid.
-static bool is_app_accessible_path(MediaProviderWrapper* mp, const string& path, uid_t uid) {
+static bool is_app_accessible_path(struct fuse* fuse, const string& path, uid_t uid) {
+ MediaProviderWrapper* mp = fuse->mp;
+
if (uid < AID_APP_START || uid == MY_UID) {
return true;
}
@@ -698,7 +782,7 @@
if (pkg == ".nomedia") {
return true;
}
- if (android::base::StartsWith(path, PRIMARY_VOLUME_PREFIX)) {
+ if (!fuse->bpf && android::base::StartsWith(path, PRIMARY_VOLUME_PREFIX)) {
// Emulated storage bind-mounts app-private data directories, and so these
// should not be accessible through FUSE anyway.
LOG(WARNING) << "Rejected access to app-private dir on FUSE: " << path
@@ -713,9 +797,52 @@
return true;
}
+void fuse_bpf_fill_entries(const string& path, const int bpf_fd, struct fuse_entry_param* e,
+ int& backing_fd) {
+ /*
+ * The file descriptor `fd` must not be closed as it is closed
+ * automatically by the kernel as soon as it consumes the FUSE reply. This
+ * mechanism is necessary because userspace doesn't know when the kernel
+ * will consume the FUSE response containing `fd`, thus it may close the
+ * `fd` too soon, with the risk of assigning a backing file which is either
+ * invalid or corresponds to the wrong file in the lower file system.
+ */
+ backing_fd = open(path.c_str(), O_CLOEXEC | O_DIRECTORY | O_RDONLY);
+ if (backing_fd < 0) {
+ PLOG(ERROR) << "Failed to open: " << path;
+ return;
+ }
+
+ e->backing_action = FUSE_ACTION_REPLACE;
+ e->backing_fd = backing_fd;
+
+ if (bpf_fd >= 0) {
+ e->bpf_action = FUSE_ACTION_REPLACE;
+ e->bpf_fd = bpf_fd;
+ } else if (bpf_fd == static_cast<int>(BpfFd::REMOVE)) {
+ e->bpf_action = FUSE_ACTION_REMOVE;
+ } else {
+ e->bpf_action = FUSE_ACTION_KEEP;
+ }
+}
+
+void fuse_bpf_install(struct fuse* fuse, struct fuse_entry_param* e, const string& child_path,
+ int& backing_fd) {
+ // TODO(b/211873756) Enable only for the primary volume. Must be
+ // extended for other media devices.
+ if (android::base::StartsWith(child_path, PRIMARY_VOLUME_PREFIX)) {
+ if (is_bpf_backing_path(child_path)) {
+ fuse_bpf_fill_entries(child_path, fuse->bpf_fd, e, backing_fd);
+ } else if (is_package_owned_path(child_path, fuse->path)) {
+ fuse_bpf_fill_entries(child_path, static_cast<int>(BpfFd::REMOVE), e, backing_fd);
+ }
+ }
+}
+
static std::regex storage_emulated_regex("^\\/storage\\/emulated\\/([0-9]+)");
static node* do_lookup(fuse_req_t req, fuse_ino_t parent, const char* name,
- struct fuse_entry_param* e, int* error_code, const FuseOp op) {
+ struct fuse_entry_param* e, int* error_code, const FuseOp op,
+ int* backing_fd = NULL) {
struct fuse* fuse = get_fuse(req);
node* parent_node = fuse->FromInode(parent);
if (!parent_node) {
@@ -725,7 +852,7 @@
string parent_path = parent_node->BuildPath();
// We should always allow lookups on the root, because failing them could cause
// bind mounts to be invalidated.
- if (!fuse->IsRoot(parent_node) && !is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+ if (!fuse->IsRoot(parent_node) && !is_app_accessible_path(fuse, parent_path, req->ctx.uid)) {
*error_code = ENOENT;
return nullptr;
}
@@ -748,20 +875,27 @@
}
}
- return make_node_entry(req, parent_node, name, child_path, e, error_code, op);
+ auto node = make_node_entry(req, parent_node, name, child_path, e, error_code, op);
+
+ if (fuse->bpf && op == FuseOp::lookup) fuse_bpf_install(fuse, e, child_path, *backing_fd);
+
+ return node;
}
static void pf_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) {
ATRACE_CALL();
struct fuse_entry_param e;
+ int backing_fd = -1;
int error_code = 0;
- if (do_lookup(req, parent, name, &e, &error_code, FuseOp::lookup)) {
+ if (do_lookup(req, parent, name, &e, &error_code, FuseOp::lookup, &backing_fd)) {
fuse_reply_entry(req, &e);
} else {
CHECK(error_code != 0);
fuse_reply_err(req, error_code);
}
+
+ if (backing_fd != -1) close(backing_fd);
}
static void do_forget(fuse_req_t req, struct fuse* fuse, fuse_ino_t ino, uint64_t nlookup) {
@@ -818,7 +952,7 @@
return;
}
const string& path = get_path(node);
- if (!is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+ if (!is_app_accessible_path(fuse, path, req->ctx.uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -846,7 +980,7 @@
return;
}
const string& path = get_path(node);
- if (!is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+ if (!is_app_accessible_path(fuse, path, req->ctx.uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -941,7 +1075,7 @@
node* node = fuse->FromInode(ino);
const string& path = node ? get_path(node) : "";
- if (node && is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+ if (node && is_app_accessible_path(fuse, path, req->ctx.uid)) {
// TODO(b/147482155): Check that uid has access to |path| and its contents
fuse_reply_canonical_path(req, path.c_str());
return;
@@ -962,7 +1096,7 @@
return;
}
string parent_path = parent_node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+ if (!is_app_accessible_path(fuse, parent_path, req->ctx.uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -1000,7 +1134,7 @@
}
const struct fuse_ctx* ctx = fuse_req_ctx(req);
const string parent_path = parent_node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, parent_path, ctx->uid)) {
+ if (!is_app_accessible_path(fuse, parent_path, ctx->uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -1041,7 +1175,7 @@
}
const struct fuse_ctx* ctx = fuse_req_ctx(req);
const string parent_path = parent_node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, parent_path, ctx->uid)) {
+ if (!is_app_accessible_path(fuse, parent_path, ctx->uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -1070,7 +1204,7 @@
return;
}
const string parent_path = parent_node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+ if (!is_app_accessible_path(fuse, parent_path, req->ctx.uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -1125,7 +1259,7 @@
if (!old_parent_node) return ENOENT;
const struct fuse_ctx* ctx = fuse_req_ctx(req);
const string old_parent_path = old_parent_node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, old_parent_path, ctx->uid)) {
+ if (!is_app_accessible_path(fuse, old_parent_path, ctx->uid)) {
return ENOENT;
}
@@ -1135,10 +1269,16 @@
return ENOENT;
}
- node* new_parent_node = fuse->FromInode(new_parent);
- if (!new_parent_node) return ENOENT;
+ node* new_parent_node;
+ if (fuse->bpf) {
+ new_parent_node = fuse->FromInodeNoThrow(new_parent);
+ if (!new_parent_node) return EXDEV;
+ } else {
+ new_parent_node = fuse->FromInode(new_parent);
+ if (!new_parent_node) return ENOENT;
+ }
const string new_parent_path = new_parent_node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, new_parent_path, ctx->uid)) {
+ if (!is_app_accessible_path(fuse, new_parent_path, ctx->uid)) {
return ENOENT;
}
@@ -1236,7 +1376,7 @@
(redaction_needed && !has_redacted) || (!redaction_needed && has_redacted);
bool is_cached_file_open = node->HasCachedHandle();
bool direct_io = open_info_direct_io || (is_cached_file_open && is_redaction_change) ||
- is_file_locked(fd, path);
+ is_file_locked(fd, path) || fuse->ShouldNotCache(path);
if (!is_cached_file_open && is_redaction_change) {
node->SetRedactedCache(redaction_needed);
@@ -1314,7 +1454,7 @@
const struct fuse_ctx* ctx = fuse_req_ctx(req);
const string& io_path = get_path(node);
const string& build_path = node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, io_path, ctx->uid)) {
+ if (!is_app_accessible_path(fuse, io_path, ctx->uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -1551,6 +1691,25 @@
}
#endif
+/*
+ * This function does nothing except being a placeholder to keep the FUSE
+ * driver handling flushes on close(2).
+ * In fact, kernels prior to 5.8 stop attempting flushing the cache on close(2)
+ * if the .flush operation is not implemented by the FUSE daemon.
+ * This has been fixed in the kernel by commit 614c026e8a46 ("fuse: always
+ * flush dirty data on close(2)"), merged in Linux 5.8, but until then
+ * userspace must mitigate this behavior by not leaving the .flush function
+ * pointer empty.
+ */
+static void pf_flush(fuse_req_t req,
+ fuse_ino_t ino,
+ struct fuse_file_info* fi) {
+ ATRACE_CALL();
+ struct fuse* fuse = get_fuse(req);
+ TRACE_NODE(nullptr, req) << "noop";
+ fuse_reply_err(req, 0);
+}
+
static void pf_release(fuse_req_t req,
fuse_ino_t ino,
struct fuse_file_info* fi) {
@@ -1609,7 +1768,7 @@
}
const struct fuse_ctx* ctx = fuse_req_ctx(req);
const string path = node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, path, ctx->uid)) {
+ if (!is_app_accessible_path(fuse, path, ctx->uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -1659,7 +1818,7 @@
return;
}
const string path = node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+ if (!is_app_accessible_path(fuse, path, req->ctx.uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -1807,7 +1966,7 @@
return;
}
const string path = node->BuildPath();
- if (path != PRIMARY_VOLUME_PREFIX && !is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+ if (path != PRIMARY_VOLUME_PREFIX && !is_app_accessible_path(fuse, path, req->ctx.uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -1872,7 +2031,7 @@
return;
}
const string parent_path = parent_node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+ if (!is_app_accessible_path(fuse, parent_path, req->ctx.uid)) {
fuse_reply_err(req, ENOENT);
return;
}
@@ -1996,7 +2155,7 @@
/*.link = pf_link,*/
.open = pf_open, .read = pf_read,
/*.write = pf_write,*/
- /*.flush = pf_flush,*/
+ .flush = pf_flush,
.release = pf_release, .fsync = pf_fsync, .opendir = pf_opendir, .readdir = pf_readdir,
.releasedir = pf_releasedir, .fsyncdir = pf_fsyncdir, .statfs = pf_statfs,
/*.setxattr = pf_setxattr,
@@ -2066,6 +2225,10 @@
return use_fuse;
}
+bool FuseDaemon::UsesFusePassthrough() const {
+ return fuse->passthrough;
+}
+
void FuseDaemon::InvalidateFuseDentryCache(const std::string& path) {
LOG(VERBOSE) << "Invalidating FUSE dentry cache";
if (active.load(std::memory_order_acquire)) {
@@ -2097,8 +2260,20 @@
return active.load(std::memory_order_acquire);
}
+bool IsFuseBpfEnabled() {
+ std::string bpf_override = android::base::GetProperty("persist.sys.fuse.bpf.override", "");
+ if (bpf_override == "true") {
+ return true;
+ } else if (bpf_override == "false") {
+ return false;
+ }
+ return android::base::GetBoolProperty("ro.fuse.bpf.enabled", false);
+}
+
void FuseDaemon::Start(android::base::unique_fd fd, const std::string& path,
- const std::vector<std::string>& supported_transcoding_relative_paths) {
+ const bool uncached_mode,
+ const std::vector<std::string>& supported_transcoding_relative_paths,
+ const std::vector<std::string>& supported_uncached_relative_paths) {
android::base::SetDefaultTag(LOG_TAG);
struct fuse_args args;
@@ -2123,7 +2298,23 @@
return;
}
- struct fuse fuse_default(path, stat.st_ino, supported_transcoding_relative_paths);
+ bool bpf_enabled = IsFuseBpfEnabled();
+ int bpf_fd = -1;
+ if (bpf_enabled) {
+ LOG(INFO) << "Using FUSE BPF";
+
+ bpf_fd = android::bpf::bpfFdGet(FUSE_BPF_PROG_PATH, BPF_F_RDONLY);
+ if (bpf_fd < 0) {
+ PLOG(ERROR) << "Failed to fetch BPF prog fd: " << bpf_fd;
+ bpf_enabled = false;
+ } else {
+ LOG(INFO) << "BPF prog fd fetched";
+ }
+ }
+
+ struct fuse fuse_default(path, stat.st_ino, uncached_mode, bpf_enabled, bpf_fd,
+ supported_transcoding_relative_paths,
+ supported_uncached_relative_paths);
fuse_default.mp = ∓
// fuse_default is stack allocated, but it's safe to save it as an instance variable because
// this method blocks and FuseDaemon#active tells if we are currently blocking
diff --git a/jni/FuseDaemon.h b/jni/FuseDaemon.h
index 4f90209..9db8583 100644
--- a/jni/FuseDaemon.h
+++ b/jni/FuseDaemon.h
@@ -38,8 +38,9 @@
/**
* Start the FUSE daemon loop that will handle filesystem calls.
*/
- void Start(android::base::unique_fd fd, const std::string& path,
- const std::vector<std::string>& supported_transcoding_relative_paths);
+ void Start(android::base::unique_fd fd, const std::string& path, const bool uncached_mode,
+ const std::vector<std::string>& supported_transcoding_relative_paths,
+ const std::vector<std::string>& supported_uncached_relative_paths);
/**
* Checks if the FUSE daemon is started.
@@ -52,6 +53,11 @@
bool ShouldOpenWithFuse(int fd, bool for_read, const std::string& path);
/**
+ * Check if the FUSE daemon uses FUSE passthrough
+ */
+ bool UsesFusePassthrough() const;
+
+ /**
* Invalidate FUSE VFS dentry cache entry for path
*/
void InvalidateFuseDentryCache(const std::string& path);
diff --git a/jni/FuseUtils.cpp b/jni/FuseUtils.cpp
index 9f30440..7b08164 100644
--- a/jni/FuseUtils.cpp
+++ b/jni/FuseUtils.cpp
@@ -35,11 +35,8 @@
return false;
}
- // Skip over the user-id by finding the next '/'
- size_t pos = path.find_first_of("/", prefix.length());
- // If we can't find another '/', or the '/' immediately follows the previous,
- // ('/storage/emulated//'), not a valid mount.
- if (pos == std::string::npos || pos == prefix.length()) {
+ size_t pos = path.find_first_of('/', prefix.length());
+ if (pos == std::string::npos) {
return false;
}
diff --git a/jni/FuseUtilsTest.cpp b/jni/FuseUtilsTest.cpp
index dede692..d76a89c 100644
--- a/jni/FuseUtilsTest.cpp
+++ b/jni/FuseUtilsTest.cpp
@@ -20,7 +20,7 @@
#include <gtest/gtest.h>
-using namespace mediaprovider::fuse;
+namespace mediaprovider::fuse {
TEST(FuseUtilsTest, testContainsMount_isTrueForAndroidDataObb) {
EXPECT_TRUE(containsMount("/storage/emulated/1234/Android"));
@@ -39,7 +39,6 @@
EXPECT_FALSE(containsMount("/storage/emulated/"));
EXPECT_FALSE(containsMount("/storage/emulated//"));
EXPECT_FALSE(containsMount("/storage/emulated/0/"));
- EXPECT_FALSE(containsMount("/storage/emulated/1234/"));
}
TEST(FuseUtilsTest, testContainsMount_isCaseInsensitive) {
@@ -51,8 +50,6 @@
}
TEST(FuseUtilsTest, testContainsMount_isFalseForPathWithAdditionalSlash) {
- EXPECT_FALSE(containsMount("/storage/emulated//Android/data"));
-
EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/"));
EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/data/"));
EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/obb/"));
@@ -61,3 +58,5 @@
EXPECT_FALSE(containsMount("/storage/emulated//1234/Android/data"));
EXPECT_FALSE(containsMount("/storage/emulated/1234//Android/data"));
}
+
+} // namespace mediaprovider::fuse
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
index 01411f5..253dbbe 100644
--- a/jni/MediaProviderWrapper.cpp
+++ b/jni/MediaProviderWrapper.cpp
@@ -31,7 +31,6 @@
namespace mediaprovider {
namespace fuse {
-using android::base::GetBoolProperty;
using std::string;
namespace {
@@ -41,6 +40,14 @@
constexpr uid_t ROOT_UID = 0;
constexpr uid_t SHELL_UID = 2000;
+// These need to stay in sync with MediaProvider.java's DIRECTORY_ACCESS_FOR_* constants.
+enum DirectoryAccessRequestType {
+ kReadDirectoryRequest = 1,
+ kWriteDirectoryRequest = 2,
+ kCreateDirectoryRequest = 3,
+ kDeleteDirectoryRequest = 4,
+};
+
/** Private helper functions **/
inline bool shouldBypassMediaProvider(uid_t uid) {
@@ -91,25 +98,12 @@
return res;
}
-int isMkdirOrRmdirAllowedInternal(JNIEnv* env, jobject media_provider_object,
- jmethodID mid_is_mkdir_or_rmdir_allowed, const string& path,
- uid_t uid, bool forCreate) {
+int isDirAccessAllowedInternal(JNIEnv* env, jobject media_provider_object,
+ jmethodID mid_is_diraccess_allowed, const string& path, uid_t uid,
+ int accessType) {
ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
- int res = env->CallIntMethod(media_provider_object, mid_is_mkdir_or_rmdir_allowed, j_path.get(),
- uid, forCreate);
-
- if (CheckForJniException(env)) {
- return EFAULT;
- }
- return res;
-}
-
-int isOpendirAllowedInternal(JNIEnv* env, jobject media_provider_object,
- jmethodID mid_is_opendir_allowed, const string& path, uid_t uid,
- bool forWrite) {
- ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
- int res = env->CallIntMethod(media_provider_object, mid_is_opendir_allowed, j_path.get(), uid,
- forWrite);
+ int res = env->CallIntMethod(media_provider_object, mid_is_diraccess_allowed, j_path.get(), uid,
+ accessType);
if (CheckForJniException(env)) {
return EFAULT;
@@ -228,37 +222,24 @@
media_provider_class_ = reinterpret_cast<jclass>(env->NewGlobalRef(media_provider_class_));
// Cache methods - Before calling a method, make sure you cache it here
- mid_insert_file_ = CacheMethod(env, "insertFileIfNecessary", "(Ljava/lang/String;I)I",
- /*is_static*/ false);
- mid_delete_file_ = CacheMethod(env, "deleteFile", "(Ljava/lang/String;I)I", /*is_static*/ false);
+ mid_insert_file_ = CacheMethod(env, "insertFileIfNecessary", "(Ljava/lang/String;I)I");
+ mid_delete_file_ = CacheMethod(env, "deleteFile", "(Ljava/lang/String;I)I");
mid_on_file_open_ = CacheMethod(env, "onFileOpen",
"(Ljava/lang/String;Ljava/lang/String;IIIZZZ)Lcom/android/"
- "providers/media/FileOpenResult;",
- /*is_static*/ false);
- mid_is_mkdir_or_rmdir_allowed_ = CacheMethod(env, "isDirectoryCreationOrDeletionAllowed",
- "(Ljava/lang/String;IZ)I", /*is_static*/ false);
- mid_is_opendir_allowed_ = CacheMethod(env, "isOpendirAllowed", "(Ljava/lang/String;IZ)I",
- /*is_static*/ false);
+ "providers/media/FileOpenResult;");
+ mid_is_diraccess_allowed_ = CacheMethod(env, "isDirAccessAllowed", "(Ljava/lang/String;II)I");
mid_get_files_in_dir_ =
- CacheMethod(env, "getFilesInDirectory", "(Ljava/lang/String;I)[Ljava/lang/String;",
- /*is_static*/ false);
- mid_rename_ = CacheMethod(env, "rename", "(Ljava/lang/String;Ljava/lang/String;I)I",
- /*is_static*/ false);
+ CacheMethod(env, "getFilesInDirectory", "(Ljava/lang/String;I)[Ljava/lang/String;");
+ mid_rename_ = CacheMethod(env, "rename", "(Ljava/lang/String;Ljava/lang/String;I)I");
mid_is_uid_allowed_access_to_data_or_obb_path_ =
- CacheMethod(env, "isUidAllowedAccessToDataOrObbPath", "(ILjava/lang/String;)Z",
- /*is_static*/ false);
- mid_on_file_created_ = CacheMethod(env, "onFileCreated", "(Ljava/lang/String;)V",
- /*is_static*/ false);
- mid_should_allow_lookup_ = CacheMethod(env, "shouldAllowLookup", "(II)Z",
- /*is_static*/ false);
- mid_is_app_clone_user_ = CacheMethod(env, "isAppCloneUser", "(I)Z",
- /*is_static*/ false);
- mid_transform_ = CacheMethod(env, "transform", "(Ljava/lang/String;Ljava/lang/String;IIIII)Z",
- /*is_static*/ false);
+ CacheMethod(env, "isUidAllowedAccessToDataOrObbPath", "(ILjava/lang/String;)Z");
+ mid_on_file_created_ = CacheMethod(env, "onFileCreated", "(Ljava/lang/String;)V");
+ mid_should_allow_lookup_ = CacheMethod(env, "shouldAllowLookup", "(II)Z");
+ mid_is_app_clone_user_ = CacheMethod(env, "isAppCloneUser", "(I)Z");
+ mid_transform_ = CacheMethod(env, "transform", "(Ljava/lang/String;Ljava/lang/String;IIIII)Z");
mid_file_lookup_ =
CacheMethod(env, "onFileLookup",
- "(Ljava/lang/String;II)Lcom/android/providers/media/FileLookupResult;",
- /*is_static*/ false);
+ "(Ljava/lang/String;II)Lcom/android/providers/media/FileLookupResult;");
// FileLookupResult
file_lookup_result_class_ = env->FindClass("com/android/providers/media/FileLookupResult");
@@ -375,9 +356,8 @@
}
JNIEnv* env = MaybeAttachCurrentThread();
- return isMkdirOrRmdirAllowedInternal(env, media_provider_object_,
- mid_is_mkdir_or_rmdir_allowed_, path, uid,
- /*forCreate*/ true);
+ return isDirAccessAllowedInternal(env, media_provider_object_, mid_is_diraccess_allowed_, path,
+ uid, kCreateDirectoryRequest);
}
int MediaProviderWrapper::IsDeletingDirAllowed(const string& path, uid_t uid) {
@@ -386,9 +366,8 @@
}
JNIEnv* env = MaybeAttachCurrentThread();
- return isMkdirOrRmdirAllowedInternal(env, media_provider_object_,
- mid_is_mkdir_or_rmdir_allowed_, path, uid,
- /*forCreate*/ false);
+ return isDirAccessAllowedInternal(env, media_provider_object_, mid_is_diraccess_allowed_, path,
+ uid, kDeleteDirectoryRequest);
}
std::vector<std::shared_ptr<DirectoryEntry>> MediaProviderWrapper::GetDirectoryEntries(
@@ -421,8 +400,9 @@
}
JNIEnv* env = MaybeAttachCurrentThread();
- return isOpendirAllowedInternal(env, media_provider_object_, mid_is_opendir_allowed_, path, uid,
- forWrite);
+ return isDirAccessAllowedInternal(env, media_provider_object_, mid_is_diraccess_allowed_, path,
+ uid,
+ forWrite ? kWriteDirectoryRequest : kReadDirectoryRequest);
}
bool MediaProviderWrapper::isUidAllowedAccessToDataOrObbPath(uid_t uid, const string& path) {
@@ -536,15 +516,12 @@
* Finds MediaProvider method and adds it to methods map so it can be quickly called later.
*/
jmethodID MediaProviderWrapper::CacheMethod(JNIEnv* env, const char method_name[],
- const char signature[], bool is_static) {
+ const char signature[]) {
jmethodID mid;
string actual_method_name(method_name);
actual_method_name.append("ForFuse");
- if (is_static) {
- mid = env->GetStaticMethodID(media_provider_class_, actual_method_name.c_str(), signature);
- } else {
- mid = env->GetMethodID(media_provider_class_, actual_method_name.c_str(), signature);
- }
+ mid = env->GetMethodID(media_provider_class_, actual_method_name.c_str(), signature);
+
if (!mid) {
LOG(FATAL) << "Error caching method: " << method_name << signature;
}
diff --git a/jni/MediaProviderWrapper.h b/jni/MediaProviderWrapper.h
index bc7c656..9fa6c4e 100644
--- a/jni/MediaProviderWrapper.h
+++ b/jni/MediaProviderWrapper.h
@@ -264,8 +264,7 @@
jmethodID mid_delete_file_;
jmethodID mid_on_file_open_;
jmethodID mid_scan_file_;
- jmethodID mid_is_mkdir_or_rmdir_allowed_;
- jmethodID mid_is_opendir_allowed_;
+ jmethodID mid_is_diraccess_allowed_;
jmethodID mid_get_files_in_dir_;
jmethodID mid_rename_;
jmethodID mid_is_uid_allowed_access_to_data_or_obb_path_;
@@ -291,8 +290,7 @@
/**
* Auxiliary for caching MediaProvider methods.
*/
- jmethodID CacheMethod(JNIEnv* env, const char method_name[], const char signature[],
- bool is_static);
+ jmethodID CacheMethod(JNIEnv* env, const char method_name[], const char signature[]);
// Attaches the current thread (if necessary) and returns the JNIEnv
// associated with it.
diff --git a/jni/RedactionInfoTest.cpp b/jni/RedactionInfoTest.cpp
index 76eec13..3a7bd27 100644
--- a/jni/RedactionInfoTest.cpp
+++ b/jni/RedactionInfoTest.cpp
@@ -24,9 +24,8 @@
#include "libfuse_jni/RedactionInfo.h"
-using namespace mediaprovider::fuse;
+namespace mediaprovider::fuse {
-using std::unique_ptr;
using std::vector;
std::ostream& operator<<(std::ostream& os, const ReadRange& rr) {
@@ -381,3 +380,5 @@
info.getReadRanges(0, 40, &out); // read offsets [0, 40)
EXPECT_EQ(0, out.size());
}
+
+} // namespace mediaprovider::fuse
diff --git a/jni/TEST_MAPPING b/jni/TEST_MAPPING
index 5ee1bc6..aec3dd0 100644
--- a/jni/TEST_MAPPING
+++ b/jni/TEST_MAPPING
@@ -9,5 +9,16 @@
{
"name": "fuse_node_test"
}
+ ],
+ "hwasan-postsubmit": [
+ {
+ "name": "FuseUtilsTest"
+ },
+ {
+ "name": "RedactionInfoTest"
+ },
+ {
+ "name": "fuse_node_test"
+ }
]
}
diff --git a/jni/com_android_providers_media_FuseDaemon.cpp b/jni/com_android_providers_media_FuseDaemon.cpp
index 54c7a87..0215fa8 100644
--- a/jni/com_android_providers_media_FuseDaemon.cpp
+++ b/jni/com_android_providers_media_FuseDaemon.cpp
@@ -36,28 +36,38 @@
static jclass gFdAccessResultClass;
static jmethodID gFdAccessResultCtor;
-static std::vector<std::string> get_supported_transcoding_relative_paths(
- JNIEnv* env, jobjectArray java_supported_transcoding_relative_paths) {
- ScopedLocalRef<jobjectArray> j_transcoding_relative_paths(
- env, java_supported_transcoding_relative_paths);
- std::vector<std::string> transcoding_relative_paths;
+static std::vector<std::string> convert_object_array_to_string_vector(
+ JNIEnv* env, jobjectArray java_object_array, const std::string& element_description) {
+ ScopedLocalRef<jobjectArray> j_ref_object_array(env, java_object_array);
+ std::vector<std::string> utf_strings;
- const int transcoding_relative_paths_count =
- env->GetArrayLength(j_transcoding_relative_paths.get());
- for (int i = 0; i < transcoding_relative_paths_count; i++) {
- ScopedLocalRef<jstring> j_ref_relative_path(
- env, (jstring)env->GetObjectArrayElement(j_transcoding_relative_paths.get(), i));
- ScopedUtfChars j_utf_relative_path(env, j_ref_relative_path.get());
- const char* relative_path = j_utf_relative_path.c_str();
+ const int object_array_length = env->GetArrayLength(j_ref_object_array.get());
+ for (int i = 0; i < object_array_length; i++) {
+ ScopedLocalRef<jstring> j_ref_string(
+ env, (jstring)env->GetObjectArrayElement(j_ref_object_array.get(), i));
+ ScopedUtfChars utf_chars(env, j_ref_string.get());
+ const char* utf_string = utf_chars.c_str();
- if (relative_path) {
- transcoding_relative_paths.push_back(relative_path);
+ if (utf_string) {
+ utf_strings.push_back(utf_string);
} else {
- LOG(ERROR) << "Error reading supported transcoding relative path at index: " << i;
+ LOG(ERROR) << "Error reading " << element_description << " at index: " << i;
}
}
- return transcoding_relative_paths;
+ return utf_strings;
+}
+
+static std::vector<std::string> get_supported_transcoding_relative_paths(
+ JNIEnv* env, jobjectArray java_supported_transcoding_relative_paths) {
+ return convert_object_array_to_string_vector(env, java_supported_transcoding_relative_paths,
+ "supported transcoding relative path");
+}
+
+static std::vector<std::string> get_supported_uncached_relative_paths(
+ JNIEnv* env, jobjectArray java_supported_uncached_relative_paths) {
+ return convert_object_array_to_string_vector(env, java_supported_uncached_relative_paths,
+ "supported uncached relative path");
}
jlong com_android_providers_media_FuseDaemon_new(JNIEnv* env, jobject self,
@@ -68,7 +78,8 @@
void com_android_providers_media_FuseDaemon_start(
JNIEnv* env, jobject self, jlong java_daemon, jint fd, jstring java_path,
- jobjectArray java_supported_transcoding_relative_paths) {
+ jboolean uncached_mode, jobjectArray java_supported_transcoding_relative_paths,
+ jobjectArray java_supported_uncached_relative_paths) {
LOG(DEBUG) << "Starting the FUSE daemon...";
fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
@@ -82,8 +93,11 @@
const std::vector<std::string>& transcoding_relative_paths =
get_supported_transcoding_relative_paths(env,
java_supported_transcoding_relative_paths);
+ const std::vector<std::string>& uncached_relative_paths =
+ get_supported_uncached_relative_paths(env, java_supported_uncached_relative_paths);
- daemon->Start(std::move(ufd), utf_chars_path.c_str(), transcoding_relative_paths);
+ daemon->Start(std::move(ufd), utf_chars_path.c_str(), uncached_mode, transcoding_relative_paths,
+ uncached_relative_paths);
}
bool com_android_providers_media_FuseDaemon_is_started(JNIEnv* env, jobject self,
@@ -117,6 +131,15 @@
return JNI_FALSE;
}
+jboolean com_android_providers_media_FuseDaemon_uses_fuse_passthrough(JNIEnv* env, jobject self,
+ jlong java_daemon) {
+ fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
+ if (daemon) {
+ return daemon->UsesFusePassthrough();
+ }
+ return JNI_FALSE;
+}
+
void com_android_providers_media_FuseDaemon_invalidate_fuse_dentry_cache(JNIEnv* env, jobject self,
jlong java_daemon,
jstring java_path) {
@@ -162,12 +185,14 @@
const JNINativeMethod methods[] = {
{"native_new", "(Lcom/android/providers/media/MediaProvider;)J",
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_new)},
- {"native_start", "(JILjava/lang/String;[Ljava/lang/String;)V",
+ {"native_start", "(JILjava/lang/String;Z[Ljava/lang/String;[Ljava/lang/String;)V",
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_start)},
{"native_delete", "(J)V",
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_delete)},
{"native_should_open_with_fuse", "(JLjava/lang/String;ZI)Z",
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_should_open_with_fuse)},
+ {"native_uses_fuse_passthrough", "(J)Z",
+ reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_uses_fuse_passthrough)},
{"native_is_fuse_thread", "()Z",
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_is_fuse_thread)},
{"native_is_started", "(J)Z",
diff --git a/jni/node-inl.h b/jni/node-inl.h
index af30248..15844e3 100644
--- a/jni/node-inl.h
+++ b/jni/node-inl.h
@@ -99,6 +99,14 @@
public:
explicit NodeTracker(std::recursive_mutex* lock) : lock_(lock) {}
+ bool Exists(__u64 ino) const {
+ if (kEnableInodeTracking) {
+ const node* node = reinterpret_cast<const class node*>(ino);
+ std::lock_guard<std::recursive_mutex> guard(*lock_);
+ return active_nodes_.find(node) != active_nodes_.end();
+ }
+ }
+
void CheckTracked(__u64 ino) const {
if (kEnableInodeTracking) {
const node* node = reinterpret_cast<const class node*>(ino);
@@ -136,15 +144,15 @@
public:
// Creates a new node with the specified parent, name and lock.
static node* Create(node* parent, const std::string& name, const std::string& io_path,
- bool should_invalidate, bool transforms_complete, const int transforms,
+ const bool transforms_complete, const int transforms,
const int transforms_reason, std::recursive_mutex* lock, ino_t ino,
NodeTracker* tracker) {
// Place the entire constructor under a critical section to make sure
// node creation, tracking (if enabled) and the addition to a parent are
// atomic.
std::lock_guard<std::recursive_mutex> guard(*lock);
- return new node(parent, name, io_path, should_invalidate, transforms_complete, transforms,
- transforms_reason, lock, ino, tracker);
+ return new node(parent, name, io_path, transforms_complete, transforms, transforms_reason,
+ lock, ino, tracker);
}
// Creates a new root node. Root nodes have no parents by definition
@@ -152,9 +160,8 @@
static node* CreateRoot(const std::string& path, std::recursive_mutex* lock, ino_t ino,
NodeTracker* tracker) {
std::lock_guard<std::recursive_mutex> guard(*lock);
- node* root = new node(nullptr, path, path, false /* should_invalidate */,
- true /* transforms_complete */, 0 /* transforms */,
- 0 /* transforms_reason */, lock, ino, tracker);
+ node* root = new node(nullptr, path, path, true /* transforms_complete */,
+ 0 /* transforms */, 0 /* transforms_reason */, lock, ino, tracker);
// The root always has one extra reference to avoid it being
// accidentally collected.
@@ -168,6 +175,12 @@
return reinterpret_cast<node*>(static_cast<uintptr_t>(ino));
}
+ // TODO(b/215235604)
+ static inline node* FromInodeNoThrow(__u64 ino, const NodeTracker* tracker) {
+ if (!tracker->Exists(ino)) return nullptr;
+ return reinterpret_cast<node*>(static_cast<uintptr_t>(ino));
+ }
+
// Maps a node to its associated inode.
static __u64 ToInode(node* node) {
return static_cast<__u64>(reinterpret_cast<uintptr_t>(node));
@@ -327,7 +340,7 @@
}
return false;
}
-
+
std::unique_ptr<FdAccessResult> CheckHandleForUid(const uid_t uid) const {
std::lock_guard<std::recursive_mutex> guard(*lock_);
@@ -346,15 +359,10 @@
return std::make_unique<FdAccessResult>(std::string(), false);
}
-
- bool ShouldInvalidate() const {
- std::lock_guard<std::recursive_mutex> guard(*lock_);
- return should_invalidate_;
- }
- void SetShouldInvalidate() {
+ void SetName(std::string name) {
std::lock_guard<std::recursive_mutex> guard(*lock_);
- should_invalidate_ = true;
+ name_ = std::move(name);
}
bool HasRedactedCache() const {
@@ -394,8 +402,8 @@
private:
node(node* parent, const std::string& name, const std::string& io_path,
- const bool should_invalidate, const bool transforms_complete, const int transforms,
- const int transforms_reason, std::recursive_mutex* lock, ino_t ino, NodeTracker* tracker)
+ const bool transforms_complete, const int transforms, const int transforms_reason,
+ std::recursive_mutex* lock, ino_t ino, NodeTracker* tracker)
: name_(name),
io_path_(io_path),
transforms_complete_(transforms_complete),
@@ -404,7 +412,6 @@
refcount_(0),
parent_(nullptr),
has_redacted_cache_(false),
- should_invalidate_(should_invalidate),
deleted_(false),
lock_(lock),
ino_(ino),
@@ -416,10 +423,6 @@
if (parent != nullptr) {
AddToParent(parent);
}
- // If the node requires transforms, we MUST never cache it in the VFS
- if (transforms) {
- CHECK(should_invalidate_);
- }
}
// Acquires a reference to a node. This maps to the "lookup count" specified
@@ -558,7 +561,6 @@
// List of directory handles associated with this node. Guarded by |lock_|.
std::vector<std::unique_ptr<dirhandle>> dirhandles_;
bool has_redacted_cache_;
- bool should_invalidate_;
bool deleted_;
std::recursive_mutex* lock_;
// Inode number of the file represented by this node.
diff --git a/jni/node_test.cpp b/jni/node_test.cpp
index 7d6bfc8..f687cad 100644
--- a/jni/node_test.cpp
+++ b/jni/node_test.cpp
@@ -7,7 +7,6 @@
#include <memory>
#include <mutex>
-using mediaprovider::fuse::dirhandle;
using mediaprovider::fuse::handle;
using mediaprovider::fuse::node;
using mediaprovider::fuse::NodeTracker;
@@ -33,7 +32,7 @@
unique_node_ptr CreateNode(node* parent, const std::string& path, const int transforms = 0) {
return unique_node_ptr(
- node::Create(parent, path, "", true, true, transforms, 0, &lock_, 0, &tracker_),
+ node::Create(parent, path, "", true, transforms, 0, &lock_, 0, &tracker_),
&NodeTest::destroy);
}
@@ -68,7 +67,7 @@
}
TEST_F(NodeTest, TestRelease) {
- node* node = node::Create(nullptr, "/path", "", false, true, 0, 0, &lock_, 0, &tracker_);
+ node* node = node::Create(nullptr, "/path", "", true, 0, 0, &lock_, 0, &tracker_);
acquire(node);
acquire(node);
ASSERT_EQ(3, GetRefCount(node));
@@ -278,10 +277,10 @@
unique_node_ptr parent = CreateNode(nullptr, "/path");
// This is the tree that we intend to delete.
- node* child = node::Create(parent.get(), "subdir", "", false, true, 0, 0, &lock_, 0, &tracker_);
- node::Create(child, "s1", "", false, true, 0, 0, &lock_, 0, &tracker_);
- node* subchild2 = node::Create(child, "s2", "", false, true, 0, 0, &lock_, 0, &tracker_);
- node::Create(subchild2, "sc2", "", false, true, 0, 0, &lock_, 0, &tracker_);
+ node* child = node::Create(parent.get(), "subdir", "", true, 0, 0, &lock_, 0, &tracker_);
+ node::Create(child, "s1", "", true, 0, 0, &lock_, 0, &tracker_);
+ node* subchild2 = node::Create(child, "s2", "", true, 0, 0, &lock_, 0, &tracker_);
+ node::Create(subchild2, "sc2", "", true, 0, 0, &lock_, 0, &tracker_);
ASSERT_EQ(child, parent->LookupChildByName("subdir", false /* acquire */));
node::DeleteTree(child);
diff --git a/legacy/Android.bp b/legacy/Android.bp
index 3b03bc8..14d7f55 100644
--- a/legacy/Android.bp
+++ b/legacy/Android.bp
@@ -1,11 +1,6 @@
-
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
android_app {
diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
index d51414b..9951bc5 100644
--- a/legacy/src/com/android/providers/media/LegacyMediaProvider.java
+++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
@@ -80,9 +80,9 @@
Logging.initPersistent(persistentDir);
mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, true, null,
- null, null, null, null, null);
+ null, null, null, null, null, false);
mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, true, null,
- null, null, null, null, null);
+ null, null, null, null, null, false);
return true;
}
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 1f44649..9eef54e 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Ontdemp video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Speel video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Onderbreek video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Wolkmedia is nou deur <xliff:g id="PKG_NAME">%1$s</xliff:g> beskikbaar"</string>
<string name="not_selected" msgid="2244008151669896758">"nie gekies nie"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie oudiolêer te wysig?}other{Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> oudiolêers te wysig?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Wysig tans oudiolêer …}other{Wysig tans <xliff:g id="COUNT">^1</xliff:g> oudiolêers …}}"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 264f1dc..d99aa0b 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"የቪዲዮን ድምፀ-ከል አንሣ"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ቪድዮ አጫውት"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ቪዲዮን ባለበት አቁም"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"የደመና ሚዲያ አሁን ከ<xliff:g id="PKG_NAME">%1$s</xliff:g> ይገኛል"</string>
<string name="not_selected" msgid="2244008151669896758">"አልተመረጠም"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> ይህን ኦዲዮ ፋይል እንዲቀይር ይፈቀድለት?}one{<xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ኦዲዮ ፋይልን እንዲቀይር ይፈቀድለት?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ኦዲዮ ፋይሎችን እንዲቀይር ይፈቀድለት?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{የኦዲዮ ፋይልን በመቀየር ላይ…}one{<xliff:g id="COUNT">^1</xliff:g> የኦዲዮ ፋይልን በመቀየር ላይ…}other{<xliff:g id="COUNT">^1</xliff:g> የኦዲዮ ፋይሎችን በመቀየር ላይ…}}"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 5ace0b6..cde7793 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"إعادة صوت الفيديو"</string>
<string name="picker_play_video" msgid="5158816108935317185">"تشغيل الفيديو"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"إيقاف الفيديو مؤقتًا"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"يتوفّر محتوى الوسائط على السحابة الإلكترونية الآن من خلال تطبيق <xliff:g id="PKG_NAME">%1$s</xliff:g>."</string>
<string name="not_selected" msgid="2244008151669896758">"غير محدّد"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_0">^1</xliff:g> بتعديل هذا الملف الصوتي؟}zero{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملف صوتي؟}two{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل ملفَين صوتيين (<xliff:g id="COUNT">^2</xliff:g>)؟}few{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملفات صوتية؟}many{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملفًا صوتيًا؟}other{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملف صوتي؟}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{جارٍ تعديل ملف صوتي واحد…}zero{جارٍ تعديل <xliff:g id="COUNT">^1</xliff:g> ملف صوتي…}two{جارٍ تعديل ملفَين صوتين (<xliff:g id="COUNT">^1</xliff:g>)…}few{جارٍ تعديل <xliff:g id="COUNT">^1</xliff:g> ملفات صوتية…}many{جارٍ تعديل <xliff:g id="COUNT">^1</xliff:g> ملفًا صوتيًا…}other{جارٍ تعديل <xliff:g id="COUNT">^1</xliff:g> ملف صوتي…}}"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index fd0d909..379a2ed 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"ভিডিঅ’ আনমিউট কৰক"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ভিডিঅ’ প্লে’ কৰক"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ভিডিঅ’ পজ কৰক"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"এতিয়া <xliff:g id="PKG_NAME">%1$s</xliff:g>ৰ পৰা ক্লাউড মিডিয়া উপলব্ধ"</string>
<string name="not_selected" msgid="2244008151669896758">"বাছনি কৰা হোৱা নাই"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g>ক এই অডিঅ’ ফাইলটো সংশোধন কৰিবলৈ অনুমতি দিবনে?}one{<xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল সংশোধন কৰিবলৈ অনুমতি দিবনে?}other{<xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল সংশোধন কৰিবলৈ অনুমতি দিবনে?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{অডিঅ’ ফাইলটো সংশোধন কৰি থকা হৈছে…}one{<xliff:g id="COUNT">^1</xliff:g> টা অডিঅ’ ফাইল সংশোধন কৰি থকা হৈছে…}other{<xliff:g id="COUNT">^1</xliff:g> টা অডিঅ’ ফাইল সংশোধন কৰি থকা হৈছে…}}"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index be69e07..a0d97d6 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Videonu səssiz rejimdən çıxarın"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Videonu oxudun"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Videonu durdurun"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Bulud mediası indi buradan əlçatandır: <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"seçilməyib"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu audio fayla dəyişiklik etmək icazəsi verilsin?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> audio fayla dəyişiklik etmək icazəsi verilsin?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Audio fayl dəyişdirilir…}other{<xliff:g id="COUNT">^1</xliff:g> audio fayl dəyişdirilir…}}"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 3f206e8..632006c 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Uključi zvuk videa"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Pusti video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pauziraj video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"<xliff:g id="PKG_NAME">%1$s</xliff:g> sada nudi medijski sadržaj u klaudu"</string>
<string name="not_selected" msgid="2244008151669896758">"nije izabrano"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izmeni ovaj audio fajl?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> audio fajl?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> audio fajla?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> audio fajlova?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Menja se audio fajl…}one{Menja se <xliff:g id="COUNT">^1</xliff:g> audio fajl…}few{Menjaju se <xliff:g id="COUNT">^1</xliff:g> audio fajla…}other{Menja se <xliff:g id="COUNT">^1</xliff:g> audio fajlova…}}"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 60cab4a..5e535f2 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Уключыць гук відэа"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Прайграць відэа"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Прыпыніць прайграванне відэа"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"З\'явіўся доступ да воблачных мультымедыя з праграмы \"<xliff:g id="PKG_NAME">%1$s</xliff:g>\""</string>
<string name="not_selected" msgid="2244008151669896758">"не выбраны"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Дазволіць праграме \"<xliff:g id="APP_NAME_0">^1</xliff:g>\" змяніць гэты аўдыяфайл?}one{Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайл?}few{Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлы?}many{Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлаў?}other{Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайла?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Змяняецца аўдыяфайл…}one{Змяняецца <xliff:g id="COUNT">^1</xliff:g> аўдыяфайл…}few{Змяняюцца <xliff:g id="COUNT">^1</xliff:g> аўдыяфайлы…}many{Змяняюцца <xliff:g id="COUNT">^1</xliff:g> аўдыяфайлаў…}other{Змяняюцца <xliff:g id="COUNT">^1</xliff:g> аўдыяфайла…}}"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 3110c39..93dcfa1 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Включване на звука на видеоклипа отново"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Пускане на видеоклипа"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Поставяне на видеоклипа на пауза"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Вече е налице мултимедия в облака от <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"не е избрано"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Да се разреши ли на <xliff:g id="APP_NAME_0">^1</xliff:g> да промени този аудиофайл?}other{Да се разреши ли на <xliff:g id="APP_NAME_1">^1</xliff:g> да промени <xliff:g id="COUNT">^2</xliff:g> аудиофайла?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Аудиофайлът се променя…}other{<xliff:g id="COUNT">^1</xliff:g> аудиофайла се променят…}}"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 7959a45..a13b8eb 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"ভিডিও আনমিউট করুন"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ভিডিও চালান"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ভিডিও পজ করুন"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ক্লাউড মিডিয়া এখন <xliff:g id="PKG_NAME">%1$s</xliff:g> থেকে উপলভ্য"</string>
<string name="not_selected" msgid="2244008151669896758">"বেছে নেওয়া হয়নি"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g>-কে এই অডিও ফাইল পরিবর্তন করার অনুমতি দিতে চান?}one{<xliff:g id="APP_NAME_1">^1</xliff:g>-কে <xliff:g id="COUNT">^2</xliff:g>টি অডিও ফাইল পরিবর্তন করার অনুমতি দিতে চান?}other{<xliff:g id="APP_NAME_1">^1</xliff:g>-কে <xliff:g id="COUNT">^2</xliff:g>টি অডিও ফাইল পরিবর্তন করার অনুমতি দিতে চান?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{অডিও ফাইলে পরিবর্তন করা হচ্ছে…}one{<xliff:g id="COUNT">^1</xliff:g>টি অডিও ফাইলে পরিবর্তন করা হচ্ছে…}other{<xliff:g id="COUNT">^1</xliff:g>টি অডিও ফাইলে পরিবর্তন করা হচ্ছে…}}"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index fe1c8dc..d2e5f61 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Uključivanje zvuka videozapisa"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Reproduciranje videozapisa"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pauziranje videozapisa"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Medijski sadržaj u oblaku je sada dostupan od usluge <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"nije odabrano"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Dozvoliti da <xliff:g id="APP_NAME_0">^1</xliff:g> izmijeni ovaj audio fajl?}one{Dozvoliti da <xliff:g id="APP_NAME_1">^1</xliff:g> izmijeni <xliff:g id="COUNT">^2</xliff:g> audio fajl?}few{Dozvoliti da <xliff:g id="APP_NAME_1">^1</xliff:g> izmijeni <xliff:g id="COUNT">^2</xliff:g> audio fajla?}other{Dozvoliti da <xliff:g id="APP_NAME_1">^1</xliff:g> izmijeni <xliff:g id="COUNT">^2</xliff:g> audio fajlova?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Mijenjanje audio fajla…}one{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audio fajla…}few{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audio fajla…}other{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audio fajlova…}}"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 8c200e8..8ff3eb3 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Deixa de silenciar el vídeo"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Reprodueix el vídeo"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Posa en pausa el vídeo"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"El contingut multimèdia al núvol ara està disponible des de <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"no seleccionat"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vols permetre que <xliff:g id="APP_NAME_0">^1</xliff:g> modifiqui aquest fitxer d\'àudio?}other{Vols permetre que <xliff:g id="APP_NAME_1">^1</xliff:g> modifiqui <xliff:g id="COUNT">^2</xliff:g> fitxers d\'àudio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{S\'està modificant el fitxer d\'àudio…}other{S\'estan modificant <xliff:g id="COUNT">^1</xliff:g> fitxers d\'àudio…}}"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 95c2a04..3445ff1 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Zapnout video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Přehrát video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pozastavit video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Cloudová média jsou teď k dispozici ze zdroje <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"nevybráno"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Povolit aplikaci <xliff:g id="APP_NAME_0">^1</xliff:g> upravit tento zvukový soubor?}few{Povolit aplikaci <xliff:g id="APP_NAME_1">^1</xliff:g> upravit <xliff:g id="COUNT">^2</xliff:g> zvukové soubory?}many{Povolit aplikaci <xliff:g id="APP_NAME_1">^1</xliff:g> upravit <xliff:g id="COUNT">^2</xliff:g> zvukového souboru?}other{Povolit aplikaci <xliff:g id="APP_NAME_1">^1</xliff:g> upravit <xliff:g id="COUNT">^2</xliff:g> zvukových souborů?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Úprava zvukového souboru…}few{Úprava <xliff:g id="COUNT">^1</xliff:g> zvukových souborů…}many{Úprava <xliff:g id="COUNT">^1</xliff:g> zvukového souboru…}other{Úprava <xliff:g id="COUNT">^1</xliff:g> zvukových souborů…}}"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 475777d..6f15ca2 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Slå videolyden til"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Afspil video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Sæt video på pause"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Medier i skyen er nu tilgængelige fra <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"ikke valgt"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vil du give <xliff:g id="APP_NAME_0">^1</xliff:g> tilladelse til at ændre denne lydfil?}one{Vil du give <xliff:g id="APP_NAME_1">^1</xliff:g> tilladelse til at ændre <xliff:g id="COUNT">^2</xliff:g> lydfil?}other{Vil du give <xliff:g id="APP_NAME_1">^1</xliff:g> tilladelse til at ændre <xliff:g id="COUNT">^2</xliff:g> lydfiler?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Ændrer lydfilen…}one{Ændrer <xliff:g id="COUNT">^1</xliff:g> lydfil…}other{Ændrer <xliff:g id="COUNT">^1</xliff:g> lydfiler…}}"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index d7f46e7..d5b49c0 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Stummschaltung des Videos aufheben"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Video ansehen"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Video anhalten"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud-Medien sind jetzt verfügbar über <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"nicht ausgewählt"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Darf <xliff:g id="APP_NAME_0">^1</xliff:g> diese Audiodatei ändern?}other{Darf <xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> Audiodateien ändern?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Audiodatei wird geändert…}other{<xliff:g id="COUNT">^1</xliff:g> Audiodateien werden geändert…}}"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 841f12f..2a77cd0 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Κατάργηση σίγασης βίντεο"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Αναπαραγωγή βίντεο"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Παύση βίντεο"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Τα μέσα cloud είναι πλέον διαθέσιμα από την εφαρμογή <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"μη επιλεγμένο"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Να επιτραπεί στην εφαρμογή <xliff:g id="APP_NAME_0">^1</xliff:g> η τροποποίηση αυτού του αρχείου ήχου;}other{Να επιτραπεί στην εφαρμογή <xliff:g id="APP_NAME_1">^1</xliff:g> η τροποποίηση <xliff:g id="COUNT">^2</xliff:g> αρχείων ήχου;}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Τροποποίηση αρχείου ήχου…}other{Τροποποίηση <xliff:g id="COUNT">^1</xliff:g> αρχείων ήχου…}}"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index bd12ebc..06d385c 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Unmute video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Play video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pause video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud media now available from <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"not selected"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Allow <xliff:g id="APP_NAME_0">^1</xliff:g> to modify this audio file?}other{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifying audio file…}other{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}}"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index bd12ebc..06d385c 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Unmute video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Play video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pause video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud media now available from <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"not selected"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Allow <xliff:g id="APP_NAME_0">^1</xliff:g> to modify this audio file?}other{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifying audio file…}other{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}}"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index bd12ebc..06d385c 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Unmute video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Play video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pause video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud media now available from <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"not selected"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Allow <xliff:g id="APP_NAME_0">^1</xliff:g> to modify this audio file?}other{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifying audio file…}other{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}}"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index bd12ebc..06d385c 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Unmute video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Play video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pause video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud media now available from <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"not selected"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Allow <xliff:g id="APP_NAME_0">^1</xliff:g> to modify this audio file?}other{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifying audio file…}other{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}}"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index 4250916..22092f9 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Unmute video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Play video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pause video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud media now available from <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"not selected"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Allow <xliff:g id="APP_NAME_0">^1</xliff:g> to modify this audio file?}other{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifying audio file…}other{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}}"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 78ef723..ca770b8 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Activar sonido del video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Reproducir video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pausar video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Contenido multimedia en la nube ahora disponible desde <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"sin seleccionar"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{¿Deseas permitir que <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este archivo de audio?}other{¿Deseas permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> archivos de audio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modificando el archivo de audio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> archivos de audio…}}"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 4a583c1..81ccee6 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Dejar de silenciar vídeo"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Reproducir vídeo"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pausar vídeo"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Contenido multimedia en la nube ahora disponible desde <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"no seleccionado"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{¿Permitir que <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este archivo de audio?}other{¿Permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> archivos de audio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modificando archivo de audio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> archivos de audio…}}"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 4b41979..fc4c00e 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Video vaigistuse tühistamine"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Esita video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Peata video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Pilvemeedia on nüüd rakenduse <xliff:g id="PKG_NAME">%1$s</xliff:g> kaudu saadaval"</string>
<string name="not_selected" msgid="2244008151669896758">"pole valitud"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Kas lubada rakendusel <xliff:g id="APP_NAME_0">^1</xliff:g> seda helifaili muuta?}other{Kas lubada rakendusel <xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> helifaili muuta?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Helifaili muutmine …}other{<xliff:g id="COUNT">^1</xliff:g> helifaili muutmine …}}"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 818866b..e87efd7 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Aktibatu bideoaren audioa"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Erreproduzitu bideoa"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pausatu bideoa"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Hodeiko multimedia edukia <xliff:g id="PKG_NAME">%1$s</xliff:g> bidez atzi daiteke orain"</string>
<string name="not_selected" msgid="2244008151669896758">"hautatu gabe"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Audio-fitxategiari aldaketak egiteko baimena eman nahi diozu <xliff:g id="APP_NAME_0">^1</xliff:g> aplikazioari?}other{<xliff:g id="COUNT">^2</xliff:g> audio-fitxategiri aldaketak egiteko baimena eman nahi diozu <xliff:g id="APP_NAME_1">^1</xliff:g> aplikazioari?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Audio-fitxategia aldatzen…}other{<xliff:g id="COUNT">^1</xliff:g> audio-fitxategi aldatzen…}}"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 62d7068..7c63184 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"صدادار کردن ویدیو"</string>
<string name="picker_play_video" msgid="5158816108935317185">"پخش ویدیو"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"مکث ویدیو"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"رسانه ابری اکنون از <xliff:g id="PKG_NAME">%1$s</xliff:g> دردسترس است"</string>
<string name="not_selected" msgid="2244008151669896758">"انتخاب نشده است"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{به <xliff:g id="APP_NAME_0">^1</xliff:g> اجازه میدهید این فایل صوتی را تغییر دهد؟}one{به <xliff:g id="APP_NAME_1">^1</xliff:g> اجازه میدهید <xliff:g id="COUNT">^2</xliff:g> فایل صوتی را تغییر دهد؟}other{به <xliff:g id="APP_NAME_1">^1</xliff:g> اجازه میدهید <xliff:g id="COUNT">^2</xliff:g> فایل صوتی را تغییر دهد؟}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{درحال اصلاح فایل صوتی…}one{درحال اصلاح <xliff:g id="COUNT">^1</xliff:g> فایل صوتی…}other{درحال اصلاح <xliff:g id="COUNT">^1</xliff:g> فایل صوتی…}}"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 5512aca..734f960 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Poista videon mykistys"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Toista video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Keskeytä video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Pilvimediaa nyt saatavilla täältä: <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"ei valittu"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Saako <xliff:g id="APP_NAME_0">^1</xliff:g> muokata tätä audiotiedostoa?}other{Saako <xliff:g id="APP_NAME_1">^1</xliff:g> muokata <xliff:g id="COUNT">^2</xliff:g> audiotiedostoa?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Muokataan audiotiedostoa…}other{Muokataan <xliff:g id="COUNT">^1</xliff:g> audiotiedostoa…}}"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 66f0867..f44a6f1 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Réactivez le son de la vidéo"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Faites jouer la vidéo"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Suspendez la vidéo"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Le contenu multimédia dans le nuage est maintenant offert par <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"non sélectionné"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Autoriser <xliff:g id="APP_NAME_0">^1</xliff:g> à modifier ce fichier audio?}one{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichier audio?}other{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichiers audio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modification du fichier audio en cours…}one{Modification de <xliff:g id="COUNT">^1</xliff:g> fichier audio en cours…}other{Modification de <xliff:g id="COUNT">^1</xliff:g> fichiers audio en cours…}}"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index e3f666e..e57c85e 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Réactiver le son de la vidéo"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Lire la vidéo"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Mettre la vidéo en pause"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Fichier multimédia cloud désormais disponible depuis <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"non sélectionné"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Autoriser <xliff:g id="APP_NAME_0">^1</xliff:g> à modifier ce fichier audio ?}one{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichier audio ?}other{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichiers audio ?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modification du fichier audio…}one{Modification de <xliff:g id="COUNT">^1</xliff:g> fichier audio…}other{Modification de <xliff:g id="COUNT">^1</xliff:g> fichiers audio…}}"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 2cb67fe..6037874 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Activar son do vídeo"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Reproducir vídeo"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pór vídeo en pausa"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Agora podes acceder desde <xliff:g id="PKG_NAME">%1$s</xliff:g> ao contido multimedia gardado na nube"</string>
<string name="not_selected" msgid="2244008151669896758">"elemento non seleccionado"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Queres permitir que <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este ficheiro de audio?}other{Queres permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> ficheiros de audio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modificando 1 ficheiro de audio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> ficheiros de audio…}}"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index fd14092..0c5669d 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"વીડિયોનો અવાજ ચાલુ કરો"</string>
<string name="picker_play_video" msgid="5158816108935317185">"વીડિયો ચલાવો"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"વીડિયો થોભાવો"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ક્લાઉડ મીડિયા હવે <xliff:g id="PKG_NAME">%1$s</xliff:g>માંથી પણ ઉપલબ્ધ છે"</string>
<string name="not_selected" msgid="2244008151669896758">"પસંદ નહીં કરેલી"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g>ને આ ઑડિયો ફાઇલમાં ફેરફાર કરવાની મંજૂરી આપીએ?}one{<xliff:g id="APP_NAME_1">^1</xliff:g>ને <xliff:g id="COUNT">^2</xliff:g> ઑડિયો ફાઇલમાં ફેરફાર કરવાની મંજૂરી આપીએ?}other{<xliff:g id="APP_NAME_1">^1</xliff:g>ને <xliff:g id="COUNT">^2</xliff:g> ઑડિયો ફાઇલમાં ફેરફાર કરવાની મંજૂરી આપીએ?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ઑડિયો ફાઇલમાં ફેરફાર કરી રહ્યાં છીએ…}one{<xliff:g id="COUNT">^1</xliff:g> ઑડિયો ફાઇલમાં ફેરફાર કરી રહ્યાં છીએ…}other{<xliff:g id="COUNT">^1</xliff:g> ઑડિયો ફાઇલમાં ફેરફાર કરી રહ્યાં છીએ…}}"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index f1a957b..337c01d 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"वीडियो अनम्यूट करें"</string>
<string name="picker_play_video" msgid="5158816108935317185">"वीडियो चलाएं"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"वीडियो रोकें"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"क्लाउड मीडिया अब <xliff:g id="PKG_NAME">%1$s</xliff:g> पर उपलब्ध है"</string>
<string name="not_selected" msgid="2244008151669896758">"नहीं चुना गया"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{क्या <xliff:g id="APP_NAME_0">^1</xliff:g> को इस ऑडियो फ़ाइल में बदलाव करने की अनुमति देनी है?}one{क्या <xliff:g id="APP_NAME_1">^1</xliff:g> को <xliff:g id="COUNT">^2</xliff:g> ऑडियो फ़ाइल में बदलाव करने की अनुमति देनी है?}other{क्या <xliff:g id="APP_NAME_1">^1</xliff:g> को <xliff:g id="COUNT">^2</xliff:g> ऑडियो फ़ाइलों में बदलाव करने की अनुमति देनी है?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ऑडियो फ़ाइल में बदलाव किया जा रहा है…}one{<xliff:g id="COUNT">^1</xliff:g> ऑडियो फ़ाइल में बदलाव किया जा रहा है…}other{<xliff:g id="COUNT">^1</xliff:g> ऑडियो फ़ाइलों में बदलाव किया जा रहा है…}}"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 196406b..e259cf1 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Uključi kameru"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Reproduciraj videozapis"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pauziraj videozapis"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Medijski sadržaj u oblaku sada je dostupan iz aplikacije <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"nije odabrano"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_0">^1</xliff:g> da izmijeni tu audiodatoteku?}one{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g> da izmijeni <xliff:g id="COUNT">^2</xliff:g> audiodatoteku?}few{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g> da izmijeni <xliff:g id="COUNT">^2</xliff:g> audiodatoteke?}other{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g> da izmijeni <xliff:g id="COUNT">^2</xliff:g> audiodatoteka?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Mijenjanje audiodatoteke…}one{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audiodatoteke…}few{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audiodatoteke…}other{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audiodatoteka…}}"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 24c5572..b383849 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Videó némításának feloldása"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Videó lejátszása"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Videó szüneteltetése"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"A felhőbeli médiatartalmak már hozzáférhetők a következőből: <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"nincs kiválasztva"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Engedélyezi a(z) <xliff:g id="APP_NAME_0">^1</xliff:g> számára ennek a hangfájlnak a módosítását?}other{Engedélyezi a(z) <xliff:g id="APP_NAME_1">^1</xliff:g> számára <xliff:g id="COUNT">^2</xliff:g> hangfájl módosítását?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Az audiofájl módosítása folyamatban van…}other{<xliff:g id="COUNT">^1</xliff:g> audiofájl módosítása folyamatban van…}}"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 4f670b3..5c6112e 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Միացնել տեսանյութի ձայնը"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Նվագարկել տեսանյութը"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Դադարեցնել տեսանյութի նվագարկումը"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Ամպային մեդիա բովանդակությունն այժմ հասանելի է <xliff:g id="PKG_NAME">%1$s</xliff:g> հավելվածից"</string>
<string name="not_selected" msgid="2244008151669896758">"ընտրված չէ"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Թույլատրե՞լ <xliff:g id="APP_NAME_0">^1</xliff:g> հավելվածին վերականգնել այս աուդիո ֆայլն աղբարկղից}one{Թույլատրե՞լ <xliff:g id="APP_NAME_1">^1</xliff:g> հավելվածին վերականգնել <xliff:g id="COUNT">^2</xliff:g> աուդիո ֆայլ աղբարկղից}other{Թույլատրե՞լ <xliff:g id="APP_NAME_1">^1</xliff:g> հավելվածին վերականգնել <xliff:g id="COUNT">^2</xliff:g> աուդիո ֆայլ աղբարկղից}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Աուդիո ֆայլը փոփոխվում է…}one{<xliff:g id="COUNT">^1</xliff:g> աուդիո ֆայլ փոփոխվում է…}other{<xliff:g id="COUNT">^1</xliff:g> աուդիո ֆայլ փոփոխվում է…}}"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index c0a6d87..4a474c3 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Bunyikan video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Putar video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Jeda video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Media cloud kini tersedia dari <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"tidak dipilih"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Izinkan <xliff:g id="APP_NAME_0">^1</xliff:g> mengubah file audio ini?}other{Izinkan <xliff:g id="APP_NAME_1">^1</xliff:g> mengubah <xliff:g id="COUNT">^2</xliff:g> file audio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Mengubah file audio …}other{Mengubah <xliff:g id="COUNT">^1</xliff:g> file audio …}}"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 5051181..b2ff858 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Hætta að þagga myndskeið"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Spila myndskeið"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Gera hlé á spilun"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Skýjaefni er nú í boði frá <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"ekki valið"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Leyfa <xliff:g id="APP_NAME_0">^1</xliff:g> að breyta þessari hljóðskrá?}one{Leyfa <xliff:g id="APP_NAME_1">^1</xliff:g> að breyta <xliff:g id="COUNT">^2</xliff:g> hljóðskrá?}other{Leyfa <xliff:g id="APP_NAME_1">^1</xliff:g> að breyta <xliff:g id="COUNT">^2</xliff:g> hljóðskrám?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Breytir hljóðskrá…}one{Breytir <xliff:g id="COUNT">^1</xliff:g> hljóðskrá…}other{Breytir <xliff:g id="COUNT">^1</xliff:g> hljóðskrám…}}"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 6d54286..5f41776 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Riattiva audio del video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Riproduci il video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Metti in pausa il video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Contenuti multimediali salvati su cloud ora disponibili dall\'app <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"Elemento non selezionato"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Consentire all\'app <xliff:g id="APP_NAME_0">^1</xliff:g> di modificare questo file audio?}other{Consentire all\'app <xliff:g id="APP_NAME_1">^1</xliff:g> di modificare <xliff:g id="COUNT">^2</xliff:g> file audio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifica del file audio in corso…}other{Modifica di <xliff:g id="COUNT">^1</xliff:g> file audio in corso…}}"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 949d19c..1499203 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"ביטול ההשתקה של הסרטון"</string>
<string name="picker_play_video" msgid="5158816108935317185">"הפעלת הסרטון"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"השהיית הסרטון"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"מדיה בענן מתוך <xliff:g id="PKG_NAME">%1$s</xliff:g> זמינה עכשיו"</string>
<string name="not_selected" msgid="2244008151669896758">"לא נבחר"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{לאפשר לאפליקציה <xliff:g id="APP_NAME_0">^1</xliff:g> לשנות את קובץ האודיו הזה?}two{לאפשר לאפליקציה <xliff:g id="APP_NAME_1">^1</xliff:g> לשנות <xliff:g id="COUNT">^2</xliff:g> קובצי אודיו?}many{לאפשר לאפליקציה <xliff:g id="APP_NAME_1">^1</xliff:g> לשנות <xliff:g id="COUNT">^2</xliff:g> קובצי אודיו?}other{לאפשר לאפליקציה <xliff:g id="APP_NAME_1">^1</xliff:g> לשנות <xliff:g id="COUNT">^2</xliff:g> קובצי אודיו?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{מתבצע שינוי בקובץ האודיו…}two{מתבצע שינוי ב-<xliff:g id="COUNT">^1</xliff:g> קובצי אודיו…}many{מתבצע שינוי ב-<xliff:g id="COUNT">^1</xliff:g> קובצי אודיו…}other{מתבצע שינוי ב-<xliff:g id="COUNT">^1</xliff:g> קובצי אודיו…}}"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 6525695..fcdd9b0 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"動画のミュートを解除します"</string>
<string name="picker_play_video" msgid="5158816108935317185">"動画を再生します"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"動画を一時停止します"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"<xliff:g id="PKG_NAME">%1$s</xliff:g> からクラウド メディアを利用できるようになりました"</string>
<string name="not_selected" msgid="2244008151669896758">"未選択"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{この音声ファイルの変更を <xliff:g id="APP_NAME_0">^1</xliff:g> に許可しますか?}other{<xliff:g id="COUNT">^2</xliff:g> 件の音声ファイルの変更を <xliff:g id="APP_NAME_1">^1</xliff:g> に許可しますか?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{音声ファイルを変更しています…}other{<xliff:g id="COUNT">^1</xliff:g> 件の音声ファイルを変更しています…}}"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 06940de..b1048d9 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"ვიდეოს დადუმების მოხსნა"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ვიდეოს დაკვრა"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ვიდეოს დაპაუზება"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ღრუბლოვანი მედია უკვე ხელმისაწვდომია <xliff:g id="PKG_NAME">%1$s</xliff:g>-ისგან"</string>
<string name="not_selected" msgid="2244008151669896758">"არ არის არჩეული"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{აძლევთ უფლებას <xliff:g id="APP_NAME_0">^1</xliff:g>-ს, შეცვალოს ეს აუდიოფაილი?}other{აძლევთ უფლებას <xliff:g id="APP_NAME_1">^1</xliff:g>-ს, შეცვალოს <xliff:g id="COUNT">^2</xliff:g> აუდიოფაილი?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{მიმდინარეობს აუდიოფაილის მოდიფიკაცია…}other{მიმდინარეობს <xliff:g id="COUNT">^1</xliff:g> აუდიოფაილის მოდიფიკაცია…}}"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index a9a4905..af8c7d6 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Бейненің дыбысын қосу"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Бейнені ойнату"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Бейнені кідірту"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Бұлтқа сақталған медиафайл енді <xliff:g id="PKG_NAME">%1$s</xliff:g> қолданбасында қолжетімді."</string>
<string name="not_selected" msgid="2244008151669896758">"таңдалмаған"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> қолданбасына осы аудиофайлды өзгертуге рұқсат етілсін бе?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> қолданбасына <xliff:g id="COUNT">^2</xliff:g> аудиофайлды өзгертуге рұқсат етілсін бе?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Аудиофайл өзгертілуде…}other{<xliff:g id="COUNT">^1</xliff:g> аудиофайл өзгертілуде…}}"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index 9900438..f479d9a 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"បើកសំឡេងវីដេអូ"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ចាក់វីដេអូ"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ផ្អាកវីដេអូ"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ឥឡូវនេះមានមេឌៀក្នុងប្រព័ន្ធពពកពី <xliff:g id="PKG_NAME">%1$s</xliff:g> ហើយ"</string>
<string name="not_selected" msgid="2244008151669896758">"មិនបានជ្រើសរើសទេ"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{អនុញ្ញាតឱ្យ <xliff:g id="APP_NAME_0">^1</xliff:g> កែឯកសារសំឡេងនេះឬ?}other{អនុញ្ញាតឱ្យ <xliff:g id="APP_NAME_1">^1</xliff:g> កែឯកសារសំឡេង <xliff:g id="COUNT">^2</xliff:g> ឬ?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{កំពុងកែឯកសារសំឡេង…}other{កំពុងកែឯកសារសំឡេង <xliff:g id="COUNT">^1</xliff:g>…}}"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index c2c2935..5d23792 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"ವೀಡಿಯೊ ಅನ್ಮ್ಯೂಟ್ ಮಾಡಿ"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ವೀಡಿಯೊವನ್ನು ಪ್ಲೇ ಮಾಡಿ"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ವೀಡಿಯೊವನ್ನು ವಿರಾಮಗೊಳಿಸಿ"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ಕ್ಲೌಡ್ ಮಾಧ್ಯಮವು ಈಗ <xliff:g id="PKG_NAME">%1$s</xliff:g> ನಿಂದ ಲಭ್ಯವಿದೆ"</string>
<string name="not_selected" msgid="2244008151669896758">"ಆಯ್ಕೆಮಾಡಲಾಗಿಲ್ಲ"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ಈ ಆಡಿಯೋ ಫೈಲ್ ಅನ್ನು ಮಾರ್ಪಡಿಸಲು <xliff:g id="APP_NAME_0">^1</xliff:g> ಗೆ ಅನುಮತಿ ನೀಡಬೇಕೇ?}one{ಈ <xliff:g id="COUNT">^2</xliff:g> ಆಡಿಯೋ ಫೈಲ್ಗಳನ್ನು ಮಾರ್ಪಡಿಸಲು <xliff:g id="APP_NAME_1">^1</xliff:g> ಗೆ ಅನುಮತಿ ನೀಡಬೇಕೇ?}other{ಈ <xliff:g id="COUNT">^2</xliff:g> ಆಡಿಯೋ ಫೈಲ್ಗಳನ್ನು ಮಾರ್ಪಡಿಸಲು <xliff:g id="APP_NAME_1">^1</xliff:g> ಗೆ ಅನುಮತಿ ನೀಡಬೇಕೇ?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ಆಡಿಯೋ ಫೈಲ್ ಅನ್ನು ಮಾರ್ಪಡಿಸಲಾಗುತ್ತಿದೆ…}one{<xliff:g id="COUNT">^1</xliff:g> ಆಡಿಯೋ ಫೈಲ್ಗಳನ್ನು ಮಾರ್ಪಡಿಸಲಾಗುತ್ತಿದೆ…}other{<xliff:g id="COUNT">^1</xliff:g> ಆಡಿಯೋ ಫೈಲ್ಗಳನ್ನು ಮಾರ್ಪಡಿಸಲಾಗುತ್ತಿದೆ…}}"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index a32089d..c2bc09f 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"동영상 음소거 해제"</string>
<string name="picker_play_video" msgid="5158816108935317185">"동영상 재생"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"동영상 일시중지"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"이제 <xliff:g id="PKG_NAME">%1$s</xliff:g>에서 클라우드 미디어를 사용할 수 있습니다."</string>
<string name="not_selected" msgid="2244008151669896758">"선택되지 않음"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g>에서 이 오디오 파일을 수정하도록 허용하시겠습니까?}other{<xliff:g id="APP_NAME_1">^1</xliff:g>에서 오디오 파일 <xliff:g id="COUNT">^2</xliff:g>개를 수정하도록 허용하시겠습니까?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{오디오 파일 수정 중…}other{오디오 파일 <xliff:g id="COUNT">^1</xliff:g>개 수정 중…}}"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index a890294..40237a1 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Видеонун үнүн чыгаруу"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Видеону ойнотуу"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Видеону тындыруу"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Булуттагы медиа эми <xliff:g id="PKG_NAME">%1$s</xliff:g> кызматында жеткиликтүү"</string>
<string name="not_selected" msgid="2244008151669896758">"тандалган жок"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> колдонмосу бул аудио файлды өзгөртсүнбү?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> колдонмосу <xliff:g id="COUNT">^2</xliff:g> аудио файлды өзгөртсүнбү?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Аудио файл өзгөртүлүүдө…}other{<xliff:g id="COUNT">^1</xliff:g> аудио файл өзгөртүлүүдө…}}"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 3920b09..5f1de6e 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"ເຊົາປິດສຽງວິດີໂອ"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ຫຼິ້ນວິດີໂອ"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ຢຸດວິດີໂອຊົ່ວຄາວ"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ຕອນນີ້ສາມາດໃຊ້ມີເດຍຄລາວຈາກ <xliff:g id="PKG_NAME">%1$s</xliff:g> ໄດ້ແລ້ວ"</string>
<string name="not_selected" msgid="2244008151669896758">"ບໍ່ໄດ້ເລືອກ"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ອະນຸຍາດໃຫ້ <xliff:g id="APP_NAME_0">^1</xliff:g> ແກ້ໄຂໄຟລ໌ສຽງນີ້ບໍ?}other{ອະນຸຍາດໃຫ້ <xliff:g id="APP_NAME_1">^1</xliff:g> ແກ້ໄຂໄຟລ໌ສຽງ <xliff:g id="COUNT">^2</xliff:g> ໄຟລ໌ບໍ?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ກຳລັງແກ້ໄຂໄຟລ໌ສຽງ…}other{ກຳລັງແກ້ໄຂໄຟລ໌ສຽງ <xliff:g id="COUNT">^1</xliff:g> ໄຟລ໌…}}"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 3cc6913..87ac100 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Įjungti vaizdo įrašo garsą"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Leisti vaizdo įrašą"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pristabdyti vaizdo įrašą"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Debesyje esanti medija dabar pasiekiama iš „<xliff:g id="PKG_NAME">%1$s</xliff:g>“"</string>
<string name="not_selected" msgid="2244008151669896758">"nepasirinkta"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Leisti programai „<xliff:g id="APP_NAME_0">^1</xliff:g>“ keisti šį garso failą?}one{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> garso failą?}few{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> garso failus?}many{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> garso failo?}other{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> garso failų?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Keičiamas garso failas…}one{Keičiamas <xliff:g id="COUNT">^1</xliff:g> garso failas…}few{Keičiami <xliff:g id="COUNT">^1</xliff:g> garso failai…}many{Keičiama <xliff:g id="COUNT">^1</xliff:g> garso failo…}other{Keičiama <xliff:g id="COUNT">^1</xliff:g> garso failų…}}"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 314c558..da84a60 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Ieslēgt video skaņu"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Atskaņot videoklipu"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Apturēt videoklipa atskaņošanu"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Tagad mākoņa multivides saturs ir pieejams, izmantojot lietotni <xliff:g id="PKG_NAME">%1$s</xliff:g>."</string>
<string name="not_selected" msgid="2244008151669896758">"nav atlasīts"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vai atļaut lietotnei <xliff:g id="APP_NAME_0">^1</xliff:g> pārveidot šo audio failu?}zero{Vai atļaut lietotnei <xliff:g id="APP_NAME_1">^1</xliff:g> pārveidot <xliff:g id="COUNT">^2</xliff:g> audio failus?}one{Vai atļaut lietotnei <xliff:g id="APP_NAME_1">^1</xliff:g> pārveidot <xliff:g id="COUNT">^2</xliff:g> audio failu?}other{Vai atļaut lietotnei <xliff:g id="APP_NAME_1">^1</xliff:g> pārveidot <xliff:g id="COUNT">^2</xliff:g> audio failus?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Notiek audio faila pārveidošana…}zero{Notiek <xliff:g id="COUNT">^1</xliff:g> audio failu pārveidošana…}one{Notiek <xliff:g id="COUNT">^1</xliff:g> audio faila pārveidošana…}other{Notiek <xliff:g id="COUNT">^1</xliff:g> audio failu pārveidošana…}}"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 48cfea5..ce83f1b 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Вклучете звук на видео"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Пушти го видеото"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Паузирај го видеото"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Аудиовизуелните содржини во облак сега се достапни од <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"не е избрано"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Да се дозволи <xliff:g id="APP_NAME_0">^1</xliff:g> да ја измени аудиодатотекава?}one{Да се дозволи <xliff:g id="APP_NAME_1">^1</xliff:g> да измени <xliff:g id="COUNT">^2</xliff:g> аудиодатотека?}other{Да се дозволи <xliff:g id="APP_NAME_1">^1</xliff:g> да измени <xliff:g id="COUNT">^2</xliff:g> аудиодатотеки?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Се изменува аудиодатотеката…}one{Се изменуваат <xliff:g id="COUNT">^1</xliff:g> аудиодатотека…}other{Се изменуваат <xliff:g id="COUNT">^1</xliff:g> аудиодатотеки…}}"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index 3c0566e..814ff17 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"വീഡിയോ അൺമ്യൂട്ട് ചെയ്യുന്നു"</string>
<string name="picker_play_video" msgid="5158816108935317185">"വീഡിയോ പ്ലേ ചെയ്യുക"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"വീഡിയോ താൽക്കാലികമായി നിർത്തുക"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ഇപ്പോൾ <xliff:g id="PKG_NAME">%1$s</xliff:g> എന്നതിൽ നിന്ന് ക്ലൗഡ് മീഡിയ ലഭ്യമാണ്"</string>
<string name="not_selected" msgid="2244008151669896758">"തിരഞ്ഞെടുത്തിട്ടില്ല"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ഈ ഓഡിയോ ഫയൽ പരിഷ്ക്കരിക്കാൻ <xliff:g id="APP_NAME_0">^1</xliff:g> എന്നതിനെ അനുവദിക്കണോ?}other{<xliff:g id="COUNT">^2</xliff:g> ഓഡിയോ ഫയലുകൾ പരിഷ്ക്കരിക്കാൻ <xliff:g id="APP_NAME_1">^1</xliff:g> എന്നതിനെ അനുവദിക്കണോ?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ഓഡിയോ ഫയൽ പരിഷ്ക്കരിക്കുന്നു…}other{<xliff:g id="COUNT">^1</xliff:g> ഓഡിയോ ഫയലുകൾ പരിഷ്ക്കരിക്കുന്നു…}}"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 7667b37..2b94d43 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Видеоны дууг нээх"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Видео тоглуулах"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Видеог түр зогсоох"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Үүлэн медиаг одоо <xliff:g id="PKG_NAME">%1$s</xliff:g>-с авах боломжтой боллоо"</string>
<string name="not_selected" msgid="2244008151669896758">"сонгоогүй"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g>-д энэ аудио файлыг өөрчлөхийг зөвшөөрөх үү?}other{<xliff:g id="APP_NAME_1">^1</xliff:g>-д <xliff:g id="COUNT">^2</xliff:g> аудио файлыг өөрчлөхийг зөвшөөрөх үү?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Аудио файлыг өөрчилж байна…}other{<xliff:g id="COUNT">^1</xliff:g> аудио файлыг өөрчилж байна…}}"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index a0f37d5..6dd793e 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"व्हिडिओ अनम्यूट करा"</string>
<string name="picker_play_video" msgid="5158816108935317185">"व्हिडिओ प्ले करा"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"व्हिडिओ थांबवा"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"आता <xliff:g id="PKG_NAME">%1$s</xliff:g> कडून क्लाउड मीडिया उपलब्ध आहे"</string>
<string name="not_selected" msgid="2244008151669896758">"निवडला नाही"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> ला या ऑडिओ फाइलमध्ये फेरबदल करण्याची अनुमती द्यायची आहे का?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> ला <xliff:g id="COUNT">^2</xliff:g> ऑडिओ फाइलमध्ये फेरबदल करण्याची अनुमती द्यायची आहे का?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ऑडिओ फाइलमध्ये फेरबदल करत आहे…}other{<xliff:g id="COUNT">^1</xliff:g> ऑडिओ फाइलमध्ये फेरबदल करत आहे…}}"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index c00e85a..a16a090 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Nyahredam video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Mainkan video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Jeda video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Media awan kini tersedia daripada <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"tidak dipilih"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Benarkan <xliff:g id="APP_NAME_0">^1</xliff:g> mengubah suai fail audio ini?}other{Benarkan <xliff:g id="APP_NAME_1">^1</xliff:g> mengubah suai <xliff:g id="COUNT">^2</xliff:g> fail audio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Mengubah suai fail audio…}other{Mengubah suai <xliff:g id="COUNT">^1</xliff:g> fail audio…}}"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 7d7d1cd..62131d8 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"ဗီဒီယိုအသံပြန်ဖွင့်ရန်"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ဗီဒီယို ဖွင့်ရန်"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ဗီဒီယို ခဏရပ်ရန်"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"<xliff:g id="PKG_NAME">%1$s</xliff:g> တွင် Cloud မီဒီယာကို ယခု ရနိုင်ပြီ"</string>
<string name="not_selected" msgid="2244008151669896758">"ရွေးချယ်မထားပါ"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> ကို ဤအသံဖိုင် ပြင်ဆင်ခွင့်ပြုမလား။}other{<xliff:g id="APP_NAME_1">^1</xliff:g> ကို အသံဖိုင် <xliff:g id="COUNT">^2</xliff:g> ဖိုင် ပြင်ဆင်ခွင့်ပြုမလား။}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{အသံဖိုင်ကို ပြင်ဆင်နေသည်…}other{အသံဖိုင် <xliff:g id="COUNT">^1</xliff:g> ခုကို ပြင်ဆင်နေသည်…}}"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index b163198..91d6669 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Slå på lyden i videoen"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Spill av videoen"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Sett videoen på pause"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Skymedier er nå tilgjengelige fra <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"ikke valgt"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vil du tillate at <xliff:g id="APP_NAME_0">^1</xliff:g> endrer denne lydfilen?}other{Vil du tillate at <xliff:g id="APP_NAME_1">^1</xliff:g> endrer <xliff:g id="COUNT">^2</xliff:g> lydfiler?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Endrer lydfilen …}other{Endrer <xliff:g id="COUNT">^1</xliff:g> lydfiler …}}"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 42c559f..b04e978 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"भिडियो अनम्युट गर्नुहोस्"</string>
<string name="picker_play_video" msgid="5158816108935317185">"भिडियो प्ले गर्नुहोस्"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"भिडियो पज गर्नुहोस्"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"क्लाउड मिडिया अब <xliff:g id="PKG_NAME">%1$s</xliff:g> मा उपलब्ध छ"</string>
<string name="not_selected" msgid="2244008151669896758">"चयन गरिएको छैन"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> लाई यो अडियो फाइल परिमार्जन गर्न दिने हो?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> लाई <xliff:g id="COUNT">^2</xliff:g> वटा अडियो फाइल परिमार्जन गर्न दिने हो?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{अडियो फाइल परिमार्जन गरिँदै छ…}other{<xliff:g id="COUNT">^1</xliff:g> वटा अडियो फाइल परिमार्जन गरिँदै छन्…}}"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index b812f76..7b45f1b 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Geluid van video aanzetten"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Video afspelen"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Video onderbreken"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Cloudmedia nu beschikbaar van <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"niet geselecteerd"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> toestaan dit audiobestand aan te passen?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> toestaan <xliff:g id="COUNT">^2</xliff:g> audiobestanden aan te passen?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Audiobestand aanpassen…}other{<xliff:g id="COUNT">^1</xliff:g> audiobestanden aanpassen…}}"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 68a9347..69eb25e 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"ଭିଡିଓକୁ ଅନମ୍ୟୁଟ କରନ୍ତୁ"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ଭିଡିଓ ଚଲାନ୍ତୁ"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ଭିଡିଓକୁ ବିରତ କରନ୍ତୁ"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ବର୍ତ୍ତମାନ <xliff:g id="PKG_NAME">%1$s</xliff:g>ରୁ କ୍ଲାଉଡ ମିଡିଆ ଉପଲବ୍ଧ ଅଛି"</string>
<string name="not_selected" msgid="2244008151669896758">"ଚୟନ କରାଯାଇନାହିଁ"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ଏହି ଅଡିଓ ଫାଇଲକୁ ପରିବର୍ତ୍ତନ କରିବା ପାଇଁ <xliff:g id="APP_NAME_0">^1</xliff:g>କୁ ଅନୁମତି ଦେବେ?}other{<xliff:g id="COUNT">^2</xliff:g>ଟି ଅଡିଓ ଫାଇଲକୁ ପରିବର୍ତ୍ତନ କରିବା ପାଇଁ <xliff:g id="APP_NAME_1">^1</xliff:g>କୁ ଅନୁମତି ଦେବେ?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ଅଡିଓ ଫାଇଲ ପରିବର୍ତ୍ତନ କରାଯାଉଛି…}other{<xliff:g id="COUNT">^1</xliff:g>ଟି ଅଡିଓ ଫାଇଲ ପରିବର୍ତ୍ତନ କରାଯାଉଛି…}}"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 429b0fa..407abb1 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"ਵੀਡੀਓ ਅਣਮਿਊਟ ਕਰੋ"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ਵੀਡੀਓ ਚਲਾਓ"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ਵੀਡੀਓ ਰੋਕੋ"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ਕਲਾਊਡ ਮੀਡੀਆ ਹੁਣ <xliff:g id="PKG_NAME">%1$s</xliff:g> ਤੋਂ ਉਪਲਬਧ ਹੈ"</string>
<string name="not_selected" msgid="2244008151669896758">"ਚੁਣਿਆ ਨਹੀਂ ਗਿਆ"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ਕੀ <xliff:g id="APP_NAME_0">^1</xliff:g> ਨੂੰ ਇਸ ਆਡੀਓ ਫ਼ਾਈਲ ਨੂੰ ਸੋਧਣ ਦੇਣਾ ਹੈ?}one{ਕੀ <xliff:g id="APP_NAME_1">^1</xliff:g> ਨੂੰ <xliff:g id="COUNT">^2</xliff:g> ਆਡੀਓ ਫ਼ਾਈਲ ਨੂੰ ਸੋਧਣ ਦੇਣਾ ਹੈ?}other{ਕੀ <xliff:g id="APP_NAME_1">^1</xliff:g> ਨੂੰ <xliff:g id="COUNT">^2</xliff:g> ਆਡੀਓ ਫ਼ਾਈਲਾਂ ਨੂੰ ਸੋਧਣ ਦੇਣਾ ਹੈ?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ਆਡੀਓ ਫ਼ਾਈਲ ਸੋਧੀ ਜਾ ਰਹੀ ਹੈ…}one{<xliff:g id="COUNT">^1</xliff:g> ਆਡੀਓ ਫ਼ਾਈਲ ਸੋਧੀ ਜਾ ਰਹੀ ਹੈ…}other{<xliff:g id="COUNT">^1</xliff:g> ਆਡੀਓ ਫ਼ਾਈਲਾਂ ਸੋਧੀਆਂ ਜਾ ਰਹੀਆਂ ਹਨ…}}"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 6937a68..f9ad1b8 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Wyłącz wyciszenie wideo"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Odtwórz film"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Wstrzymaj film"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Multimedia w chmurze są teraz dostępne z poziomu aplikacji <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"nie wybrano"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Zezwolić aplikacji <xliff:g id="APP_NAME_0">^1</xliff:g> na zmodyfikowanie tego pliku audio?}few{Zezwolić aplikacji <xliff:g id="APP_NAME_1">^1</xliff:g> na zmodyfikowanie <xliff:g id="COUNT">^2</xliff:g> plików audio?}many{Zezwolić aplikacji <xliff:g id="APP_NAME_1">^1</xliff:g> na zmodyfikowanie <xliff:g id="COUNT">^2</xliff:g> plików audio?}other{Zezwolić aplikacji <xliff:g id="APP_NAME_1">^1</xliff:g> na zmodyfikowanie <xliff:g id="COUNT">^2</xliff:g> pliku audio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modyfikuję plik audio…}few{Modyfikuję <xliff:g id="COUNT">^1</xliff:g> pliki audio…}many{Modyfikuję <xliff:g id="COUNT">^1</xliff:g> plików audio…}other{Modyfikuję <xliff:g id="COUNT">^1</xliff:g> pliku audio…}}"</string>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index 9e757d2..00a78ba 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Ativar o som do vídeo"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Iniciar vídeo"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pausar vídeo"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Mídia em nuvem agora disponível no app <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"não selecionado"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Permitir que o app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique esse arquivo de áudio?}one{Permitir que o app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> arquivo de áudio?}other{Permitir que o app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> arquivos de áudio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modificando o arquivo de áudio…}one{Modificando <xliff:g id="COUNT">^1</xliff:g> arquivo de áudio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> arquivos de áudio…}}"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 16f5f59..9dd0a1d 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Reative o som do vídeo"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Reproduzir vídeo"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pausar vídeo"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Multimédia da nuvem já disponível da app <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"não selecionado"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este ficheiro de áudio?}other{Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> ficheiros de áudio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{A modificar o ficheiro de áudio…}other{A modificar <xliff:g id="COUNT">^1</xliff:g> ficheiro(s) de áudio…}}"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 9e757d2..00a78ba 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Ativar o som do vídeo"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Iniciar vídeo"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pausar vídeo"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Mídia em nuvem agora disponível no app <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"não selecionado"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Permitir que o app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique esse arquivo de áudio?}one{Permitir que o app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> arquivo de áudio?}other{Permitir que o app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> arquivos de áudio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modificando o arquivo de áudio…}one{Modificando <xliff:g id="COUNT">^1</xliff:g> arquivo de áudio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> arquivos de áudio…}}"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index e99e826..9d552a9 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Activați sunetul videoclipului"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Redați videoclipul"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Întrerupeți videoclipul"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Conținutul media în cloud este acum disponibil din <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"neselectat"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Permiteți ca <xliff:g id="APP_NAME_0">^1</xliff:g> să modifice acest fișier audio?}few{Permiteți ca <xliff:g id="APP_NAME_1">^1</xliff:g> să modifice <xliff:g id="COUNT">^2</xliff:g> fișiere audio?}other{Permiteți ca <xliff:g id="APP_NAME_1">^1</xliff:g> să modifice <xliff:g id="COUNT">^2</xliff:g> de fișiere audio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Se modifică fișierul audio…}few{Se modifică <xliff:g id="COUNT">^1</xliff:g> fișiere audio…}other{Se modifică <xliff:g id="COUNT">^1</xliff:g> de fișiere audio…}}"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 43e69a0..e9c51f0 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Включить звук видео"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Воспроизвести видео"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Приостановить видео"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Медиаконтент из облака теперь доступен в приложении \"<xliff:g id="PKG_NAME">%1$s</xliff:g>\"."</string>
<string name="not_selected" msgid="2244008151669896758">"не выбрано"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Разрешить приложению \"<xliff:g id="APP_NAME_0">^1</xliff:g>\" изменить этот аудиофайл?}one{Разрешить приложению \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" изменить <xliff:g id="COUNT">^2</xliff:g> аудиофайл?}few{Разрешить приложению \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" изменить <xliff:g id="COUNT">^2</xliff:g> аудиофайла?}many{Разрешить приложению \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" изменить <xliff:g id="COUNT">^2</xliff:g> аудиофайлов?}other{Разрешить приложению \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" изменить <xliff:g id="COUNT">^2</xliff:g> аудиофайла?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Изменение аудиофайла…}one{Изменение <xliff:g id="COUNT">^1</xliff:g> аудиофайла…}few{Изменение <xliff:g id="COUNT">^1</xliff:g> аудиофайлов…}many{Изменение <xliff:g id="COUNT">^1</xliff:g> аудиофайлов…}other{Изменение <xliff:g id="COUNT">^1</xliff:g> аудиофайла…}}"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 4bfca28..351e4b3 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"වීඩියෝව නිහඬ කිරීම ඉවත් කරන්න"</string>
<string name="picker_play_video" msgid="5158816108935317185">"වීඩියෝව ධාවනය කරන්න"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"විඩියෝව විරාම කරන්න"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ක්ලවුඩ් මාධ්ය දැන් <xliff:g id="PKG_NAME">%1$s</xliff:g> වෙතින් ලබා ගත හැකිය"</string>
<string name="not_selected" msgid="2244008151669896758">"තෝරා නොමැත"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> හට මෙම ශ්රව්ය ගොනුව වෙනස් කිරීමට ඉඩ දෙන්නද?}one{<xliff:g id="APP_NAME_1">^1</xliff:g> හට ශ්රව්ය ගොනු <xliff:g id="COUNT">^2</xliff:g>ක් වෙනස් කිරීමට ඉඩ දෙන්නද?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> හට ශ්රව්ය ගොනු <xliff:g id="COUNT">^2</xliff:g>ක් වෙනස් කිරීමට ඉඩ දෙන්නද?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ශ්රව්ය ගොනුව වෙනස් කරමින්…}one{ශ්රව්ය ගොනු <xliff:g id="COUNT">^1</xliff:g>ක් වෙනස් කරමින්…}other{ශ්රව්ය ගොනු <xliff:g id="COUNT">^1</xliff:g>ක් වෙනස් කරමින්…}}"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index c722798..938c805 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Zapnúť zvuk videa"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Prehrať video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pozastaviť video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Cloudové médiá sú teraz k dispozícii z aplikácie <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"nevybrané"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Chcete povoliť aplikácii <xliff:g id="APP_NAME_0">^1</xliff:g> upraviť tento zvukový súbor?}few{Chcete povoliť aplikácii <xliff:g id="APP_NAME_1">^1</xliff:g> upraviť <xliff:g id="COUNT">^2</xliff:g> zvukové súbory?}many{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}other{Chcete povoliť aplikácii <xliff:g id="APP_NAME_1">^1</xliff:g> upraviť <xliff:g id="COUNT">^2</xliff:g> zvukových súborov?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Upravuje sa zvukový súbor…}few{Upravujú sa <xliff:g id="COUNT">^1</xliff:g> zvukové súbory…}many{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}other{Upravuje sa <xliff:g id="COUNT">^1</xliff:g> zvukových súborov…}}"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 814c9ac..dc649a9 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Vklopi zvok videa"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Predvajanje videoposnetka"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Začasna zaustavitev videoposnetka"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Predstavnost v oblaku je zdaj na voljo v aplikaciji <xliff:g id="PKG_NAME">%1$s</xliff:g>."</string>
<string name="not_selected" msgid="2244008151669896758">"ni izbrano"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Želite dovoliti aplikaciji <xliff:g id="APP_NAME_0">^1</xliff:g>, da spremeni to zvočno datoteko?}one{Želite dovoliti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g>, da spremeni <xliff:g id="COUNT">^2</xliff:g> zvočno datoteko?}two{Želite dovoliti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g>, da spremeni <xliff:g id="COUNT">^2</xliff:g> zvočni datoteki?}few{Želite dovoliti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g>, da spremeni <xliff:g id="COUNT">^2</xliff:g> zvočne datoteke?}other{Želite dovoliti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g>, da spremeni <xliff:g id="COUNT">^2</xliff:g> zvočnih datotek?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Spreminjanje zvočne datoteke …}one{Spreminjanje <xliff:g id="COUNT">^1</xliff:g> zvočne datoteke …}two{Spreminjanje <xliff:g id="COUNT">^1</xliff:g> zvočnih datotek …}few{Spreminjanje <xliff:g id="COUNT">^1</xliff:g> zvočnih datotek …}other{Spreminjanje <xliff:g id="COUNT">^1</xliff:g> zvočnih datotek …}}"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 4793f05..2c7928e 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Aktivizo zërin e videos"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Luaj videon"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Vendos videon në pauzë"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Media në renë kompjuterike tani ofrohet nga <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"nuk është zgjedhur"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Të lejohet <xliff:g id="APP_NAME_0">^1</xliff:g> që ta modifikojë këtë skedar audio?}other{Të lejohet <xliff:g id="APP_NAME_1">^1</xliff:g> që të modifikojë <xliff:g id="COUNT">^2</xliff:g> skedarë audio?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Skedari audio po modifikohet…}other{<xliff:g id="COUNT">^1</xliff:g> skedarë audio po modifikohen…}}"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 1f94934..95d9152 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Укључи звук видеа"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Пусти видео"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Паузирај видео"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"<xliff:g id="PKG_NAME">%1$s</xliff:g> сада нуди медијски садржај у клауду"</string>
<string name="not_selected" msgid="2244008151669896758">"није изабрано"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Желите ли да дозволите да <xliff:g id="APP_NAME_0">^1</xliff:g> измени овај аудио фајл?}one{Желите ли да дозволите да <xliff:g id="APP_NAME_1">^1</xliff:g> измени <xliff:g id="COUNT">^2</xliff:g> аудио фајл?}few{Желите ли да дозволите да <xliff:g id="APP_NAME_1">^1</xliff:g> измени <xliff:g id="COUNT">^2</xliff:g> аудио фајла?}other{Желите ли да дозволите да <xliff:g id="APP_NAME_1">^1</xliff:g> измени <xliff:g id="COUNT">^2</xliff:g> аудио фајлова?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Мења се аудио фајл…}one{Мења се <xliff:g id="COUNT">^1</xliff:g> аудио фајл…}few{Мењају се <xliff:g id="COUNT">^1</xliff:g> аудио фајла…}other{Мења се <xliff:g id="COUNT">^1</xliff:g> аудио фајлова…}}"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index f36cf58..8d7feff 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Slå på ljudet för videon"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Spela upp video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Pausa video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Molnmedia är nu tillgänglig från <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"inte valt"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vill du tillåta att <xliff:g id="APP_NAME_0">^1</xliff:g> ändrar den här ljudfilen?}other{Vill du tillåta att <xliff:g id="APP_NAME_1">^1</xliff:g> ändrar <xliff:g id="COUNT">^2</xliff:g> ljudfiler?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Ljudfilen ändras …}other{<xliff:g id="COUNT">^1</xliff:g> ljudfiler ändras …}}"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index b3e871a..bea0fd2 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Rejesha sauti ya video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Cheza video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Sitisha video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Maudhui ya kwenye wingu sasa yanapatikana katika <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"haijachaguliwa"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Ungependa kuruhusu <xliff:g id="APP_NAME_0">^1</xliff:g> ibadilishe faili hii ya sauti?}other{Ungependa kuruhusu <xliff:g id="APP_NAME_1">^1</xliff:g> ibadilishe faili <xliff:g id="COUNT">^2</xliff:g> za sauti?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Inarekebisha faili ya sauti…}other{Inarekebisha faili <xliff:g id="COUNT">^1</xliff:g> za sauti…}}"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 9e24716..bd19a87 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"வீடியோவின் ஒலியை இயக்கும்"</string>
<string name="picker_play_video" msgid="5158816108935317185">"வீடியோவைப் பிளே செய்யும்"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"வீடியோவை இடைநிறுத்தும்"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"கிளவுட் மீடியா <xliff:g id="PKG_NAME">%1$s</xliff:g> ஆப்ஸில் தற்போது கிடைக்கிறது"</string>
<string name="not_selected" msgid="2244008151669896758">"தேர்ந்தெடுக்கப்படவில்லை"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{இந்த ஆடியோ ஃபைலில் மாற்றங்களைச் செய்ய <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?}other{<xliff:g id="COUNT">^2</xliff:g> ஆடியோ ஃபைல்களில் மாற்றங்களைச் செய்ய <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ஆடியோ ஃபைலை மாற்றியமைக்கிறது…}other{<xliff:g id="COUNT">^1</xliff:g> ஆடியோ ஃபைல்களை மாற்றியமைக்கிறது…}}"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index c805f73..337b0f9 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"వీడియోను అన్మ్యూట్ చేయండి"</string>
<string name="picker_play_video" msgid="5158816108935317185">"వీడియోను ప్లే చేయండి"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"వీడియోను పాజ్ చేయండి"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"క్లౌడ్ మీడియా ఇప్పుడు <xliff:g id="PKG_NAME">%1$s</xliff:g> నుండి అందుబాటులో ఉంది"</string>
<string name="not_selected" msgid="2244008151669896758">"ఎంచుకోబడలేదు"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ఈ ఆడియో ఫైల్ను ఎడిట్ చేయడానికి <xliff:g id="APP_NAME_0">^1</xliff:g>ను అనుమతించాలా?}other{<xliff:g id="COUNT">^2</xliff:g> ఆడియో ఫైళ్లను ఎడిట్ చేయడానికి <xliff:g id="APP_NAME_1">^1</xliff:g>ను అనుమతించాలా?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ఆడియో ఫైల్ను సవరిస్తోంది…}other{<xliff:g id="COUNT">^1</xliff:g> ఆడియో ఫైళ్లను సవరిస్తోంది…}}"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 8a7e961..9906f64 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"เปิดเสียงวิดีโอ"</string>
<string name="picker_play_video" msgid="5158816108935317185">"เล่นวิดีโอ"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"หยุดวิดีโอชั่วคราว"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"ไฟล์สื่อจาก <xliff:g id="PKG_NAME">%1$s</xliff:g> ในระบบคลาวด์พร้อมให้ใช้งานแล้ว"</string>
<string name="not_selected" msgid="2244008151669896758">"ไม่ได้เลือกไว้"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{อนุญาตให้ <xliff:g id="APP_NAME_0">^1</xliff:g> แก้ไขไฟล์เสียงนี้ไหม}other{อนุญาตให้ <xliff:g id="APP_NAME_1">^1</xliff:g> แก้ไขไฟล์เสียง <xliff:g id="COUNT">^2</xliff:g> ไฟล์ไหม}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{กำลังแก้ไขไฟล์เสียง…}other{กำลังแก้ไขไฟล์เสียง <xliff:g id="COUNT">^1</xliff:g> ไฟล์…}}"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 4cae5c5..25a169c 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"I-unmute ang video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"I-play ang video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"I-pause ang video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Available na ang cloud media sa <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"hindi pinili"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Payagan ang <xliff:g id="APP_NAME_0">^1</xliff:g> na baguhin ang audio file na ito?}one{Payagan ang <xliff:g id="APP_NAME_1">^1</xliff:g> na baguhin ang <xliff:g id="COUNT">^2</xliff:g> audio file?}other{Payagan ang <xliff:g id="APP_NAME_1">^1</xliff:g> na baguhin ang <xliff:g id="COUNT">^2</xliff:g> na audio file?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Binabago ang audio file…}one{Nagbabago ng <xliff:g id="COUNT">^1</xliff:g> audio file…}other{Nagbabago ng <xliff:g id="COUNT">^1</xliff:g> na audio file…}}"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index d49562a..8b826d0 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Videonun sesini aç"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Videoyu oynat"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Videoyu duraklat"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Bulut üzerinde saklanan medya dosyaları artık <xliff:g id="PKG_NAME">%1$s</xliff:g> uygulamasından kullanılabilir"</string>
<string name="not_selected" msgid="2244008151669896758">"seçili değil"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> uygulamasının bu ses dosyasını değiştirmesine izin verilsin mi?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> uygulamasının <xliff:g id="COUNT">^2</xliff:g> ses dosyasını değiştirmesine izin verilsin mi?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Ses dosyası değiştiriliyor…}other{<xliff:g id="COUNT">^1</xliff:g> ses dosyası değiştiriliyor…}}"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index a296f87..8a90aa1 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Увімкнути звук у відео"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Відтворити відео"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Призупинити відео"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Тепер медіаконтент із хмари доступний у додатку <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"не вибрано"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Дозволити додатку <xliff:g id="APP_NAME_0">^1</xliff:g> змінити цей аудіофайл?}one{Дозволити додатку <xliff:g id="APP_NAME_1">^1</xliff:g> змінити <xliff:g id="COUNT">^2</xliff:g> аудіофайл?}few{Дозволити додатку <xliff:g id="APP_NAME_1">^1</xliff:g> змінити <xliff:g id="COUNT">^2</xliff:g> аудіофайли?}many{Дозволити додатку <xliff:g id="APP_NAME_1">^1</xliff:g> змінити <xliff:g id="COUNT">^2</xliff:g> аудіофайлів?}other{Дозволити додатку <xliff:g id="APP_NAME_1">^1</xliff:g> змінити <xliff:g id="COUNT">^2</xliff:g> аудіофайлу?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Змінення аудіофайлу…}one{Змінення <xliff:g id="COUNT">^1</xliff:g> аудіофайлу…}few{Змінення <xliff:g id="COUNT">^1</xliff:g> аудіофайлів…}many{Змінення <xliff:g id="COUNT">^1</xliff:g> аудіофайлів…}other{Змінення <xliff:g id="COUNT">^1</xliff:g> аудіофайлу…}}"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 4a11e46..a7b76d8 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"ویڈیو کی آواز چالو کریں"</string>
<string name="picker_play_video" msgid="5158816108935317185">"ویڈیو چلائیں"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"ویڈیو موقوف کریں"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"کلاؤڈ میڈیا اب <xliff:g id="PKG_NAME">%1$s</xliff:g> سے دستیاب ہے"</string>
<string name="not_selected" msgid="2244008151669896758">"غیر منتخب کردہ"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> کو اس آڈیو فائل میں ترمیم کرنے کی اجازت دیں؟}other{<xliff:g id="APP_NAME_1">^1</xliff:g> کو <xliff:g id="COUNT">^2</xliff:g> آڈیو فائلز میں ترمیم کرنے کی اجازت دیں؟}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{آڈیو فائل میں ترمیم کی جا رہی ہے…}other{<xliff:g id="COUNT">^1</xliff:g> آڈیو فائلز میں ترمیم کی جا رہی ہے…}}"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 9a114b6..9677a11 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Video ovozini yoqish"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Videoni ochish"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Videoni pauza qilish"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Endi <xliff:g id="PKG_NAME">%1$s</xliff:g> bulutli media kontenti mavjud"</string>
<string name="not_selected" msgid="2244008151669896758">"tanlanmagan"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> ilovasiga bu audio faylni oʻzgartirishi uchun ruxsat berilsinmi?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> ilovasiga <xliff:g id="COUNT">^2</xliff:g> ta audio faylni oʻzgartirishi uchun ruxsat berilsinmi?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Audio fayl oʻzgartirilmoqda…}other{<xliff:g id="COUNT">^1</xliff:g> ta audio fayl oʻzgartirilmoqda…}}"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index b29d2b9..5566ffc 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Bật tiếng video"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Phát video"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Tạm dừng video"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Hiện đã có phương tiện đám mây từ <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"chưa được chọn"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Cho phép <xliff:g id="APP_NAME_0">^1</xliff:g> sửa đổi tệp âm thanh này?}other{Cho phép <xliff:g id="APP_NAME_1">^1</xliff:g> sửa đổi <xliff:g id="COUNT">^2</xliff:g> tệp âm thanh?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Đang sửa đổi tệp âm thanh…}other{Đang sửa đổi <xliff:g id="COUNT">^1</xliff:g> tệp âm thanh…}}"</string>
diff --git a/res/values-watch/dimens.xml b/res/values-watch/dimens.xml
new file mode 100644
index 0000000..ed5fa00
--- /dev/null
+++ b/res/values-watch/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+
+<resources>
+ <dimen name="permission_dialog_width">200dp</dimen>
+</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 9e0e3d5..8fbfa4a 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"将视频取消静音"</string>
<string name="picker_play_video" msgid="5158816108935317185">"播放视频"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"暂停视频"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"现在可以从“<xliff:g id="PKG_NAME">%1$s</xliff:g>”获取云端媒体"</string>
<string name="not_selected" msgid="2244008151669896758">"未选择"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{要允许<xliff:g id="APP_NAME_0">^1</xliff:g>修改这个音频文件吗?}other{要允许<xliff:g id="APP_NAME_1">^1</xliff:g>修改这 <xliff:g id="COUNT">^2</xliff:g> 个音频文件吗?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{正在修改音频文件…}other{正在修改 <xliff:g id="COUNT">^1</xliff:g> 个音频文件…}}"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index a9f5219..5c3fdd4 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"將影片取消靜音"</string>
<string name="picker_play_video" msgid="5158816108935317185">"播放影片"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"暫停影片"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"現可透過「<xliff:g id="PKG_NAME">%1$s</xliff:g>」使用雲端媒體"</string>
<string name="not_selected" msgid="2244008151669896758">"未揀"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{允許 <xliff:g id="APP_NAME_0">^1</xliff:g> 修改此影片嗎?}other{允許 <xliff:g id="APP_NAME_1">^1</xliff:g> 修改 <xliff:g id="COUNT">^2</xliff:g> 部影片嗎?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{正在修改音訊檔案…}other{正在修改 <xliff:g id="COUNT">^1</xliff:g> 個音訊檔案…}}"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 41c15ef..1dd87a6 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"將影片取消靜音"</string>
<string name="picker_play_video" msgid="5158816108935317185">"播放影片"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"暫停播放影片"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"現在可以透過「<xliff:g id="PKG_NAME">%1$s</xliff:g>」存取雲端媒體"</string>
<string name="not_selected" msgid="2244008151669896758">"未選取"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{允許「<xliff:g id="APP_NAME_0">^1</xliff:g>」修改這個音訊檔案嗎?}other{允許「<xliff:g id="APP_NAME_1">^1</xliff:g>」修改這 <xliff:g id="COUNT">^2</xliff:g> 個音訊檔案嗎?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{正在修改音訊檔案…}other{正在修改 <xliff:g id="COUNT">^1</xliff:g> 個音訊檔案…}}"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 429ba74..9b06733 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -77,6 +77,7 @@
<string name="picker_unmute_video" msgid="6611741290641963568">"Susa ukuthula kuvidiyo"</string>
<string name="picker_play_video" msgid="5158816108935317185">"Dlala ividiyo"</string>
<string name="picker_pause_video" msgid="7239492902901477371">"Misa ividiyo"</string>
+ <string name="picker_cloud_sync" msgid="997251377538536319">"Imidiya ye-cloud manje iyatholakala kusuka ku-<xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
<string name="not_selected" msgid="2244008151669896758">"akukhethiwe"</string>
<string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vumela i-<xliff:g id="APP_NAME_0">^1</xliff:g> ukuguqula leli fayela lomsindo?}one{Vumela i-<xliff:g id="APP_NAME_1">^1</xliff:g> ukuguqula amafayela omsindo angu-<xliff:g id="COUNT">^2</xliff:g>?}other{Vumela i-<xliff:g id="APP_NAME_1">^1</xliff:g> ukuguqula amafayela omsindo angu-<xliff:g id="COUNT">^2</xliff:g>?}}"</string>
<string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Ilungisa ifayela lomsindo…}one{Ilungisa amafayela womsindo angu-<xliff:g id="COUNT">^1</xliff:g>…}other{Ilungisa amafayela womsindo angu-<xliff:g id="COUNT">^1</xliff:g>…}}"</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index 9d3cb2c..5705b20 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -19,4 +19,6 @@
<string-array name="config_supported_transcoding_relative_paths" translatable="false">
<item>DCIM/Camera/</item>
</string-array>
+ <string-array name="config_supported_uncached_relative_paths" translatable="false">
+ </string-array>
</resources>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
index abddaa1..c35a43c 100644
--- a/res/values/overlayable.xml
+++ b/res/values/overlayable.xml
@@ -19,6 +19,7 @@
<policy type="product|system|vendor">
<item type="string" name="config_default_cloud_provider_authority"/>
<item type="array" name="config_supported_transcoding_relative_paths"/>
+ <item type="array" name="config_supported_uncached_relative_paths"/>
</policy>
</overlayable>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6aeeca8..4f1570f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -196,6 +196,9 @@
<!-- Content description for a button that pauses the current video. [CHAR LIMIT=50] -->
<string name="picker_pause_video">Pause video</string>
+ <!-- Toast notifying user that cloud media content is now available from an app on their device. [CHAR LIMIT=NONE] -->
+ <string name="picker_cloud_sync">Cloud media now available from <xliff:g id="pkg_name" example="Gmail">%1$s</xliff:g></string>
+
<!-- Default not selected text used by accessibility for an element that can be unselected. [CHAR LIMIT=NONE] -->
<string name="not_selected">not selected</string>
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index fa813a1..e4700b0 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -20,6 +20,7 @@
import static com.android.providers.media.util.Logging.LOGV;
import static com.android.providers.media.util.Logging.TAG;
+import android.annotation.SuppressLint;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -37,8 +38,10 @@
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
+import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.MediaStore;
@@ -63,6 +66,7 @@
import androidx.annotation.VisibleForTesting;
import com.android.modules.utils.BackgroundThread;
+import com.android.providers.media.dao.FileRow;
import com.android.providers.media.playlist.Playlist;
import com.android.providers.media.util.DatabaseUtils;
import com.android.providers.media.util.FileUtils;
@@ -79,12 +83,16 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import java.util.function.UnaryOperator;
@@ -105,6 +113,45 @@
@VisibleForTesting
public static final String TEST_CLEAN_DB = "test_clean";
+ /**
+ * Key name of xattr used to set next row id for internal DB.
+ */
+ private static final String INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY = "user.intdbnextrowid".concat(
+ String.valueOf(UserHandle.myUserId()));
+
+ /**
+ * Key name of xattr used to set next row id for external DB.
+ */
+ private static final String EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY = "user.extdbnextrowid".concat(
+ String.valueOf(UserHandle.myUserId()));
+
+ /**
+ * Key name of xattr used to set session id for internal DB.
+ */
+ private static final String INTERNAL_DB_SESSION_ID_XATTR_KEY = "user.intdbsessionid".concat(
+ String.valueOf(UserHandle.myUserId()));
+
+ /**
+ * Key name of xattr used to set session id for external DB.
+ */
+ private static final String EXTERNAL_DB_SESSION_ID_XATTR_KEY = "user.extdbsessionid".concat(
+ String.valueOf(UserHandle.myUserId()));
+
+ /** Indicates a billion value used when next row id is not present in respective xattr. */
+ private static final Long NEXT_ROW_ID_DEFAULT_BILLION_VALUE = Double.valueOf(
+ Math.pow(10, 9)).longValue();
+
+ private static final Long INVALID_ROW_ID = -1L;
+
+ /**
+ * Path used for setting next row id and database session id for each user profile. Storing here
+ * because media provider does not have required permission on path /data/media/<user-id> for
+ * work profiles.
+ * For devices with adoptable storage support, opting for adoptable storage will not delete
+ * /data/media/0 directory.
+ */
+ public static final String DATA_MEDIA_XATTR_DIRECTORY_PATH = "/data/media/0";
+
static final String INTERNAL_DATABASE_NAME = "internal.db";
static final String EXTERNAL_DATABASE_NAME = "external.db";
@@ -133,6 +180,7 @@
private final String mMigrationFileName;
long mScanStartTime;
long mScanStopTime;
+ private boolean mEnableNextRowIdRecovery;
/**
* Unfortunately we can have multiple instances of DatabaseHelper, causing
@@ -158,26 +206,27 @@
private static Object sMigrationLockInternal = new Object();
private static Object sMigrationLockExternal = new Object();
+ /**
+ * Object used to synchronise sequence of next row id in database.
+ */
+ private static final Object sRecoveryLock = new Object();
+
+ /** Stores cached value of next row id of the database which optimises new id inserts. */
+ private AtomicLong mNextRowIdBackup = new AtomicLong(INVALID_ROW_ID);
+
public interface OnSchemaChangeListener {
void onSchemaChange(@NonNull String volumeName, int versionFrom, int versionTo,
long itemCount, long durationMillis, String databaseUuid);
}
public interface OnFilesChangeListener {
- void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
- int mediaType, boolean isDownload, boolean isPending);
+ void onInsert(@NonNull DatabaseHelper helper, @NonNull FileRow insertedRow);
- void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
- long oldId, int oldMediaType, boolean oldIsDownload,
- long newId, int newMediaType, boolean newIsDownload,
- boolean oldIsTrashed, boolean newIsTrashed,
- boolean oldIsPending, boolean newIsPending,
- boolean oldIsFavorite, boolean newIsFavorite,
- int oldSpecialFormat, int newSpecialFormat,
- String oldOwnerPackage, String newOwnerPackage, String oldPath);
+ void onUpdate(@NonNull DatabaseHelper helper, @NonNull FileRow oldRow,
+ @NonNull FileRow newRow);
- void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
- int mediaType, boolean isDownload, String ownerPackage, String path);
+ /** Method invoked on database row delete. */
+ void onDelete(@NonNull DatabaseHelper helper, @NonNull FileRow deletedRow);
}
public interface OnLegacyMigrationListener {
@@ -196,10 +245,10 @@
@Nullable OnSchemaChangeListener schemaListener,
@Nullable OnFilesChangeListener filesListener,
@NonNull OnLegacyMigrationListener migrationListener,
- @Nullable UnaryOperator<String> idGenerator) {
+ @Nullable UnaryOperator<String> idGenerator, boolean enableNextRowIdRecovery) {
this(context, name, getDatabaseVersion(context), earlyUpgrade, legacyProvider,
columnAnnotation, exportedSinceAnnotation, schemaListener, filesListener,
- migrationListener, idGenerator);
+ migrationListener, idGenerator, enableNextRowIdRecovery);
}
public DatabaseHelper(Context context, String name, int version,
@@ -209,7 +258,7 @@
@Nullable OnSchemaChangeListener schemaListener,
@Nullable OnFilesChangeListener filesListener,
@NonNull OnLegacyMigrationListener migrationListener,
- @Nullable UnaryOperator<String> idGenerator) {
+ @Nullable UnaryOperator<String> idGenerator, boolean enableNextRowIdRecovery) {
super(context, name, null, version);
mContext = context;
mName = name;
@@ -230,6 +279,7 @@
mMigrationListener = migrationListener;
mIdGenerator = idGenerator;
mMigrationFileName = "." + mVolumeName;
+ this.mEnableNextRowIdRecovery = enableNextRowIdRecovery;
// Configure default filters until we hear differently
if (isInternal()) {
@@ -308,10 +358,15 @@
final boolean isDownload = Integer.parseInt(split[3]) != 0;
final boolean isPending = Integer.parseInt(split[4]) != 0;
+ FileRow insertedRow = FileRow.newBuilder(id)
+ .setVolumeName(volumeName)
+ .setMediaType(mediaType)
+ .setIsDownload(isDownload)
+ .setIsPending(isPending)
+ .build();
Trace.beginSection("_INSERT");
try {
- mFilesListener.onInsert(DatabaseHelper.this, volumeName, id,
- mediaType, isDownload, isPending);
+ mFilesListener.onInsert(DatabaseHelper.this, insertedRow);
} finally {
Trace.endSection();
}
@@ -341,13 +396,31 @@
final String newOwnerPackage = split[16];
final String oldPath = split[17];
+ FileRow oldRow = FileRow.newBuilder(oldId)
+ .setVolumeName(volumeName)
+ .setMediaType(oldMediaType)
+ .setIsDownload(oldIsDownload)
+ .setIsTrashed(oldIsTrashed)
+ .setIsPending(oldIsPending)
+ .setIsFavorite(oldIsFavorite)
+ .setSpecialFormat(oldSpecialFormat)
+ .setOwnerPackageName(oldOwnerPackage)
+ .setPath(oldPath)
+ .build();
+ FileRow newRow = FileRow.newBuilder(newId)
+ .setVolumeName(volumeName)
+ .setMediaType(newMediaType)
+ .setIsDownload(newIsDownload)
+ .setIsTrashed(newIsTrashed)
+ .setIsPending(newIsPending)
+ .setIsFavorite(newIsFavorite)
+ .setSpecialFormat(newSpecialFormat)
+ .setOwnerPackageName(newOwnerPackage)
+ .build();
+
Trace.beginSection("_UPDATE");
try {
- mFilesListener.onUpdate(DatabaseHelper.this, volumeName, oldId,
- oldMediaType, oldIsDownload, newId, newMediaType, newIsDownload,
- oldIsTrashed, newIsTrashed, oldIsPending, newIsPending,
- oldIsFavorite, newIsFavorite, oldSpecialFormat, newSpecialFormat,
- oldOwnerPackage, newOwnerPackage, oldPath);
+ mFilesListener.onUpdate(DatabaseHelper.this, oldRow, newRow);
} finally {
Trace.endSection();
}
@@ -365,10 +438,16 @@
final String ownerPackage = split[4];
final String path = split[5];
+ FileRow deletedRow = FileRow.newBuilder(id)
+ .setVolumeName(volumeName)
+ .setMediaType(mediaType)
+ .setIsDownload(isDownload)
+ .setOwnerPackageName(ownerPackage)
+ .setPath(path)
+ .build();
Trace.beginSection("_DELETE");
try {
- mFilesListener.onDelete(DatabaseHelper.this, volumeName, id,
- mediaType, isDownload, ownerPackage, path);
+ mFilesListener.onDelete(DatabaseHelper.this, deletedRow);
} finally {
Trace.endSection();
}
@@ -421,22 +500,96 @@
@Override
public void onDowngrade(final SQLiteDatabase db, final int oldV, final int newV) {
- Log.v(TAG, "onDowngrade() for " + mName + " from " + oldV + " to " + newV);
- mSchemaLock.writeLock().lock();
- try {
- downgradeDatabase(db, oldV, newV);
- } finally {
- mSchemaLock.writeLock().unlock();
+ Log.w(TAG, String.format(Locale.ROOT,
+ "onDowngrade() for %s from %s to %s. Deleting database:%s in case of a "
+ + "downgrade.", mName, oldV, newV, mName));
+ deleteDatabaseFiles();
+ throw new IllegalStateException(
+ String.format(Locale.ROOT, "Crashing MP process on database downgrade of %s.",
+ mName));
+ }
+
+ private void deleteDatabaseFiles() {
+ File dbDir = mContext.getDatabasePath(mName).getParentFile();
+ File[] files = dbDir.listFiles();
+ if (files == null) {
+ Log.w(TAG, String.format(Locale.ROOT, "No database files found on path:%s.",
+ dbDir.getAbsolutePath()));
+ return;
+ }
+
+ for (File file : files) {
+ if (file.getName().startsWith(mName)) {
+ file.delete();
+ Log.w(TAG, String.format(Locale.ROOT, "Database file:%s deleted.",
+ file.getAbsolutePath()));
+ }
}
}
+
@Override
public void onOpen(final SQLiteDatabase db) {
Log.v(TAG, "onOpen() for " + mName);
-
+ // Recovering before migration from legacy because recovery process will clear up data to
+ // read from xattrs once ids are persisted in xattrs.
+ tryRecoverRowIdSequence(db);
tryMigrateFromLegacy(db);
}
+ private void tryRecoverRowIdSequence(SQLiteDatabase db) {
+ if (!isNextRowIdBackupEnabled()) {
+ Log.d(TAG, "Skipping row id recovery as backup is not enabled.");
+ return;
+ }
+
+ synchronized (sRecoveryLock) {
+ boolean isLastUsedDatabaseSession = isLastUsedDatabaseSession(db);
+ Optional<Long> nextRowIdFromXattrOptional = getNextRowIdFromXattr();
+ if (isLastUsedDatabaseSession && nextRowIdFromXattrOptional.isPresent()) {
+ Log.i(TAG, String.format(Locale.ROOT,
+ "No database change across sequential open calls for %s.", mName));
+ mNextRowIdBackup.set(nextRowIdFromXattrOptional.get());
+ updateSessionIdInDatabaseAndExternalStorage(db);
+ return;
+ }
+
+ Log.w(TAG, String.format(Locale.ROOT,
+ "%s database inconsistent: isLastUsedDatabaseSession:%b, "
+ + "nextRowIdOptionalPresent:%b", mName, isLastUsedDatabaseSession,
+ nextRowIdFromXattrOptional.isPresent()));
+ // TODO(b/222313219): Add an assert to ensure that next row id xattr is always
+ // present when DB session id matches across sequential open calls.
+ updateNextRowIdInDatabaseAndExternalStorage(db);
+ updateSessionIdInDatabaseAndExternalStorage(db);
+ }
+ }
+
+ @GuardedBy("sRecoveryLock")
+ private boolean isLastUsedDatabaseSession(SQLiteDatabase db) {
+ Optional<String> lastUsedSessionIdFromDatabasePathXattr = getXattr(db.getPath(),
+ getSessionIdXattrKeyForDatabase());
+ Optional<String> lastUsedSessionIdFromExternalStoragePathXattr = getXattr(
+ DATA_MEDIA_XATTR_DIRECTORY_PATH, getSessionIdXattrKeyForDatabase());
+
+ return lastUsedSessionIdFromDatabasePathXattr.isPresent()
+ && lastUsedSessionIdFromExternalStoragePathXattr.isPresent()
+ && lastUsedSessionIdFromDatabasePathXattr.get().equals(
+ lastUsedSessionIdFromExternalStoragePathXattr.get());
+ }
+
+ @GuardedBy("sRecoveryLock")
+ private void updateSessionIdInDatabaseAndExternalStorage(SQLiteDatabase db) {
+ final String uuid = UUID.randomUUID().toString();
+ boolean setOnDatabase = setXattr(db.getPath(), getSessionIdXattrKeyForDatabase(), uuid);
+ boolean setOnExternalStorage = setXattr(DATA_MEDIA_XATTR_DIRECTORY_PATH,
+ getSessionIdXattrKeyForDatabase(), uuid);
+ if (setOnDatabase && setOnExternalStorage) {
+ Log.i(TAG, String.format(Locale.ROOT, "SessionId set to %s on paths %s and %s.", uuid,
+ db.getPath(), DATA_MEDIA_XATTR_DIRECTORY_PATH));
+ }
+ }
+
private void tryMigrateFromLegacy(SQLiteDatabase db) {
final Object migrationLock;
if (isInternal()) {
@@ -1656,7 +1809,8 @@
}
private void updateUserId(SQLiteDatabase db) {
- db.execSQL(String.format("ALTER TABLE files ADD COLUMN _user_id INTEGER DEFAULT %d;",
+ db.execSQL(String.format(Locale.ROOT,
+ "ALTER TABLE files ADD COLUMN _user_id INTEGER DEFAULT %d;",
UserHandle.myUserId()));
}
@@ -2108,4 +2262,137 @@
return false;
}
}
+
+ @SuppressLint("DefaultLocale")
+ @GuardedBy("sRecoveryLock")
+ private void updateNextRowIdInDatabaseAndExternalStorage(SQLiteDatabase db) {
+ Optional<Long> nextRowIdOptional = getNextRowIdFromXattr();
+ // Use a billion as the next row id if not found on external storage.
+ long nextRowId = nextRowIdOptional.orElse(NEXT_ROW_ID_DEFAULT_BILLION_VALUE);
+
+ backupNextRowId(nextRowId);
+ // Insert and delete a row to update sqlite_sequence counter
+ db.execSQL(String.format(Locale.ROOT, "INSERT INTO files(_ID) VALUES (%d)", nextRowId));
+ db.execSQL(String.format(Locale.ROOT, "DELETE FROM files WHERE _ID=%d", nextRowId));
+ Log.i(TAG, String.format(Locale.ROOT, "Updated sqlite counter of Files table of %s to %d.",
+ mName, nextRowId));
+ }
+
+ /**
+ * Backs up next row id value in xattr to {@code nextRowId} + BackupFrequency. Also updates
+ * respective in-memory next row id cached value.
+ */
+ protected void backupNextRowId(long nextRowId) {
+ long backupId = nextRowId + getNextRowIdBackupFrequency();
+ boolean setOnExternalStorage = setXattr(DATA_MEDIA_XATTR_DIRECTORY_PATH,
+ getNextRowIdXattrKeyForDatabase(),
+ String.valueOf(backupId));
+ if (setOnExternalStorage) {
+ mNextRowIdBackup.set(backupId);
+ Log.i(TAG, String.format(Locale.ROOT, "Backed up next row id as:%d on path:%s for %s.",
+ backupId, DATA_MEDIA_XATTR_DIRECTORY_PATH, mName));
+ }
+ }
+
+ protected Optional<Long> getNextRowIdFromXattr() {
+ try {
+ return Optional.of(Long.parseLong(new String(
+ Os.getxattr(DATA_MEDIA_XATTR_DIRECTORY_PATH,
+ getNextRowIdXattrKeyForDatabase()))));
+ } catch (Exception e) {
+ Log.e(TAG, String.format(Locale.ROOT, "Xattr:%s not found on external storage.",
+ getNextRowIdXattrKeyForDatabase()), e);
+ return Optional.empty();
+ }
+ }
+
+ protected String getNextRowIdXattrKeyForDatabase() {
+ if (isInternal()) {
+ return INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY;
+ } else if (isExternal()) {
+ return EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY;
+ }
+ throw new RuntimeException(
+ String.format(Locale.ROOT, "Next row id xattr key not defined for database:%s.",
+ mName));
+ }
+
+ protected String getSessionIdXattrKeyForDatabase() {
+ if (isInternal()) {
+ return INTERNAL_DB_SESSION_ID_XATTR_KEY;
+ } else if (isExternal()) {
+ return EXTERNAL_DB_SESSION_ID_XATTR_KEY;
+ }
+ throw new RuntimeException(
+ String.format(Locale.ROOT, "Session id xattr key not defined for database:%s.",
+ mName));
+ }
+
+ protected static boolean setXattr(String path, String key, String value) {
+ try (ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(path),
+ ParcelFileDescriptor.MODE_READ_ONLY)) {
+ // Map id value to xattr key
+ Os.setxattr(path, key, value.getBytes(), 0);
+ Os.fsync(pfd.getFileDescriptor());
+ Log.d(TAG, String.format("xattr set to %s for key:%s on path: %s.", value, key, path));
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, String.format(Locale.ROOT, "Failed to set xattr:%s to %s for path: %s.", key,
+ value, path), e);
+ return false;
+ }
+ }
+
+ protected static Optional<String> getXattr(String path, String key) {
+ try {
+ return Optional.of(Arrays.toString(Os.getxattr(path, key)));
+ } catch (Exception e) {
+ Log.w(TAG, String.format(Locale.ROOT,
+ "Exception encountered while reading xattr:%s from path:%s.", key, path));
+ return Optional.empty();
+ }
+ }
+
+ protected Optional<Long> getNextRowId() {
+ if (mNextRowIdBackup.get() == INVALID_ROW_ID) {
+ return getNextRowIdFromXattr();
+ }
+
+ return Optional.of(mNextRowIdBackup.get());
+ }
+
+ boolean isNextRowIdBackupEnabled() {
+ if (!mEnableNextRowIdRecovery) {
+ return false;
+ }
+
+ if (mVersion < VERSION_R) {
+ // Do not back up next row id if DB version is less than R. This is unlikely to hit
+ // as we will backport row id backup changes till Android R.
+ Log.v(TAG, "Skipping next row id backup for android versions less than R.");
+ return false;
+ }
+
+ if (isInternal()) {
+ // Skip id reuse fix for internal db as it can lead to ids starting from a billion
+ // and can cause aberrant behaviour in Ringtones Manager. Reference: b/229153534.
+ Log.v(TAG, "Skipping next row id backup for internal database.");
+ return false;
+ }
+
+ if (!(new File(DATA_MEDIA_XATTR_DIRECTORY_PATH)).exists()) {
+ Log.w(TAG, String.format(Locale.ROOT,
+ "Skipping row id recovery as path:%s does not exist.",
+ DATA_MEDIA_XATTR_DIRECTORY_PATH));
+ return false;
+ }
+
+ return SystemProperties.getBoolean("persist.sys.fuse.backup.nextrowid_enabled",
+ true);
+ }
+
+ public static int getNextRowIdBackupFrequency() {
+ return SystemProperties.getInt("persist.sys.fuse.backup.nextrowid_backup_frequency",
+ 1000);
+ }
}
diff --git a/src/com/android/providers/media/FileAccessAttributes.java b/src/com/android/providers/media/FileAccessAttributes.java
new file mode 100644
index 0000000..e9dc556
--- /dev/null
+++ b/src/com/android/providers/media/FileAccessAttributes.java
@@ -0,0 +1,82 @@
+/*
+ * 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 android.database.Cursor;
+
+/**
+ * Class to represent the file metadata stored in the database (SQLite/xAttr)
+ */
+public final class FileAccessAttributes {
+ private final long mId;
+ private final int mMediaType;
+ private final boolean mIsPending;
+ private final boolean mIsTrashed;
+ // TODO(b/227348809): Remove ownerId field when we add the logic to check ownerId from xattr
+ private final int mOwnerId;
+ private final String mOwnerPackageName;
+
+ public FileAccessAttributes(long id, int mediaType, boolean isPending,
+ boolean isTrashed, int ownerId, String ownerPackageName) {
+ this.mId = id;
+ this.mMediaType = mediaType;
+ this.mIsPending = isPending;
+ this.mIsTrashed = isTrashed;
+ this.mOwnerId = ownerId;
+ this.mOwnerPackageName = ownerPackageName;
+ }
+
+ public static FileAccessAttributes fromCursor(Cursor c) {
+ final long id = c.getLong(0);
+ String ownerPackageName = c.getString(1);
+ final boolean isPending = c.getInt(2) != 0;
+ final int mediaType = c.getInt(3);
+ final boolean isTrashed = c.getInt(4) != 0;
+ return new FileAccessAttributes(id, mediaType, isPending, isTrashed, -1,
+ ownerPackageName);
+ }
+
+ public String toString() {
+ return String.format("Id: %s, Mediatype: %s, isPending: %s, "
+ + "isTrashed: %s, ownerpackageName: %s", this.mId, this.mMediaType,
+ mIsPending, mIsTrashed, mOwnerId);
+ }
+
+ public long getId() {
+ return this.mId;
+ }
+
+ public int getMediaType() {
+ return this.mMediaType;
+ }
+
+ public int getOwnerId() {
+ return this.mOwnerId;
+ }
+
+ public boolean isTrashed() {
+ return this.mIsTrashed;
+ }
+
+ public boolean isPending() {
+ return this.mIsPending;
+ }
+
+ public String getOwnerPackageName() {
+ return this.mOwnerPackageName;
+ }
+}
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 6191457..7387b44 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -17,7 +17,6 @@
package com.android.providers.media;
import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
-import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.permissionToOp;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
@@ -306,6 +305,7 @@
}
private boolean hasPermissionInternal(int permission) {
+ boolean targetSdkIsAtLeastT = getTargetSdkVersion() > Build.VERSION_CODES.S_V2;
// While we're here, enforce any broad user-level restrictions
if ((uid == Process.SHELL_UID) && context.getSystemService(UserManager.class)
.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
@@ -338,13 +338,13 @@
case PERMISSION_READ_AUDIO:
return checkPermissionReadAudio(
- context, pid, uid, getPackageName(), attributionTag);
+ context, pid, uid, getPackageName(), attributionTag, targetSdkIsAtLeastT);
case PERMISSION_READ_VIDEO:
return checkPermissionReadVideo(
- context, pid, uid, getPackageName(), attributionTag);
+ context, pid, uid, getPackageName(), attributionTag, targetSdkIsAtLeastT);
case PERMISSION_READ_IMAGES:
return checkPermissionReadImages(
- context, pid, uid, getPackageName(), attributionTag);
+ context, pid, uid, getPackageName(), attributionTag, targetSdkIsAtLeastT);
case PERMISSION_WRITE_AUDIO:
return checkPermissionWriteAudio(
context, pid, uid, getPackageName(), attributionTag);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 1093f52..680cde0 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -106,6 +106,7 @@
import static com.android.providers.media.util.SyntheticPathUtils.isRedactedPath;
import static com.android.providers.media.util.SyntheticPathUtils.isSyntheticPath;
+import android.annotation.IntDef;
import android.app.AppOpsManager;
import android.app.AppOpsManager.OnOpActiveChangedListener;
import android.app.AppOpsManager.OnOpChangedListener;
@@ -220,6 +221,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.DatabaseHelper.OnFilesChangeListener;
import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
+import com.android.providers.media.dao.FileRow;
import com.android.providers.media.fuse.ExternalStorageServiceImpl;
import com.android.providers.media.fuse.FuseDaemon;
import com.android.providers.media.metrics.PulledMetrics;
@@ -227,7 +229,6 @@
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.ExternalDbFacade;
import com.android.providers.media.photopicker.data.PickerDbFacade;
-import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.playlist.Playlist;
import com.android.providers.media.scan.MediaScanner;
import com.android.providers.media.scan.ModernMediaScanner;
@@ -241,10 +242,12 @@
import com.android.providers.media.util.Metrics;
import com.android.providers.media.util.MimeUtils;
import com.android.providers.media.util.PermissionUtils;
+import com.android.providers.media.util.Preconditions;
import com.android.providers.media.util.SQLiteQueryBuilder;
import com.android.providers.media.util.SpecialFormatDetector;
import com.android.providers.media.util.StringUtils;
import com.android.providers.media.util.UserCache;
+import com.android.providers.media.util.XAttrUtils;
import com.android.providers.media.util.XmpInterface;
import com.google.common.hash.Hashing;
@@ -257,6 +260,8 @@
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
@@ -616,6 +621,7 @@
if (pkg != null) {
invalidateLocalCallingIdentityCache(pkg, "package " + intent.getAction());
if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
+ mUserCache.invalidateWorkProfileOwnerApps(pkg);
mPickerSyncController.notifyPackageRemoval(pkg);
}
} else {
@@ -724,58 +730,62 @@
*/
private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() {
@Override
- public void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
- int mediaType, boolean isDownload, boolean isPending) {
- handleInsertedRowForFuse(id);
- acceptWithExpansion(helper::notifyInsert, volumeName, id, mediaType, isDownload);
-
+ public void onInsert(@NonNull DatabaseHelper helper, @NonNull FileRow insertedRow) {
+ handleInsertedRowForFuse(insertedRow.getId());
+ acceptWithExpansion(helper::notifyInsert, insertedRow.getVolumeName(),
+ insertedRow.getId(), insertedRow.getMediaType(), insertedRow.isDownload());
+ updateNextRowIdXattr(helper, insertedRow.getId());
helper.postBackground(() -> {
if (helper.isExternal()) {
// Update the quota type on the filesystem
- Uri fileUri = MediaStore.Files.getContentUri(volumeName, id);
- updateQuotaTypeForUri(fileUri, mediaType);
+ Uri fileUri = MediaStore.Files.getContentUri(insertedRow.getVolumeName(),
+ insertedRow.getId());
+ updateQuotaTypeForUri(fileUri, insertedRow.getMediaType());
}
// Tell our SAF provider so it knows when views are no longer empty
- MediaDocumentsProvider.onMediaStoreInsert(getContext(), volumeName, mediaType, id);
+ MediaDocumentsProvider.onMediaStoreInsert(getContext(), insertedRow.getVolumeName(),
+ insertedRow.getMediaType(), insertedRow.getId());
- if (mExternalDbFacade.onFileInserted(mediaType, isPending)) {
+ if (mExternalDbFacade.onFileInserted(insertedRow.getMediaType(),
+ insertedRow.isPending())) {
mPickerSyncController.notifyMediaEvent();
}
});
}
@Override
- public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
- long oldId, int oldMediaType, boolean oldIsDownload,
- long newId, int newMediaType, boolean newIsDownload,
- boolean oldIsTrashed, boolean newIsTrashed,
- boolean oldIsPending, boolean newIsPending,
- boolean oldIsFavorite, boolean newIsFavorite,
- int oldSpecialFormat, int newSpecialFormat,
- String oldOwnerPackage, String newOwnerPackage, String oldPath) {
- final boolean isDownload = oldIsDownload || newIsDownload;
- final Uri fileUri = MediaStore.Files.getContentUri(volumeName, oldId);
- handleUpdatedRowForFuse(oldPath, oldOwnerPackage, oldId, newId);
- handleOwnerPackageNameChange(oldPath, oldOwnerPackage, newOwnerPackage);
- acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, oldMediaType, isDownload);
-
+ public void onUpdate(@NonNull DatabaseHelper helper, @NonNull FileRow oldRow,
+ @NonNull FileRow newRow) {
+ final boolean isDownload = oldRow.isDownload() || newRow.isDownload();
+ final Uri fileUri = MediaStore.Files.getContentUri(oldRow.getVolumeName(),
+ oldRow.getId());
+ handleUpdatedRowForFuse(oldRow.getPath(), oldRow.getOwnerPackageName(), oldRow.getId(),
+ newRow.getId());
+ handleOwnerPackageNameChange(oldRow.getPath(), oldRow.getOwnerPackageName(),
+ newRow.getOwnerPackageName());
+ acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(),
+ oldRow.getMediaType(), isDownload);
+ updateNextRowIdXattr(helper, newRow.getId());
helper.postBackground(() -> {
if (helper.isExternal()) {
// Update the quota type on the filesystem
- updateQuotaTypeForUri(fileUri, newMediaType);
+ updateQuotaTypeForUri(fileUri, newRow.getMediaType());
}
- if (mExternalDbFacade.onFileUpdated(oldId, oldMediaType, newMediaType, oldIsTrashed,
- newIsTrashed, oldIsPending, newIsPending, oldIsFavorite,
- newIsFavorite, oldSpecialFormat, newSpecialFormat)) {
+ if (mExternalDbFacade.onFileUpdated(oldRow.getId(),
+ oldRow.getMediaType(), newRow.getMediaType(),
+ oldRow.isTrashed(), newRow.isTrashed(),
+ oldRow.isPending(), newRow.isPending(),
+ oldRow.isFavorite(), newRow.isFavorite(),
+ oldRow.getSpecialFormat(), newRow.getSpecialFormat())) {
mPickerSyncController.notifyMediaEvent();
}
});
- if (newMediaType != oldMediaType) {
- acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, newMediaType,
- isDownload);
+ if (newRow.getMediaType() != oldRow.getMediaType()) {
+ acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(),
+ newRow.getMediaType(), isDownload);
helper.postBackground(() -> {
// Invalidate any thumbnails when the media type changes
@@ -785,12 +795,13 @@
}
@Override
- public void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
- int mediaType, boolean isDownload, String ownerPackageName, String path) {
- handleDeletedRowForFuse(path, ownerPackageName, id);
- acceptWithExpansion(helper::notifyDelete, volumeName, id, mediaType, isDownload);
+ public void onDelete(@NonNull DatabaseHelper helper, @NonNull FileRow deletedRow) {
+ handleDeletedRowForFuse(deletedRow.getPath(), deletedRow.getOwnerPackageName(),
+ deletedRow.getId());
+ acceptWithExpansion(helper::notifyDelete, deletedRow.getVolumeName(),
+ deletedRow.getId(), deletedRow.getMediaType(), deletedRow.isDownload());
// Remove cached transcoded file if any
- mTranscodeHelper.deleteCachedTranscodeFile(id);
+ mTranscodeHelper.deleteCachedTranscodeFile(deletedRow.getId());
helper.postBackground(() -> {
// Item no longer exists, so revoke all access to it
@@ -798,32 +809,58 @@
try {
acceptWithExpansion((uri) -> {
getContext().revokeUriPermission(uri, ~0);
- }, volumeName, id, mediaType, isDownload);
+ },
+ deletedRow.getVolumeName(), deletedRow.getId(),
+ deletedRow.getMediaType(), deletedRow.isDownload());
} finally {
Trace.endSection();
}
- switch (mediaType) {
+ switch (deletedRow.getMediaType()) {
case FileColumns.MEDIA_TYPE_PLAYLIST:
case FileColumns.MEDIA_TYPE_AUDIO:
if (helper.isExternal()) {
- removePlaylistMembers(mediaType, id);
+ removePlaylistMembers(deletedRow.getMediaType(), deletedRow.getId());
}
}
// Invalidate any thumbnails now that media is gone
- invalidateThumbnails(MediaStore.Files.getContentUri(volumeName, id));
+ invalidateThumbnails(MediaStore.Files.getContentUri(deletedRow.getVolumeName(),
+ deletedRow.getId()));
// Tell our SAF provider so it can revoke too
- MediaDocumentsProvider.onMediaStoreDelete(getContext(), volumeName, mediaType, id);
+ MediaDocumentsProvider.onMediaStoreDelete(getContext(), deletedRow.getVolumeName(),
+ deletedRow.getMediaType(), deletedRow.getId());
- if (mExternalDbFacade.onFileDeleted(id, mediaType)) {
+ if (mExternalDbFacade.onFileDeleted(deletedRow.getId(),
+ deletedRow.getMediaType())) {
mPickerSyncController.notifyMediaEvent();
}
});
}
};
+ protected void updateNextRowIdXattr(DatabaseHelper helper, long id) {
+ if (!helper.isNextRowIdBackupEnabled()) {
+ Log.v(TAG, "Skipping next row id backup.");
+ return;
+ }
+
+ Optional<Long> nextRowIdBackupOptional = helper.getNextRowId();
+ if (!nextRowIdBackupOptional.isPresent()) {
+ throw new RuntimeException(
+ String.format(Locale.ROOT, "Cannot find next row id xattr for %s.",
+ helper.getDatabaseName()));
+ }
+
+ if (id >= nextRowIdBackupOptional.get()) {
+ helper.backupNextRowId(id);
+ } else {
+ Log.v(TAG, String.format(Locale.ROOT, "Inserted id:%d less than next row id backup:%d.",
+ id, nextRowIdBackupOptional.get()));
+ }
+ }
+
private final UnaryOperator<String> mIdGenerator = path -> {
final long rowId = mCallingIdentity.get().getDeletedRowId(path);
if (rowId != -1 && isFuseThread()) {
@@ -907,11 +944,16 @@
}
/**
- * Ensure that default folders are created on mounted primary storage
- * devices. We only do this once per volume so we don't annoy the user if
- * deleted manually.
+ * Ensure that default folders are created on mounted storage devices.
+ * We only do this once per volume so we don't annoy the user if deleted
+ * manually.
*/
private void ensureDefaultFolders(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) {
+ if (volume.isExternallyManaged()) {
+ // Default folders should not be automatically created inside volumes managed from
+ // outside Android.
+ return;
+ }
final String volumeName = volume.getName();
String key;
if (volumeName.equals(MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
@@ -947,6 +989,12 @@
* disk, then all thumbnails will be considered stable and will be deleted.
*/
private void ensureThumbnailsValid(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) {
+ if (volume.isExternallyManaged()) {
+ // Default folders and thumbnail directories should not be automatically created inside
+ // volumes managed from outside Android, and there is no need to ensure the validity of
+ // their thumbnails here.
+ return;
+ }
final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db);
try {
for (File dir : getThumbnailDirectories(volume)) {
@@ -1015,13 +1063,22 @@
mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, false,
Column.class, ExportedSince.class, Metrics::logSchemaChange, mFilesListener,
- MIGRATION_LISTENER, mIdGenerator);
+ MIGRATION_LISTENER, mIdGenerator, true);
mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, false,
Column.class, ExportedSince.class, Metrics::logSchemaChange, mFilesListener,
- MIGRATION_LISTENER, mIdGenerator);
+ MIGRATION_LISTENER, mIdGenerator, true);
mExternalDbFacade = new ExternalDbFacade(getContext(), mExternalDatabase, mVolumeCache);
mPickerDbFacade = new PickerDbFacade(context);
- mPickerSyncController = new PickerSyncController(context, mPickerDbFacade, this);
+
+ final String localPickerProvider = PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+ final String allowedCloudProviders =
+ getStringDeviceConfig(PickerSyncController.ALLOWED_CLOUD_PROVIDERS_KEY,
+ /* default */ "");
+ final int pickerSyncDelayMs = getIntDeviceConfig(PickerSyncController.SYNC_DELAY_MS,
+ /* default */ 5000);
+
+ mPickerSyncController = new PickerSyncController(context, mPickerDbFacade,
+ localPickerProvider, allowedCloudProviders, pickerSyncDelayMs);
mPickerDataLayer = new PickerDataLayer(context, mPickerDbFacade, mPickerSyncController);
mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade);
@@ -1053,9 +1110,18 @@
@Override
public void onStateChanged(@NonNull StorageVolume volume) {
updateVolumes();
- }
+ }
});
+ if (SdkLevel.isAtLeastT()) {
+ try {
+ mStorageManager.setCloudMediaProvider(mPickerSyncController.getCloudProvider());
+ } catch (SecurityException e) {
+ // This can happen in unit tests
+ Log.w(TAG, "Failed to update the system_server with the latest cloud provider", e);
+ }
+ }
+
updateVolumes();
attachVolume(MediaVolume.fromInternal(), /* validate */ false);
for (MediaVolume volume : mVolumeCache.getExternalVolumes()) {
@@ -1069,6 +1135,12 @@
mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE,
null /* all packages */, mModeListener);
+ mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_AUDIO,
+ null /* all packages */, mModeListener);
+ mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_IMAGES,
+ null /* all packages */, mModeListener);
+ mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_VIDEO,
+ null /* all packages */, mModeListener);
mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE,
null /* all packages */, mModeListener);
mAppOpsManager.startWatchingMode(permissionToOp(ACCESS_MEDIA_LOCATION),
@@ -1113,6 +1185,16 @@
return true;
}
+ Optional<DatabaseHelper> getDatabaseHelper(String dbName) {
+ if (dbName.equalsIgnoreCase(INTERNAL_DATABASE_NAME)) {
+ return Optional.of(mInternalDatabase);
+ } else if (dbName.equalsIgnoreCase(EXTERNAL_DATABASE_NAME)) {
+ return Optional.of(mExternalDatabase);
+ }
+
+ return Optional.empty();
+ }
+
@Override
public void onCallingPackageChanged() {
// Identity of the current thread has changed, so invalidate caches
@@ -2874,9 +2956,8 @@
final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath));
final boolean isSameMimeType = newMimeType.equalsIgnoreCase(oldMimeType);
- final ContentValues contentValues = getContentValuesForFuseRename(newPath, newMimeType,
+ ContentValues contentValues = getContentValuesForFuseRename(newPath, newMimeType,
wasHidden, isHidden, isSameMimeType);
-
if (!updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues)) {
if (!bypassRestrictions) {
// Check for other URI format grants for oldPath only. Check right before
@@ -6262,9 +6343,9 @@
case MediaStore.SET_CLOUD_PROVIDER_CALL: {
// TODO(b/190713331): Remove after initial development
final String cloudProvider = extras.getString(MediaStore.EXTRA_CLOUD_PROVIDER);
- Log.i(TAG, "Developer initiated cloud provider switch: " + cloudProvider);
- mPickerSyncController.setCloudProvider(cloudProvider);
- // fall through
+ Log.i(TAG, "Test initiated cloud provider switch: " + cloudProvider);
+ mPickerSyncController.forceSetCloudProvider(cloudProvider);
+ // fall-through
}
case MediaStore.SYNC_PROVIDERS_CALL: {
syncAllMedia();
@@ -6300,6 +6381,20 @@
notifyCloudEventResult);
return bundle;
}
+ case MediaStore.USES_FUSE_PASSTHROUGH: {
+ boolean isEnabled = false;
+ try {
+ FuseDaemon daemon = getFuseDaemonForFile(new File(arg));
+ if (daemon != null) {
+ isEnabled = daemon.usesFusePassthrough();
+ }
+ } catch (FileNotFoundException e) {
+ }
+
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(MediaStore.USES_FUSE_PASSTHROUGH_RESULT, isEnabled);
+ return bundle;
+ }
default:
throw new UnsupportedOperationException("Unsupported call: " + method);
}
@@ -6310,8 +6405,7 @@
// local_provider while running as MediaProvider
final long t = Binder.clearCallingIdentity();
try {
- // TODO(b/190713331): Remove after initial development
- Log.v(TAG, "Developer initiated provider sync");
+ Log.v(TAG, "Test initiated cloud provider sync");
mPickerSyncController.syncAllMedia();
} finally {
Binder.restoreCallingIdentity(t);
@@ -7819,7 +7913,15 @@
// level from #3
// 5. Return the fd from #4 to the app or throw an exception if any of the conditions
// are not met
- return getOriginalMediaFormatFileDescriptor(opts);
+ try {
+ return getOriginalMediaFormatFileDescriptor(opts);
+ } finally {
+ // Clearing the Bundle closes the underlying Parcel, ensuring that the input fd
+ // owned by the Parcel is closed immediately and not at the next GC.
+ // This works around a change in behavior introduced by:
+ // aosp/Icfe8880cad00c3cd2afcbe4b92400ad4579e680e
+ opts.clear();
+ }
}
// This is needed for thumbnail resolution as it doesn't go through openFileCommon
@@ -8020,8 +8122,14 @@
*/
Cursor queryForSingleItem(Uri uri, String[] projection, String selection,
String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException {
- final Cursor c = query(uri, projection,
- DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null), signal, true);
+ Cursor c = null;
+ try {
+ c = query(uri, projection,
+ DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null),
+ signal, true);
+ } catch (IllegalArgumentException e) {
+ throw new FileNotFoundException("Volume not found for " + uri);
+ }
if (c == null) {
throw new FileNotFoundException("Missing cursor for " + uri);
} else if (c.getCount() < 1) {
@@ -8741,6 +8849,69 @@
return !matcher.matches();
}
+ private FileAccessAttributes queryForFileAttributes(final String path)
+ throws FileNotFoundException {
+ Trace.beginSection("queryFileAttr");
+ final Uri contentUri = FileUtils.getContentUriForPath(path);
+ final String[] projection = new String[]{
+ MediaColumns._ID,
+ MediaColumns.OWNER_PACKAGE_NAME,
+ MediaColumns.IS_PENDING,
+ FileColumns.MEDIA_TYPE,
+ MediaColumns.IS_TRASHED
+ };
+ final String selection = MediaColumns.DATA + "=?";
+ final String[] selectionArgs = new String[]{path};
+ FileAccessAttributes fileAccessAttributes;
+ try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection,
+ selection,
+ selectionArgs, null)) {
+ fileAccessAttributes = FileAccessAttributes.fromCursor(c);
+ }
+ Trace.endSection();
+ return fileAccessAttributes;
+ }
+
+ private void checkIfFileOpenIsPermitted(String path,
+ FileAccessAttributes fileAccessAttributes, String redactedUriId,
+ boolean forWrite) throws FileNotFoundException {
+ final File file = new File(path);
+ Uri fileUri = MediaStore.Files.getContentUri(extractVolumeName(path),
+ fileAccessAttributes.getId());
+ // We don't check ownership for files with IS_PENDING set by FUSE
+ // Please note that even if ownerPackageName is null, the check below will throw an
+ // IllegalStateException
+ if (fileAccessAttributes.isTrashed() || (fileAccessAttributes.isPending()
+ && !isPendingFromFuse(new File(path)))) {
+ requireOwnershipForItem(fileAccessAttributes.getOwnerPackageName(), fileUri);
+ }
+
+ // Check that path looks consistent before uri checks
+ if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
+ checkWorldReadAccess(file.getAbsolutePath());
+ }
+
+ try {
+ // checkAccess throws FileNotFoundException only from checkWorldReadAccess(),
+ // which we already check above. Hence, handling only SecurityException.
+ if (redactedUriId != null) {
+ fileUri = ContentUris.removeId(fileUri).buildUpon().appendPath(
+ redactedUriId).build();
+ }
+ checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
+ } catch (SecurityException e) {
+ // Check for other Uri formats only when the single uri check flow fails.
+ // Throw the previous exception if the multi-uri checks failed.
+ final String uriId = redactedUriId == null
+ ? Long.toString(fileAccessAttributes.getId()) : redactedUriId;
+ if (getOtherUriGrantsForPath(path, fileAccessAttributes.getMediaType(),
+ uriId, forWrite) == null) {
+ throw e;
+ }
+ }
+ }
+
+
/**
* Checks if the app identified by the given UID is allowed to open the given file for the given
* access mode.
@@ -8824,59 +8995,23 @@
return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
mediaCapabilitiesUid, new long[0]);
}
-
- final Uri contentUri = FileUtils.getContentUriForPath(path);
- final String[] projection = new String[]{
- MediaColumns._ID,
- MediaColumns.OWNER_PACKAGE_NAME,
- MediaColumns.IS_PENDING,
- FileColumns.MEDIA_TYPE,
- MediaColumns.IS_TRASHED
- };
- final String selection = MediaColumns.DATA + "=?";
- final String[] selectionArgs = new String[]{path};
- final long id;
- final int mediaType;
- final boolean isPending;
- final boolean isTrashed;
- String ownerPackageName = null;
- try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection,
- selection,
- selectionArgs, null)) {
- id = c.getLong(0);
- ownerPackageName = c.getString(1);
- isPending = c.getInt(2) != 0;
- mediaType = c.getInt(3);
- isTrashed = c.getInt(4) != 0;
- }
- final File file = new File(path);
- Uri fileUri = MediaStore.Files.getContentUri(extractVolumeName(path), id);
- // We don't check ownership for files with IS_PENDING set by FUSE
- if (isTrashed || (isPending && !isPendingFromFuse(new File(path)))) {
- requireOwnershipForItem(ownerPackageName, fileUri);
- }
-
- // Check that path looks consistent before uri checks
- if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
- checkWorldReadAccess(file.getAbsolutePath());
- }
-
- try {
- // checkAccess throws FileNotFoundException only from checkWorldReadAccess(),
- // which we already check above. Hence, handling only SecurityException.
- if (redactedUriId != null) {
- fileUri = ContentUris.removeId(fileUri).buildUpon().appendPath(
- redactedUriId).build();
- }
- checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
- } catch (SecurityException e) {
- // Check for other Uri formats only when the single uri check flow fails.
- // Throw the previous exception if the multi-uri checks failed.
- final String uriId = redactedUriId == null ? Long.toString(id) : redactedUriId;
- if (getOtherUriGrantsForPath(path, mediaType, uriId, forWrite) == null) {
- throw e;
+ // TODO: Fetch owner id from Android/media directory and check if caller is owner
+ FileAccessAttributes fileAttributes = null;
+ if (XAttrUtils.ENABLE_XATTR_METADATA_FOR_FUSE) {
+ Optional<FileAccessAttributes> fileAttributesThroughXattr =
+ XAttrUtils.getFileAttributesFromXAttr(path,
+ XAttrUtils.FILE_ACCESS_XATTR_KEY);
+ if (fileAttributesThroughXattr.isPresent()) {
+ fileAttributes = fileAttributesThroughXattr.get();
}
}
+
+ // FileAttributes will be null if the xattr call failed or the flag to enable xattr
+ // metadata support is not set
+ if (fileAttributes == null) {
+ fileAttributes = queryForFileAttributes(path);
+ }
+ checkIfFileOpenIsPermitted(path, fileAttributes, redactedUriId, forWrite);
isSuccess = true;
return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid,
redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid,
@@ -9268,80 +9403,52 @@
}
}
- /**
- * Checks if the app with the given UID is allowed to create or delete the directory with the
- * given path.
- *
- * @param path File path of the directory that the app wants to create/delete
- * @param uid UID of the app that wants to create/delete the directory
- * @param forCreate denotes whether the operation is directory creation or deletion
- * @return 0 if the operation is allowed, or the following {@code errno} values:
- * <ul>
- * <li>{@link OsConstants#EACCES} if the app tries to create/delete a dir in another app's
- * external directory, or if the calling package is a legacy app that doesn't have
- * WRITE_EXTERNAL_STORAGE permission.
- * <li>{@link OsConstants#EPERM} if the app tries to create/delete a top-level directory.
- * </ul>
- *
- * Called from JNI in jni/MediaProviderWrapper.cpp
- */
- @Keep
- public int isDirectoryCreationOrDeletionAllowedForFuse(
- @NonNull String path, int uid, boolean forCreate) {
- final LocalCallingIdentity token =
- clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
- PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
+ // These need to stay in sync with MediaProviderWrapper.cpp's DirectoryAccessRequestType enum
+ @IntDef(flag = true, prefix = { "DIRECTORY_ACCESS_FOR_" }, value = {
+ DIRECTORY_ACCESS_FOR_READ,
+ DIRECTORY_ACCESS_FOR_WRITE,
+ DIRECTORY_ACCESS_FOR_CREATE,
+ DIRECTORY_ACCESS_FOR_DELETE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @VisibleForTesting
+ @interface DirectoryAccessType {}
- try {
- // App dirs are not indexed, so we don't create an entry for the file.
- if (isPrivatePackagePathNotAccessibleByCaller(path)) {
- Log.e(TAG, "Can't modify another app's external directory!");
- return OsConstants.EACCES;
- }
+ @VisibleForTesting
+ static final int DIRECTORY_ACCESS_FOR_READ = 1;
- if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) {
- return 0;
- }
- // Legacy apps that made is this far don't have the right storage permission and hence
- // are not allowed to access anything other than their external app directory
- if (isCallingPackageRequestingLegacy()) {
- return OsConstants.EACCES;
- }
+ @VisibleForTesting
+ static final int DIRECTORY_ACCESS_FOR_WRITE = 2;
- final String[] relativePath = sanitizePath(extractRelativePath(path));
- final boolean isTopLevelDir =
- relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
- if (isTopLevelDir) {
- // We allow creating the default top level directories only, all other operations on
- // top level directories are not allowed.
- if (forCreate && FileUtils.isDefaultDirectoryName(extractDisplayName(path))) {
- return 0;
- }
- Log.e(TAG,
- "Creating a non-default top level directory or deleting an existing"
- + " one is not allowed!");
- return OsConstants.EPERM;
- }
- return 0;
- } finally {
- restoreLocalCallingIdentity(token);
- }
- }
+ @VisibleForTesting
+ static final int DIRECTORY_ACCESS_FOR_CREATE = 3;
+
+ @VisibleForTesting
+ static final int DIRECTORY_ACCESS_FOR_DELETE = 4;
/**
- * Checks whether the app with the given UID is allowed to open the directory denoted by the
+ * Checks whether the app with the given UID is allowed to access the directory denoted by the
* given path.
*
* @param path directory's path
* @param uid UID of the requesting app
- * @return 0 if it's allowed to open the diretory, {@link OsConstants#EACCES} if the calling
- * package is a legacy app that doesn't have READ_EXTERNAL_STORAGE permission,
- * {@link OsConstants#ENOENT} otherwise.
+ * @param accessType type of access being requested - eg {@link
+ * MediaProvider#DIRECTORY_ACCESS_FOR_READ}
+ * @return 0 if it's allowed to access the directory, {@link OsConstants#ENOENT} for attempts
+ * to access a private package path in Android/data or Android/obb the caller doesn't have
+ * access to, and otherwise {@link OsConstants#EACCES} if the calling package is a legacy app
+ * that doesn't have READ_EXTERNAL_STORAGE permission or for other invalid attempts to access
+ * Android/data or Android/obb dirs.
*
* Called from JNI in jni/MediaProviderWrapper.cpp
*/
@Keep
- public int isOpendirAllowedForFuse(@NonNull String path, int uid, boolean forWrite) {
+ public int isDirAccessAllowedForFuse(@NonNull String path, int uid,
+ @DirectoryAccessType int accessType) {
+ Preconditions.checkArgumentInRange(accessType, 1, DIRECTORY_ACCESS_FOR_DELETE,
+ "accessType");
+
+ final boolean forRead = accessType == DIRECTORY_ACCESS_FOR_READ;
final LocalCallingIdentity token =
clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
@@ -9354,14 +9461,16 @@
return OsConstants.ENOENT;
}
- if (shouldBypassFuseRestrictions(forWrite, path)) {
+ if (shouldBypassFuseRestrictions(/* forWrite= */ !forRead, path)) {
return 0;
}
- // Do not allow apps to open Android/data or Android/obb dirs.
- // On primary volumes, apps that get special access to these directories get it via
- // mount views of lowerfs. On secondary volumes, such apps would return early from
- // shouldBypassFuseRestrictions above.
+ // Do not allow apps that reach this point to access Android/data or Android/obb dirs.
+ // Creation should be via getContext().getExternalFilesDir() etc methods.
+ // Reads and writes on primary volumes should be via mount views of lowerfs for apps
+ // that get special access to these directories.
+ // Reads and writes on secondary volumes would be provided via an early return from
+ // shouldBypassFuseRestrictions above (again just for apps with special access).
if (isDataOrObbPath(path)) {
return OsConstants.EACCES;
}
@@ -9373,22 +9482,34 @@
}
// This is a non-legacy app. Rest of the directories are generally writable
// except for non-default top-level directories.
- if (forWrite) {
+ if (!forRead) {
final String[] relativePath = sanitizePath(extractRelativePath(path));
if (relativePath.length == 0) {
- Log.e(TAG, "Directoy write not allowed on invalid relative path for " + path);
+ Log.e(TAG,
+ "Directory update not allowed on invalid relative path for " + path);
return OsConstants.EPERM;
}
final boolean isTopLevelDir =
relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
if (isTopLevelDir) {
- if (FileUtils.isDefaultDirectoryName(extractDisplayName(path))) {
- return 0;
- } else {
- Log.e(TAG,
- "Writing to a non-default top level directory is not allowed!");
+ // We don't allow deletion of any top-level folders
+ if (accessType == DIRECTORY_ACCESS_FOR_DELETE) {
+ Log.e(TAG, "Deleting top level directories are not allowed!");
return OsConstants.EACCES;
}
+
+ // We allow creating or writing to default top-level folders, but we don't
+ // allow creation or writing to non-default top-level folders.
+ if ((accessType == DIRECTORY_ACCESS_FOR_CREATE
+ || accessType == DIRECTORY_ACCESS_FOR_WRITE)
+ && FileUtils.isDefaultDirectoryName(extractDisplayName(path))) {
+ return 0;
+ }
+
+ Log.e(TAG,
+ "Creating or writing to a non-default top level directory is not "
+ + "allowed!");
+ return OsConstants.EACCES;
}
}
@@ -9999,6 +10120,12 @@
return mTranscodeHelper.getSupportedRelativePaths();
}
+ public List<String> getSupportedUncachedRelativePaths() {
+ return StringUtils.verifySupportedUncachedRelativePaths(
+ StringUtils.getStringArrayConfig(getContext(),
+ R.array.config_supported_uncached_relative_paths));
+ }
+
/**
* Creating a new method for Transcoding to avoid any merge conflicts.
* TODO(b/170465810): Remove this when the code is refactored.
diff --git a/src/com/android/providers/media/MediaService.java b/src/com/android/providers/media/MediaService.java
index f406a59..cf154ab 100644
--- a/src/com/android/providers/media/MediaService.java
+++ b/src/com/android/providers/media/MediaService.java
@@ -27,7 +27,6 @@
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
-import android.os.Bundle;
import android.os.Trace;
import android.os.UserHandle;
import android.os.storage.StorageVolume;
@@ -151,6 +150,16 @@
public static void onScanVolume(Context context, MediaVolume volume, int reason)
throws IOException {
final String volumeName = volume.getName();
+ if (!MediaStore.VOLUME_INTERNAL.equals(volumeName) && volume.getPath() == null) {
+ /* This is a very unexpected state and can only ever happen with app-cloned users.
+ In general, MediaVolumes should always be mounted and have a path, however, if the
+ user failed to unlock properly, MediaProvider still gets the volume from the
+ StorageManagerService because MediaProvider is special cased there. See
+ StorageManagerService#getVolumeList. Reference bug: b/207723670. */
+ Log.w(TAG, String.format("Skipping volume scan for %s when volume path is null.",
+ volumeName));
+ return;
+ }
UserHandle owner = volume.getUser();
if (owner == null) {
// Can happen for the internal volume
diff --git a/src/com/android/providers/media/MediaUpgradeReceiver.java b/src/com/android/providers/media/MediaUpgradeReceiver.java
index 983892a..38cdc75 100644
--- a/src/com/android/providers/media/MediaUpgradeReceiver.java
+++ b/src/com/android/providers/media/MediaUpgradeReceiver.java
@@ -17,17 +17,17 @@
package com.android.providers.media;
import android.content.BroadcastReceiver;
+import android.content.ContentProviderClient;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
-import android.provider.Column;
-import android.provider.ExportedSince;
+import android.provider.MediaStore;
import android.util.Log;
import com.android.providers.media.util.ForegroundThread;
-import com.android.providers.media.util.Metrics;
import java.io.File;
+import java.util.Optional;
/**
* This will be launched during system boot, after the core system has
@@ -68,26 +68,25 @@
File dbDir = context.getDatabasePath("foo").getParentFile();
String[] files = dbDir.list();
if (files == null) return;
- for (int i=0; i<files.length; i++) {
+
+ MediaProvider mediaProvider = getMediaProvider(context);
+ for (int i = 0; i < files.length; i++) {
String file = files[i];
- if (MediaProvider.isMediaDatabaseName(file)) {
+ Optional<DatabaseHelper> helper = mediaProvider.getDatabaseHelper(file);
+ if (helper.isPresent()) {
long startTime = System.currentTimeMillis();
Log.i(TAG, "---> Start upgrade of media database " + file);
try {
- DatabaseHelper helper = new DatabaseHelper(context, file, false, false,
- Column.class, ExportedSince.class, Metrics::logSchemaChange, null,
- MediaProvider.MIGRATION_LISTENER, null);
- helper.runWithTransaction((db) -> {
+ helper.get().runWithTransaction((db) -> {
// Perform just enough to force database upgrade
return db.getVersion();
});
- helper.close();
} catch (Throwable t) {
Log.wtf(TAG, "Error during upgrade of media db " + file, t);
- } finally {
}
+
Log.i(TAG, "<--- Finished upgrade of media database " + file
- + " in " + (System.currentTimeMillis()-startTime) + "ms");
+ + " in " + (System.currentTimeMillis() - startTime) + "ms");
}
}
} catch (Throwable t) {
@@ -95,4 +94,14 @@
Log.wtf(TAG, "Error during upgrade attempt.", t);
}
}
+
+ private MediaProvider getMediaProvider(Context context) {
+ try (ContentProviderClient cpc =
+ context.getContentResolver().acquireContentProviderClient(
+ MediaStore.AUTHORITY)) {
+ return (MediaProvider) cpc.getLocalContentProvider();
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to acquire MediaProvider", e);
+ }
+ }
}
diff --git a/src/com/android/providers/media/MediaVolume.java b/src/com/android/providers/media/MediaVolume.java
index 3a2d792..4934365 100644
--- a/src/com/android/providers/media/MediaVolume.java
+++ b/src/com/android/providers/media/MediaVolume.java
@@ -153,7 +153,8 @@
UserHandle user = storageVolume.getOwner();
File path = storageVolume.getDirectory();
String id = storageVolume.getId();
- boolean externallyManaged = false;
+ boolean externallyManaged =
+ SdkLevel.isAtLeastT() ? storageVolume.isExternallyManaged() : false;
boolean publicVolume = !externallyManaged && !storageVolume.isPrimary();
return new MediaVolume(name, user, path, id, externallyManaged, publicVolume);
}
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index d50c574..a79890a 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -17,6 +17,8 @@
package com.android.providers.media;
import static com.android.providers.media.MediaProvider.AUDIO_MEDIA_ID;
+import static com.android.providers.media.MediaProvider.AUDIO_PLAYLISTS_ID;
+import static com.android.providers.media.MediaProvider.FILES_ID;
import static com.android.providers.media.MediaProvider.IMAGES_MEDIA_ID;
import static com.android.providers.media.MediaProvider.VIDEO_MEDIA_ID;
import static com.android.providers.media.MediaProvider.collectUris;
@@ -25,7 +27,10 @@
import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation;
import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia;
import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
import android.app.Activity;
import android.app.AlertDialog;
@@ -47,9 +52,9 @@
import android.graphics.ImageDecoder.Source;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Icon;
-import android.icu.text.MessageFormat;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
@@ -71,6 +76,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.MediaProvider.LocalUriMatcher;
import com.android.providers.media.util.Metrics;
import com.android.providers.media.util.StringUtils;
@@ -78,10 +84,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.List;
-import java.util.Locale;
-import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
@@ -122,6 +125,13 @@
progressDialog.show();
};
+ private boolean mShouldCheckReadAudio;
+ private boolean mShouldCheckReadAudioOrReadVideo;
+ private boolean mShouldCheckReadImages;
+ private boolean mShouldCheckReadVideo;
+ private boolean mShouldCheckMediaPermissions;
+ private boolean mShouldForceShowingDialog;
+
private static final Long LEAST_SHOW_PROGRESS_TIME_MS = 300L;
private static final Long BEFORE_SHOW_PROGRESS_TIME_MS = 300L;
@@ -164,7 +174,7 @@
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
getWindow().setDimAmount(0.0f);
-
+ boolean isTargetSdkAtLeastT = false;
// All untrusted input values here were validated when generating the
// original PendingIntent
try {
@@ -174,6 +184,8 @@
appInfo = resolveCallingAppInfo();
label = resolveAppLabel(appInfo);
verb = resolveVerb();
+ isTargetSdkAtLeastT = appInfo.targetSdkVersion > Build.VERSION_CODES.S_V2;
+ mShouldCheckMediaPermissions = isTargetSdkAtLeastT && SdkLevel.isAtLeastT();
data = resolveData();
volumeName = MediaStore.getVolumeName(uris.get(0));
} catch (Exception e) {
@@ -186,8 +198,23 @@
// Create Progress dialog
createProgressDialog();
- if (!shouldShowActionDialog(this, -1 /* pid */, appInfo.uid, getCallingPackage(),
- null /* attributionTag */, verb)) {
+ final boolean shouldShowActionDialog;
+ if (mShouldCheckMediaPermissions) {
+ if (mShouldForceShowingDialog) {
+ shouldShowActionDialog = true;
+ } else {
+ shouldShowActionDialog = shouldShowActionDialog(this, -1 /* pid */, appInfo.uid,
+ getCallingPackage(), null /* attributionTag */, verb,
+ mShouldCheckMediaPermissions, mShouldCheckReadAudio, mShouldCheckReadImages,
+ mShouldCheckReadVideo, mShouldCheckReadAudioOrReadVideo,
+ isTargetSdkAtLeastT);
+ }
+ } else {
+ shouldShowActionDialog = shouldShowActionDialog(this, -1 /* pid */, appInfo.uid,
+ getCallingPackage(), null /* attributionTag */, verb);
+ }
+
+ if (!shouldShowActionDialog) {
onPositiveAction(null, 0);
return;
}
@@ -382,6 +409,18 @@
@VisibleForTesting
static boolean shouldShowActionDialog(@NonNull Context context, int pid, int uid,
@NonNull String packageName, @Nullable String attributionTag, @NonNull String verb) {
+ return shouldShowActionDialog(context, pid, uid, packageName, attributionTag,
+ verb, /* shouldCheckMediaPermissions */ false, /* shouldCheckReadAudio */ false,
+ /* shouldCheckReadImages */ false, /* shouldCheckReadVideo */ false,
+ /* mShouldCheckReadAudioOrReadVideo */ false, /* isTargetSdkAtLeastT */ false);
+ }
+
+ @VisibleForTesting
+ static boolean shouldShowActionDialog(@NonNull Context context, int pid, int uid,
+ @NonNull String packageName, @Nullable String attributionTag, @NonNull String verb,
+ boolean shouldCheckMediaPermissions, boolean shouldCheckReadAudio,
+ boolean shouldCheckReadImages, boolean shouldCheckReadVideo,
+ boolean mShouldCheckReadAudioOrReadVideo, boolean isTargetSdkAtLeastT) {
// Favorite-related requests are automatically granted for now; we still
// make developers go through this no-op dialog flow to preserve our
// ability to start prompting in the future
@@ -389,12 +428,49 @@
return false;
}
- // check READ_EXTERNAL_STORAGE and MANAGE_EXTERNAL_STORAGE permissions
- if (!checkPermissionReadStorage(context, pid, uid, packageName, attributionTag)
- && !checkPermissionManager(context, pid, uid, packageName, attributionTag)) {
- Log.d(TAG, "No permission READ_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE");
- return true;
+ // no MANAGE_EXTERNAL_STORAGE permission
+ if (!checkPermissionManager(context, pid, uid, packageName, attributionTag)) {
+ if (shouldCheckMediaPermissions) {
+ // check READ_MEDIA_AUDIO
+ if (shouldCheckReadAudio && !checkPermissionReadAudio(context, pid, uid,
+ packageName, attributionTag, isTargetSdkAtLeastT)) {
+ Log.d(TAG, "No permission READ_MEDIA_AUDIO or MANAGE_EXTERNAL_STORAGE");
+ return true;
+ }
+
+ // check READ_MEDIA_IMAGES
+ if (shouldCheckReadImages && !checkPermissionReadImages(context, pid, uid,
+ packageName, attributionTag, isTargetSdkAtLeastT)) {
+ Log.d(TAG, "No permission READ_MEDIA_IMAGES or MANAGE_EXTERNAL_STORAGE");
+ return true;
+ }
+
+ // check READ_MEDIA_VIDEO
+ if (shouldCheckReadVideo && !checkPermissionReadVideo(context, pid, uid,
+ packageName, attributionTag, isTargetSdkAtLeastT)) {
+ Log.d(TAG, "No permission READ_MEDIA_VIDEO or MANAGE_EXTERNAL_STORAGE");
+ return true;
+ }
+
+ // For subtitle case, check READ_MEDIA_AUDIO or READ_MEDIA_VIDEO
+ if (mShouldCheckReadAudioOrReadVideo
+ && !checkPermissionReadAudio(context, pid, uid, packageName, attributionTag,
+ isTargetSdkAtLeastT)
+ && !checkPermissionReadVideo(context, pid, uid, packageName, attributionTag,
+ isTargetSdkAtLeastT)) {
+ Log.d(TAG, "No permission READ_MEDIA_AUDIO, READ_MEDIA_VIDEO or "
+ + "MANAGE_EXTERNAL_STORAGE");
+ return true;
+ }
+ } else {
+ // check READ_EXTERNAL_STORAGE
+ if (!checkPermissionReadStorage(context, pid, uid, packageName, attributionTag)) {
+ Log.d(TAG, "No permission READ_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE");
+ return true;
+ }
+ }
}
+
// check MANAGE_MEDIA permission
if (!checkPermissionManageMedia(context, pid, uid, packageName, attributionTag)) {
Log.d(TAG, "No permission MANAGE_MEDIA");
@@ -490,13 +566,41 @@
private @NonNull String resolveData() {
final LocalUriMatcher matcher = new LocalUriMatcher(MediaStore.AUTHORITY);
final int firstMatch = matcher.matchUri(uris.get(0), false);
+ parseDataToCheckPermissions(firstMatch);
+ boolean isMixedTypes = false;
+
for (int i = 1; i < uris.size(); i++) {
final int match = matcher.matchUri(uris.get(i), false);
if (match != firstMatch) {
+ // If we don't need to check new permission, we can return DATA_GENERIC here. We
+ // don't need to resolve the other uris.
+ if (!mShouldCheckMediaPermissions) {
+ return DATA_GENERIC;
+ }
// Any mismatch means we need to use generic strings
- return DATA_GENERIC;
+ isMixedTypes = true;
+ }
+
+ parseDataToCheckPermissions(match);
+
+ if (isMixedTypes && mShouldForceShowingDialog) {
+ // Already know the data is mixed types and should force showing dialog. Don't need
+ // to resolve the other uris.
+ break;
+ }
+
+ if (mShouldCheckReadAudio && mShouldCheckReadImages && mShouldCheckReadVideo
+ && mShouldCheckReadAudioOrReadVideo) {
+ // Already need to check all permissions for the mixed types. Don't need to resolve
+ // the other uris.
+ break;
}
}
+
+ if (isMixedTypes) {
+ return DATA_GENERIC;
+ }
+
switch (firstMatch) {
case AUDIO_MEDIA_ID: return DATA_AUDIO;
case VIDEO_MEDIA_ID: return DATA_VIDEO;
@@ -505,6 +609,31 @@
}
}
+ private void parseDataToCheckPermissions(int match) {
+ switch (match) {
+ case AUDIO_MEDIA_ID:
+ case AUDIO_PLAYLISTS_ID:
+ mShouldCheckReadAudio = true;
+ break;
+ case VIDEO_MEDIA_ID:
+ mShouldCheckReadVideo = true;
+ break;
+ case IMAGES_MEDIA_ID:
+ mShouldCheckReadImages = true;
+ break;
+ case FILES_ID:
+ // PermissionActivity is not exported. And we have a check in
+ // MediaProvider#createRequest method. If it matches FILES_ID, it is subtitle case.
+ // Check audio or video for it.
+ mShouldCheckReadAudioOrReadVideo = true;
+ break;
+ default:
+ // It is not the expected case. Force showing the dialog
+ mShouldForceShowingDialog = true;
+
+ }
+ }
+
/**
* Resolve the dialog title string to be displayed to the user. All
* arguments have been bound and this string is ready to be displayed.
diff --git a/src/com/android/providers/media/TranscodeHelperImpl.java b/src/com/android/providers/media/TranscodeHelperImpl.java
index b1f0502..09e132f 100644
--- a/src/com/android/providers/media/TranscodeHelperImpl.java
+++ b/src/com/android/providers/media/TranscodeHelperImpl.java
@@ -50,8 +50,6 @@
import android.content.pm.InstallSourceInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.Property;
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.media.ApplicationMediaCapabilities;
@@ -99,6 +97,7 @@
import com.android.providers.media.util.FileUtils;
import com.android.providers.media.util.ForegroundThread;
import com.android.providers.media.util.SQLiteQueryBuilder;
+import com.android.providers.media.util.StringUtils;
import java.io.BufferedReader;
import java.io.File;
@@ -113,7 +112,6 @@
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@@ -286,8 +284,8 @@
MAX_TRANSCODE_DURATION_MS);
mTranscodeDenialController = new TranscodeDenialController(mActivityManager,
mTranscodingUiNotifier, maxTranscodeDurationMs);
- mSupportedRelativePaths = verifySupportedRelativePaths(getStringArrayConfig(
- R.array.config_supported_transcoding_relative_paths));
+ mSupportedRelativePaths = verifySupportedRelativePaths(StringUtils.getStringArrayConfig(
+ mContext, R.array.config_supported_transcoding_relative_paths));
mHasHdrPlugin = hasHDRPlugin();
parseTranscodeCompatManifest();
@@ -889,16 +887,6 @@
return verifiedPaths;
}
- private List<String> getStringArrayConfig(int resId) {
- final Resources res = mContext.getResources();
- try {
- final String[] configValue = res.getStringArray(resId);
- return Arrays.asList(configValue);
- } catch (NotFoundException e) {
- return new ArrayList<String>();
- }
- }
-
private Optional<Boolean> checkAppCompatSupport(int uid, int fileFlags) {
int supportedFlags = 0;
int unsupportedFlags = 0;
diff --git a/src/com/android/providers/media/dao/FileRow.java b/src/com/android/providers/media/dao/FileRow.java
new file mode 100644
index 0000000..3410c0c
--- /dev/null
+++ b/src/com/android/providers/media/dao/FileRow.java
@@ -0,0 +1,157 @@
+/*
+ * 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.dao;
+
+/** DAO object representing database row of Files table of a MediaProvider database. */
+public class FileRow {
+
+ private final long mId;
+ private String mPath;
+ private String mOwnerPackageName;
+ private String mVolumeName;
+ private int mMediaType;
+ private boolean mIsDownload;
+ private boolean mIsPending;
+ private boolean mIsTrashed;
+ private boolean mIsFavorite;
+ private int mSpecialFormat;
+
+ public static class Builder {
+ private final long mId;
+ private String mPath;
+ private String mOwnerPackageName;
+ private String mVolumeName;
+ private int mMediaType;
+ private boolean mIsDownload;
+ private boolean mIsPending;
+ private boolean mIsTrashed;
+ private boolean mIsFavorite;
+ private int mSpecialFormat;
+
+ Builder(long id) {
+ this.mId = id;
+ }
+
+ public Builder setPath(String path) {
+ this.mPath = path;
+ return this;
+ }
+
+ public Builder setOwnerPackageName(String ownerPackageName) {
+ this.mOwnerPackageName = ownerPackageName;
+ return this;
+ }
+
+ public Builder setVolumeName(String volumeName) {
+ this.mVolumeName = volumeName;
+ return this;
+ }
+
+ public Builder setMediaType(int mediaType) {
+ this.mMediaType = mediaType;
+ return this;
+ }
+
+ public Builder setIsDownload(boolean download) {
+ mIsDownload = download;
+ return this;
+ }
+
+ public Builder setIsPending(boolean pending) {
+ mIsPending = pending;
+ return this;
+ }
+
+ public Builder setIsTrashed(boolean trashed) {
+ mIsTrashed = trashed;
+ return this;
+ }
+
+ public Builder setIsFavorite(boolean favorite) {
+ mIsFavorite = favorite;
+ return this;
+ }
+
+ public Builder setSpecialFormat(int specialFormat) {
+ this.mSpecialFormat = specialFormat;
+ return this;
+ }
+
+ public FileRow build() {
+ FileRow fileRow = new FileRow(this.mId);
+ fileRow.mPath = this.mPath;
+ fileRow.mOwnerPackageName = this.mOwnerPackageName;
+ fileRow.mVolumeName = this.mVolumeName;
+ fileRow.mMediaType = this.mMediaType;
+ fileRow.mIsDownload = this.mIsDownload;
+ fileRow.mIsPending = this.mIsPending;
+ fileRow.mIsTrashed = this.mIsTrashed;
+ fileRow.mIsFavorite = this.mIsFavorite;
+ fileRow.mSpecialFormat = this.mSpecialFormat;
+
+ return fileRow;
+ }
+ }
+
+ public static Builder newBuilder(long id) {
+ return new Builder(id);
+ }
+
+ private FileRow(long id) {
+ this.mId = id;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+
+ public String getOwnerPackageName() {
+ return mOwnerPackageName;
+ }
+
+ public String getVolumeName() {
+ return mVolumeName;
+ }
+
+ public int getMediaType() {
+ return mMediaType;
+ }
+
+ public boolean isDownload() {
+ return mIsDownload;
+ }
+
+ public boolean isPending() {
+ return mIsPending;
+ }
+
+ public boolean isTrashed() {
+ return mIsTrashed;
+ }
+
+ public boolean isFavorite() {
+ return mIsFavorite;
+ }
+
+ public int getSpecialFormat() {
+ return mSpecialFormat;
+ }
+}
diff --git a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
index c82cfc0..84f4205 100644
--- a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
+++ b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
@@ -23,6 +23,7 @@
import android.os.Environment;
import android.os.OperationCanceledException;
import android.os.ParcelFileDescriptor;
+import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.MediaStore;
import android.service.storage.ExternalStorageService;
@@ -31,6 +32,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.MediaProvider;
import com.android.providers.media.MediaService;
import com.android.providers.media.MediaVolume;
@@ -64,6 +66,17 @@
MediaProvider mediaProvider = getMediaProvider();
+ boolean uncachedMode = false;
+ if (SdkLevel.isAtLeastT()) {
+ StorageVolume vol =
+ getSystemService(StorageManager.class).getStorageVolume(upperFileSystemPath);
+ if (vol != null && vol.isExternallyManaged()) {
+ // Cache should be disabled when the volume is externally managed.
+ Log.i(TAG, "Disabling cache for externally managed volume " + upperFileSystemPath);
+ uncachedMode = true;
+ }
+ }
+
synchronized (sLock) {
if (sFuseDaemons.containsKey(sessionId)) {
Log.w(TAG, "Session already started with id: " + sessionId);
@@ -74,8 +87,11 @@
// mounts of the lower filesystem.
final String[] supportedTranscodingRelativePaths =
mediaProvider.getSupportedTranscodingRelativePaths().toArray(new String[0]);
+ final String[] supportedUncachedRelativePaths =
+ mediaProvider.getSupportedUncachedRelativePaths().toArray(new String[0]);
FuseDaemon daemon = new FuseDaemon(mediaProvider, this, deviceFd, sessionId,
- upperFileSystemPath.getPath(), supportedTranscodingRelativePaths);
+ upperFileSystemPath.getPath(), uncachedMode,
+ supportedTranscodingRelativePaths, supportedUncachedRelativePaths);
daemon.start();
sFuseDaemons.put(sessionId, daemon);
}
diff --git a/src/com/android/providers/media/fuse/FuseDaemon.java b/src/com/android/providers/media/fuse/FuseDaemon.java
index 9595843..97e14fe 100644
--- a/src/com/android/providers/media/fuse/FuseDaemon.java
+++ b/src/com/android/providers/media/fuse/FuseDaemon.java
@@ -42,22 +42,27 @@
private final MediaProvider mMediaProvider;
private final int mFuseDeviceFd;
private final String mPath;
+ private final boolean mUncachedMode;
private final String[] mSupportedTranscodingRelativePaths;
+ private final String[] mSupportedUncachedRelativePaths;
private final ExternalStorageServiceImpl mService;
@GuardedBy("mLock")
private long mPtr;
public FuseDaemon(@NonNull MediaProvider mediaProvider,
@NonNull ExternalStorageServiceImpl service, @NonNull ParcelFileDescriptor fd,
- @NonNull String sessionId, @NonNull String path,
- String[] supportedTranscodingRelativePaths) {
+ @NonNull String sessionId, @NonNull String path, boolean uncachedMode,
+ String[] supportedTranscodingRelativePaths, String[] supportedUncachedRelativePaths) {
mMediaProvider = Objects.requireNonNull(mediaProvider);
mService = Objects.requireNonNull(service);
setName(Objects.requireNonNull(sessionId));
mFuseDeviceFd = Objects.requireNonNull(fd).detachFd();
mPath = Objects.requireNonNull(path);
+ mUncachedMode = uncachedMode;
mSupportedTranscodingRelativePaths
= Objects.requireNonNull(supportedTranscodingRelativePaths);
+ mSupportedUncachedRelativePaths
+ = Objects.requireNonNull(supportedUncachedRelativePaths);
}
/** Starts a FUSE session. Does not return until the lower filesystem is unmounted. */
@@ -73,7 +78,9 @@
}
Log.i(TAG, "Starting thread for " + getName() + " ...");
- native_start(ptr, mFuseDeviceFd, mPath, mSupportedTranscodingRelativePaths); // Blocks
+ native_start(ptr, mFuseDeviceFd, mPath, mUncachedMode,
+ mSupportedTranscodingRelativePaths,
+ mSupportedUncachedRelativePaths); // Blocks
Log.i(TAG, "Exiting thread for " + getName() + " ...");
synchronized (mLock) {
@@ -161,6 +168,21 @@
}
/**
+ * Checks if the FuseDaemon uses the FUSE passthrough feature.
+ *
+ * @return {@code true} if the FuseDaemon uses FUSE passthrough, {@code false} otherwise
+ */
+ public boolean usesFusePassthrough() {
+ synchronized (mLock) {
+ if (mPtr == 0) {
+ Log.i(TAG, "usesFusePassthrough failed, FUSE daemon unavailable");
+ return false;
+ }
+ return native_uses_fuse_passthrough(mPtr);
+ }
+ }
+
+ /**
* Invalidates FUSE VFS dentry cache for {@code path}
*/
public void invalidateFuseDentryCache(String path) {
@@ -187,11 +209,13 @@
// Takes ownership of the passed in file descriptor!
private native void native_start(long daemon, int deviceFd, String path,
- String[] supportedTranscodingRelativePaths);
+ boolean uncachedMode, String[] supportedTranscodingRelativePaths,
+ String[] supportedUncachedRelativePaths);
private native void native_delete(long daemon);
private native boolean native_should_open_with_fuse(long daemon, String path, boolean readLock,
int fd);
+ private native boolean native_uses_fuse_passthrough(long daemon);
private native void native_invalidate_fuse_dentry_cache(long daemon, String path);
private native boolean native_is_started(long daemon);
private native FdAccessResult native_check_fd_access(long daemon, int fd, int uid);
diff --git a/src/com/android/providers/media/metrics/MPUiEventLoggerImpl.java b/src/com/android/providers/media/metrics/MPUiEventLoggerImpl.java
index 68f951e..fadb70c 100644
--- a/src/com/android/providers/media/metrics/MPUiEventLoggerImpl.java
+++ b/src/com/android/providers/media/metrics/MPUiEventLoggerImpl.java
@@ -31,6 +31,11 @@
}
@Override
+ public void log(@NonNull UiEventEnum event, @Nullable InstanceId instance) {
+ logWithInstanceId(event, 0, null, instance);
+ }
+
+ @Override
public void log(@NonNull UiEventEnum event, int uid, @Nullable String packageName) {
final int eventID = event.getId();
if (eventID > 0) {
@@ -66,7 +71,8 @@
/* event_id = 1 */ eventID,
/* package_name = 2 */ packageName,
/* instance_id = 3 */ 0,
- /* position_picked = 4 */ position);
+ /* position_picked = 4 */ position,
+ /* is_pinned = 5 */ false);
}
}
@@ -79,7 +85,8 @@
/* event_id = 1 */ eventID,
/* package_name = 2 */ packageName,
/* instance_id = 3 */ instance.getId(),
- /* position_picked = 4 */ position);
+ /* position_picked = 4 */ position,
+ /* is_pinned = 5 */ false);
} else {
logWithPosition(event, uid, packageName, position);
}
diff --git a/src/com/android/providers/media/metrics/PulledMetrics.java b/src/com/android/providers/media/metrics/PulledMetrics.java
index 599eee5..b1986f2 100644
--- a/src/com/android/providers/media/metrics/PulledMetrics.java
+++ b/src/com/android/providers/media/metrics/PulledMetrics.java
@@ -80,21 +80,27 @@
if (!isInitialized) {
return;
}
-
- storageAccessMetrics.logMimeType(uid, mimeType);
+ BackgroundThread.getExecutor().execute(() -> {
+ storageAccessMetrics.logMimeType(uid, mimeType);
+ });
}
/**
* Logs the storage access and attributes it to the given {@code uid}.
*
- * <p>Should only be called from a FUSE thread.
+ * <p>This is a no-op if it's called from a non-FUSE thread.
*/
public static void logFileAccessViaFuse(int uid, @NonNull String file) {
if (!isInitialized) {
return;
}
-
- storageAccessMetrics.logAccessViaFuse(uid, file);
+ // Log only if it's a FUSE thread
+ if (!FuseDaemon.native_is_fuse_thread()) {
+ return;
+ }
+ BackgroundThread.getExecutor().execute(() -> {
+ storageAccessMetrics.logAccessViaFuse(uid, file);
+ });
}
/**
@@ -111,7 +117,9 @@
if (FuseDaemon.native_is_fuse_thread()) {
return;
}
- storageAccessMetrics.logAccessViaMediaProvider(uid, volumeName);
+ BackgroundThread.getExecutor().execute(() -> {
+ storageAccessMetrics.logAccessViaMediaProvider(uid, volumeName);
+ });
}
private static class StatsPullCallbackHandler implements StatsManager.StatsPullAtomCallback {
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
index ee69ecf..34d6040 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
@@ -16,62 +16,34 @@
package com.android.providers.media.photopicker;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_PAUSED;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_STARTED;
+import static android.provider.CloudMediaProviderContract.EXTRA_AUTHORITY;
import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_BUFFERING;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_COMPLETED;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_MEDIA_SIZE_CHANGED;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY;
+import static android.provider.CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB;
import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
-import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
-import android.annotation.DurationMillisLong;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
-import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.Point;
-import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.Looper;
import android.os.OperationCanceledException;
import android.os.ParcelFileDescriptor;
import android.provider.CloudMediaProvider;
import android.provider.MediaStore;
-import android.util.Log;
-import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.providers.media.LocalCallingIdentity;
import com.android.providers.media.MediaProvider;
import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
import com.android.providers.media.photopicker.data.ExternalDbFacade;
import com.android.providers.media.photopicker.ui.remotepreview.RemotePreviewHandler;
+import com.android.providers.media.photopicker.ui.remotepreview.RemoteSurfaceController;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.google.android.exoplayer2.DefaultLoadControl;
-import com.google.android.exoplayer2.DefaultRenderersFactory;
-import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.LoadControl;
-import com.google.android.exoplayer2.MediaItem;
-import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.Player.State;
-import com.google.android.exoplayer2.analytics.AnalyticsCollector;
-import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
-import com.google.android.exoplayer2.source.ProgressiveMediaSource;
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.upstream.ContentDataSource;
-import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
-import com.google.android.exoplayer2.util.Clock;
-import com.google.android.exoplayer2.video.VideoSize;
-
-import java.io.File;
import java.io.FileNotFoundException;
/**
@@ -124,9 +96,16 @@
final Bundle opts = new Bundle();
opts.putParcelable(ContentResolver.EXTRA_SIZE, size);
+ String mimeTypeFilter = null;
+ if (extras.getBoolean(EXTRA_MEDIASTORE_THUMB)) {
+ // This is a request for thumbnail, set "image/*" to get cached thumbnails from
+ // MediaProvider.
+ mimeTypeFilter = "image/*";
+ }
+
final LocalCallingIdentity token = mMediaProvider.clearLocalCallingIdentity();
try {
- return mMediaProvider.openTypedAssetFile(fromMediaId(mediaId), "image/*", opts);
+ return mMediaProvider.openTypedAssetFile(fromMediaId(mediaId), mimeTypeFilter, opts);
} finally {
mMediaProvider.restoreLocalCallingIdentity(token);
}
@@ -157,10 +136,12 @@
public CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull Bundle config,
CloudMediaSurfaceStateChangedCallback callback) {
if (RemotePreviewHandler.isRemotePreviewEnabled()) {
- boolean enableLoop = config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, false);
- boolean muteAudio = config.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
+ final String authority = config.getString(EXTRA_AUTHORITY,
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
+ final boolean enableLoop = config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, false);
+ final boolean muteAudio = config.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
false);
- return new CloudMediaSurfaceControllerImpl(getContext(), enableLoop, muteAudio,
+ return new RemoteSurfaceController(getContext(), authority, enableLoop, muteAudio,
callback);
}
return null;
@@ -179,247 +160,4 @@
return MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL,
Long.parseLong(mediaId));
}
-
- private static final class CloudMediaSurfaceControllerImpl extends CloudMediaSurfaceController {
-
- // The minimum duration of media that the player will attempt to ensure is buffered at all
- // times.
- private static final int MIN_BUFFER_MS = 1000;
- // The maximum duration of media that the player will attempt to buffer.
- private static final int MAX_BUFFER_MS = 2000;
- // The duration of media that must be buffered for playback to start or resume following a
- // user action such as a seek.
- private static final int BUFFER_FOR_PLAYBACK_MS = 1000;
- // The default duration of media that must be buffered for playback to resume after a
- // rebuffer.
- private static final int BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 1000;
- private static final LoadControl sLoadControl = new DefaultLoadControl.Builder()
- .setBufferDurationsMs(
- MIN_BUFFER_MS,
- MAX_BUFFER_MS,
- BUFFER_FOR_PLAYBACK_MS,
- BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS).build();
-
- private final Context mContext;
- private final CloudMediaSurfaceStateChangedCallback mCallback;
- private final Handler mHandler = new Handler(Looper.getMainLooper());
- private final Player.Listener mEventListener = new Player.Listener() {
- @Override
- public void onPlaybackStateChanged(@State int state) {
- Log.d(TAG, "Received player event " + state);
-
- switch (state) {
- case Player.STATE_READY:
- mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_READY,
- null);
- return;
- case Player.STATE_BUFFERING:
- mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_BUFFERING,
- null);
- return;
- case Player.STATE_ENDED:
- mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_COMPLETED,
- null);
- return;
- default:
- }
- }
-
- @Override
- public void onIsPlayingChanged(boolean isPlaying) {
- mCallback.setPlaybackState(mCurrentSurfaceId, isPlaying ? PLAYBACK_STATE_STARTED :
- PLAYBACK_STATE_PAUSED, null);
- }
-
- @Override
- public void onVideoSizeChanged(VideoSize videoSize) {
- Point size = new Point(videoSize.width, videoSize.height);
- Bundle bundle = new Bundle();
- bundle.putParcelable(ContentResolver.EXTRA_SIZE, size);
- mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_MEDIA_SIZE_CHANGED,
- bundle);
- }
- };
-
- private boolean mEnableLoop;
- private boolean mMuteAudio;
- private ExoPlayer mPlayer;
- private int mCurrentSurfaceId = -1;
-
- CloudMediaSurfaceControllerImpl(Context context, boolean enableLoop, boolean muteAudio,
- CloudMediaSurfaceStateChangedCallback callback) {
- mCallback = callback;
- mContext = context;
- mEnableLoop = enableLoop;
- mMuteAudio = muteAudio;
- Log.d(TAG, "Surface controller created.");
- }
-
- @Override
- public void onPlayerCreate() {
- mHandler.post(() -> {
- mPlayer = createExoPlayer();
- mPlayer.addListener(mEventListener);
- updateLoopingPlaybackStatus();
- updateAudioMuteStatus();
- Log.d(TAG, "Player created.");
- });
- }
-
- @Override
- public void onPlayerRelease() {
- mHandler.post(() -> {
- mPlayer.removeListener(mEventListener);
- mPlayer.release();
- mPlayer = null;
- Log.d(TAG, "Player released.");
- });
- }
-
- @Override
- public void onSurfaceCreated(int surfaceId, @NonNull Surface surface,
- @NonNull String mediaId) {
- mHandler.post(() -> {
- try {
- // onSurfaceCreated may get called while the player is already rendering on a
- // different surface. In that case, pause the player before preparing it for
- // rendering on the new surface.
- // Unfortunately, Exoplayer#stop doesn't seem to work here. If we call stop(),
- // as soon as the player becomes ready again, it automatically starts to play
- // the new media. The reason is that Exoplayer treats play/pause as calls to
- // the method Exoplayer#setPlayWhenReady(boolean) with true and false
- // respectively. So, if we don't pause(), then since the previous play() call
- // had set setPlayWhenReady to true, the player would start the playback as soon
- // as it gets ready with the new media item.
- if (mPlayer.isPlaying()) {
- mPlayer.pause();
- }
-
- mCurrentSurfaceId = surfaceId;
-
- final Uri mediaUri =
- Uri.parse(
- MediaStore.Files.getContentUri(
- MediaStore.VOLUME_EXTERNAL)
- + File.separator + mediaId);
- mPlayer.setMediaItem(MediaItem.fromUri(mediaUri));
- mPlayer.setVideoSurface(surface);
- mPlayer.prepare();
-
- Log.d(TAG, "Surface prepared: " + surfaceId + ". Surface: " + surface
- + ". MediaId: " + mediaId);
- } catch (RuntimeException e) {
- Log.e(TAG, "Error preparing player with surface.", e);
- }
- });
- }
-
- @Override
- public void onSurfaceChanged(int surfaceId, int format, int width, int height) {
- Log.d(TAG, "Surface changed: " + surfaceId + ". Format: " + format + ". Width: "
- + width + ". Height: " + height);
- }
-
- @Override
- public void onSurfaceDestroyed(int surfaceId) {
- mHandler.post(() -> {
- if (mCurrentSurfaceId != surfaceId) {
- // This means that the player is already using some other surface, hence
- // nothing to do.
- return;
- }
- if (mPlayer.isPlaying()) {
- mPlayer.stop();
- }
- mPlayer.clearVideoSurface();
- mCurrentSurfaceId = -1;
-
- Log.d(TAG, "Surface released: " + surfaceId);
- });
- }
-
- @Override
- public void onMediaPlay(int surfaceId) {
- mHandler.post(() -> {
- mPlayer.play();
- Log.d(TAG, "Media played: " + surfaceId);
- });
- }
-
- @Override
- public void onMediaPause(int surfaceId) {
- mHandler.post(() -> {
- if (mPlayer.isPlaying()) {
- mPlayer.pause();
- Log.d(TAG, "Media paused: " + surfaceId);
- }
- });
- }
-
- @Override
- public void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis) {
- mHandler.post(() -> {
- mPlayer.seekTo((int) timestampMillis);
- Log.d(TAG, "Media seeked: " + surfaceId + ". Timestamp: " + timestampMillis);
- });
- }
-
- @Override
- public void onConfigChange(@NonNull Bundle config) {
- final boolean enableLoop = config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
- mEnableLoop);
- final boolean muteAudio = config.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
- mMuteAudio);
- mHandler.post(() -> {
- if (mEnableLoop != enableLoop) {
- mEnableLoop = enableLoop;
- updateLoopingPlaybackStatus();
- }
-
- if (mMuteAudio != muteAudio) {
- mMuteAudio = muteAudio;
- updateAudioMuteStatus();
- }
- });
- Log.d(TAG, "Config changed. Updated config params: " + config);
- }
-
- @Override
- public void onDestroy() {
- Log.d(TAG, "Surface controller destroyed.");
- }
-
- private ExoPlayer createExoPlayer() {
- // ProgressiveMediaFactory will be enough for video playback of videos on the device.
- // This also reduces apk size.
- ProgressiveMediaSource.Factory mediaSourceFactory = new ProgressiveMediaSource.Factory(
- () -> new ContentDataSource(mContext), MediaParserExtractorAdapter.FACTORY);
-
- return new ExoPlayer.Builder(mContext,
- new DefaultRenderersFactory(mContext),
- new DefaultTrackSelector(mContext),
- mediaSourceFactory,
- sLoadControl,
- DefaultBandwidthMeter.getSingletonInstance(mContext),
- new AnalyticsCollector(Clock.DEFAULT)).buildExoPlayer();
- }
-
- private void updateLoopingPlaybackStatus() {
- mPlayer.setRepeatMode(mEnableLoop ? Player.REPEAT_MODE_ONE : Player.REPEAT_MODE_OFF);
- }
-
- private void updateAudioMuteStatus() {
- if (mMuteAudio) {
- mPlayer.setVolume(0f);
- } else {
- AudioManager audioManager = mContext.getSystemService(AudioManager.class);
- if (audioManager == null) {
- Log.e(TAG, "Couldn't find AudioManager while trying to set volume,"
- + " unable to set volume");
- return;
- }
- mPlayer.setVolume(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
- }
- }
- }
}
diff --git a/src/com/android/providers/media/photopicker/PickerDataLayer.java b/src/com/android/providers/media/photopicker/PickerDataLayer.java
index 23a8d73..a62ba39 100644
--- a/src/com/android/providers/media/photopicker/PickerDataLayer.java
+++ b/src/com/android/providers/media/photopicker/PickerDataLayer.java
@@ -69,6 +69,11 @@
// Refresh the 'media' table
mSyncController.syncAllMedia();
+ if (TextUtils.isEmpty(albumId)) {
+ // Notify that the picker is launched in case there's any pending UI notification
+ mSyncController.notifyPickerLaunch();
+ }
+
// Fetch all merged and deduped cloud and local media from 'media' table
// This also matches 'merged' albums like Favorites because |authority| will
// be null, hence we have to fetch the data from the picker db
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index 5632228..d0fb856 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -16,44 +16,46 @@
package com.android.providers.media.photopicker;
+import static android.content.ContentResolver.EXTRA_HONORED_ARGS;
import static android.provider.CloudMediaProviderContract.EXTRA_ALBUM_ID;
-import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION;
import static android.provider.CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID;
import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN;
import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION;
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
-import static android.content.ContentResolver.EXTRA_HONORED_ARGS;
-import static com.android.providers.media.PickerUriResolver.getMediaUri;
+
import static com.android.providers.media.PickerUriResolver.getDeletedMediaUri;
import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri;
+import static com.android.providers.media.PickerUriResolver.getMediaUri;
import android.annotation.IntDef;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Process;
-import android.os.SystemProperties;
+import android.os.storage.StorageManager;
import android.provider.CloudMediaProvider;
import android.provider.CloudMediaProviderContract;
-import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
+import android.widget.Toast;
+
import androidx.annotation.GuardedBy;
import androidx.annotation.VisibleForTesting;
+
import com.android.modules.utils.BackgroundThread;
-import com.android.providers.media.MediaProvider;
-import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.R;
-import com.android.providers.media.util.DeviceConfigUtils;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.util.ForegroundThread;
import com.android.providers.media.util.StringUtils;
import java.lang.annotation.Retention;
@@ -71,11 +73,12 @@
public class PickerSyncController {
private static final String TAG = "PickerSyncController";
- public static final String PROP_DEFAULT_SYNC_DELAY_MS = "pickerdb.default_sync_delay_ms";
+ public static final String SYNC_DELAY_MS = "default_sync_delay_ms";
+ public static final String ALLOWED_CLOUD_PROVIDERS_KEY = "allowed_cloud_providers";
private static final String PREFS_KEY_CLOUD_PROVIDER_AUTHORITY = "cloud_provider_authority";
- private static final String PREFS_KEY_CLOUD_PROVIDER_PKGNAME = "cloud_provider_pkg_name";
- private static final String PREFS_KEY_CLOUD_PROVIDER_UID = "cloud_provider_uid";
+ private static final String PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION =
+ "cloud_provider_pending_notification";
private static final String PREFS_KEY_CLOUD_PREFIX = "cloud_provider:";
private static final String PREFS_KEY_LOCAL_PREFIX = "local_provider:";
@@ -84,14 +87,6 @@
public static final String LOCAL_PICKER_PROVIDER_AUTHORITY =
"com.android.providers.media.photopicker";
- public static final String PROP_USE_ALLOWED_CLOUD_PROVIDERS =
- "persist.sys.photopicker.use_allowed_cloud_providers";
- public static final String ALLOWED_CLOUD_PROVIDERS_KEY = "allowed_cloud_providers";
-
- private static final String DEFAULT_CLOUD_PROVIDER_AUTHORITY = null;
- private static final String DEFAULT_CLOUD_PROVIDER_PKGNAME = null;
- private static final int DEFAULT_CLOUD_PROVIDER_UID = -1;
-
private static final int SYNC_TYPE_NONE = 0;
private static final int SYNC_TYPE_MEDIA_INCREMENTAL = 1;
private static final int SYNC_TYPE_MEDIA_FULL = 2;
@@ -120,14 +115,7 @@
private CloudProviderInfo mCloudProviderInfo;
public PickerSyncController(Context context, PickerDbFacade dbFacade,
- MediaProvider mediaProvider) {
- this(context, dbFacade, LOCAL_PICKER_PROVIDER_AUTHORITY, mediaProvider.getIntDeviceConfig(
- DeviceConfig.NAMESPACE_STORAGE, PROP_DEFAULT_SYNC_DELAY_MS, 5000));
- }
-
- @VisibleForTesting
- PickerSyncController(Context context, PickerDbFacade dbFacade,
- String localProvider, long syncDelayMs) {
+ String localProvider, String allowedCloudProviders, long syncDelayMs) {
mContext = context;
mSyncPrefs = mContext.getSharedPreferences(PICKER_SYNC_PREFS_FILE_NAME,
Context.MODE_PRIVATE);
@@ -138,30 +126,23 @@
mSyncDelayMs = syncDelayMs;
mSyncAllMediaCallback = this::syncAllMedia;
- final String cloudProviderAuthority = mUserPrefs.getString(
- PREFS_KEY_CLOUD_PROVIDER_AUTHORITY,
- DEFAULT_CLOUD_PROVIDER_AUTHORITY);
- final String cloudProviderPackageName = mUserPrefs.getString(
- PREFS_KEY_CLOUD_PROVIDER_PKGNAME,
- DEFAULT_CLOUD_PROVIDER_PKGNAME);
- final int cloudProviderUid = mUserPrefs.getInt(PREFS_KEY_CLOUD_PROVIDER_UID,
- DEFAULT_CLOUD_PROVIDER_UID);
+ final String cachedAuthority = mUserPrefs.getString(
+ PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, null);
- if (SystemProperties.getBoolean(PROP_USE_ALLOWED_CLOUD_PROVIDERS, false)) {
- mAllowedCloudProviders = getAllowedCloudProviders();
+ mAllowedCloudProviders = parseAllowedCloudProviders(allowedCloudProviders);
+
+ final CloudProviderInfo defaultInfo = getDefaultCloudProviderInfo(cachedAuthority);
+
+ if (Objects.equals(defaultInfo.authority, cachedAuthority)) {
+ // Just set it without persisting since it's not changing and persisting would
+ // notify the user that cloud media is now available
+ mCloudProviderInfo = defaultInfo;
} else {
- mAllowedCloudProviders = new ArraySet<>();
+ // Persist it so that we notify the user that cloud media is now available
+ persistCloudProviderInfo(defaultInfo);
}
- if (cloudProviderAuthority == null) {
- // TODO: Only get default if it wasn't set by the user
- final CloudProviderInfo defaultCloudProviderInfo = getDefaultCloudProviderInfo();
- Log.i(TAG, "Cloud provider is set to Default " + defaultCloudProviderInfo.authority);
- setCloudProviderInfo(defaultCloudProviderInfo);
- } else {
- mCloudProviderInfo = new CloudProviderInfo(cloudProviderAuthority,
- cloudProviderPackageName, cloudProviderUid);
- }
+ Log.d(TAG, "Initialized cloud provider to: " + mCloudProviderInfo.authority);
}
/**
@@ -215,7 +196,7 @@
* Returns the supported cloud {@link CloudMediaProvider} infos.
*/
public CloudProviderInfo getCloudProviderInfo(String authority) {
- for (CloudProviderInfo info : getSupportedCloudProviders()) {
+ for (CloudProviderInfo info : getSupportedCloudProviders(/* ignoreAllowList */ false)) {
if (info.authority.equals(authority)) {
return info;
}
@@ -229,15 +210,12 @@
*/
@VisibleForTesting
List<CloudProviderInfo> getSupportedCloudProviders() {
+ return getSupportedCloudProviders(/* ignoreAllowList */ false);
+ }
+
+ private List<CloudProviderInfo> getSupportedCloudProviders(boolean ignoreAllowList) {
final List<CloudProviderInfo> result = new ArrayList<>();
- final boolean useAllowedCloudProviders =
- SystemProperties.getBoolean(PROP_USE_ALLOWED_CLOUD_PROVIDERS, false);
-
- if (useAllowedCloudProviders && mAllowedCloudProviders.isEmpty()) {
- return result;
- }
-
final PackageManager pm = mContext.getPackageManager();
final Intent intent = new Intent(CloudMediaProviderContract.PROVIDER_INTERFACE);
final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, /* flags */ 0);
@@ -247,8 +225,8 @@
if (providerInfo.authority != null
&& CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION.equals(
providerInfo.readPermission)
- && (!useAllowedCloudProviders
- || mAllowedCloudProviders.contains(providerInfo.authority))) {
+ && (ignoreAllowList
+ || mAllowedCloudProviders.contains(providerInfo.authority))) {
result.add(new CloudProviderInfo(providerInfo.authority,
providerInfo.applicationInfo.packageName,
providerInfo.applicationInfo.uid));
@@ -281,7 +259,7 @@
if (authority == null || !newProviderInfo.isEmpty()) {
synchronized (mLock) {
final String oldAuthority = mCloudProviderInfo.authority;
- setCloudProviderInfo(newProviderInfo);
+ persistCloudProviderInfo(newProviderInfo);
resetCachedMediaCollectionInfo(newProviderInfo.authority);
// Disable cloud provider queries on the db until next sync
@@ -301,6 +279,20 @@
return false;
}
+ /**
+ * Set cloud provider and update allowed cloud providers
+ */
+ @VisibleForTesting
+ public void forceSetCloudProvider(String authority) {
+ if (authority == null) {
+ mAllowedCloudProviders.clear();
+ } else {
+ mAllowedCloudProviders.add(authority);
+ }
+
+ setCloudProvider(authority);
+ }
+
public String getCloudProvider() {
synchronized (mLock) {
return mCloudProviderInfo.authority;
@@ -345,7 +337,11 @@
return true;
}
- final List<CloudProviderInfo> infos = getSupportedCloudProviders();
+ // TODO(b/232738117): Enforce allow list here. This works around some CTS failure late in
+ // Android T. The current implementation is fine since cloud providers is only supported
+ // for app developers testing.
+ final List<CloudProviderInfo> infos = getSupportedCloudProviders(
+ /* ignoreAllowList */ true);
for (CloudProviderInfo info : infos) {
if (info.uid == uid && info.authority.equals(authority)) {
return true;
@@ -380,6 +376,48 @@
}
}
+ /**
+ * Notifies about picker UI launched
+ */
+ public void notifyPickerLaunch() {
+ final String packageName;
+ synchronized (mLock) {
+ packageName = mCloudProviderInfo.packageName;
+ }
+
+ final boolean hasPendingNotification = mUserPrefs.getBoolean(
+ PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false);
+
+ if (!hasPendingNotification || (packageName == null)) {
+ Log.d(TAG, "No pending UI notification");
+ return;
+ }
+
+ // Offload showing the UI on a fg thread to avoid the expensive binder request
+ // to fetch the app name blocking the picker launch
+ ForegroundThread.getHandler().post(() -> {
+ Log.i(TAG, "Cloud media now available in the picker");
+
+ final PackageManager pm = mContext.getPackageManager();
+ String appName = packageName;
+ try {
+ ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0);
+ appName = (String) pm.getApplicationLabel(appInfo);
+ } catch (final NameNotFoundException e) {
+ Log.i(TAG, "Failed to get appName for package: " + packageName);
+ }
+
+ final String message = mContext.getResources().getString(R.string.picker_cloud_sync,
+ appName);
+ Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
+ });
+
+ // Clear the notification
+ final SharedPreferences.Editor editor = mUserPrefs.edit();
+ editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false);
+ editor.apply();
+ }
+
private void syncAlbumMediaFromProvider(String authority, String albumId) {
final Bundle queryArgs = new Bundle();
queryArgs.putString(EXTRA_ALBUM_ID, albumId);
@@ -515,24 +553,36 @@
}
}
- private void setCloudProviderInfo(CloudProviderInfo info) {
+ private void persistCloudProviderInfo(CloudProviderInfo info) {
synchronized (mLock) {
mCloudProviderInfo = info;
}
+ final String authority = info.authority;
final SharedPreferences.Editor editor = mUserPrefs.edit();
if (info.isEmpty()) {
editor.remove(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY);
- editor.remove(PREFS_KEY_CLOUD_PROVIDER_PKGNAME);
- editor.remove(PREFS_KEY_CLOUD_PROVIDER_UID);
+ editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false);
} else {
- editor.putString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, info.authority);
- editor.putString(PREFS_KEY_CLOUD_PROVIDER_PKGNAME, info.packageName);
- editor.putInt(PREFS_KEY_CLOUD_PROVIDER_UID, info.uid);
+ editor.putString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, authority);
+ editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, true);
}
editor.apply();
+
+ if (SdkLevel.isAtLeastT()) {
+ try {
+ StorageManager sm = mContext.getSystemService(StorageManager.class);
+ sm.setCloudMediaProvider(authority);
+ } catch (SecurityException e) {
+ // When run as part of the unit tests, the notification fails because only the
+ // MediaProvider uid can notify
+ Log.w(TAG, "Failed to notify the system of cloud provider update to: " + authority);
+ }
+ }
+
+ Log.d(TAG, "Updated cloud provider to: " + authority);
}
private void cacheMediaCollectionInfo(String authority, Bundle bundle) {
@@ -674,8 +724,9 @@
+ totalRowcount + ". Cursor count: " + cursorCount);
}
- private CloudProviderInfo getDefaultCloudProviderInfo() {
- final List<CloudProviderInfo> infos = getSupportedCloudProviders();
+ private CloudProviderInfo getDefaultCloudProviderInfo(String cachedProvider) {
+ final List<CloudProviderInfo> infos =
+ getSupportedCloudProviders(/* ignoreAllowList */ false);
if (infos.size() == 1) {
Log.i(TAG, "Only 1 cloud provider found, hence "
@@ -684,7 +735,16 @@
} else {
final String defaultCloudProviderAuthority = StringUtils.getStringConfig(
mContext, R.string.config_default_cloud_provider_authority);
- Log.i(TAG, "Default cloud provider to be used is " + defaultCloudProviderAuthority);
+ Log.i(TAG, "Found multiple cloud providers but OEM default is: "
+ + defaultCloudProviderAuthority);
+
+ if (cachedProvider != null) {
+ for (CloudProviderInfo info : infos) {
+ if (info.authority.equals(defaultCloudProviderAuthority)) {
+ return info;
+ }
+ }
+ }
if (defaultCloudProviderAuthority != null) {
for (CloudProviderInfo info : infos) {
@@ -699,10 +759,9 @@
return CloudProviderInfo.EMPTY;
}
- private Set<String> getAllowedCloudProviders() {
+ private Set<String> parseAllowedCloudProviders(String config) {
Set<String> allowedProviders = new ArraySet<>();
- final String[] allowedProvidersConfig = DeviceConfigUtils.getStringDeviceConfig(
- ALLOWED_CLOUD_PROVIDERS_KEY, "").split(",");
+ final String[] allowedProvidersConfig = config.split(",");
if (allowedProvidersConfig.length == 0 || allowedProvidersConfig[0].isEmpty()) {
Log.i(TAG, "Empty allowed cloud providers");
@@ -762,9 +821,9 @@
private final int uid;
private CloudProviderInfo() {
- this.authority = DEFAULT_CLOUD_PROVIDER_AUTHORITY;
- this.packageName = DEFAULT_CLOUD_PROVIDER_PKGNAME;
- this.uid = DEFAULT_CLOUD_PROVIDER_UID;
+ this.authority = null;
+ this.packageName = null;
+ this.uid = -1;
}
CloudProviderInfo(String authority, String packageName, int uid) {
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
index 5034760..2477caf 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
@@ -16,16 +16,17 @@
package com.android.providers.media.photopicker.data.glide;
+import static com.android.providers.media.photopicker.ui.ImageLoader.THUMBNAIL_REQUEST;
+
import android.content.Context;
+import android.content.UriMatcher;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
-import android.provider.MediaStore;
-
-import com.android.providers.media.photopicker.PickerSyncController;
+import android.provider.CloudMediaProviderContract;
import com.bumptech.glide.load.Options;
-import com.bumptech.glide.signature.ObjectKey;
import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.signature.ObjectKey;
/**
* Custom {@link ModelLoader} to load thumbnails from cloud media provider.
@@ -40,22 +41,19 @@
@Override
public LoadData<ParcelFileDescriptor> buildLoadData(Uri model, int width, int height,
Options options) {
+ final boolean isThumbRequest = Boolean.TRUE.equals(options.get(THUMBNAIL_REQUEST));
return new LoadData<>(new ObjectKey(model),
- new PickerThumbnailFetcher(mContext, model, width, height));
+ new PickerThumbnailFetcher(mContext, model, width, height, isThumbRequest));
}
@Override
public boolean handles(Uri model) {
- if (model == null) return false;
+ final int pickerId = 1;
+ final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
+ matcher.addURI(model.getAuthority(),
+ CloudMediaProviderContract.URI_PATH_MEDIA + "/*", pickerId);
- String authority = model.getAuthority();
- // TODO(b/210190677): Handle all local picker provider uris irrespective of cloud or local.
- // PickerModuleLoader fetches thumbnail data by forwarding the request to corresponding
- // ContentProvider. For local provider uris, this request goes to MediaProvider where video
- // thumbnail is obtained from the mid-point of the video. For PhotoPicker, we need the
- // thumbnail from the first frame. Hence, as a temporary fix, local provider uris will be
- // handled by default Glide module.
- return !PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY.equals(authority)
- && !MediaStore.AUTHORITY.equals(authority);
+ // Matches picker URIs of the form content://<authority>/media
+ return matcher.match(model) == pickerId;
}
}
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java b/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
index 15827ff..9dcb211 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
@@ -23,6 +23,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
+import android.provider.CloudMediaProviderContract;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
@@ -37,27 +38,36 @@
*/
public class PickerThumbnailFetcher implements DataFetcher<ParcelFileDescriptor> {
- private final Context context;
- private final Uri model;
- private final int width;
- private final int height;
+ private final Context mContext;
+ private final Uri mModel;
+ private final int mWidth;
+ private final int mHeight;
+ private final boolean mIsThumbRequest;
- PickerThumbnailFetcher(Context context, Uri model, int width, int height) {
- this.context = context;
- this.model = model;
- this.width = width;
- this.height = height;
+ PickerThumbnailFetcher(Context context, Uri model, int width, int height,
+ boolean isThumbRequest) {
+ mContext = context;
+ mModel = model;
+ mWidth = width;
+ mHeight = height;
+ mIsThumbRequest = isThumbRequest;
}
@Override
public void loadData(Priority priority, DataCallback<? super ParcelFileDescriptor> callback) {
- ContentResolver contentResolver = context.getContentResolver();
+ ContentResolver contentResolver = mContext.getContentResolver();
final Bundle opts = new Bundle();
- opts.putParcelable(ContentResolver.EXTRA_SIZE, new Point(width, height));
- try (AssetFileDescriptor afd = contentResolver.openTypedAssetFileDescriptor(model,
+ opts.putParcelable(ContentResolver.EXTRA_SIZE, new Point(mWidth, mHeight));
+ opts.putBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL, true);
+
+ if (mIsThumbRequest) {
+ opts.putBoolean(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB, true);
+ }
+
+ try (AssetFileDescriptor afd = contentResolver.openTypedAssetFileDescriptor(mModel,
/* mimeType */ "image/*", opts, /* cancellationSignal */ null)) {
if (afd == null) {
- final String err = "Failed to load data for " + model;
+ final String err = "Failed to load data for " + mModel;
callback.onLoadFailed(new FileNotFoundException(err));
return;
}
diff --git a/src/com/android/providers/media/photopicker/ui/DevicePolicyResources.java b/src/com/android/providers/media/photopicker/ui/DevicePolicyResources.java
new file mode 100644
index 0000000..383a260
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/DevicePolicyResources.java
@@ -0,0 +1,102 @@
+/*
+ * 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.photopicker.ui;
+
+import android.app.admin.DevicePolicyManager;
+
+/**
+ * Class containing the required identifiers to update device management resources.
+ *
+ * <p>See {@link DevicePolicyManager#getDrawable} and {@link DevicePolicyManager#getString}.
+ */
+public class DevicePolicyResources {
+
+ /**
+ * Class containing the identifiers used to update device management-related system strings.
+ */
+ public static final class Strings {
+ private static final String PREFIX = "MediaProvider.";
+
+ /**
+ * The text shown to switch to the work profile in PhotoPicker.
+ */
+ public static final String SWITCH_TO_WORK_MESSAGE =
+ PREFIX + "SWITCH_TO_WORK_MESSAGE";
+
+ /**
+ * The text shown to switch to the personal profile in PhotoPicker.
+ */
+ public static final String SWITCH_TO_PERSONAL_MESSAGE =
+ PREFIX + "SWITCH_TO_PERSONAL_MESSAGE";
+
+ /**
+ * The title for error dialog in PhotoPicker when the admin blocks cross user
+ * interaction for the intent.
+ */
+ public static final String BLOCKED_BY_ADMIN_TITLE =
+ PREFIX + "BLOCKED_BY_ADMIN_TITLE";
+
+ /**
+ * The message for error dialog in PhotoPicker when the admin blocks cross user
+ * interaction from the personal profile.
+ */
+ public static final String BLOCKED_FROM_PERSONAL_MESSAGE =
+ PREFIX + "BLOCKED_FROM_PERSONAL_MESSAGE";
+
+ /**
+ * The message for error dialog in PhotoPicker when the admin blocks cross user
+ * interaction from the work profile.
+ */
+ public static final String BLOCKED_FROM_WORK_MESSAGE =
+ PREFIX + "BLOCKED_FROM_WORK_MESSAGE";
+
+ /**
+ * The title of the error dialog in PhotoPicker when the user tries to switch to work
+ * content, but work profile is off.
+ */
+ public static final String WORK_PROFILE_PAUSED_TITLE =
+ PREFIX + "WORK_PROFILE_PAUSED_TITLE";
+
+ /**
+ * The message of the error dialog in PhotoPicker when the user tries to switch to work
+ * content, but work profile is off.
+ */
+ public static final String WORK_PROFILE_PAUSED_MESSAGE =
+ PREFIX + "WORK_PROFILE_PAUSED_MESSAGE";
+ }
+
+ /**
+ * Class containing the identifiers used to update device management-related system drawable.
+ */
+ public static final class Drawables {
+ /**
+ * General purpose work profile icon (i.e. generic icon badging).
+ */
+ public static final String WORK_PROFILE_ICON = "WORK_PROFILE_ICON";
+
+ /**
+ * Class containing the style identifiers used to update device management-related system
+ * drawable.
+ */
+ public static final class Style {
+ /**
+ * A style identifier indicating that the updatable drawable is an outline.
+ */
+ public static final String OUTLINE = "OUTLINE";
+ }
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java b/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
index fe36d35..fd891a4 100644
--- a/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
+++ b/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
@@ -34,7 +34,7 @@
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.analytics.AnalyticsCollector;
+import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector;
import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@@ -126,11 +126,11 @@
return new ExoPlayer.Builder(mContext,
new DefaultRenderersFactory(mContext),
- new DefaultTrackSelector(mContext),
mediaSourceFactory,
+ new DefaultTrackSelector(mContext),
sLoadControl,
DefaultBandwidthMeter.getSingletonInstance(mContext),
- new AnalyticsCollector(Clock.DEFAULT)).buildExoPlayer();
+ new DefaultAnalyticsCollector(Clock.DEFAULT)).build();
}
private void setupPlayerLayout(StyledPlayerView styledPlayerView, ImageView imageView) {
diff --git a/src/com/android/providers/media/photopicker/ui/ImageLoader.java b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
index 9c6fc94..d629471 100644
--- a/src/com/android/providers/media/photopicker/ui/ImageLoader.java
+++ b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
@@ -20,25 +20,29 @@
import android.graphics.ImageDecoder;
import android.graphics.drawable.Drawable;
import android.net.Uri;
+import android.provider.CloudMediaProviderContract;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.ImageView;
import androidx.annotation.NonNull;
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.request.RequestOptions;
-import com.bumptech.glide.signature.ObjectKey;
-
import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.Item;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.Option;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.signature.ObjectKey;
+
/**
* A class to assist with loading and managing the Images (i.e. thumbnails and preview) associated
* with item.
*/
public class ImageLoader {
+ public static final Option<Boolean> THUMBNAIL_REQUEST =
+ Option.memory(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB, false);
private static final String TAG = "ImageLoader";
private final Context mContext;
@@ -59,7 +63,7 @@
Glide.with(mContext)
.asBitmap()
.load(category.getCoverUri())
- .thumbnail()
+ .apply(RequestOptions.option(THUMBNAIL_REQUEST, true))
.into(imageView);
}
@@ -78,7 +82,7 @@
.asBitmap()
.load(uri)
.signature(getGlideSignature(item, /* prefix */ ""))
- .thumbnail()
+ .apply(RequestOptions.option(THUMBNAIL_REQUEST, true))
.into(imageView);
}
@@ -91,6 +95,7 @@
public void loadImagePreview(@NonNull Item item, @NonNull ImageView imageView) {
if (item.isGif()) {
Glide.with(mContext)
+ .asGif()
.load(item.getContentUri())
.signature(getGlideSignature(item, /* prefix */ ""))
.into(imageView);
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
index e28ef87..8ce7311 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
@@ -42,8 +42,7 @@
private final ImageLoader mImageLoader;
private final RemotePreviewHandler mRemotePreviewHandler;
private final PlaybackHandler mPlaybackHandler;
- private final boolean mIsRemotePreviewEnabled =
- RemotePreviewHandler.isRemotePreviewEnabled();
+ private final boolean mIsRemotePreviewEnabled = RemotePreviewHandler.isRemotePreviewEnabled();
PreviewAdapter(Context context, MuteStatus muteStatus) {
mImageLoader = new ImageLoader(context);
diff --git a/src/com/android/providers/media/photopicker/ui/ProfileDialogFragment.java b/src/com/android/providers/media/photopicker/ui/ProfileDialogFragment.java
index 630029c..8ae8e1a 100644
--- a/src/com/android/providers/media/photopicker/ui/ProfileDialogFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/ProfileDialogFragment.java
@@ -16,16 +16,29 @@
package com.android.providers.media.photopicker.ui;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.Style.OUTLINE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.BLOCKED_BY_ADMIN_TITLE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.BLOCKED_FROM_PERSONAL_MESSAGE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.BLOCKED_FROM_WORK_MESSAGE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.WORK_PROFILE_PAUSED_MESSAGE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.WORK_PROFILE_PAUSED_TITLE;
+
import android.app.Dialog;
+import android.app.admin.DevicePolicyManager;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.os.Bundle;
import android.util.Log;
+import androidx.annotation.RequiresApi;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
+import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.R;
import com.android.providers.media.photopicker.data.UserIdManager;
import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
@@ -41,24 +54,11 @@
final PickerViewModel pickerViewModel = new ViewModelProvider(requireActivity()).get(
PickerViewModel.class);
final UserIdManager userIdManager = pickerViewModel.getUserIdManager();
-
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
if (userIdManager.isBlockedByAdmin()) {
- builder.setIcon(R.drawable.ic_lock);
- builder.setTitle(getString(R.string.picker_profile_admin_title));
- final String message = userIdManager.isManagedUserSelected() ?
- getString(R.string.picker_profile_admin_msg_from_work) :
- getString(R.string.picker_profile_admin_msg_from_personal);
- builder.setMessage(message);
- builder.setPositiveButton(android.R.string.ok, null);
+ setBlockedByAdminParams(userIdManager.isManagedUserSelected(), builder);
} else if (userIdManager.isWorkProfileOff()) {
- builder.setIcon(R.drawable.ic_work_outline);
- builder.setTitle(getString(R.string.picker_profile_work_paused_title));
- builder.setMessage(getString(R.string.picker_profile_work_paused_msg));
- // TODO(b/197199728): Add listener to turn on apps. This maybe a bit tricky because
- // after turning on Work profile, work profile MediaProvider may not be available
- // immediately.
- builder.setPositiveButton(android.R.string.ok, null);
+ setWorkProfileOffParams(builder);
} else {
Log.e(TAG, "Unknown error for profile dialog");
return null;
@@ -66,6 +66,68 @@
return builder.create();
}
+ private void setBlockedByAdminParams(
+ boolean isManagedUserSelected, MaterialAlertDialogBuilder builder) {
+ String title;
+ String message;
+ if (SdkLevel.isAtLeastT()) {
+ title = getUpdatedEnterpriseString(
+ BLOCKED_BY_ADMIN_TITLE, R.string.picker_profile_admin_title);
+ message = isManagedUserSelected
+ ? getUpdatedEnterpriseString(
+ BLOCKED_FROM_WORK_MESSAGE, R.string.picker_profile_admin_msg_from_work)
+ : getUpdatedEnterpriseString(
+ BLOCKED_FROM_PERSONAL_MESSAGE,
+ R.string.picker_profile_admin_msg_from_personal);
+ } else {
+ title = getString(R.string.picker_profile_admin_title);
+ message = isManagedUserSelected
+ ? getString(R.string.picker_profile_admin_msg_from_work)
+ : getString(R.string.picker_profile_admin_msg_from_personal);
+ }
+ builder.setIcon(R.drawable.ic_lock);
+ builder.setTitle(title);
+ builder.setMessage(message);
+ builder.setPositiveButton(android.R.string.ok, null);
+ }
+
+ private void setWorkProfileOffParams(MaterialAlertDialogBuilder builder) {
+ Drawable icon;
+ String title;
+ String message;
+ if (SdkLevel.isAtLeastT()) {
+ icon = getUpdatedWorkProfileIcon();
+ title = getUpdatedEnterpriseString(
+ WORK_PROFILE_PAUSED_TITLE, R.string.picker_profile_work_paused_title);
+ message = getUpdatedEnterpriseString(
+ WORK_PROFILE_PAUSED_MESSAGE, R.string.picker_profile_work_paused_msg);
+ } else {
+ icon = getContext().getDrawable(R.drawable.ic_work_outline);
+ title = getContext().getString(R.string.picker_profile_work_paused_title);
+ message = getContext().getString(R.string.picker_profile_work_paused_msg);
+ }
+ builder.setIcon(icon);
+ builder.setTitle(title);
+ builder.setMessage(message);
+ // TODO(b/197199728): Add listener to turn on apps. This maybe a bit tricky because
+ // after turning on Work profile, work profile MediaProvider may not be available
+ // immediately.
+ builder.setPositiveButton(android.R.string.ok, null);
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private String getUpdatedEnterpriseString(String updatableStringId, int defaultStringId) {
+ final DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
+ return dpm.getResources().getString(updatableStringId, () -> getString(defaultStringId));
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private Drawable getUpdatedWorkProfileIcon() {
+ final DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
+ return dpm.getResources().getDrawable(WORK_PROFILE_ICON, OUTLINE, () ->
+ getContext().getDrawable(R.drawable.ic_work_outline));
+ }
+
public static void show(FragmentManager fm) {
FragmentTransaction ft = fm.beginTransaction();
Fragment f = new ProfileDialogFragment();
diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java
index 19d2ee6..d037ba7 100644
--- a/src/com/android/providers/media/photopicker/ui/TabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java
@@ -15,9 +15,17 @@
*/
package com.android.providers.media.photopicker.ui;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.Style.OUTLINE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.SWITCH_TO_PERSONAL_MESSAGE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.SWITCH_TO_WORK_MESSAGE;
+
+import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
@@ -32,12 +40,14 @@
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.R;
import com.android.providers.media.photopicker.PhotoPickerActivity;
import com.android.providers.media.photopicker.data.Selection;
@@ -297,17 +307,56 @@
}
private void updateProfileButtonContent(boolean isManagedUserSelected) {
- final int iconResId;
- final int textResId;
+ final Drawable icon;
+ final String text;
if (isManagedUserSelected) {
- iconResId = R.drawable.ic_personal_mode;
- textResId = R.string.picker_personal_profile;
+ icon = getContext().getDrawable(R.drawable.ic_personal_mode);
+ text = getSwitchToPersonalMessage();
} else {
- iconResId = R.drawable.ic_work_outline;
- textResId = R.string.picker_work_profile;
+ icon = getWorkProfileIcon();
+ text = getSwitchToWorkMessage();
}
- mProfileButton.setIconResource(iconResId);
- mProfileButton.setText(textResId);
+ mProfileButton.setIcon(icon);
+ mProfileButton.setText(text);
+ }
+
+ private String getSwitchToPersonalMessage() {
+ if (SdkLevel.isAtLeastT()) {
+ return getUpdatedEnterpriseString(
+ SWITCH_TO_PERSONAL_MESSAGE, R.string.picker_personal_profile);
+ } else {
+ return getContext().getString(R.string.picker_personal_profile);
+ }
+ }
+
+ private String getSwitchToWorkMessage() {
+ if (SdkLevel.isAtLeastT()) {
+ return getUpdatedEnterpriseString(
+ SWITCH_TO_WORK_MESSAGE, R.string.picker_work_profile);
+ } else {
+ return getContext().getString(R.string.picker_work_profile);
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private String getUpdatedEnterpriseString(String updatableStringId, int defaultStringId) {
+ final DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
+ return dpm.getResources().getString(updatableStringId, () -> getString(defaultStringId));
+ }
+
+ private Drawable getWorkProfileIcon() {
+ if (SdkLevel.isAtLeastT()) {
+ return getUpdatedWorkProfileIcon();
+ } else {
+ return getContext().getDrawable(R.drawable.ic_work_outline);
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private Drawable getUpdatedWorkProfileIcon() {
+ DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
+ return dpm.getResources().getDrawable(WORK_PROFILE_ICON, OUTLINE, () ->
+ getContext().getDrawable(R.drawable.ic_work_outline));
}
private void updateProfileButtonColor(boolean isDisabled) {
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
index 7b299ba..ccf2885 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
@@ -16,6 +16,7 @@
package com.android.providers.media.photopicker.ui.remotepreview;
+import static android.provider.CloudMediaProviderContract.EXTRA_AUTHORITY;
import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER;
import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
@@ -41,6 +42,7 @@
import android.view.SurfaceHolder;
import android.view.SurfaceView;
+import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.MuteStatus;
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.ui.PreviewVideoHolder;
@@ -75,8 +77,11 @@
private boolean mIsInBackground = false;
private int mSurfaceCounter = 0;
+ /**
+ * Returns {@code true} if remote preview is enabled.
+ */
public static boolean isRemotePreviewEnabled() {
- return SystemProperties.getBoolean("sys.photopicker.remote_preview", false);
+ return SystemProperties.getBoolean("sys.photopicker.remote_preview", true);
}
public RemotePreviewHandler(Context context, MuteStatus muteStatus) {
@@ -89,26 +94,16 @@
*
* @param viewHolder {@link PreviewVideoHolder} for the media item under preview
* @param item {@link Item} to be previewed
- * @return true if the given {@link Item} can be previewed remotely, else false
*/
- public boolean onViewAttachedToWindow(PreviewVideoHolder viewHolder, Item item) {
- RemotePreviewSession session = createRemotePreviewSession(item, viewHolder);
- if (session == null) {
- Log.w(TAG, "Failed to create RemotePreviewSession.");
- return false;
- }
+ public void onViewAttachedToWindow(PreviewVideoHolder viewHolder, Item item) {
+ final RemotePreviewSession session = createRemotePreviewSession(item, viewHolder);
+ final SurfaceHolder holder = viewHolder.getSurfaceHolder();
- SurfaceHolder holder = viewHolder.getSurfaceHolder();
mSessionMap.put(holder, session);
// Ensure that we don't add the same callback twice, since we don't remove callbacks
// anywhere else.
holder.removeCallback(mSurfaceHolderCallback);
holder.addCallback(mSurfaceHolderCallback);
-
- mCurrentPreviewState.item = item;
- mCurrentPreviewState.viewHolder = viewHolder;
-
- return true;
}
/**
@@ -134,6 +129,9 @@
return false;
}
+ mCurrentPreviewState.item = item;
+ mCurrentPreviewState.viewHolder = session.getPreviewVideoHolder();
+
session.requestPlayMedia();
return true;
}
@@ -158,9 +156,11 @@
private RemotePreviewSession createRemotePreviewSession(Item item,
PreviewVideoHolder previewVideoHolder) {
String authority = item.getContentUri().getAuthority();
- SurfaceControllerProxy controller = getSurfaceController(authority);
+ SurfaceControllerProxy controller = getSurfaceController(authority, false);
if (controller == null) {
- return null;
+ Log.w(TAG, "Failed to create RemotePreviewSession for " + authority
+ + ". Fallback to openPreview");
+ controller = getSurfaceController(authority, true);
}
return new RemotePreviewSession(mSurfaceCounter++, item.getId(), authority, controller,
@@ -175,7 +175,7 @@
}
mSessionMap.put(holder, session);
- session.surfaceCreated(holder.getSurface());
+ session.surfaceCreated();
session.requestPlayMedia();
}
@@ -200,14 +200,15 @@
}
@Nullable
- private SurfaceControllerProxy getSurfaceController(String authority) {
+ private SurfaceControllerProxy getSurfaceController(String authority,
+ boolean localControllerFallback) {
if (mControllers.containsKey(authority)) {
return mControllers.get(authority);
}
SurfaceControllerProxy controller = null;
try {
- controller = createController(authority);
+ controller = createController(authority, localControllerFallback);
if (controller != null) {
mControllers.put(authority, controller);
}
@@ -228,12 +229,20 @@
mControllers.clear();
}
- private SurfaceControllerProxy createController(String authority) {
- Log.i(TAG, "Creating new SurfaceController for authority: " + authority);
+ private SurfaceControllerProxy createController(String authority,
+ boolean localControllerFallback) {
+ Log.i(TAG, "Creating new SurfaceController for authority: " + authority
+ + ". localControllerFallback: " + localControllerFallback);
Bundle extras = new Bundle();
extras.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, true);
extras.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, mMuteStatus.isVolumeMuted());
extras.putBinder(EXTRA_SURFACE_STATE_CALLBACK, mSurfaceStateChangedCallbackWrapper);
+
+ if (localControllerFallback) {
+ extras.putString(EXTRA_AUTHORITY, authority);
+ authority = PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+ }
+
final Bundle surfaceControllerBundle = mContext.getContentResolver().call(
createSurfaceControllerUri(authority),
METHOD_CREATE_SURFACE_CONTROLLER, /* arg */ null, extras);
@@ -284,7 +293,7 @@
Surface surface = holder.getSurface();
RemotePreviewSession session = mSessionMap.get(holder);
- session.surfaceCreated(surface);
+ session.surfaceCreated();
}
@Override
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
index b3bc2b6..4cd7e8e 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
@@ -33,7 +33,6 @@
import android.os.RemoteException;
import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PlaybackState;
import android.util.Log;
-import android.view.Surface;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
@@ -89,7 +88,9 @@
private final AccessibilityStateChangeListener mAccessibilityStateChangeListener =
this::updateAccessibilityState;
+ private SurfaceChangeData mSurfaceChangeData;
private boolean mIsSurfaceCreated = false;
+ private boolean mIsSurfaceCreationNotified = false;
private boolean mIsPlaybackRequested = false;
@PlaybackState
private int mCurrentPlaybackState = PLAYBACK_STATE_BUFFERING;
@@ -126,20 +127,36 @@
return mAuthority;
}
- void surfaceCreated(@NonNull Surface surface) {
+ @NonNull
+ PreviewVideoHolder getPreviewVideoHolder() {
+ return mPreviewVideoHolder;
+ }
+
+ void surfaceCreated() {
if (mIsSurfaceCreated) {
throw new IllegalStateException("Surface is already created.");
}
-
- if (surface == null) {
- throw new IllegalStateException("surfaceCreated() called with null surface.");
+ if (mIsSurfaceCreationNotified) {
+ throw new IllegalStateException(
+ "Surface creation has been already notified to SurfaceController.");
}
- try {
- mSurfaceController.onSurfaceCreated(mSurfaceId, surface, mMediaId);
- mIsSurfaceCreated = true;
- } catch (RemoteException e) {
- Log.e(TAG, "Failure in onSurfaceCreated().", e);
+ mIsSurfaceCreated = true;
+
+ // Notify surface creation only if playback has been already requested, else this will be
+ // done in requestPlayMedia() when playback is explicitly requested.
+ if (mIsPlaybackRequested) {
+ notifySurfaceCreated();
+ }
+ }
+
+ void surfaceChanged(int format, int width, int height) {
+ mSurfaceChangeData = new SurfaceChangeData(format, width, height);
+
+ // Notify surface change only if playback has been already requested, else this will be
+ // done in requestPlayMedia() when playback is explicitly requested.
+ if (mIsPlaybackRequested) {
+ notifySurfaceChanged();
}
}
@@ -148,8 +165,16 @@
throw new IllegalStateException("Surface is not created.");
}
+ mSurfaceChangeData = null;
+
tearDownUI();
+ if (!mIsSurfaceCreationNotified) {
+ // If we haven't notified surface creation yet, then no need to notify surface
+ // destruction either.
+ return;
+ }
+
try {
mSurfaceController.onSurfaceDestroyed(mSurfaceId);
} catch (RemoteException e) {
@@ -157,18 +182,6 @@
}
}
- void surfaceChanged(int format, int width, int height) {
- if (!mIsSurfaceCreated) {
- throw new IllegalStateException("Surface is not created.");
- }
-
- try {
- mSurfaceController.onSurfaceChanged(mSurfaceId, format, width, height);
- } catch (RemoteException e) {
- Log.e(TAG, "Failure in onSurfaceChanged().", e);
- }
- }
-
void requestPlayMedia() {
// When the user is at the first item in ViewPager, swiping further right would trigger the
// callback {@link ViewPager2.PageTransformer#transforPage(View, int)}, which would call
@@ -186,6 +199,15 @@
return;
}
+ // Now that playback has been requested, try to notify surface creation and surface change
+ // so that player can be prepared with the surface.
+ if (mIsSurfaceCreated) {
+ notifySurfaceCreated();
+ }
+ if (mSurfaceChangeData != null) {
+ notifySurfaceChanged();
+ }
+
mIsPlaybackRequested = true;
}
@@ -219,10 +241,54 @@
}
}
+ private void notifySurfaceCreated() {
+ if (!mIsSurfaceCreated) {
+ throw new IllegalStateException("Surface is not created.");
+ }
+ if (mIsSurfaceCreationNotified) {
+ throw new IllegalStateException(
+ "Surface creation has already been notified to SurfaceController.");
+ }
+
+ try {
+ mSurfaceController.onSurfaceCreated(mSurfaceId,
+ mPreviewVideoHolder.getSurfaceHolder().getSurface(), mMediaId);
+ mIsSurfaceCreationNotified = true;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failure in notifySurfaceCreated().", e);
+ }
+ }
+
+ private void notifySurfaceChanged() {
+ if (!mIsSurfaceCreated) {
+ throw new IllegalStateException("Surface is not created.");
+ }
+ if (!mIsSurfaceCreationNotified) {
+ throw new IllegalStateException(
+ "Surface creation has not been notified to SurfaceController.");
+ }
+
+ if (mSurfaceChangeData == null) {
+ throw new IllegalStateException("No surface change data present.");
+ }
+
+ try {
+ mSurfaceController.onSurfaceChanged(mSurfaceId, mSurfaceChangeData.getFormat(),
+ mSurfaceChangeData.getWidth(), mSurfaceChangeData.getHeight());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failure in notifySurfaceChanged().", e);
+ }
+ }
+
private void playMedia() {
if (!mIsSurfaceCreated) {
throw new IllegalStateException("Surface is not created.");
}
+ if (!mIsSurfaceCreationNotified) {
+ throw new IllegalStateException(
+ "Surface creation has not been notified to SurfaceController.");
+ }
+
if (mCurrentPlaybackState == PLAYBACK_STATE_STARTED) {
throw new IllegalStateException("Player is already playing.");
}
@@ -238,6 +304,11 @@
if (!mIsSurfaceCreated) {
throw new IllegalStateException("Surface is not created.");
}
+ if (!mIsSurfaceCreationNotified) {
+ throw new IllegalStateException(
+ "Surface creation has not been notified to SurfaceController.");
+ }
+
if (mCurrentPlaybackState != PLAYBACK_STATE_STARTED) {
throw new IllegalStateException("Player is not playing.");
}
@@ -269,9 +340,8 @@
}
private void initUI() {
- // We hide the player view and show the thumbnail till the player is ready and we know the
- // media size. However, since we want the surface to be created, we cannot use View.GONE
- // here.
+ // We show the thumbnail view till the player is ready and when we know the
+ // media size, then we hide the thumbnail view.
mPreviewVideoHolder.getPlayerContainer().setVisibility(View.INVISIBLE);
mPreviewVideoHolder.getThumbnailView().setVisibility(View.VISIBLE);
mPreviewVideoHolder.getPlayerControlsRoot().setVisibility(View.GONE);
@@ -333,4 +403,29 @@
visible ? View.VISIBLE : View.GONE);
mPlayerControlsVisibilityStatus.setShouldShowPlayerControlsForNextItem(visible);
}
+
+ private static final class SurfaceChangeData {
+
+ private int mFormat;
+ private int mWidth;
+ private int mHeight;
+
+ SurfaceChangeData(int format, int width, int height) {
+ mFormat = format;
+ mWidth = width;
+ mHeight = height;
+ }
+
+ int getFormat() {
+ return mFormat;
+ }
+
+ int getWidth() {
+ return mWidth;
+ }
+
+ int getHeight() {
+ return mHeight;
+ }
+ }
}
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemoteSurfaceController.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemoteSurfaceController.java
new file mode 100644
index 0000000..37a5921
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemoteSurfaceController.java
@@ -0,0 +1,307 @@
+/*
+ * 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.photopicker.ui.remotepreview;
+
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceController;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_BUFFERING;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_COMPLETED;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_MEDIA_SIZE_CHANGED;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_PAUSED;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_STARTED;
+import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
+import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
+
+import android.annotation.DurationMillisLong;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Point;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+
+import com.android.providers.media.PickerUriResolver;
+
+import com.google.android.exoplayer2.DefaultLoadControl;
+import com.google.android.exoplayer2.DefaultRenderersFactory;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.LoadControl;
+import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Player.State;
+import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector;
+import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.upstream.ContentDataSource;
+import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.video.VideoSize;
+
+/**
+ * Implements a {@link CloudMediaSurfaceController} for a cloud provider authority and initializes
+ * an ExoPlayer instance to render cloud media to {@link Surface} instances.
+ */
+public class RemoteSurfaceController extends CloudMediaSurfaceController {
+ private static final String TAG = "RemoteSurfaceController";
+
+ // The minimum duration of media that the player will attempt to ensure is buffered at all
+ // times.
+ private static final int MIN_BUFFER_MS = 1000;
+ // The maximum duration of media that the player will attempt to buffer.
+ private static final int MAX_BUFFER_MS = 2000;
+ // The duration of media that must be buffered for playback to start or resume following a
+ // user action such as a seek.
+ private static final int BUFFER_FOR_PLAYBACK_MS = 1000;
+ // The default duration of media that must be buffered for playback to resume after a
+ // rebuffer.
+ private static final int BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 1000;
+ private static final LoadControl sLoadControl = new DefaultLoadControl.Builder()
+ .setBufferDurationsMs(
+ MIN_BUFFER_MS,
+ MAX_BUFFER_MS,
+ BUFFER_FOR_PLAYBACK_MS,
+ BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS).build();
+
+ private final String mAuthority;
+ private final Context mContext;
+ private final CloudMediaSurfaceStateChangedCallback mCallback;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final Player.Listener mEventListener = new Player.Listener() {
+ @Override
+ public void onPlaybackStateChanged(@State int state) {
+ Log.d(TAG, "Received player event " + state);
+
+ switch (state) {
+ case Player.STATE_READY:
+ mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_READY,
+ null);
+ return;
+ case Player.STATE_BUFFERING:
+ mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_BUFFERING,
+ null);
+ return;
+ case Player.STATE_ENDED:
+ mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_COMPLETED,
+ null);
+ return;
+ default:
+ }
+ }
+
+ @Override
+ public void onIsPlayingChanged(boolean isPlaying) {
+ mCallback.setPlaybackState(mCurrentSurfaceId, isPlaying ? PLAYBACK_STATE_STARTED :
+ PLAYBACK_STATE_PAUSED, null);
+ }
+
+ @Override
+ public void onVideoSizeChanged(VideoSize videoSize) {
+ Point size = new Point(videoSize.width, videoSize.height);
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(ContentResolver.EXTRA_SIZE, size);
+ mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_MEDIA_SIZE_CHANGED,
+ bundle);
+ }
+ };
+
+ private boolean mEnableLoop;
+ private boolean mMuteAudio;
+ private ExoPlayer mPlayer;
+ private int mCurrentSurfaceId = -1;
+
+ public RemoteSurfaceController(Context context, String authority, boolean enableLoop,
+ boolean muteAudio, CloudMediaSurfaceStateChangedCallback callback) {
+ mAuthority = authority;
+ mCallback = callback;
+ mContext = context;
+ mEnableLoop = enableLoop;
+ mMuteAudio = muteAudio;
+ Log.d(TAG, "Surface controller created.");
+ }
+
+ @Override
+ public void onPlayerCreate() {
+ mHandler.post(() -> {
+ mPlayer = createExoPlayer();
+ mPlayer.addListener(mEventListener);
+ updateLoopingPlaybackStatus();
+ updateAudioMuteStatus();
+ Log.d(TAG, "Player created.");
+ });
+ }
+
+ @Override
+ public void onPlayerRelease() {
+ mHandler.post(() -> {
+ mPlayer.removeListener(mEventListener);
+ mPlayer.release();
+ mPlayer = null;
+ Log.d(TAG, "Player released.");
+ });
+ }
+
+ @Override
+ public void onSurfaceCreated(int surfaceId, @NonNull Surface surface,
+ @NonNull String mediaId) {
+ mHandler.post(() -> {
+ try {
+ // onSurfaceCreated may get called while the player is already rendering on a
+ // different surface. In that case, pause the player before preparing it for
+ // rendering on the new surface.
+ // Unfortunately, Exoplayer#stop doesn't seem to work here. If we call stop(),
+ // as soon as the player becomes ready again, it automatically starts to play
+ // the new media. The reason is that Exoplayer treats play/pause as calls to
+ // the method Exoplayer#setPlayWhenReady(boolean) with true and false
+ // respectively. So, if we don't pause(), then since the previous play() call
+ // had set setPlayWhenReady to true, the player would start the playback as soon
+ // as it gets ready with the new media item.
+ if (mPlayer.isPlaying()) {
+ mPlayer.pause();
+ }
+
+ mCurrentSurfaceId = surfaceId;
+
+ final Uri mediaUri = PickerUriResolver.getMediaUri(mAuthority).buildUpon()
+ .appendPath(mediaId).build();
+ mPlayer.setMediaItem(MediaItem.fromUri(mediaUri));
+ mPlayer.setVideoSurface(surface);
+ mPlayer.prepare();
+
+ Log.d(TAG, "Surface prepared: " + surfaceId + ". Surface: " + surface
+ + ". MediaId: " + mediaId);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Error preparing player with surface.", e);
+ }
+ });
+ }
+
+ @Override
+ public void onSurfaceChanged(int surfaceId, int format, int width, int height) {
+ Log.d(TAG, "Surface changed: " + surfaceId + ". Format: " + format + ". Width: "
+ + width + ". Height: " + height);
+ }
+
+ @Override
+ public void onSurfaceDestroyed(int surfaceId) {
+ mHandler.post(() -> {
+ if (mCurrentSurfaceId != surfaceId) {
+ // This means that the player is already using some other surface, hence
+ // nothing to do.
+ return;
+ }
+ if (mPlayer.isPlaying()) {
+ mPlayer.stop();
+ }
+ mPlayer.clearVideoSurface();
+ mCurrentSurfaceId = -1;
+
+ Log.d(TAG, "Surface released: " + surfaceId);
+ });
+ }
+
+ @Override
+ public void onMediaPlay(int surfaceId) {
+ mHandler.post(() -> {
+ mPlayer.play();
+ Log.d(TAG, "Media played: " + surfaceId);
+ });
+ }
+
+ @Override
+ public void onMediaPause(int surfaceId) {
+ mHandler.post(() -> {
+ if (mPlayer.isPlaying()) {
+ mPlayer.pause();
+ Log.d(TAG, "Media paused: " + surfaceId);
+ }
+ });
+ }
+
+ @Override
+ public void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis) {
+ mHandler.post(() -> {
+ mPlayer.seekTo((int) timestampMillis);
+ Log.d(TAG, "Media seeked: " + surfaceId + ". Timestamp: " + timestampMillis);
+ });
+ }
+
+ @Override
+ public void onConfigChange(@NonNull Bundle config) {
+ final boolean enableLoop = config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
+ mEnableLoop);
+ final boolean muteAudio = config.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
+ mMuteAudio);
+ mHandler.post(() -> {
+ if (mEnableLoop != enableLoop) {
+ mEnableLoop = enableLoop;
+ updateLoopingPlaybackStatus();
+ }
+
+ if (mMuteAudio != muteAudio) {
+ mMuteAudio = muteAudio;
+ updateAudioMuteStatus();
+ }
+ });
+ Log.d(TAG, "Config changed. Updated config params: " + config);
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.d(TAG, "Surface controller destroyed.");
+ }
+
+ private ExoPlayer createExoPlayer() {
+ // ProgressiveMediaFactory will be enough for video playback of videos on the device.
+ // This also reduces apk size.
+ ProgressiveMediaSource.Factory mediaSourceFactory = new ProgressiveMediaSource.Factory(
+ () -> new ContentDataSource(mContext), MediaParserExtractorAdapter.FACTORY);
+
+ return new ExoPlayer.Builder(mContext,
+ new DefaultRenderersFactory(mContext),
+ mediaSourceFactory,
+ new DefaultTrackSelector(mContext),
+ sLoadControl,
+ DefaultBandwidthMeter.getSingletonInstance(mContext),
+ new DefaultAnalyticsCollector(Clock.DEFAULT)).build();
+ }
+
+ private void updateLoopingPlaybackStatus() {
+ mPlayer.setRepeatMode(mEnableLoop ? Player.REPEAT_MODE_ONE : Player.REPEAT_MODE_OFF);
+ }
+
+ private void updateAudioMuteStatus() {
+ if (mMuteAudio) {
+ mPlayer.setVolume(0f);
+ } else {
+ AudioManager audioManager = mContext.getSystemService(AudioManager.class);
+ if (audioManager == null) {
+ Log.e(TAG, "Couldn't find AudioManager while trying to set volume,"
+ + " unable to set volume");
+ return;
+ }
+ mPlayer.setVolume(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
+ }
+ }
+}
diff --git a/src/com/android/providers/media/util/DeviceConfigUtils.java b/src/com/android/providers/media/util/DeviceConfigUtils.java
deleted file mode 100644
index 7740d1d..0000000
--- a/src/com/android/providers/media/util/DeviceConfigUtils.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.util;
-
-import static com.android.providers.media.util.Logging.TAG;
-
-import android.os.Binder;
-import android.provider.DeviceConfig;
-import android.util.Log;
-import com.android.modules.utils.build.SdkLevel;
-
-public class DeviceConfigUtils {
-
- public static String getStringDeviceConfig(String key, String defaultValue) {
- if (!canReadDeviceConfig(key, defaultValue)) {
- return defaultValue;
- }
-
- final long token = Binder.clearCallingIdentity();
- try {
- return DeviceConfig.getString(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, key,
- defaultValue);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- private static <T> boolean canReadDeviceConfig(String key, T defaultValue) {
- if (SdkLevel.isAtLeastS()) {
- return true;
- }
-
- Log.w(TAG, "Cannot read device config before Android S. Returning defaultValue: "
- + defaultValue + " for key: " + key);
- return false;
- }
-}
diff --git a/src/com/android/providers/media/util/LongArray.java b/src/com/android/providers/media/util/LongArray.java
index 630b41f..7ea750e 100644
--- a/src/com/android/providers/media/util/LongArray.java
+++ b/src/com/android/providers/media/util/LongArray.java
@@ -31,7 +31,7 @@
private LongArray(long[] array, int size) {
mValues = array;
- mSize = checkArgumentInRange(size, 0, array.length, "size");
+ mSize = Preconditions.checkArgumentInRange(size, 0, array.length, "size");
}
/**
@@ -73,7 +73,7 @@
* created from the current content of this LongArray padded with 0s.
*/
public void resize(int newSize) {
- checkArgumentNonnegative(newSize);
+ Preconditions.checkArgumentNonnegative(newSize);
if (newSize <= mValues.length) {
Arrays.fill(mValues, newSize, mValues.length, 0);
} else {
@@ -222,29 +222,6 @@
return true;
}
- public static int checkArgumentNonnegative(final int value) {
- if (value < 0) {
- throw new IllegalArgumentException();
- }
-
- return value;
- }
-
- public static int checkArgumentInRange(int value, int lower, int upper,
- String valueName) {
- if (value < lower) {
- throw new IllegalArgumentException(
- String.format(
- "%s is out of range of [%d, %d] (too low)", valueName, lower, upper));
- } else if (value > upper) {
- throw new IllegalArgumentException(
- String.format(
- "%s is out of range of [%d, %d] (too high)", valueName, lower, upper));
- }
-
- return value;
- }
-
public static void checkBounds(int len, int index) {
if (index < 0 || len <= index) {
throw new ArrayIndexOutOfBoundsException("length=" + len + "; index=" + index);
diff --git a/src/com/android/providers/media/util/Memory.java b/src/com/android/providers/media/util/Memory.java
index 355cabe..f2306e3 100644
--- a/src/com/android/providers/media/util/Memory.java
+++ b/src/com/android/providers/media/util/Memory.java
@@ -46,4 +46,54 @@
dst[offset ] = (byte) ((value >> 24) & 0xff);
}
}
+
+ public static long peekLong(byte[] src, int offset, ByteOrder order) {
+ if (order == ByteOrder.BIG_ENDIAN) {
+ int h = ((src[offset++] & 0xff) << 24) |
+ ((src[offset++] & 0xff) << 16) |
+ ((src[offset++] & 0xff) << 8) |
+ ((src[offset++] & 0xff) << 0);
+ int l = ((src[offset++] & 0xff) << 24) |
+ ((src[offset++] & 0xff) << 16) |
+ ((src[offset++] & 0xff) << 8) |
+ ((src[offset ] & 0xff) << 0);
+ return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
+ } else {
+ int l = ((src[offset++] & 0xff) << 0) |
+ ((src[offset++] & 0xff) << 8) |
+ ((src[offset++] & 0xff) << 16) |
+ ((src[offset++] & 0xff) << 24);
+ int h = ((src[offset++] & 0xff) << 0) |
+ ((src[offset++] & 0xff) << 8) |
+ ((src[offset++] & 0xff) << 16) |
+ ((src[offset ] & 0xff) << 24);
+ return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
+ }
+ }
+
+ public static void pokeLong(byte[] dst, int offset, long value, ByteOrder order) {
+ if (order == ByteOrder.BIG_ENDIAN) {
+ int i = (int) (value >> 32);
+ dst[offset++] = (byte) ((i >> 24) & 0xff);
+ dst[offset++] = (byte) ((i >> 16) & 0xff);
+ dst[offset++] = (byte) ((i >> 8) & 0xff);
+ dst[offset++] = (byte) ((i >> 0) & 0xff);
+ i = (int) value;
+ dst[offset++] = (byte) ((i >> 24) & 0xff);
+ dst[offset++] = (byte) ((i >> 16) & 0xff);
+ dst[offset++] = (byte) ((i >> 8) & 0xff);
+ dst[offset ] = (byte) ((i >> 0) & 0xff);
+ } else {
+ int i = (int) value;
+ dst[offset++] = (byte) ((i >> 0) & 0xff);
+ dst[offset++] = (byte) ((i >> 8) & 0xff);
+ dst[offset++] = (byte) ((i >> 16) & 0xff);
+ dst[offset++] = (byte) ((i >> 24) & 0xff);
+ i = (int) (value >> 32);
+ dst[offset++] = (byte) ((i >> 0) & 0xff);
+ dst[offset++] = (byte) ((i >> 8) & 0xff);
+ dst[offset++] = (byte) ((i >> 16) & 0xff);
+ dst[offset ] = (byte) ((i >> 24) & 0xff);
+ }
+ }
}
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index 28dc14b..b1d9836 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -23,15 +23,18 @@
import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
import static android.Manifest.permission.MANAGE_MEDIA;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.Manifest.permission.READ_MEDIA_AUDIO;
+import static android.Manifest.permission.READ_MEDIA_IMAGES;
+import static android.Manifest.permission.READ_MEDIA_VIDEO;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.app.AppOpsManager.MODE_ALLOWED;
-import static android.app.AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES;
import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE;
import static android.app.AppOpsManager.OPSTR_NO_ISOLATED_STORAGE;
import static android.app.AppOpsManager.OPSTR_READ_MEDIA_AUDIO;
import static android.app.AppOpsManager.OPSTR_READ_MEDIA_IMAGES;
import static android.app.AppOpsManager.OPSTR_READ_MEDIA_VIDEO;
+import static android.app.AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES;
import static android.app.AppOpsManager.OPSTR_WRITE_MEDIA_AUDIO;
import static android.app.AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES;
import static android.app.AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO;
@@ -47,6 +50,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+
public class PermissionUtils {
// Callers must hold both the old and new permissions, so that we can
@@ -147,9 +152,18 @@
return checkNoIsolatedStorageGranted(context, uid, packageName, attributionTag);
}
- public static boolean checkPermissionReadAudio(@NonNull Context context, int pid, int uid,
- @NonNull String packageName, @Nullable String attributionTag) {
- if (!checkPermissionForPreflight(context, READ_EXTERNAL_STORAGE, pid, uid, packageName)) {
+ public static boolean checkPermissionReadAudio(
+ @NonNull Context context,
+ int pid,
+ int uid,
+ @NonNull String packageName,
+ @Nullable String attributionTag,
+ boolean targetSdkIsAtLeastT) {
+
+ String permission = targetSdkIsAtLeastT && SdkLevel.isAtLeastT()
+ ? READ_MEDIA_AUDIO : READ_EXTERNAL_STORAGE;
+
+ if (!checkPermissionForPreflight(context, permission, pid, uid, packageName)) {
return false;
}
return checkAppOpAllowingLegacy(context, OPSTR_READ_MEDIA_AUDIO, pid,
@@ -168,11 +182,20 @@
generateAppOpMessage(packageName, sOpDescription.get()));
}
- public static boolean checkPermissionReadVideo(@NonNull Context context, int pid, int uid,
- @NonNull String packageName, @Nullable String attributionTag) {
- if (!checkPermissionForPreflight(context, READ_EXTERNAL_STORAGE, pid, uid, packageName)) {
+ public static boolean checkPermissionReadVideo(
+ @NonNull Context context,
+ int pid,
+ int uid,
+ @NonNull String packageName,
+ @Nullable String attributionTag,
+ boolean targetSdkIsAtLeastT) {
+ String permission = targetSdkIsAtLeastT && SdkLevel.isAtLeastT()
+ ? READ_MEDIA_VIDEO : READ_EXTERNAL_STORAGE;
+
+ if (!checkPermissionForPreflight(context, permission, pid, uid, packageName)) {
return false;
}
+
return checkAppOpAllowingLegacy(context, OPSTR_READ_MEDIA_VIDEO, pid,
uid, packageName, attributionTag,
generateAppOpMessage(packageName, sOpDescription.get()));
@@ -189,11 +212,20 @@
generateAppOpMessage(packageName, sOpDescription.get()));
}
- public static boolean checkPermissionReadImages(@NonNull Context context, int pid, int uid,
- @NonNull String packageName, @Nullable String attributionTag) {
- if (!checkPermissionForPreflight(context, READ_EXTERNAL_STORAGE, pid, uid, packageName)) {
+ public static boolean checkPermissionReadImages(
+ @NonNull Context context,
+ int pid,
+ int uid,
+ @NonNull String packageName,
+ @Nullable String attributionTag,
+ boolean targetSdkIsAtLeastT) {
+ String permission = targetSdkIsAtLeastT && SdkLevel.isAtLeastT()
+ ? READ_MEDIA_IMAGES : READ_EXTERNAL_STORAGE;
+
+ if (!checkPermissionForPreflight(context, permission, pid, uid, packageName)) {
return false;
}
+
return checkAppOpAllowingLegacy(context, OPSTR_READ_MEDIA_IMAGES, pid,
uid, packageName, attributionTag,
generateAppOpMessage(packageName, sOpDescription.get()));
@@ -424,6 +456,7 @@
return checkRuntimePermission(context, permission, pid, uid, packageName,
attributionTag, message, forDataDelivery);
}
+
return context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED;
}
@@ -441,6 +474,9 @@
case ACCESS_MEDIA_LOCATION:
case READ_EXTERNAL_STORAGE:
case WRITE_EXTERNAL_STORAGE:
+ case READ_MEDIA_AUDIO:
+ case READ_MEDIA_VIDEO:
+ case READ_MEDIA_IMAGES:
return true;
}
return false;
diff --git a/src/com/android/providers/media/util/Preconditions.java b/src/com/android/providers/media/util/Preconditions.java
new file mode 100644
index 0000000..fb87130
--- /dev/null
+++ b/src/com/android/providers/media/util/Preconditions.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 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.util;
+
+public final class Preconditions {
+
+ /**
+ * Ensures that that the argument numeric value is non-negative (greater than or equal to 0).
+ *
+ * @param value a numeric int value
+ * @return the validated numeric value
+ * @throws IllegalArgumentException if {@code value} was negative
+ */
+ public static int checkArgumentNonnegative(final int value) {
+ if (value < 0) {
+ throw new IllegalArgumentException();
+ }
+
+ return value;
+ }
+
+ /**
+ * Ensures that the argument int value is within the inclusive range.
+ *
+ * @param value a int value
+ * @param lower the lower endpoint of the inclusive range
+ * @param upper the upper endpoint of the inclusive range
+ * @param valueName the name of the argument to use if the check fails
+ *
+ * @return the validated int value
+ *
+ * @throws IllegalArgumentException if {@code value} was not within the range
+ */
+ public static int checkArgumentInRange(int value, int lower, int upper,
+ String valueName) {
+ if (value < lower) {
+ throw new IllegalArgumentException(
+ String.format(
+ "%s is out of range of [%d, %d] (too low)", valueName, lower, upper));
+ } else if (value > upper) {
+ throw new IllegalArgumentException(
+ String.format(
+ "%s is out of range of [%d, %d] (too high)", valueName, lower, upper));
+ }
+
+ return value;
+ }
+}
diff --git a/src/com/android/providers/media/util/StringUtils.java b/src/com/android/providers/media/util/StringUtils.java
index 0d9d0e3..49bf935 100644
--- a/src/com/android/providers/media/util/StringUtils.java
+++ b/src/com/android/providers/media/util/StringUtils.java
@@ -16,17 +16,24 @@
package com.android.providers.media.util;
+import static com.android.providers.media.util.Logging.TAG;
+
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.icu.text.MessageFormat;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
-import androidx.annotation.Nullable;
-
public class StringUtils {
/**
@@ -68,4 +75,41 @@
public static boolean equalIgnoreCase(@Nullable String a, @Nullable String b) {
return (a != null) && a.equalsIgnoreCase(b);
}
+
+ /**
+ * Returns a string array config as a {@code List<String>}.
+ */
+ public static List<String> getStringArrayConfig(Context context, int resId) {
+ final Resources res = context.getResources();
+ try {
+ final String[] configValue = res.getStringArray(resId);
+ return Arrays.asList(configValue);
+ } catch (NotFoundException e) {
+ return new ArrayList<String>();
+ }
+ }
+
+ /**
+ * Returns the list of uncached relative paths after removing invalid ones.
+ */
+ public static List<String> verifySupportedUncachedRelativePaths(List<String> unverifiedPaths) {
+ final List<String> verifiedPaths = new ArrayList<>();
+ for (final String path : unverifiedPaths) {
+ if (path == null) {
+ continue;
+ }
+ if (path.startsWith("/")) {
+ Log.w(TAG, "Relative path config must not start with '/'. Ignoring: " + path);
+ continue;
+ }
+ if (!path.endsWith("/")) {
+ Log.w(TAG, "Relative path config must end with '/'. Ignoring: " + path);
+ continue;
+ }
+
+ verifiedPaths.add(path);
+ }
+
+ return verifiedPaths;
+ }
}
diff --git a/src/com/android/providers/media/util/UserCache.java b/src/com/android/providers/media/util/UserCache.java
index cc58b67..fa46779 100644
--- a/src/com/android/providers/media/util/UserCache.java
+++ b/src/com/android/providers/media/util/UserCache.java
@@ -18,7 +18,9 @@
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+import static com.android.providers.media.util.Logging.TAG;
+import android.annotation.SuppressLint;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -26,7 +28,9 @@
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
+import android.util.Log;
import android.util.LongSparseArray;
+import android.util.SparseArray;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
@@ -49,14 +53,21 @@
* aren't guaranteed to be received before the volume events for a user.
*/
public class UserCache {
+ // This is being used for non work profile users. It is introduced to remove the necessity of
+ // second cache i.e. mUserIsWorkProfile
+ private static final String NO_WORK_PROFILE_OWNER_APP = "No Work Profile Owner App";
+
final Object mLock = new Object();
final Context mContext;
final UserManager mUserManager;
@GuardedBy("mLock")
final LongSparseArray<Context> mUserContexts = new LongSparseArray<>();
+
+ // This contains a mapping from userId to packageName of the Profile Owner App
+ // or NO_WORK_PROFILE_OWNER_APP
@GuardedBy("mLock")
- final LongSparseArray<Boolean> mUserIsWorkProfile = new LongSparseArray<>();
+ final SparseArray<String> mWorkProfileOwnerApps = new SparseArray<>();
@GuardedBy("mLock")
final ArrayList<UserHandle> mUsers = new ArrayList<>();
@@ -68,6 +79,7 @@
update();
}
+ @SuppressLint("NewApi")
private void update() {
List<UserHandle> profiles = mUserManager.getEnabledProfiles();
synchronized (mLock) {
@@ -122,10 +134,11 @@
// Owner user can not have a work profile
return false;
}
+
synchronized (mLock) {
- int index = mUserIsWorkProfile.indexOfKey(userId);
+ int index = mWorkProfileOwnerApps.indexOfKey(userId);
if (index >= 0) {
- return mUserIsWorkProfile.valueAt(index);
+ return !NO_WORK_PROFILE_OWNER_APP.equals(mWorkProfileOwnerApps.valueAt(index));
}
}
@@ -137,12 +150,19 @@
for (ApplicationInfo ai : packageManager.getInstalledApplications(
MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE)) {
if (policyManager.isProfileOwnerApp(ai.packageName)) {
+ synchronized (mLock) {
+ mWorkProfileOwnerApps.put(userId, ai.packageName);
+ }
isWorkProfile = true;
+ break;
}
}
- synchronized (mLock) {
- mUserIsWorkProfile.put(userId, isWorkProfile);
+ if(!isWorkProfile) {
+ synchronized (mLock) {
+ // NO_WORK_PROFILE_OWNER_APP is being used for all the non work profile users
+ mWorkProfileOwnerApps.put(userId, NO_WORK_PROFILE_OWNER_APP);
+ }
}
return isWorkProfile;
@@ -210,4 +230,28 @@
}
}
}
+
+ public void invalidateWorkProfileOwnerApps(@NonNull String packageName) {
+ synchronized (mLock) {
+ if (mWorkProfileOwnerApps.size() == 0) {
+ Log.w(TAG, "WorkProfileOwnerApps cache is empty");
+ return;
+ }
+
+ boolean cacheMissForGivenPackage = true;
+ for (int i = 0; i < mWorkProfileOwnerApps.size(); i++) {
+ final int userId = mWorkProfileOwnerApps.keyAt(i);
+ if (packageName.equals(mWorkProfileOwnerApps.get(userId))) {
+ Log.i(TAG, "Invalidating WorkProfileOwnerApps cache for package " + packageName
+ + ". UserId: " + userId);
+ mWorkProfileOwnerApps.remove(userId);
+ cacheMissForGivenPackage = false;
+ }
+ }
+
+ if(cacheMissForGivenPackage) {
+ Log.w(TAG, "WorkProfileOwnerApps cache miss for package " + packageName);
+ }
+ }
+ }
}
diff --git a/src/com/android/providers/media/util/XAttrUtils.java b/src/com/android/providers/media/util/XAttrUtils.java
new file mode 100644
index 0000000..236f7ed
--- /dev/null
+++ b/src/com/android/providers/media/util/XAttrUtils.java
@@ -0,0 +1,139 @@
+/*
+ * 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.util;
+
+import static com.android.providers.media.util.FileUtils.extractDisplayName;
+import static com.android.providers.media.util.FileUtils.extractRelativePath;
+import static com.android.providers.media.util.Logging.TAG;
+
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.providers.media.FileAccessAttributes;
+
+import java.nio.ByteOrder;
+import java.util.Optional;
+
+public class XAttrUtils {
+
+ /**
+ * Path on which {@link XAttrUtils#DATA_MEDIA_XATTR_DIRECTORY_PATH} is set.
+ * /storage/emulated/.. can point to /data/media/.. on ext4/f2fs on modern devices. However, for
+ * legacy devices with sdcardfs, it points to /mnt/runtime/.. which then points to
+ * /data/media/.. sdcardfs does not support xattrs, hence xattrs are set on /data/media/.. path.
+ *
+ * TODO(b/220895679): Add logic to handle external sd cards with primary volume with paths
+ * /mnt/expand/<volume>/media/<user-id>.
+ */
+ static final String DATA_MEDIA_XATTR_DIRECTORY_PATH = String.format(
+ "/data/media/%s", UserHandle.myUserId());
+
+ static final int SIZE_OF_FILE_ATTRIBUTES = 18;
+
+ /**
+ * Flag to turn on reading file metadata through xattr in FUSE file open calls
+ */
+ public static final boolean ENABLE_XATTR_METADATA_FOR_FUSE =
+ SystemProperties.getBoolean("persist.sys.fuse.perf.xattr_metadata_enabled",
+ false);
+
+ /**
+ * XAttribute key against which the file metadata is stored
+ */
+ public static final String FILE_ACCESS_XATTR_KEY = "user.fattr";
+
+ public static Optional<FileAccessAttributes> getFileAttributesFromXAttr(String path,
+ String key) {
+ Trace.beginSection("getFileAttributesFromXAttr");
+ String relativePathWithDisplayName = DATA_MEDIA_XATTR_DIRECTORY_PATH + "/"
+ + extractRelativePath(path) + extractDisplayName(path);
+ try {
+ return Optional.of(deserializeFileAccessAttributes(
+ Os.getxattr(relativePathWithDisplayName, key)));
+ } catch (ErrnoException e) {
+ Log.w(TAG,
+ String.format("Exception encountered while reading xattr:%s from path:%s.", key,
+ path));
+ return Optional.empty();
+ } finally {
+ Trace.endSection();
+ }
+ }
+
+ /**
+ * Serializes file access attributes into byte array that will be stored in the xattr.
+ * This method serializes only the id, mediaType, isPending, isTrashed and ownerId fields.
+ * @param fileAccessAttributes File attributes to be stored as byte[] in the file inode
+ * @return byte[]
+ */
+ public static byte[] serializeFileAccessAttributes(
+ FileAccessAttributes fileAccessAttributes) {
+ byte[] bytes = new byte[SIZE_OF_FILE_ATTRIBUTES];
+ int offset = 0;
+ ByteOrder byteOrder = ByteOrder.nativeOrder();
+
+ Memory.pokeLong(bytes, offset, fileAccessAttributes.getId(), byteOrder);
+ offset += Long.BYTES;
+
+ // TODO(b/227753174): Merge mediaType and the booleans in a single byte
+ Memory.pokeInt(bytes, offset, fileAccessAttributes.getMediaType(), byteOrder);
+ offset += Integer.BYTES;
+
+ bytes[offset++] = (byte) (fileAccessAttributes.isPending() ? 1 : 0);
+ bytes[offset++] = (byte) (fileAccessAttributes.isTrashed() ? 1 : 0);
+
+ Memory.pokeInt(bytes, offset, fileAccessAttributes.getOwnerId(), byteOrder);
+ offset += Integer.BYTES;
+ if (offset != SIZE_OF_FILE_ATTRIBUTES) {
+ Log.wtf(TAG, "Error: Serialized byte[] is of unexpected size");
+ }
+ return bytes;
+ }
+
+ /**
+ * Deserialize the byte[] data into the corresponding fields - id, mediaType, isPending,
+ * isTrashed and ownerId in that order, and returns an instance of FileAccessAttributes
+ * containing this deserialized data.
+ * @param data Data that is read from the file inode as a result of the xattr call
+ * @return FileAccessAttributes
+ */
+ public static FileAccessAttributes deserializeFileAccessAttributes(byte[] data) {
+ ByteOrder byteOrder = ByteOrder.nativeOrder();
+ int offset = 0;
+
+ long id = Memory.peekLong(data, offset, byteOrder);
+ offset += Long.BYTES;
+
+ int mediaType = Memory.peekInt(data, offset, byteOrder);
+ offset += Integer.BYTES;
+
+ boolean isPending = data[offset++] != 0;
+ boolean isTrashed = data[offset++] != 0;
+
+ int ownerId = Memory.peekInt(data, offset, byteOrder);
+ offset += Integer.BYTES;
+ if (offset != SIZE_OF_FILE_ATTRIBUTES) {
+ Log.wtf(TAG, " Error: Deserialized attributes are of unexpected size");
+ }
+ return new FileAccessAttributes(id, mediaType, isPending, isTrashed,
+ ownerId, null);
+ }
+}
diff --git a/tests/Android.bp b/tests/Android.bp
index 9626125..05aa556 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -1,3 +1,8 @@
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_test_helper_app {
name: "MediaProviderTestAppForPermissionActivity",
manifest: "test_app/TestAppForPermissionActivity.xml",
@@ -18,6 +23,25 @@
}
android_test_helper_app {
+ name: "MediaProviderTestAppForPermissionActivity33",
+ manifest: "test_app/TestAppForPermissionActivity33.xml",
+ srcs: [
+ "test_app/src/**/*.java",
+ "src/com/android/providers/media/util/TestUtils.java",
+ ],
+ static_libs: [
+ "cts-install-lib",
+ ],
+ sdk_version: "test_current",
+ target_sdk_version: "33",
+ min_sdk_version: "30",
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ ],
+}
+
+android_test_helper_app {
name: "MediaProviderTestAppWithStoragePerms",
manifest: "test_app/TestAppWithStoragePerms.xml",
srcs: [
@@ -37,6 +61,25 @@
}
android_test_helper_app {
+ name: "MediaProviderTestAppWithMediaPerms",
+ manifest: "test_app/TestAppWithMediaPerms.xml",
+ srcs: [
+ "test_app/src/**/*.java",
+ "src/com/android/providers/media/util/TestUtils.java",
+ ],
+ static_libs: [
+ "cts-install-lib",
+ ],
+ sdk_version: "test_current",
+ target_sdk_version: "30",
+ min_sdk_version: "30",
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ ],
+}
+
+android_test_helper_app {
name: "MediaProviderTestAppWithoutPerms",
manifest: "test_app/TestAppWithoutPerms.xml",
srcs: [
@@ -79,15 +122,6 @@
// on the device being tested, so we can't sign our tests with a key that
// will allow instrumentation. Thus we pull all the sources we need to
// run tests against into the test itself.
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
-}
-
android_test {
name: "MediaProviderTests",
test_suites: [
@@ -162,8 +196,10 @@
java_resources: [
":MediaProviderTestAppWithStoragePerms",
+ ":MediaProviderTestAppWithMediaPerms",
":MediaProviderTestAppWithoutPerms",
":MediaProviderTestAppForPermissionActivity",
+ ":MediaProviderTestAppForPermissionActivity33",
":LegacyMediaProviderTestApp",
],
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
index fc561af..7be4b76 100644
--- a/tests/AndroidTest.xml
+++ b/tests/AndroidTest.xml
@@ -17,7 +17,9 @@
<target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
<option name="test-file-name" value="MediaProviderTests.apk" />
<option name="test-file-name" value="MediaProviderTestAppForPermissionActivity.apk" />
+ <option name="test-file-name" value="MediaProviderTestAppForPermissionActivity33.apk" />
<option name="test-file-name" value="MediaProviderTestAppWithStoragePerms.apk" />
+ <option name="test-file-name" value="MediaProviderTestAppWithMediaPerms.apk" />
<option name="test-file-name" value="MediaProviderTestAppWithoutPerms.apk" />
<option name="test-file-name" value="LegacyMediaProviderTestApp.apk" />
<option name="install-arg" value="-g" />
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 1541da6..7e51828 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -1,10 +1,6 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
android_test {
diff --git a/tests/src/com/android/providers/media/DatabaseHelperTest.java b/tests/src/com/android/providers/media/DatabaseHelperTest.java
index 36ffd64..f1ce350 100644
--- a/tests/src/com/android/providers/media/DatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/DatabaseHelperTest.java
@@ -18,20 +18,18 @@
import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY;
-import static com.android.providers.media.DatabaseHelper.VERSION_LATEST;
-import static com.android.providers.media.DatabaseHelper.VERSION_S;
-import static com.android.providers.media.DatabaseHelper.makePristineSchema;
+import static com.android.providers.media.DatabaseHelper.TEST_CLEAN_DB;
+import static com.android.providers.media.DatabaseHelper.TEST_DOWNGRADE_DB;
import static com.android.providers.media.DatabaseHelper.TEST_RECOMPUTE_DB;
import static com.android.providers.media.DatabaseHelper.TEST_UPGRADE_DB;
-import static com.android.providers.media.DatabaseHelper.TEST_DOWNGRADE_DB;
-import static com.android.providers.media.DatabaseHelper.TEST_CLEAN_DB;
+import static com.android.providers.media.DatabaseHelper.makePristineSchema;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.Manifest;
@@ -43,8 +41,8 @@
import android.os.UserHandle;
import android.provider.Column;
import android.provider.ExportedSince;
-import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.Audio;
+import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.Files.FileColumns;
import android.util.Log;
@@ -270,6 +268,7 @@
try (DatabaseHelper helper = before.getConstructor(Context.class, String.class)
.newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
SQLiteDatabase db = helper.getWritableDatabaseForTest();
+ assertThat(sIsolatedContext.getDatabasePath(TEST_DOWNGRADE_DB).exists()).isTrue();
{
final ContentValues values = new ContentValues();
values.put(FileColumns.DATA,
@@ -285,13 +284,11 @@
}
}
- // Downgrade will wipe data, but at least we don't crash
+ // Downgrade will delete the database file and crash the process
try (DatabaseHelper helper = after.getConstructor(Context.class, String.class)
.newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
- SQLiteDatabase db = helper.getWritableDatabaseForTest();
- try (Cursor c = db.query("files", null, null, null, null, null, null, null)) {
- assertEquals(0, c.getCount());
- }
+ assertThrows(RuntimeException.class, helper::getWritableDatabaseForTest);
+ assertThat(sIsolatedContext.getDatabasePath(TEST_DOWNGRADE_DB).exists()).isFalse();
}
}
@@ -569,52 +566,6 @@
}
}
- /**
- * Test that database downgrade changed the UUID saved in database file.
- */
- @Test
- public void testDowngradeChangesUUID() throws Exception {
- Class<? extends DatabaseHelper> dbVersionHigher = DatabaseHelperT.class;
- Class<? extends DatabaseHelper> dbVersionLower = DatabaseHelperS.class;
- String originalUUID;
- int originalVersion;
- // Create the database with database version = dbVersionLower
- try (DatabaseHelper helper = dbVersionLower.getConstructor(Context.class, String.class)
- .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
- SQLiteDatabase db = helper.getWritableDatabaseForTest();
- originalUUID = DatabaseHelper.getOrCreateUuid(db);
- originalVersion = db.getVersion();
- // Verify that original version of the database is dbVersionLower.
- assertWithMessage("Current database version")
- .that(db.getVersion()).isEqualTo(VERSION_S);
- }
- // Upgrade the database by changing the version to dbVersionHigher
- try (DatabaseHelper helper = dbVersionHigher.getConstructor(Context.class, String.class)
- .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
- SQLiteDatabase db = helper.getWritableDatabaseForTest();
- // Verify that upgrade resulted in database version change.
- assertWithMessage("Current database version after upgrade")
- .that(db.getVersion()).isNotEqualTo(originalVersion);
- // Verify that upgrade resulted in database version same as latest version.
- assertWithMessage("Current database version after upgrade")
- .that(db.getVersion()).isEqualTo(DatabaseHelper.VERSION_T);
- // Verify that upgrade didn't change UUID
- assertWithMessage("Current database UUID after upgrade")
- .that(DatabaseHelper.getOrCreateUuid(db)).isEqualTo(originalUUID);
- }
- // Downgrade the database by changing the version to dbVersionLower
- try (DatabaseHelper helper = dbVersionLower.getConstructor(Context.class, String.class)
- .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
- SQLiteDatabase db = helper.getWritableDatabaseForTest();
- // Verify that downgraded version is same as original database version before upgrade
- assertWithMessage("Current database version after downgrade")
- .that(db.getVersion()).isEqualTo(originalVersion);
- // Verify that downgrade changed UUID
- assertWithMessage("Current database UUID after downgrade")
- .that(DatabaseHelper.getOrCreateUuid(db)).isNotEqualTo(originalUUID);
- }
- }
-
private static String normalize(String sql) {
return sql != null ? sql.replace(", ", ",") : null;
}
@@ -634,7 +585,7 @@
private static class DatabaseHelperO extends DatabaseHelper {
public DatabaseHelperO(Context context, String name) {
super(context, name, DatabaseHelper.VERSION_O, false, false, Column.class,
- ExportedSince.class, null, null, null, null);
+ ExportedSince.class, null, null, null, null, false);
}
@Override
@@ -646,7 +597,7 @@
private static class DatabaseHelperP extends DatabaseHelper {
public DatabaseHelperP(Context context, String name) {
super(context, name, DatabaseHelper.VERSION_P, false, false, Column.class,
- ExportedSince.class, null, null, null, null);
+ ExportedSince.class, null, null, null, null, false);
}
@Override
@@ -658,7 +609,7 @@
private static class DatabaseHelperQ extends DatabaseHelper {
public DatabaseHelperQ(Context context, String name) {
super(context, name, DatabaseHelper.VERSION_Q, false, false, Column.class,
- ExportedSince.class, null, null, null, null);
+ ExportedSince.class, null, null, null, null, false);
}
@Override
@@ -670,7 +621,7 @@
private static class DatabaseHelperR extends DatabaseHelper {
public DatabaseHelperR(Context context, String name) {
super(context, name, DatabaseHelper.VERSION_R, false, false, Column.class,
- ExportedSince.class, null, null, MediaProvider.MIGRATION_LISTENER, null);
+ ExportedSince.class, null, null, MediaProvider.MIGRATION_LISTENER, null, false);
}
@Override
@@ -682,7 +633,7 @@
private static class DatabaseHelperS extends DatabaseHelper {
public DatabaseHelperS(Context context, String name) {
super(context, name, VERSION_S, false, false, Column.class, ExportedSince.class, null,
- null, MediaProvider.MIGRATION_LISTENER, null);
+ null, MediaProvider.MIGRATION_LISTENER, null, false);
}
@@ -695,7 +646,7 @@
private static class DatabaseHelperT extends DatabaseHelper {
public DatabaseHelperT(Context context, String name) {
super(context, name, DatabaseHelper.VERSION_T, false, false, Column.class,
- ExportedSince.class, null, null, MediaProvider.MIGRATION_LISTENER, null);
+ ExportedSince.class, null, null, MediaProvider.MIGRATION_LISTENER, null, false);
}
}
diff --git a/tests/src/com/android/providers/media/MediaProviderForFuseTest.java b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
index 2a0aa48..df9f575 100644
--- a/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
@@ -16,6 +16,11 @@
package com.android.providers.media;
+import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_READ;
+import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_WRITE;
+import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_CREATE;
+import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_DELETE;
+
import android.Manifest;
import android.app.UiAutomation;
import android.content.ContentResolver;
@@ -25,6 +30,7 @@
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
+import android.system.OsConstants;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -107,7 +113,10 @@
// We can write our file
FileOpenResult result = sMediaProvider.onFileOpenForFuse(
- file.getPath(), file.getPath(), sTestUid, 0 /* tid */, 0 /* transforms_reason */,
+ file.getPath(),
+ file.getPath(),
+ sTestUid,
+ 0 /* tid */, 0 /* transforms_reason */,
true /* forWrite */, false /* redact */, false /* transcode_metrics */);
Truth.assertThat(result.status).isEqualTo(0);
Truth.assertThat(result.redactionRanges).isEqualTo(new long[0]);
@@ -228,14 +237,76 @@
}
@Test
- public void test_isOpendirAllowedForFuse() throws Exception {
- Truth.assertThat(sMediaProvider.isOpendirAllowedForFuse(
- sTestDir.getPath(), sTestUid, /* forWrite */ false)).isEqualTo(0);
- }
+ public void test_isDirAccessAllowedForFuse() throws Exception {
+ //verify can create and write but not delete top-level default folder
+ final File topLevelDefaultDir = Environment.buildExternalStoragePublicDirs(
+ Environment.DIRECTORY_PICTURES)[0];
+ final String topLevelDefaultDirPath = topLevelDefaultDir.getPath();
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ topLevelDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ topLevelDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(0);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ topLevelDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(0);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ topLevelDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(
+ OsConstants.EACCES);
- @Test
- public void test_isDirectoryCreationOrDeletionAllowedForFuse() throws Exception {
- Truth.assertThat(sMediaProvider.isDirectoryCreationOrDeletionAllowedForFuse(
- sTestDir.getPath(), sTestUid, true)).isEqualTo(0);
+ //verify cannot create or write top-level non-default folder, but can read it
+ final File topLevelNonDefaultDir = Environment.buildExternalStoragePublicDirs(
+ "non-default-dir")[0];
+ final String topLevelNonDefaultDirPath = topLevelNonDefaultDir.getPath();
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ topLevelNonDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ topLevelNonDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(
+ OsConstants.EACCES);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ topLevelNonDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(OsConstants.EACCES);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ topLevelNonDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(OsConstants.EACCES);
+
+ //verify can read, create, write and delete random non-top-level folder
+ final File lowerLevelNonDefaultDir = new File(topLevelDefaultDir,
+ "subdir" + System.nanoTime());
+ lowerLevelNonDefaultDir.mkdirs();
+ final String lowerLevelNonDefaultDirPath = lowerLevelNonDefaultDir.getPath();
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ lowerLevelNonDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ lowerLevelNonDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(0);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ lowerLevelNonDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(0);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ lowerLevelNonDefaultDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(0);
+
+ //verify cannot update outside /storage folder
+ final File rootDir = new File("/myfolder");
+ final String rootDirPath = rootDir.getPath();
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ rootDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ rootDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(OsConstants.EPERM);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ rootDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(OsConstants.EPERM);
+ Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+ rootDirPath, sTestUid,
+ DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(OsConstants.EPERM);
+
}
}
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 6df8ab3..a04f57b 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -1576,9 +1576,8 @@
Bundle opts = new Bundle();
opts.putString(MediaStore.EXTRA_MODE, "w");
- try {
- AssetFileDescriptor afd = sContext.getContentResolver().openTypedAssetFile(mediaUri,
- "*/*", opts, null);
+ try (AssetFileDescriptor afd = sContext.getContentResolver().openTypedAssetFile(mediaUri,
+ "*/*", opts, null)) {
String rawText = "Hello";
Os.write(afd.getFileDescriptor(), rawText.getBytes(StandardCharsets.UTF_8),
0, rawText.length());
diff --git a/tests/src/com/android/providers/media/PermissionActivityTest.java b/tests/src/com/android/providers/media/PermissionActivityTest.java
index a7fe5b4..281e93a 100644
--- a/tests/src/com/android/providers/media/PermissionActivityTest.java
+++ b/tests/src/com/android/providers/media/PermissionActivityTest.java
@@ -21,6 +21,9 @@
import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
import static android.Manifest.permission.MANAGE_MEDIA;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.Manifest.permission.READ_MEDIA_AUDIO;
+import static android.Manifest.permission.READ_MEDIA_IMAGES;
+import static android.Manifest.permission.READ_MEDIA_VIDEO;
import static android.Manifest.permission.UPDATE_APP_OPS_STATS;
import static androidx.test.InstrumentationRegistry.getContext;
@@ -33,7 +36,10 @@
import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation;
import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia;
import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
import static com.android.providers.media.util.TestUtils.adoptShellPermission;
import static com.android.providers.media.util.TestUtils.dropShellPermission;
@@ -75,6 +81,8 @@
public class PermissionActivityTest {
private static final String TEST_APP_PACKAGE_NAME =
"com.android.providers.media.testapp.permission";
+ private static final String TEST_APP_33_PACKAGE_NAME =
+ "com.android.providers.media.testapp.permissionmedia";
private static final String OP_ACCESS_MEDIA_LOCATION =
AppOpsManager.permissionToOp(ACCESS_MEDIA_LOCATION);
@@ -84,6 +92,12 @@
AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE);
private static final String OP_READ_EXTERNAL_STORAGE =
AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE);
+ private static final String OP_READ_MEDIA_IMAGES =
+ AppOpsManager.permissionToOp(READ_MEDIA_IMAGES);
+ private static final String OP_READ_MEDIA_AUDIO =
+ AppOpsManager.permissionToOp(READ_MEDIA_AUDIO);
+ private static final String OP_READ_MEDIA_VIDEO =
+ AppOpsManager.permissionToOp(READ_MEDIA_VIDEO);
// The list is used to restore the permissions after the test is finished.
// The default value for these app ops is {@link AppOpsManager#MODE_DEFAULT}
@@ -104,10 +118,12 @@
private static final int TEST_APP_PID = -1;
private int mTestAppUid = -1;
+ private int mTestAppUid33 = -1;
@Before
public void setUp() throws Exception {
mTestAppUid = getContext().getPackageManager().getPackageUid(TEST_APP_PACKAGE_NAME, 0);
+ mTestAppUid33 = getContext().getPackageManager().getPackageUid(TEST_APP_33_PACKAGE_NAME, 0);
}
@Test
@@ -147,7 +163,8 @@
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
- setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+ setupPermissions(
+ mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue();
@@ -158,6 +175,103 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testShouldShowActionDialog_noRMAAndMES_true_33() throws Exception {
+ final String[] enableAppOpsList =
+ {OP_MANAGE_MEDIA, OP_READ_MEDIA_IMAGES, OP_READ_MEDIA_VIDEO};
+ final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_MEDIA_AUDIO};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ setupPermissions(
+ mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+ TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+ /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ true,
+ /* shouldCheckReadImages */ false, /* shouldCheckReadVideo */ false,
+ /* mShouldCheckReadAudioOrReadVideo */ false,
+ /* isTargetSdkAtLeastT */ true)).isTrue();
+ } finally {
+ restoreDefaultAppOpPermissions(mTestAppUid33);
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testShouldShowActionDialog_noRMIAndMES_true_33() throws Exception {
+ final String[] enableAppOpsList =
+ {OP_MANAGE_MEDIA, OP_READ_MEDIA_AUDIO, OP_READ_MEDIA_VIDEO};
+ final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_MEDIA_IMAGES};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ setupPermissions(
+ mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+ TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+ /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ false,
+ /* shouldCheckReadImages */ true, /* shouldCheckReadVideo */ false,
+ /* mShouldCheckReadAudioOrReadVideo */ false,
+ /* isTargetSdkAtLeastT */ true)).isTrue();
+ } finally {
+ restoreDefaultAppOpPermissions(mTestAppUid33);
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testShouldShowActionDialog_noRMVAndMES_true_33() throws Exception {
+ final String[] enableAppOpsList =
+ {OP_MANAGE_MEDIA, OP_READ_MEDIA_AUDIO, OP_READ_MEDIA_IMAGES};
+ final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_MEDIA_VIDEO};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ setupPermissions(
+ mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+ TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+ /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ false,
+ /* shouldCheckReadImages */ false, /* shouldCheckReadVideo */ true,
+ /* mShouldCheckReadAudioOrReadVideo */ false,
+ /* isTargetSdkAtLeastT */ true)).isTrue();
+ } finally {
+ restoreDefaultAppOpPermissions(mTestAppUid33);
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testShouldShowActionDialogForSubtitle_noRMARMVAndMES_true_33() throws Exception {
+ final String[] enableAppOpsList =
+ {OP_MANAGE_MEDIA, OP_READ_MEDIA_IMAGES};
+ final String[] disableAppOpsList =
+ {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_MEDIA_AUDIO, OP_READ_MEDIA_VIDEO};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ setupPermissions(
+ mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+ TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+ /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ false,
+ /* shouldCheckReadImages */ false, /* shouldCheckReadVideo */ false,
+ /* mShouldCheckReadAudioOrReadVideo */ true,
+ /* isTargetSdkAtLeastT */ true)).isTrue();
+ } finally {
+ restoreDefaultAppOpPermissions(mTestAppUid33);
+ dropShellPermission();
+ }
+ }
+
+ @Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public void testShouldShowActionDialog_noMANAGE_MEDIA_true() throws Exception {
final String[] enableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_EXTERNAL_STORAGE};
@@ -165,7 +279,8 @@
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
- setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+ setupPermissions(
+ mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue();
@@ -176,14 +291,43 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testShouldShowActionDialog_noMANAGE_MEDIA_true_33() throws Exception {
+ final String[] enableAppOpsList = {
+ OP_MANAGE_EXTERNAL_STORAGE,
+ OP_READ_MEDIA_AUDIO,
+ OP_READ_MEDIA_VIDEO,
+ OP_READ_MEDIA_IMAGES
+ };
+ final String[] disableAppOpsList = {OP_MANAGE_MEDIA};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ setupPermissions(
+ mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+ TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+ /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ true,
+ /* shouldCheckReadImages */ true, /* shouldCheckReadVideo */ true,
+ /* mShouldCheckReadAudioOrReadVideo */ true,
+ /* isTargetSdkAtLeastT */ true)).isTrue();
+ } finally {
+ restoreDefaultAppOpPermissions(mTestAppUid33);
+ dropShellPermission();
+ }
+ }
+
+ @Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
- public void testShouldShowActionDialog_hasPermissionWithRES_false() throws Exception {
+ public void testShouldShowActionDialog_hasMMWithRES_false() throws Exception {
final String[] enableAppOpsList = {OP_MANAGE_MEDIA, OP_READ_EXTERNAL_STORAGE};
final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE};
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
- setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+ setupPermissions(
+ mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse();
@@ -194,14 +338,40 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testShouldShowActionDialog_hasMMWithRM_false_33() throws Exception {
+ final String[] enableAppOpsList = {
+ OP_MANAGE_MEDIA, OP_READ_MEDIA_AUDIO, OP_READ_MEDIA_VIDEO, OP_READ_MEDIA_IMAGES
+ };
+ final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ setupPermissions(
+ mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+ TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+ /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ true,
+ /* shouldCheckReadImages */ true, /* shouldCheckReadVideo */ true,
+ /* mShouldCheckReadAudioOrReadVideo */ true,
+ /* isTargetSdkAtLeastT */ true)).isFalse();
+ } finally {
+ restoreDefaultAppOpPermissions(mTestAppUid33);
+ dropShellPermission();
+ }
+ }
+
+ @Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
- public void testShouldShowActionDialog_hasPermissionWithMES_false() throws Exception {
+ public void testShouldShowActionDialog_hasMMWithMES_false() throws Exception {
final String[] enableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_MANAGE_MEDIA};
final String[] disableAppOpsList = {OP_READ_EXTERNAL_STORAGE};
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
- setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+ setupPermissions(
+ mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse();
@@ -212,6 +382,32 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testShouldShowActionDialog_hasMMWithMES_false_33() throws Exception {
+ final String[] enableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_MANAGE_MEDIA};
+ final String[] disableAppOpsList = {
+ OP_READ_MEDIA_AUDIO, OP_READ_MEDIA_VIDEO, OP_READ_MEDIA_IMAGES
+ };
+
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ setupPermissions(
+ mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+ TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+ /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ true,
+ /* shouldCheckReadImages */ true, /* shouldCheckReadVideo */ true,
+ /* mShouldCheckReadAudioOrReadVideo */ true,
+ /* isTargetSdkAtLeastT */ true)).isFalse();
+ } finally {
+ restoreDefaultAppOpPermissions(mTestAppUid33);
+ dropShellPermission();
+ }
+ }
+
+ @Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public void testShouldShowActionDialog_writeNoACCESS_MEDIA_LOCATION_true() throws Exception {
final String[] enableAppOpsList =
@@ -220,7 +416,8 @@
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
- setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+ setupPermissions(
+ mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isTrue();
@@ -242,7 +439,8 @@
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
- setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+ setupPermissions(
+ mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isFalse();
@@ -253,7 +451,7 @@
}
private static void setupPermissions(int uid, @NonNull String[] enableAppOpsList,
- @NonNull String[] disableAppOpsList) throws Exception {
+ @NonNull String[] disableAppOpsList, @NonNull String packageName) throws Exception {
for (String op : enableAppOpsList) {
modifyAppOp(uid, op, AppOpsManager.MODE_ALLOWED);
}
@@ -262,8 +460,10 @@
modifyAppOp(uid, op, AppOpsManager.MODE_ERRORED);
}
- pollForAppOpPermissions(TEST_APP_PID, uid, enableAppOpsList, /* hasPermission= */ true);
- pollForAppOpPermissions(TEST_APP_PID, uid, disableAppOpsList, /* hasPermission= */ false);
+ pollForAppOpPermissions(
+ TEST_APP_PID, packageName, uid, enableAppOpsList, /* hasPermission= */ true);
+ pollForAppOpPermissions(
+ TEST_APP_PID, packageName, uid, disableAppOpsList, /* hasPermission= */ false);
}
private static void restoreDefaultAppOpPermissions(int uid) {
@@ -296,16 +496,16 @@
getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
}
- private static void pollForAppOpPermissions(int pid, int uid, String[] opList,
- boolean hasPermission) throws Exception {
+ private static void pollForAppOpPermissions(int pid, @NonNull String packageName, int uid,
+ String[] opList, boolean hasPermission) throws Exception {
long current = System.currentTimeMillis();
final long timeout = current + TIMEOUT_MILLIS;
final HashSet<String> checkedOpSet = new HashSet<>();
while (current < timeout && checkedOpSet.size() < opList.length) {
for (String op : opList) {
- if (!checkedOpSet.contains(op) && checkPermission(op, pid, uid,
- TEST_APP_PACKAGE_NAME, hasPermission)) {
+ if (!checkedOpSet.contains(op)
+ && checkPermission(op, pid, uid, packageName, hasPermission)) {
checkedOpSet.add(op);
continue;
}
@@ -326,6 +526,15 @@
if (TextUtils.equals(op, OP_READ_EXTERNAL_STORAGE)) {
return expected == checkPermissionReadStorage(context, pid, uid, packageName,
/* attributionTag= */ null);
+ } else if (TextUtils.equals(op, OP_READ_MEDIA_IMAGES)) {
+ return expected == checkPermissionReadImages(
+ context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true);
+ } else if (TextUtils.equals(op, OP_READ_MEDIA_AUDIO)) {
+ return expected == checkPermissionReadAudio(
+ context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true);
+ } else if (TextUtils.equals(op, OP_READ_MEDIA_VIDEO)) {
+ return expected == checkPermissionReadVideo(
+ context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true);
} else if (TextUtils.equals(op, OP_MANAGE_EXTERNAL_STORAGE)) {
return expected == checkPermissionManager(context, pid, uid, packageName,
/* attributionTag= */ null);
diff --git a/tests/src/com/android/providers/media/PickerUriResolverTest.java b/tests/src/com/android/providers/media/PickerUriResolverTest.java
index b691155..3de26ec 100644
--- a/tests/src/com/android/providers/media/PickerUriResolverTest.java
+++ b/tests/src/com/android/providers/media/PickerUriResolverTest.java
@@ -24,10 +24,10 @@
import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import android.Manifest;
import android.content.ContentUris;
@@ -57,8 +57,8 @@
import org.junit.runner.RunWith;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
@RunWith(AndroidJUnit4.class)
public class PickerUriResolverTest {
@@ -350,17 +350,17 @@
}
private void testOpenFile(Uri uri) throws Exception {
- ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(uri, "r", /* signal */ null,
- /* callingPid */ -1, /* callingUid */ -1);
-
- assertThat(pfd).isNotNull();
+ try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(uri, "r", /* signal */ null,
+ /* callingPid */ -1, /* callingUid */ -1)) {
+ assertThat(pfd).isNotNull();
+ }
}
private void testOpenTypedAssetFile(Uri uri) throws Exception {
- AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(uri, "image/*",
- /* opts */ null, /* signal */ null, /* callingPid */ -1, /* callingUid */ -1);
-
- assertThat(afd).isNotNull();
+ try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(uri, "image/*",
+ /* opts */ null, /* signal */ null, /* callingPid */ -1, /* callingUid */ -1)) {
+ assertThat(afd).isNotNull();
+ }
}
private void testQuery(Uri uri) throws Exception {
diff --git a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
index 0ba045f..7608c92 100644
--- a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
@@ -16,12 +16,12 @@
package com.android.providers.media.photopicker;
-import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES;
-import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS;
-import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
+import static android.provider.CloudMediaProviderContract.AlbumColumns;
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA;
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS;
-import static android.provider.CloudMediaProviderContract.AlbumColumns;
+import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES;
+import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
+import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS;
import static android.provider.CloudMediaProviderContract.MediaColumns;
import static android.provider.MediaStore.VOLUME_EXTERNAL;
@@ -41,16 +41,12 @@
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
-import android.provider.DeviceConfig;
import android.provider.MediaStore;
import androidx.test.InstrumentationRegistry;
-import com.android.providers.media.photopicker.data.ExternalDbFacade;
import com.android.providers.media.photopicker.data.ItemsProvider;
-import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.data.model.Category;
-import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.data.model.UserId;
import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
@@ -634,11 +630,13 @@
}
private void assertCategoryUriIsValid(Uri uri) throws Exception {
- final AssetFileDescriptor fd1 = mIsolatedResolver.openTypedAssetFile(uri, "image/*", null,
- null);
- assertThat(fd1).isNotNull();
- final ParcelFileDescriptor fd2 = mIsolatedResolver.openFileDescriptor(uri, "r");
- assertThat(fd2).isNotNull();
+ try (AssetFileDescriptor fd1 = mIsolatedResolver.openTypedAssetFile(uri, "image/*",
+ null, null)) {
+ assertThat(fd1).isNotNull();
+ }
+ try (ParcelFileDescriptor fd2 = mIsolatedResolver.openFileDescriptor(uri, "r")) {
+ assertThat(fd2).isNotNull();
+ }
}
private void assertCategoriesNoMatch(String expectedCategoryName) {
diff --git a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
index 47033cc..6931cd6 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
@@ -119,8 +119,10 @@
mDbHelper = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, mDbHelper);
+ final String allowedCloudProviders = CLOUD_PRIMARY_PROVIDER_AUTHORITY + ","
+ + CLOUD_SECONDARY_PROVIDER_AUTHORITY;
mController = new PickerSyncController(mContext, mFacade, LOCAL_PROVIDER_AUTHORITY,
- /* syncDelay */ 0);
+ allowedCloudProviders, /* syncDelay */ 0);
mDataLayer = new PickerDataLayer(mContext, mFacade, mController);
// Set cloud provider to null to discard
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index 224e252..53d6837 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -18,22 +18,21 @@
import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
import static com.android.providers.media.photopicker.PickerSyncController.CloudProviderInfo;
+
import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.content.Context;
-import android.content.SharedPreferences;
import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Process;
import android.os.SystemClock;
-import android.os.SystemProperties;
-import android.provider.CloudMediaProviderContract;
+import android.os.storage.StorageManager;
import android.provider.CloudMediaProviderContract.MediaColumns;
import android.provider.MediaStore;
import android.util.Pair;
@@ -41,24 +40,20 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
-import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.modules.utils.BackgroundThread;
import com.android.providers.media.PickerProviderMediaGenerator;
+import com.android.providers.media.R;
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
import com.android.providers.media.photopicker.data.PickerDbFacade;
-import com.android.providers.media.R;
-import com.android.providers.media.util.DeviceConfigUtils;
-
-import java.io.File;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.MockitoSession;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public class PickerSyncControllerTest {
@@ -129,8 +124,11 @@
mDbHelper = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, mDbHelper);
+
+ final String allowedCloudProviders = CLOUD_PRIMARY_PROVIDER_AUTHORITY + ","
+ + CLOUD_SECONDARY_PROVIDER_AUTHORITY;
mController = new PickerSyncController(mContext, mFacade, LOCAL_PROVIDER_AUTHORITY,
- /* syncDelay */ 0);
+ allowedCloudProviders, /* syncDelay */ 0);
// Set cloud provider to null to avoid trying to sync it during other tests
// that might be using an IsolatedContext
@@ -609,57 +607,34 @@
@Test
public void testGetSupportedCloudProviders_useAllowList() {
- MockitoSession mockSession = ExtendedMockito.mockitoSession()
- .mockStatic(DeviceConfigUtils.class)
- .mockStatic(SystemProperties.class)
- .startMocking();
+ CloudProviderInfo primaryInfo = new CloudProviderInfo(CLOUD_PRIMARY_PROVIDER_AUTHORITY,
+ PACKAGE_NAME,
+ Process.myUid());
+ CloudProviderInfo secondaryInfo = new CloudProviderInfo(
+ CLOUD_SECONDARY_PROVIDER_AUTHORITY,
+ PACKAGE_NAME,
+ Process.myUid());
- try {
- when(SystemProperties.getBoolean(
- PickerSyncController.PROP_USE_ALLOWED_CLOUD_PROVIDERS, false))
- .thenReturn(true);
+ // 1. Allow list is subset of existing providers list
+ PickerSyncController controller = new PickerSyncController(mContext, mFacade,
+ LOCAL_PROVIDER_AUTHORITY, CLOUD_PRIMARY_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+ List<CloudProviderInfo> providers = controller.getSupportedCloudProviders();
+ assertThat(providers).containsExactly(primaryInfo);
- CloudProviderInfo primaryInfo = new CloudProviderInfo(CLOUD_PRIMARY_PROVIDER_AUTHORITY,
- PACKAGE_NAME,
- Process.myUid());
- CloudProviderInfo secondaryInfo = new CloudProviderInfo(
- CLOUD_SECONDARY_PROVIDER_AUTHORITY,
- PACKAGE_NAME,
- Process.myUid());
+ String allowedCloudProviders = CLOUD_PRIMARY_PROVIDER_AUTHORITY + ","
+ + CLOUD_SECONDARY_PROVIDER_AUTHORITY;
+ controller = new PickerSyncController(mContext, mFacade,
+ LOCAL_PROVIDER_AUTHORITY, allowedCloudProviders, SYNC_DELAY_MS);
+ providers = controller.getSupportedCloudProviders();
+ assertThat(providers).containsExactly(primaryInfo, secondaryInfo);
- // 1. Allow list is subset of existing providers list
- when(DeviceConfigUtils.getStringDeviceConfig(
- PickerSyncController.ALLOWED_CLOUD_PROVIDERS_KEY, ""))
- .thenReturn(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
- PickerSyncController controller = new PickerSyncController(mContext, mFacade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
- List<CloudProviderInfo> providers = controller.getSupportedCloudProviders();
- assertThat(providers).containsExactly(primaryInfo);
-
- // 2. Allow list is exactly the same as existing providers list
- when(DeviceConfigUtils.getStringDeviceConfig(
- PickerSyncController.ALLOWED_CLOUD_PROVIDERS_KEY, ""))
- .thenReturn(CLOUD_PRIMARY_PROVIDER_AUTHORITY
- + "," + CLOUD_SECONDARY_PROVIDER_AUTHORITY);
- controller = new PickerSyncController(mContext, mFacade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
- providers = controller.getSupportedCloudProviders();
- assertThat(providers).containsExactly(primaryInfo, secondaryInfo);
-
- // 3. Allow list containing existing providers list + others
- when(DeviceConfigUtils.getStringDeviceConfig(
- PickerSyncController.ALLOWED_CLOUD_PROVIDERS_KEY, ""))
- .thenReturn(CLOUD_PRIMARY_PROVIDER_AUTHORITY
- + "," + CLOUD_SECONDARY_PROVIDER_AUTHORITY
- + "," + CLOUD_PRIMARY_PROVIDER_AUTHORITY + "invalid");
- controller = new PickerSyncController(mContext, mFacade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
- providers = controller.getSupportedCloudProviders();
- assertThat(providers).containsExactly(primaryInfo, secondaryInfo);
- }
- finally {
- mockSession.finishMocking();
- }
+ allowedCloudProviders = CLOUD_PRIMARY_PROVIDER_AUTHORITY
+ + "," + CLOUD_SECONDARY_PROVIDER_AUTHORITY
+ + "," + CLOUD_PRIMARY_PROVIDER_AUTHORITY + "invalid";
+ controller = new PickerSyncController(mContext, mFacade,
+ LOCAL_PROVIDER_AUTHORITY, allowedCloudProviders, SYNC_DELAY_MS);
+ providers = controller.getSupportedCloudProviders();
+ assertThat(providers).containsExactly(primaryInfo, secondaryInfo);
}
@Test
@@ -712,7 +687,7 @@
@Test
public void testNotifyMediaEvent() {
PickerSyncController controller = new PickerSyncController(mContext, mFacade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+ LOCAL_PROVIDER_AUTHORITY, "", SYNC_DELAY_MS);
// 1. Add media and notify
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
@@ -737,7 +712,7 @@
PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
dbHelper);
PickerSyncController controller = new PickerSyncController(mContext, facade,
- LOCAL_PROVIDER_AUTHORITY, /* syncDelay */ 0);
+ LOCAL_PROVIDER_AUTHORITY, "", /* syncDelay */ 0);
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
controller.syncAllMedia();
@@ -756,7 +731,7 @@
facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, dbHelper);
controller = new PickerSyncController(mContext, facade,
- LOCAL_PROVIDER_AUTHORITY, /* syncDelay */ 0);
+ LOCAL_PROVIDER_AUTHORITY, "", /* syncDelay */ 0);
// Initially empty db
try (Cursor cr = queryMedia(facade)) {
@@ -779,7 +754,7 @@
PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
dbHelperV1);
PickerSyncController controller = new PickerSyncController(mContext, facade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+ LOCAL_PROVIDER_AUTHORITY, "", SYNC_DELAY_MS);
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
controller.syncAllMedia();
@@ -795,7 +770,7 @@
PickerDatabaseHelper dbHelperV2 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_2);
facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, dbHelperV2);
controller = new PickerSyncController(mContext, facade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+ LOCAL_PROVIDER_AUTHORITY, "", SYNC_DELAY_MS);
// Initially empty db
try (Cursor cr = queryMedia(facade)) {
@@ -818,7 +793,7 @@
PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
dbHelperV2);
PickerSyncController controller = new PickerSyncController(mContext, facade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+ LOCAL_PROVIDER_AUTHORITY, "", SYNC_DELAY_MS);
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
controller.syncAllMedia();
@@ -835,7 +810,7 @@
facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
dbHelperV1);
controller = new PickerSyncController(mContext, facade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+ LOCAL_PROVIDER_AUTHORITY, "", SYNC_DELAY_MS);
// Initially empty db
try (Cursor cr = queryMedia(facade)) {
@@ -967,7 +942,7 @@
PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
dbHelperV1);
PickerSyncController controller = new PickerSyncController(mContext, facade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+ LOCAL_PROVIDER_AUTHORITY, CLOUD_PRIMARY_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
controller.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -978,7 +953,7 @@
facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
dbHelperV2);
controller = new PickerSyncController(mContext, facade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+ LOCAL_PROVIDER_AUTHORITY, CLOUD_PRIMARY_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -1077,14 +1052,19 @@
when(mockContext.getResources()).thenReturn(mockResources);
when(mockContext.getPackageManager()).thenReturn(mContext.getPackageManager());
+ when(mockContext.getSystemService(StorageManager.class))
+ .thenReturn(mContext.getSystemService(StorageManager.class));
when(mockContext.getSharedPreferences(anyString(), anyInt())).thenAnswer(i -> {
return mContext.getSharedPreferences((String)i.getArgument(0), (int)i.getArgument(1));
});
when(mockResources.getString(R.string.config_default_cloud_provider_authority))
.thenReturn(defaultProvider);
+ final String allowedCloudProviders = CLOUD_PRIMARY_PROVIDER_AUTHORITY + ","
+ + CLOUD_SECONDARY_PROVIDER_AUTHORITY;
+
return new PickerSyncController(mockContext, mFacade,
- LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+ LOCAL_PROVIDER_AUTHORITY, allowedCloudProviders, SYNC_DELAY_MS);
}
private static void assertCursor(Cursor cursor, String id, String expectedAuthority) {
diff --git a/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java b/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java
index d253bf9..3a37efa 100644
--- a/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java
@@ -620,7 +620,6 @@
assertThat(cursor.getCount()).isEqualTo(0);
}
-
try (Cursor cursor = facade.queryMedia(/* generation */ 0,
ALBUM_ID_CAMERA, VIDEO_MIME_TYPE)) {
assertThat(cursor.getCount()).isEqualTo(0);
@@ -739,27 +738,14 @@
// We verify the order of the albums:
// Camera, Screenshots and Downloads
cursor.moveToNext();
- assertAlbumColumns(facade,
- cursor,
- ALBUM_ID_CAMERA,
- /* mediaCoverId */ "1",
- DATE_TAKEN_MS1,
+ assertAlbumColumns(facade, cursor, ALBUM_ID_CAMERA, DATE_TAKEN_MS1, /* count */ 1);
+
+ cursor.moveToNext();
+ assertAlbumColumns(facade, cursor, ALBUM_ID_SCREENSHOTS, DATE_TAKEN_MS2,
/* count */ 1);
cursor.moveToNext();
- assertAlbumColumns(facade,
- cursor,
- ALBUM_ID_SCREENSHOTS,
- /* mediaCoverId */ "2",
- DATE_TAKEN_MS2,
- /* count */ 1);
-
- cursor.moveToNext();
- assertAlbumColumns(facade,
- cursor,
- ALBUM_ID_DOWNLOADS,
- /* mediaCoverId */ "3",
- DATE_TAKEN_MS3,
+ assertAlbumColumns(facade, cursor, ALBUM_ID_DOWNLOADS, DATE_TAKEN_MS3,
/* count */ 1);
}
}
@@ -791,12 +777,7 @@
// We verify the order of the albums only the image in camera is shown
cursor.moveToNext();
- assertAlbumColumns(facade,
- cursor,
- ALBUM_ID_CAMERA,
- /* mediaCoverId */ "1",
- DATE_TAKEN_MS1,
- /* count */ 1);
+ assertAlbumColumns(facade, cursor, ALBUM_ID_CAMERA, DATE_TAKEN_MS1, /* count */ 1);
}
}
}
@@ -888,7 +869,7 @@
}
private static void assertAlbumColumns(ExternalDbFacade facade, Cursor cursor,
- String displayName, String mediaCoverId, long dateTakenMs, long count) {
+ String displayName, long dateTakenMs, long count) {
int displayNameIndex = cursor.getColumnIndex(
CloudMediaProviderContract.AlbumColumns.DISPLAY_NAME);
int idIndex = cursor.getColumnIndex(CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID);
@@ -897,7 +878,7 @@
int countIndex = cursor.getColumnIndex(CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT);
assertThat(cursor.getString(displayNameIndex)).isEqualTo(displayName);
- assertThat(cursor.getString(idIndex)).isEqualTo(mediaCoverId);
+ assertThat(cursor.getString(idIndex)).isNotNull();
assertThat(cursor.getLong(dateTakenIndex)).isEqualTo(dateTakenMs);
assertThat(cursor.getLong(countIndex)).isEqualTo(count);
}
@@ -930,7 +911,8 @@
private static class TestDatabaseHelper extends DatabaseHelper {
public TestDatabaseHelper(Context context) {
- super(context, TEST_CLEAN_DB, 1, false, false, null, null, null, null, null, null);
+ super(context, TEST_CLEAN_DB, 1, false, false, null, null, null, null, null, null,
+ false);
}
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java b/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
index a3c3526..0ab97db 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
@@ -37,6 +37,7 @@
import com.android.providers.media.R;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -50,6 +51,7 @@
public ActivityScenarioRule<PhotoPickerTestActivity> mRule =
new ActivityScenarioRule<>(PhotoPickerBaseTest.getMultiSelectionIntent());
+ @Ignore("b/227478958 Odd failure to verify Downloads album")
@Test
public void testAlbumGrid() {
// Goto Albums page
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
index 2fbba6c..de6ad97 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
@@ -101,9 +101,11 @@
try (ViewPager2IdlingResource idlingResource
= ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
- // Verify video player is displayed
assertMultiSelectLongPressCommonLayoutMatches();
- onView(withId(R.id.preview_player_view)).check(matches(isDisplayed()));
+ // Verify thumbnail view is displayed
+ onView(withId(R.id.preview_video_image)).check(matches(isDisplayed()));
+ // TODO (b/232792753): Assert video player visibility using custom IdlingResource
+
// Verify no special format icon is previewed
onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
@@ -250,4 +252,4 @@
onView(withId(R.id.preview_selected_check_button)).check(matches(not(isDisplayed())));
onView(withId(R.id.preview_add_button)).check(matches(not(isDisplayed())));
}
-}
\ No newline at end of file
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
index fd1908f..3920059 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
@@ -65,7 +65,7 @@
@RunWith(AndroidJUnit4ClassRunner.class)
public class PreviewMultiSelectTest extends PhotoPickerBaseTest {
- private static final int PLAYER_VIEW_ID = R.id.preview_player_view;
+ private static final int VIDEO_PREVIEW_THUMBNAIL_ID = R.id.preview_video_image;
@Rule
public ActivityScenarioRule<PhotoPickerTestActivity> mRule
@@ -207,7 +207,7 @@
// TODO(b/197083539): We don't check the video image to be visible or not because its
// visibility is time sensitive. Try waiting till player is ready and assert that video
// image is no more visible.
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, VIDEO_PREVIEW_THUMBNAIL_ID))
.check(matches(isDisplayed()));
// Verify no special format icon is previewed
assertSpecialFormatBadgeDoesNotExist();
@@ -313,16 +313,17 @@
// Verify that "View Selected" shows the video item, not the image item that was
// previewed earlier with preview on long press
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, VIDEO_PREVIEW_THUMBNAIL_ID))
.check(matches(isDisplayed()));
// Swipe and verify we don't preview the image item
swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, VIDEO_PREVIEW_THUMBNAIL_ID))
.check(matches(isDisplayed()));
swipeRightAndWait(PREVIEW_VIEW_PAGER_ID);
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, VIDEO_PREVIEW_THUMBNAIL_ID))
.check(matches(isDisplayed()));
+ // TODO (b/232792753): Assert video player visibility using custom IdlingResource
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
index edeccd8..df11c29 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
@@ -134,9 +134,11 @@
try (ViewPager2IdlingResource idlingResource
= ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
- // Verify video player is displayed
assertSingleSelectCommonLayoutMatches();
- onView(withId(R.id.preview_player_view)).check(matches(isDisplayed()));
+ // Verify thumbnail view is displayed
+ onView(withId(R.id.preview_video_image)).check(matches(isDisplayed()));
+ // TODO (b/232792753): Assert video player visibility using custom IdlingResource
+
// Verify no special format icon is previewed
onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
diff --git a/tests/src/com/android/providers/media/scan/MediaScannerTest.java b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
index afb7597..f8c27e6 100644
--- a/tests/src/com/android/providers/media/scan/MediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
@@ -46,13 +46,14 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
-import com.android.providers.media.PickerUriResolver;
+import com.android.providers.media.DatabaseHelper;
import com.android.providers.media.MediaDocumentsProvider;
import com.android.providers.media.MediaProvider;
+import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.R;
-import com.android.providers.media.util.FileUtils;
import com.android.providers.media.photopicker.PhotoPickerProvider;
import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.util.FileUtils;
import org.junit.Before;
import org.junit.Ignore;
@@ -124,6 +125,11 @@
public void addOnPropertiesChangedListener(OnPropertiesChangedListener listener) {
// Ignore
}
+
+ @Override
+ protected void updateNextRowIdXattr(DatabaseHelper helper, long id) {
+ // Ignoring this as test app would not have access to update xattr.
+ }
};
mProvider.attachInfo(this, info);
mResolver.addProvider(MediaStore.AUTHORITY, mProvider);
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 574a2db..266c144 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -971,16 +971,24 @@
public void testExtractPathOwnerPackageName() {
assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/data/foo"))
.isEqualTo("foo");
+ assertThat(extractPathOwnerPackageName("/storage/emulated/0/android/data/foo"))
+ .isEqualTo("foo");
assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/obb/foo"))
.isEqualTo("foo");
+ assertThat(extractPathOwnerPackageName("/storage/emulated/0/android/obb/foo"))
+ .isEqualTo("foo");
assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/media/foo"))
.isEqualTo("foo");
+ assertThat(extractPathOwnerPackageName("/storage/emulated/0/android/media/foo"))
+ .isEqualTo("foo");
assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/data/foo"))
.isEqualTo("foo");
assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/obb/foo"))
.isEqualTo("foo");
assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/media/foo"))
.isEqualTo("foo");
+ assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/android/media/foo"))
+ .isEqualTo("foo");
assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/data")).isNull();
assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/obb")).isNull();
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index c434bd2..45e9316 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -51,11 +51,8 @@
import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage;
import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteVideo;
import static com.android.providers.media.util.PermissionUtils.checkWriteImagesOrVideoAppOps;
-import static com.android.providers.media.util.TestUtils.QUERY_TYPE;
-import static com.android.providers.media.util.TestUtils.RUN_INFINITE_ACTIVITY;
import static com.android.providers.media.util.TestUtils.adoptShellPermission;
import static com.android.providers.media.util.TestUtils.dropShellPermission;
-import static com.android.providers.media.util.TestUtils.getPid;
import static com.google.common.truth.Truth.assertThat;
@@ -63,26 +60,29 @@
import android.app.AppOpsManager;
import android.content.Context;
-import android.content.Intent;
import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
import com.android.cts.install.lib.TestApp;
+import com.android.modules.utils.build.SdkLevel;
-import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.HashMap;
-import java.util.Map;
-
@RunWith(AndroidJUnit4.class)
public class PermissionUtilsTest {
private static final TestApp TEST_APP_WITH_STORAGE_PERMS = new TestApp(
"TestAppWithStoragePerms",
"com.android.providers.media.testapp.withstorageperms", 1, false,
"MediaProviderTestAppWithStoragePerms.apk");
+ private static final TestApp TEST_APP_WITH_MEDIA_PERMS =
+ new TestApp(
+ "TestAppWithMediaPerms",
+ "com.android.providers.media.testapp.withmediaperms",
+ 1,
+ false,
+ "MediaProviderTestAppWithMediaPerms.apk");
private static final TestApp TEST_APP_WITHOUT_PERMS = new TestApp("TestAppWithoutPerms",
"com.android.providers.media.testapp.withoutperms", 1, false,
"MediaProviderTestAppWithoutPerms.apk");
@@ -120,11 +120,11 @@
assertThat(checkPermissionReadStorage(context, pid, uid, packageName, null)).isTrue();
assertThat(checkPermissionWriteStorage(context, pid, uid, packageName, null)).isTrue();
- assertThat(checkPermissionReadAudio(context, pid, uid, packageName, null)).isTrue();
+ assertThat(checkPermissionReadAudio(context, pid, uid, packageName, null, false)).isTrue();
assertThat(checkPermissionWriteAudio(context, pid, uid, packageName, null)).isFalse();
- assertThat(checkPermissionReadVideo(context, pid, uid, packageName, null)).isTrue();
+ assertThat(checkPermissionReadVideo(context, pid, uid, packageName, null, false)).isTrue();
assertThat(checkPermissionWriteVideo(context, pid, uid, packageName, null)).isFalse();
- assertThat(checkPermissionReadImages(context, pid, uid, packageName, null)).isTrue();
+ assertThat(checkPermissionReadImages(context, pid, uid, packageName, null, false)).isTrue();
assertThat(checkPermissionWriteImages(context, pid, uid, packageName, null)).isFalse();
assertThat(checkPermissionInstallPackages(context, pid, uid, packageName, null)).isFalse();
}
@@ -142,6 +142,9 @@
@Test
public void testDefaultPermissionsOnTestAppWithStoragePerms() throws Exception {
+ if (!SdkLevel.isAtLeastT()) {
+ return;
+ }
String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
adoptShellPermission(UPDATE_APP_OPS_STATS);
@@ -159,9 +162,49 @@
checkPermissionAccessMtp(getContext(), TEST_APP_PID, testAppUid, packageName,
null)).isFalse();
assertThat(
- checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
+ checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null)).isTrue();
+ assertThat(
+ checkPermissionReadStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
null)).isTrue();
- checkReadPermissions(TEST_APP_PID, testAppUid, packageName, true);
+ assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+ false /* targetSdkIsAtLeastT */, true /* expected */);
+ // APPs with W_E_S can also read media.
+ assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+ true /* targetSdkIsAtLeastT */, true /* expected */);
+
+ } finally {
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ public void testDefaultPermissionsOnTestAppWithMediaPerms() throws Exception {
+ if (!SdkLevel.isAtLeastT()) {
+ return;
+ }
+ String packageName = TEST_APP_WITH_MEDIA_PERMS.getPackageName();
+ int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
+ adoptShellPermission(UPDATE_APP_OPS_STATS);
+
+ try {
+ assertThat(checkPermissionSelf(getContext(), TEST_APP_PID, testAppUid)).isFalse();
+ assertThat(checkPermissionShell(getContext(), TEST_APP_PID, testAppUid)).isFalse();
+ assertThat(checkIsLegacyStorageGranted(getContext(), testAppUid, packageName, null))
+ .isFalse();
+ assertThat(checkPermissionInstallPackages(
+ getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+ assertThat(checkPermissionAccessMtp(
+ getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+ assertThat(checkPermissionWriteStorage(
+ getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+ assertThat(checkPermissionReadStorage(
+ getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+ assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+ true /* targetSdkIsAtLeastT */, true /* expected */);
+ assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+ false /* targetSdkIsAtLeastT */, false /* expected */);
+
} finally {
dropShellPermission();
}
@@ -193,15 +236,20 @@
null)).isFalse();
assertThat(
checkPermissionInstallPackages(getContext(), TEST_APP_PID, testAppUid,
- packageName,
- null)).isFalse();
+ packageName, null)).isFalse();
assertThat(
checkPermissionAccessMtp(getContext(), TEST_APP_PID, testAppUid, packageName,
null)).isFalse();
assertThat(
checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
null)).isFalse();
- checkReadPermissions(TEST_APP_PID, testAppUid, packageName, false);
+ assertThat(
+ checkPermissionReadStorage( getContext(), TEST_APP_PID, testAppUid, packageName,
+ null)).isFalse();
+ assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+ false /* targetSdkIsAtLeastT */, false /* expected */);
+ assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+ true /* targetSdkIsAtLeastT */, false /* expected */);
} finally {
dropShellPermission();
}
@@ -238,9 +286,12 @@
checkPermissionAccessMtp(getContext(), TEST_APP_PID, testAppUid, packageName,
null)).isFalse();
assertThat(
- checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isFalse();
- checkReadPermissions(TEST_APP_PID, testAppUid, packageName, true);
+ checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null)).isFalse();
+ assertThat(checkPermissionReadStorage(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null)).isTrue();
+ assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+ false /* targetSdkIsAtLeastT */, true /* expected */);
} finally {
dropShellPermission();
}
@@ -346,26 +397,32 @@
}
@Test
- public void testReadVideoOnTestApp() throws Exception {
- final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
- int testAppUid = getContext().getPackageManager().getPackageUid(
- packageName, 0);
+ public void testReadVideoOnTestAppWithStoragePerms() throws Exception {
+ assertReadVideoOnTestApp(TEST_APP_WITH_STORAGE_PERMS);
+ }
+
+ @Test
+ public void testReadVideoOnTestAppWithMediaPerms() throws Exception {
+ if (!SdkLevel.isAtLeastT()) {
+ return;
+ }
+ assertReadVideoOnTestApp(TEST_APP_WITH_MEDIA_PERMS);
+ }
+
+ private static void assertReadVideoOnTestApp(TestApp app) throws Exception {
+ boolean isAtLeastT = (app == TEST_APP_WITH_MEDIA_PERMS) ? true : false;
+ final String packageName = app.getPackageName();
+ int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
-
try {
- assertThat(
- checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isTrue();
-
+ assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null, isAtLeastT)).isTrue();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VIDEO, AppOpsManager.MODE_ERRORED);
- assertThat(
- checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isFalse();
-
+ assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null, isAtLeastT)).isFalse();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VIDEO, AppOpsManager.MODE_ALLOWED);
- assertThat(
- checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isTrue();
+ assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null, isAtLeastT)).isTrue();
} finally {
dropShellPermission();
}
@@ -398,51 +455,68 @@
}
@Test
- public void testReadAudioOnTestApp() throws Exception {
- final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
- int testAppUid = getContext().getPackageManager().getPackageUid(
- packageName, 0);
- adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+ public void testReadAudioOnTestAppWithStoragePerms() throws Exception {
+ assertReadAudioOnTestApp(TEST_APP_WITH_STORAGE_PERMS);
+ }
+ @Test
+ public void testReadAudioOnTestAppWithMediaPerms() throws Exception {
+ if (!SdkLevel.isAtLeastT()) {
+ return;
+ }
+ assertReadAudioOnTestApp(TEST_APP_WITH_MEDIA_PERMS);
+ }
+
+ private static void assertReadAudioOnTestApp(TestApp app) throws Exception {
+ boolean isAtLeastT = (app == TEST_APP_WITH_MEDIA_PERMS) ? true : false;
+ final String packageName = app.getPackageName();
+ int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
- assertThat(
- checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isTrue();
+ assertThat(checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null, isAtLeastT)).isTrue();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_AUDIO, AppOpsManager.MODE_ERRORED);
- assertThat(
- checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isFalse();
+ assertThat(checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null, isAtLeastT)).isFalse();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_AUDIO, AppOpsManager.MODE_ALLOWED);
- assertThat(
- checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isTrue();
+ assertThat(checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null, isAtLeastT)).isTrue();
} finally {
dropShellPermission();
}
}
@Test
- public void testReadImagesOnTestApp() throws Exception {
- final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
+ public void testReadImagesOnTestAppWithStoragePerms() throws Exception {
+ assertReadImagesOnTestApp(TEST_APP_WITH_STORAGE_PERMS);
+ }
+
+ @Test
+ public void testReadImagesOnTestAppWithMediaPerms() throws Exception {
+ if (!SdkLevel.isAtLeastT()) {
+ return;
+ }
+ assertReadImagesOnTestApp(TEST_APP_WITH_MEDIA_PERMS);
+ }
+
+ private static void assertReadImagesOnTestApp(TestApp app) throws Exception {
+ boolean isAtLeastT = (app == TEST_APP_WITH_MEDIA_PERMS) ? true : false;
+ final String packageName = app.getPackageName();
int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
-
try {
- assertThat(
- checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isTrue();
+ assertThat(checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null, isAtLeastT)).isTrue();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_IMAGES, AppOpsManager.MODE_ERRORED);
- assertThat(
- checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isFalse();
+ assertThat(checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null, isAtLeastT)).isFalse();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_IMAGES, AppOpsManager.MODE_ALLOWED);
- assertThat(
- checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isTrue();
+ assertThat(checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid,
+ packageName, null, isAtLeastT)).isTrue();
} finally {
dropShellPermission();
}
@@ -491,15 +565,19 @@
.isFalse();
}
- static private void checkReadPermissions(int pid, int uid, String packageName,
- boolean expected) {
- assertEquals(expected,
- checkPermissionReadStorage(getContext(), pid, uid, packageName, null));
- assertEquals(expected,
- checkPermissionReadAudio(getContext(), pid, uid, packageName, null));
- assertEquals(expected,
- checkPermissionReadImages(getContext(), pid, uid, packageName, null));
- assertEquals(expected,
- checkPermissionReadVideo(getContext(), pid, uid, packageName, null));
+ private static void assertMediaReadPermissions(
+ int pid, int uid, String packageName, boolean targetSdkIsAtLeastT, boolean expected) {
+ assertEquals(
+ expected,
+ checkPermissionReadAudio(
+ getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT));
+ assertEquals(
+ expected,
+ checkPermissionReadImages(
+ getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT));
+ assertEquals(
+ expected,
+ checkPermissionReadVideo(
+ getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT));
}
}
diff --git a/tests/src/com/android/providers/media/util/StringUtilsTest.java b/tests/src/com/android/providers/media/util/StringUtilsTest.java
index 51a571e..c4556c4 100644
--- a/tests/src/com/android/providers/media/util/StringUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/StringUtilsTest.java
@@ -18,7 +18,9 @@
import static com.android.providers.media.util.StringUtils.equalIgnoreCase;
import static com.android.providers.media.util.StringUtils.startsWithIgnoreCase;
+import static com.android.providers.media.util.StringUtils.verifySupportedUncachedRelativePaths;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -27,6 +29,10 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
@RunWith(AndroidJUnit4.class)
public class StringUtilsTest {
@Test
@@ -52,4 +58,14 @@
assertFalse(startsWithIgnoreCase(null, "audio/"));
assertFalse(startsWithIgnoreCase(null, null));
}
+
+ @Test public void testVerifySupportedUncachedRelativePaths() throws Exception {
+ assertEquals(
+ new ArrayList<String>(Arrays.asList("path/", "path/path/")),
+ verifySupportedUncachedRelativePaths(
+ new ArrayList<String>(
+ Arrays.asList(null, "", "/",
+ "path", "/path", "path/", "/path/",
+ "path/path", "/path/path", "path/path/"))));
+ }
}
diff --git a/tests/test_app/TestAppForPermissionActivity33.xml b/tests/test_app/TestAppForPermissionActivity33.xml
new file mode 100644
index 0000000..4e8e74b
--- /dev/null
+++ b/tests/test_app/TestAppForPermissionActivity33.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.media.testapp.permissionmedia"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="33" />
+
+ <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
+ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.MANAGE_MEDIA"/>
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
+ <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
+ <uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
+
+ <application android:label="TestAppPerms33">
+ <activity android:name="com.android.providers.media.util.TestAppActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/tests/test_app/TestAppWithMediaPerms.xml b/tests/test_app/TestAppWithMediaPerms.xml
new file mode 100644
index 0000000..5671919
--- /dev/null
+++ b/tests/test_app/TestAppWithMediaPerms.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.media.testapp.withmediaperms"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="32" />
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
+ <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
+ <uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
+
+ <application android:label="TestAppWithMediaPerms">
+ <activity android:name="com.android.providers.media.util.TestAppActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/tools/dialogs/Android.bp b/tools/dialogs/Android.bp
index d0ccff2..98267be 100644
--- a/tools/dialogs/Android.bp
+++ b/tools/dialogs/Android.bp
@@ -1,10 +1,6 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
android_app {
diff --git a/tools/dialogs/AndroidManifest.xml b/tools/dialogs/AndroidManifest.xml
index 960cb13..73527cc 100644
--- a/tools/dialogs/AndroidManifest.xml
+++ b/tools/dialogs/AndroidManifest.xml
@@ -4,6 +4,9 @@
package="com.android.providers.media.tools.dialogs">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
+ <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_MEDIA"/>
diff --git a/tools/dialogs/src/com/android/providers/media/tools/dialogs/DialogsActivity.java b/tools/dialogs/src/com/android/providers/media/tools/dialogs/DialogsActivity.java
index 867bfaf..f52354d 100644
--- a/tools/dialogs/src/com/android/providers/media/tools/dialogs/DialogsActivity.java
+++ b/tools/dialogs/src/com/android/providers/media/tools/dialogs/DialogsActivity.java
@@ -17,6 +17,9 @@
package com.android.providers.media.tools.dialogs;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.Manifest.permission.READ_MEDIA_AUDIO;
+import static android.Manifest.permission.READ_MEDIA_IMAGES;
+import static android.Manifest.permission.READ_MEDIA_VIDEO;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
@@ -25,6 +28,7 @@
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.MediaStore.Files.FileColumns;
@@ -62,11 +66,23 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- if (checkSelfPermission(READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED
- || checkSelfPermission(WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
- requestPermissions(new String[] { READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE }, 42);
- finish();
- return;
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+ if (checkSelfPermission(READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED
+ || checkSelfPermission(WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
+ requestPermissions(new String[]{READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE}, 42);
+ android.util.Log.d("doc", "finish");
+ finish();
+ return;
+ }
+ } else {
+ if (checkSelfPermission(READ_MEDIA_AUDIO) != PERMISSION_GRANTED
+ || checkSelfPermission(READ_MEDIA_IMAGES) != PERMISSION_GRANTED
+ || checkSelfPermission(READ_MEDIA_VIDEO) != PERMISSION_GRANTED) {
+ requestPermissions(
+ new String[]{READ_MEDIA_AUDIO, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO}, 42);
+ finish();
+ return;
+ }
}
mBody = new LinearLayout(this);
diff --git a/tools/photopicker/Android.bp b/tools/photopicker/Android.bp
index f606c22..d05c935 100644
--- a/tools/photopicker/Android.bp
+++ b/tools/photopicker/Android.bp
@@ -1,10 +1,6 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
android_app {