Snap for 8982257 from 31af7a102d3761062419ccb9939609f26b2fd51f to mainline-conscrypt-release
Change-Id: I4e5592ac2ab51a0c14bd2bb416871522acfd4e2e
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index dfe0ce0..7df12ff 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -49,9 +49,13 @@
<permission android:name="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
android:protectionLevel="signature" />
+ <!-- We use Photo Picker app icon for this package. It is necessary for Photo Picker GET_CONTENT
+ take over. Some apps query packages that can handle GET_CONTENT and want to display the
+ icon and label of the package to the user. -->
<application
android:name="com.android.providers.media.MediaApplication"
android:label="@string/app_label"
+ android:icon="@mipmap/picker_app_icon"
android:allowBackup="false"
android:supportsRtl="true"
android:forceQueryable="true"
@@ -166,7 +170,6 @@
android:name="com.android.providers.media.photopicker.PhotoPickerActivity"
android:process=":PhotoPicker"
android:label="@string/picker_app_label"
- android:icon="@mipmap/picker_app_icon"
android:theme="@style/PickerDefaultTheme"
android:exported="true"
android:excludeFromRecents="true"
diff --git a/apex/Android.bp b/apex/Android.bp
index c735880..ccc26f5 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -40,7 +40,11 @@
sdk {
name: "mediaprovider-module-sdk",
- bootclasspath_fragments: ["com.android.mediaprovider-bootclasspath-fragment"],
+ apexes: [
+ // Adds exportable dependencies of the APEX to the sdk,
+ // e.g. *classpath_fragments.
+ "com.android.mediaprovider",
+ ],
}
// Encapsulate the contributions made by the com.android.mediaprovider to the bootclasspath.
diff --git a/apex/apex_manifest.json b/apex/apex_manifest.json
index fe2ed11..527b9f7 100644
--- a/apex/apex_manifest.json
+++ b/apex/apex_manifest.json
@@ -1,4 +1,7 @@
{
"name": "com.android.mediaprovider",
- "version": 339990000
+
+ // Placeholder module version to be replaced during build.
+ // Do not change!
+ "version": 0
}
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index ca67b8a..852535b 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -234,6 +234,8 @@
public static final String EXTRA_RESULT = "result";
/** {@hide} */
public static final String EXTRA_FILE_DESCRIPTOR = "file_descriptor";
+ /** {@hide} */
+ public static final String EXTRA_LOCAL_PROVIDER = "local_provider";
/** {@hide} */
public static final String IS_SYSTEM_GALLERY_CALL = "is_system_gallery";
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index b2b8ab3..740b562 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -496,9 +496,8 @@
}
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 || should_inval || is_package_owned_path(path, fuse->path) ||
- android::base::StartsWithIgnoreCase(path, media_path) || fuse->ShouldNotCache(path)) {
+ fuse->ShouldNotCache(path)) {
// We set dentry timeout to 0 for the following reasons:
// 1. The dentry cache was completely disabled for the entire volume.
// 2.1 Case-insensitive lookups need to invalidate other case-insensitive dentry matches
@@ -507,10 +506,7 @@
// 3. With app data isolation enabled, app A should not guess existence of app B from the
// Android/{data,obb}/<package> paths, hence we prevent the kernel from caching that
// information.
- // 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.
+ // 4. The dentry cache was completely disabled for the given path.
return 0;
}
return std::numeric_limits<double>::max();
diff --git a/res/layout-v33/safety_protection_section.xml b/res/layout-v33/safety_protection_section.xml
new file mode 100644
index 0000000..155132f
--- /dev/null
+++ b/res/layout-v33/safety_protection_section.xml
@@ -0,0 +1,34 @@
+<?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.
+ -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@android:drawable/ic_safety_protection"
+ android:contentDescription="@string/safety_protection_icon_label"/>
+
+ <TextView
+ android:id="@+id/safety_protection_display_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingStart="8dp"
+ android:paddingEnd="0dp"
+ android:paddingTop="3dp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="12sp" />
+</merge>
\ No newline at end of file
diff --git a/res/layout/activity_photo_picker.xml b/res/layout/activity_photo_picker.xml
index 1680806..fc73702 100644
--- a/res/layout/activity_photo_picker.xml
+++ b/res/layout/activity_photo_picker.xml
@@ -47,6 +47,12 @@
android:src="@drawable/ic_drag"
android:contentDescription="@null"/>
+ <com.android.providers.media.photopicker.ui.SafetyProtectionSectionView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="5dp"
+ android:layout_gravity="center" />
+
<TextView
android:id="@+id/privacy_text"
android:layout_width="wrap_content"
diff --git a/res/layout/item_cloud_video_preview.xml b/res/layout/item_cloud_video_preview.xml
deleted file mode 100644
index f8c3b6e..0000000
--- a/res/layout/item_cloud_video_preview.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<?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.
- -->
-
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <FrameLayout
- android:id="@+id/preview_player_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center">
-
- <com.google.android.exoplayer2.ui.AspectRatioFrameLayout
- android:id="@+id/preview_player_frame"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center">
-
- <SurfaceView
- android:id="@+id/preview_player_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
- </com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
-
- <FrameLayout
- android:id="@+id/preview_player_controls"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center">
-
- <include
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- layout="@layout/preview_video_controls" />
- </FrameLayout>
- </FrameLayout>
-
- <ImageView
- android:id="@+id/preview_video_image"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center"
- android:scaleType="fitCenter"
- android:contentDescription="@null" />
-</FrameLayout>
diff --git a/res/layout/item_video_preview.xml b/res/layout/item_video_preview.xml
index 1d7d32d..f8c3b6e 100644
--- a/res/layout/item_video_preview.xml
+++ b/res/layout/item_video_preview.xml
@@ -1,36 +1,55 @@
<?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.
--->
+<!--
+ ~ 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.
+ -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
-
- <com.google.android.exoplayer2.ui.StyledPlayerView
- android:id="@+id/preview_player_view"
+ <FrameLayout
+ android:id="@+id/preview_player_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:visibility="gone"
- app:use_controller="true"
- app:animation_enabled="true"
- app:show_timeout="0"
- app:auto_show="false"
- app:controller_layout_id="@layout/preview_video_controls"/>
+ android:layout_gravity="center">
+
+ <com.google.android.exoplayer2.ui.AspectRatioFrameLayout
+ android:id="@+id/preview_player_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center">
+
+ <SurfaceView
+ android:id="@+id/preview_player_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+ </com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
+
+ <FrameLayout
+ android:id="@+id/preview_player_controls"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center">
+
+ <include
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ layout="@layout/preview_video_controls" />
+ </FrameLayout>
+ </FrameLayout>
<ImageView
android:id="@+id/preview_video_image"
diff --git a/res/values/drawables.xml b/res/mipmap-anydpi/picker_app_icon.xml
similarity index 70%
rename from res/values/drawables.xml
rename to res/mipmap-anydpi/picker_app_icon.xml
index a5a0b3d..e9f989a 100644
--- a/res/values/drawables.xml
+++ b/res/mipmap-anydpi/picker_app_icon.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
@@ -13,9 +14,8 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<resources>
- <!-- Override StyledPlayerView default control buttons -->
- <drawable name="exo_styled_controls_play">@drawable/ic_preview_play</drawable>
- <drawable name="exo_styled_controls_pause">@drawable/ic_preview_pause</drawable>
-</resources>
\ No newline at end of file
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@android:color/white"/>
+ <foreground android:drawable="@mipmap/picker_app_icon_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/res/mipmap-anydpi/picker_app_icon_foreground.xml b/res/mipmap-anydpi/picker_app_icon_foreground.xml
new file mode 100644
index 0000000..a386171
--- /dev/null
+++ b/res/mipmap-anydpi/picker_app_icon_foreground.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <group android:scaleX="2.295"
+ android:scaleY="2.295"
+ android:translateX="26.46"
+ android:translateY="26.46">
+ <path
+ android:pathData="M9,14h10l-3.45,-4.5 -2.3,3 -1.55,-2ZM8,18q-0.825,0 -1.412,-0.587Q6,16.825 6,16L6,4q0,-0.825 0.588,-1.413Q7.175,2 8,2h12q0.825,0 1.413,0.587Q22,3.175 22,4v12q0,0.825 -0.587,1.413Q20.825,18 20,18ZM8,16h12L20,4L8,4v12ZM4,22q-0.825,0 -1.412,-0.587Q2,20.825 2,20L2,6h2v14h14v2ZM8,4v12L8,4Z"
+ android:fillColor="#C4C7C5"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/res/mipmap-hdpi/picker_app_icon.png b/res/mipmap-hdpi/picker_app_icon.png
deleted file mode 100644
index c911ffc..0000000
--- a/res/mipmap-hdpi/picker_app_icon.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-mdpi/picker_app_icon.png b/res/mipmap-mdpi/picker_app_icon.png
deleted file mode 100644
index 9a0f98a..0000000
--- a/res/mipmap-mdpi/picker_app_icon.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-xhdpi/picker_app_icon.png b/res/mipmap-xhdpi/picker_app_icon.png
deleted file mode 100644
index ca55edd..0000000
--- a/res/mipmap-xhdpi/picker_app_icon.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-xxhdpi/picker_app_icon.png b/res/mipmap-xxhdpi/picker_app_icon.png
deleted file mode 100644
index 531c94f..0000000
--- a/res/mipmap-xxhdpi/picker_app_icon.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-xxxhdpi/picker_app_icon.png b/res/mipmap-xxxhdpi/picker_app_icon.png
deleted file mode 100644
index ce72805..0000000
--- a/res/mipmap-xxxhdpi/picker_app_icon.png
+++ /dev/null
Binary files differ
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 8195a2f..6198f78 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"Verwerk media …"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Kanselleer"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wag"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Veiligheidbeskerming"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"Toestelspesifieke kodewisselingopletberigte"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"Toestelspesifieke kodewisselingvordering"</string>
</resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 735486f..d5301bd 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"ሚዲያን በማሰናዳት ላይ…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"ይቅር"</string>
<string name="transcode_wait" msgid="8909773149560697501">"ጠብቅ"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"የደህንነት ጥበቃ"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index f370817..c1df5aa 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"جارٍ معالجة الوسائط…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"إلغاء"</string>
<string name="transcode_wait" msgid="8909773149560697501">"الانتظار"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"حماية الأمن الشخصي"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index a034a1a..0b09c9d 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"মিডিয়াৰ প্ৰক্ৰিয়াকৰণ কৰি থকা হৈছে…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"বাতিল কৰক"</string>
<string name="transcode_wait" msgid="8909773149560697501">"অপেক্ষা কৰক"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"সুৰক্ষিত নিৰাপত্তা"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index 19832e3..8944beb 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Media emal edilir…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Ləğv edin"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Gözləyin"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Güvənlik qoruması"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index fe84f33..254344e 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Obrađuju se mediji…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Otkaži"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Sačekaj"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Sigurnosna zaštita"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index f01a7dc..b3be1da 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Ідзе апрацоўка мультымедыя…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Скасаваць"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Пачакаць"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Ахова бяспекі"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index c2d966c..09b63ea 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Мултимедията се обработва…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Отказ"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Изчакване"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Защита на безопасността"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index a89424f..350ee31 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"মিডিয়া ফাইল প্রসেস করা হচ্ছে…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"বাতিল করুন"</string>
<string name="transcode_wait" msgid="8909773149560697501">"অপেক্ষা করুন"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"নিরাপত্তার সুরক্ষা"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"নেটিভ ট্রান্সকোড অ্যালার্ট"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"নেটিভ ট্রান্সকোড প্রোগ্রেস"</string>
</resources>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index f0bbc59..2af68dd 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Obrada medijskih fajlova…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Otkaži"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Sačekaj"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Zaštita sigurnosti"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index b4f9df8..1b05b7c 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"S\'està processant el contingut multimèdia…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel·la"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Espera"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protecció de seguretat"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index a95d75a..5e4b182 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Mediální obsah se zpracovává…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Zrušit"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Počkat"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Bezpečnostní ochrana"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index b605b8e..6865a55 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Behandler medier…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Annuller"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Vent"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Beskyttelse"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 592438e..556b4ce 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Medien werden verarbeitet…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Abbrechen"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Warten"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Schutz"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 5ed5abe..37b4550 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Επεξεργασία μέσων…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Ακύρωση"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Αναμονή"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety Protection"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 31be367..ddc73c3 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"Processing media…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wait"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"Native transcode alerts"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"Native transcode progress"</string>
</resources>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 31be367..ddc73c3 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"Processing media…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wait"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"Native transcode alerts"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"Native transcode progress"</string>
</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 31be367..ddc73c3 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"Processing media…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wait"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"Native transcode alerts"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"Native transcode progress"</string>
</resources>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 31be367..ddc73c3 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"Processing media…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wait"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"Native transcode alerts"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"Native transcode progress"</string>
</resources>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index f6566e4..b68bcf3 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"Processing media…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wait"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 7dd01b9..3b775b6 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Procesando contenido multimedia…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Esperar"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protección de seguridad"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 05d275a..2099b4c 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Procesando elementos multimedia…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Espera"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protección de seguridad"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 7c89a9c..0453a28 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Meedia töötlemine …"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Tühista"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Oota"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Ohutuskaitse"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index f85e04c..78c46a2 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Multimedia-edukia prozesatzen…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Utzi"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Itxaron"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Segurtasun-babesa"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index d50fce9..802cbd3 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"درحال پردازش رسانه…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"لغو"</string>
<string name="transcode_wait" msgid="8909773149560697501">"انتظار"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"محافظت امنیتی"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 5975059..a945565 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Käsitellään mediasisältöä…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Peru"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Odota"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Turvallisuuden varmistaminen"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index d4f4dbf..529e07d 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Traitement du contenu multimédia en cours…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Annuler"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Patienter"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protection de sécurité"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 488291d..6c10bd6 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Traitement des contenus multimédias…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Annuler"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Attendre"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protection de sécurité"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 7a3965b..cd1128a 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Procesando contido multimedia…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Esperar"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protección de seguranza"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 6edca72..eba7e3b 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"મીડિયા પર પ્રક્રિયા થઈ રહી છે…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"રદ કરો"</string>
<string name="transcode_wait" msgid="8909773149560697501">"રાહ જુઓ"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"સલામતી સંરક્ષણ"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 25d7c37..bfba395 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"मीडिया को प्रोसेस किया जा रहा है…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"अभी नहीं"</string>
<string name="transcode_wait" msgid="8909773149560697501">"इंतज़ार करें"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"सुरक्षा के लिए बचाव"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index ed77a55..a8b5aee 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Obrada medijskih sadržaja…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Odustani"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Pričekaj"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Osiguranje"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 03b231e..a843ecd 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Médiatartalom feldolgozása…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Mégse"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Várakozás"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Biztonsági védelem"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 129fd1c..2a804e3 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Մեդիաֆայլը մշակվում է…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Չեղարկել"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Սպասել"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Անվտանգության պաշտպանություն"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index d7a135b..b2a023d 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Memproses media…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Batal"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Tunggu"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Perlindungan keselamatan"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 7b874de..ad4c23d 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Vinnur úr efni…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Hætta við"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Bíða"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Öryggisbúnaður"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index e136751..aec7b0b 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Elaborazione dei contenuti multimediali in corso…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Annulla"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Attendi"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protezione di sicurezza"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 2b3c0c1..7d74c20 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"המדיה בעיבוד…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"ביטול"</string>
<string name="transcode_wait" msgid="8909773149560697501">"המתנה"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"הגנה על בטיחות"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"התראות של המרת קידוד מקורית"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"התקדמות של המרת קידוד מקורית"</string>
</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 8a89764..3c1157c 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"メディアを処理しています…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"キャンセル"</string>
<string name="transcode_wait" msgid="8909773149560697501">"待機"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"安全保護"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 0ce5cb0..dd85e46 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"მედია მუშავდება…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"გაუქმება"</string>
<string name="transcode_wait" msgid="8909773149560697501">"მოცდა"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"უსაფრთხოების დაცვა"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"ტრანსკოდირების ადგილობრივი გაფრთხილება"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"ტრანსკოდირების ადგილობრივი პროგრესი"</string>
</resources>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index f9b1551..fe71d9d 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Медиафайл өңделуде…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Бас тарту"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Күту"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Қауіпсіздікті қорғау"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index cc694f1..f36c8b5 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"កំពុងដំណើរការមេឌៀ…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"បោះបង់"</string>
<string name="transcode_wait" msgid="8909773149560697501">"រង់ចាំ"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"ការការពារសុវត្ថិភាព"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 17552ad..b75795c 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"ಪ್ರಕ್ರಿಯೆಯಲ್ಲಿರುವ ಮಾಧ್ಯಮ…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"ರದ್ದುಮಾಡಿ"</string>
<string name="transcode_wait" msgid="8909773149560697501">"ನಿರೀಕ್ಷಿಸಿ"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"ಭದ್ರತಾ ರಕ್ಷಣೆ"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 5b11b4c..99d1428 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"미디어 처리 중…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"취소"</string>
<string name="transcode_wait" msgid="8909773149560697501">"대기"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"안전 보안"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 65de3c4..943f7fd 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Медиа иштетилүүдө…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Жокко чыгаруу"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Күтүү"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Коопсуздукту коргоо"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 2412919..45a0b5f 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"ກຳລັງປະມວນຜົນມີເດຍ…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"ຍົກເລີກ"</string>
<string name="transcode_wait" msgid="8909773149560697501">"ລໍຖ້າ"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"ການປ້ອງກັນຄວາມປອດໄພ"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 5e7b7d5..ff61ba7 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Medija apdorojama…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Atšaukti"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Palaukti"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Apsauga"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index a201f12..9a8764e 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Notiek multivides apstrāde…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Atcelt"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Gaidīt"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Drošības aizsardzība"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index b8491d1..2a5a069 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Се транскодира…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Откажи"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Почекајте"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Безбедносна заштита"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index 96bfc40..08ca678 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"മീഡിയ പ്രോസസ് ചെയ്യുന്നു…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"റദ്ദാക്കുക"</string>
<string name="transcode_wait" msgid="8909773149560697501">"കാത്തിരിക്കുക"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"സുരക്ഷാ പരിരക്ഷ"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"നേറ്റീവ് ട്രാൻസ്കോഡ് മുന്നറിയിപ്പുകൾ"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"നേറ്റീവ് ട്രാൻസ്കോഡ് പുരോഗതി"</string>
</resources>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index da976d1..0892e40 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"Медиаг боловсруулж байна…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Цуцлах"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Хүлээх"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Аюулгүй байдлын хамгаалалт"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"Уугуул хөрвүүлгийн сэрэмжлүүлэг"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"Уугуул хөрвүүлгийн явц"</string>
</resources>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index 474863c..7d3b8f7 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"मीडियावर प्रक्रिया सुरू आहे…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"रद्द करा"</string>
<string name="transcode_wait" msgid="8909773149560697501">"प्रतीक्षा करा"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"सुरक्षितता संरक्षण"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"मूळ ट्रान्सकोड सूचना"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"मूळ ट्रान्सकोड प्रगती"</string>
</resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index bb9f21a..c468525 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Memproses media…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Batal"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Tunggu"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Perlindungan keselamatan"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 5d33238..572e607 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"မီဒီယာကို လုပ်ဆောင်နေသည်…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"မလုပ်တော့"</string>
<string name="transcode_wait" msgid="8909773149560697501">"စောင့်ရန်"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"လုံခြုံရေး ကာကွယ်မှု"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index e73aea8..626bd68 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Mediene behandles …"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Avbryt"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Vent"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Beskyttelse"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index f1ea063..db20b5d 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"मिडिया प्रोसेस गरिँदै छ…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"रद्द गर्नुहोस्"</string>
<string name="transcode_wait" msgid="8909773149560697501">"पर्खनुहोस्"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"सेफ्टी प्रोटेक्सन"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 832d8bc..d5fa95c 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"Media verwerken…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Annuleren"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wachten"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Beveiliging"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"Meldingen voor native transcodering"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"Voortgang van native transcodering"</string>
</resources>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 05511da..cbd8834 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -118,6 +118,11 @@
<string name="transcode_processing_success" msgid="447288876429730122">"ମିଡିଆ ପ୍ରକ୍ରିୟାକରଣ ସଫଳ ହୋଇଛି"</string>
<string name="transcode_processing_started" msgid="7789086308155361523">"ମିଡିଆ ପ୍ରକ୍ରିୟାକରଣ ଆରମ୍ଭ କରାଯାଇଛି"</string>
<string name="transcode_processing" msgid="6753136468864077258">"ମିଡିଆ ପ୍ରକ୍ରିୟାକରଣ କରାଯାଉଛି…"</string>
- <string name="transcode_cancel" msgid="8555752601907598192">"ବାତିଲ୍ କରନ୍ତୁ"</string>
+ <string name="transcode_cancel" msgid="8555752601907598192">"ବାତିଲ କରନ୍ତୁ"</string>
<string name="transcode_wait" msgid="8909773149560697501">"ଅପେକ୍ଷା କରନ୍ତୁ"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"ସୁରକ୍ଷିତ ସୁରକ୍ଷା"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index b1c7c35..82e51f4 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"ਮੀਡੀਆ \'ਤੇ ਪ੍ਰਕਿਰਿਆ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"ਰੱਦ ਕਰੋ"</string>
<string name="transcode_wait" msgid="8909773149560697501">"ਉਡੀਕ ਕਰੋ"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"ਸੁਰੱਖਿਆ ਬਚਾਅ"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index e40e6d4..53774f1 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Przetwarzam multimedia…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Anuluj"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Czekaj"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Sprzęt zabezpieczający"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index f896284..1fafd2f 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Processando mídia…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Aguardar"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteção"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 4db74cf..5d3bd21 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"A processar multimédia…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Aguardar"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteção de segurança"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas de transcodificação nativa"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresso de transcodificação nativa"</string>
</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index f896284..1fafd2f 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Processando mídia…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Aguardar"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteção"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 0e5d4b5..f749308 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Se procesează conținutul media…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Anulați"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Așteptați"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protecția în caz de accidente"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 4844c64..589d08b 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Обработка медиафайла…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Отмена"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Подождать"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Защита безопасности"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index c9ffffc..89e5c46 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"මාධ්යය සකසමින්…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"අවලංගු කරන්න"</string>
<string name="transcode_wait" msgid="8909773149560697501">"රැඳී සිටින්න"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"සුරක්ෂිතතා ආරක්ෂණය"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 934f91e..e41c880 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Spracúvajú sa médiá…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Zrušiť"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Počkať"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Bezpečnosť"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index a3653ac..0376321 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"Obdelovanje predstavnosti …"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Prekliči"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Počakaj"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Varnostna zaščita"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"Opozorila o izvornem prekodiranju"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"Napredek izvornega prekodiranja"</string>
</resources>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 35f975f..4348f24 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Media po përpunohet…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Anulo"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Prit"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Mbrojtja e sigurisë"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 07e5a2a..6f99c0e 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Обрађују се медији…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Откажи"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Сачекај"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Сигурносна заштита"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 262b83a..3f01d60 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Bearbetar media …"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Avbryt"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Vänta"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Säkerhet"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 735c55a..73cbc8e 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Inachakata maudhui…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Ghairi"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Subiri"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Ulinzi wa Usalama"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 468586c..5fe91c2 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"மீடியாவைச் செயலாக்குகிறது…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"ரத்துசெய்"</string>
<string name="transcode_wait" msgid="8909773149560697501">"காத்திருங்கள்"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"பாதுகாப்பு வளையம்"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index a70fab9..1c64d71 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"మీడియాను ప్రాసెస్ చేస్తోంది…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"రద్దు చేయండి"</string>
<string name="transcode_wait" msgid="8909773149560697501">"వేచి ఉండు"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"భద్రత రక్షణ"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"స్థానిక ట్రాన్స్కోడ్ అలర్ట్లు"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"స్థానిక ట్రాన్స్కోడ్ ప్రోగ్రెస్"</string>
</resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index b666c7c..7112494 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"กำลังประมวลผลสื่อ…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"ยกเลิก"</string>
<string name="transcode_wait" msgid="8909773149560697501">"รอ"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"การปกป้องเพื่อความปลอดภัย"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index ac006cc..8820e5f 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Pinoproseso ang media…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Kanselahin"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Maghintay"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteksyon sa kaligtasan"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index b03bb29..f568d01 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Medya işleniyor…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"İptal"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Bekle"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Güvenlik koruması"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 39ff525..1ec73d1 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Обробка медіафайлів…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Скасувати"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Зачекати"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Захист"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index ae76c01..09d7810 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -120,4 +120,7 @@
<string name="transcode_processing" msgid="6753136468864077258">"میڈیا پر کارروائی ہو رہی ہے…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"منسوخ کریں"</string>
<string name="transcode_wait" msgid="8909773149560697501">"انتظار کریں"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"سیفٹی پروٹیکشن"</string>
+ <string name="transcode_alert_channel" msgid="997332371757680478">"مقامی ٹرانسکوڈ کے الرٹس"</string>
+ <string name="transcode_progress_channel" msgid="6905136787933058387">"مقامی ٹرانسکوڈ کی پیشرفت"</string>
</resources>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index d3babbb..a535ae5 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Mediaga ishlov berilmoqda…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Bekor qilish"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Kutish"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Xavfsizlik himoyasi"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 84d8836..f9da470 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Đang xử lý nội dung nghe nhìn..."</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Hủy"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Đợi"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Bảo vệ an toàn"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index b256d48..9558b5f 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"正在处理媒体内容…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"取消"</string>
<string name="transcode_wait" msgid="8909773149560697501">"等待"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"安全保护"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index cbc74c5..cad3528 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"正在處理媒體…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"取消"</string>
<string name="transcode_wait" msgid="8909773149560697501">"等待"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"安全保護"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index a1e6f78..6434772 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"正在處理媒體…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"取消"</string>
<string name="transcode_wait" msgid="8909773149560697501">"等待"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"安全防護"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 1fd0029..5f12bef 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -120,4 +120,9 @@
<string name="transcode_processing" msgid="6753136468864077258">"Icubungula imidiya…"</string>
<string name="transcode_cancel" msgid="8555752601907598192">"Khansela"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Linda"</string>
+ <string name="safety_protection_icon_label" msgid="6714354052747723623">"Ukuvikeleka kokuphepha"</string>
+ <!-- no translation found for transcode_alert_channel (997332371757680478) -->
+ <skip />
+ <!-- no translation found for transcode_progress_channel (6905136787933058387) -->
+ <skip />
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d6b8deb..1b3efa8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -406,4 +406,13 @@
<!-- Transcode intermediate ANR dialog wait button. -->
<string name="transcode_wait">Wait</string>
+
+ <!-- Safety Protection shield icon. -->
+ <string name="safety_protection_icon_label">Safety protection</string>
+
+ <!-- Transcode alert channel name. -->
+ <string name="transcode_alert_channel">Native Transcode Alerts</string>
+
+ <!-- Transcode progress channel name. -->
+ <string name="transcode_progress_channel">Native Transcode Progress</string>
</resources>
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index e4700b0..90ad9c7 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -853,17 +853,21 @@
*/
public void notifyChange(@NonNull Uri uri, int flags) {
if (LOGV) Log.v(TAG, "Notifying " + uri);
+
+ // Also sync change to the network.
+ final int notifyFlags = flags | ContentResolver.NOTIFY_SYNC_TO_NETWORK;
+
final TransactionState state = mTransactionState.get();
if (state != null) {
- ArraySet<Uri> set = state.notifyChanges.get(flags);
+ ArraySet<Uri> set = state.notifyChanges.get(notifyFlags);
if (set == null) {
set = new ArraySet<>();
- state.notifyChanges.put(flags, set);
+ state.notifyChanges.put(notifyFlags, set);
}
set.add(uri);
} else {
ForegroundThread.getExecutor().execute(() -> {
- notifySingleChangeInternal(uri, flags);
+ notifySingleChangeInternal(uri, notifyFlags);
});
}
}
@@ -1529,7 +1533,8 @@
+ "||':'||new._id||':'||new.media_type||':'||new.is_download"
+ "||':'||old.is_trashed||':'||new.is_trashed"
+ "||':'||old.is_pending||':'||new.is_pending"
- + "||':'||old.is_favorite||':'||new.is_favorite"
+ + "||':'||ifnull(old.is_favorite,0)"
+ + "||':'||ifnull(new.is_favorite,0)"
+ "||':'||ifnull(old._special_format,0)"
+ "||':'||ifnull(new._special_format,0)"
+ "||':'||ifnull(old.owner_package_name,'null')"
@@ -1877,7 +1882,7 @@
static final int VERSION_S = 1209;
// Leave some gaps in database version tagging to allow S schema changes
// to go independent of T schema changes.
- static final int VERSION_T = 1307;
+ static final int VERSION_T = 1308;
public static final int VERSION_LATEST = VERSION_T;
/**
@@ -2074,6 +2079,9 @@
// This is to ensure Animated Webp files are tagged
updateSpecialFormatToNotDetected(db);
}
+ if (fromVersion < 1308) {
+ // Empty version bump to ensure triggers are recreated
+ }
// If this is the legacy database, it's not worth recomputing data
// values locally, since they'll be recomputed after the migration
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 2db5a31..d822966 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -35,6 +35,7 @@
import static android.provider.MediaStore.MATCH_EXCLUDE;
import static android.provider.MediaStore.MATCH_INCLUDE;
import static android.provider.MediaStore.MATCH_ONLY;
+import static android.provider.MediaStore.MEDIA_IGNORE_FILENAME;
import static android.provider.MediaStore.MY_UID;
import static android.provider.MediaStore.PER_USER_RANGE;
import static android.provider.MediaStore.QUERY_ARG_DEFER_SCAN;
@@ -43,7 +44,6 @@
import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED;
import static android.provider.MediaStore.QUERY_ARG_REDACTED_URI;
import static android.provider.MediaStore.QUERY_ARG_RELATED_URI;
-import static android.provider.MediaStore.VOLUME_EXTERNAL;
import static android.provider.MediaStore.getVolumeName;
import static android.system.OsConstants.F_GETFL;
@@ -627,6 +627,7 @@
if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
mUserCache.invalidateWorkProfileOwnerApps(pkg);
mPickerSyncController.notifyPackageRemoval(pkg);
+ invalidateDentryForExternalStorage(pkg);
}
} else {
Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction());
@@ -636,6 +637,18 @@
}
};
+ private void invalidateDentryForExternalStorage(String packageName) {
+ for (MediaVolume vol : mVolumeCache.getExternalVolumes()) {
+ try {
+ invalidateFuseDentry(String.format(Locale.ROOT,
+ "%s/Android/media/%s/", getVolumePath(vol.getName()).getAbsolutePath(),
+ packageName));
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "External volume path not found for " + vol.getName(), e);
+ }
+ }
+ }
+
private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -740,7 +753,7 @@
insertedRow.getId(), insertedRow.getMediaType(), insertedRow.isDownload());
updateNextRowIdXattr(helper, insertedRow.getId());
helper.postBackground(() -> {
- if (helper.isExternal()) {
+ if (helper.isExternal() && !isFuseThread()) {
// Update the quota type on the filesystem
Uri fileUri = MediaStore.Files.getContentUri(insertedRow.getVolumeName(),
insertedRow.getId());
@@ -1312,7 +1325,8 @@
// Cleaning media files for users that have been removed
cleanMediaFilesForRemovedUser(signal);
- // Populate _SPECIAL_FORMAT column for files which have column value as NULL
+ // Calculate standard_mime_type_extension column for files which have SPECIAL_FORMAT column
+ // value as NULL, and update the same in the picker db
detectSpecialFormat(signal);
final long durationMillis = (SystemClock.elapsedRealtime() - startTime);
@@ -1426,9 +1440,14 @@
private void updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal) {
// This is to ensure we only do a bounded iteration over the rows as updates can fail, and
// we don't want to keep running the query/update indefinitely.
- final int totalRowsToUpdate = getPendingSpecialFormatRowsCount(db,signal);
- for (int i = 0 ; i < totalRowsToUpdate ; i += IDLE_MAINTENANCE_ROWS_LIMIT) {
- updateSpecialFormatForLimitedRows(db, signal);
+ final int totalRowsToUpdate = getPendingSpecialFormatRowsCount(db, signal);
+ for (int i = 0; i < totalRowsToUpdate; i += IDLE_MAINTENANCE_ROWS_LIMIT) {
+ try (PickerDbFacade.UpdateMediaOperation operation =
+ mPickerDbFacade.beginUpdateMediaOperation(
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)) {
+ updateSpecialFormatForLimitedRows(db, signal, operation);
+ operation.setSuccess();
+ }
}
}
@@ -1442,14 +1461,12 @@
}
}
- private void updateSpecialFormatForLimitedRows(SQLiteDatabase db,
- @NonNull CancellationSignal signal) {
- final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE, FILES,
- Files.getContentUri(VOLUME_EXTERNAL), Bundle.EMPTY, null);
+ private void updateSpecialFormatForLimitedRows(SQLiteDatabase externalDb,
+ @NonNull CancellationSignal signal, PickerDbFacade.UpdateMediaOperation operation) {
// Accumulate all the new SPECIAL_FORMAT updates with their ids
ArrayMap<Long, Integer> newSpecialFormatValues = new ArrayMap<>();
final String limit = String.valueOf(IDLE_MAINTENANCE_ROWS_LIMIT);
- try (Cursor c = queryForPendingSpecialFormatColumns(db, limit, signal)) {
+ try (Cursor c = queryForPendingSpecialFormatColumns(externalDb, limit, signal)) {
while (c.moveToNext() && !signal.isCanceled()) {
final long id = c.getLong(0);
final String path = c.getString(1);
@@ -1457,25 +1474,35 @@
}
}
- // Now, update all the new SPECIAL_FORMAT values.
- final ContentValues values = new ContentValues();
+ // Now, update all the new SPECIAL_FORMAT values in both external db and picker db.
+ final ContentValues pickerDbValues = new ContentValues();
+ final ContentValues externalDbValues = new ContentValues();
int count = 0;
- for (long id: newSpecialFormatValues.keySet()) {
+ for (long id : newSpecialFormatValues.keySet()) {
if (signal.isCanceled()) {
return;
}
- values.clear();
- values.put(_SPECIAL_FORMAT, newSpecialFormatValues.get(id));
- final String selection = MediaColumns._ID + "=?";
- final String[] selectionArgs = new String[]{String.valueOf(id)};
- if (qbForUpdate.update(db, values, selection, selectionArgs) == 1) {
+ int specialFormat = newSpecialFormatValues.get(id);
+
+ pickerDbValues.clear();
+ pickerDbValues.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION, specialFormat);
+ boolean pickerDbWriteSuccess = operation.execute(String.valueOf(id), pickerDbValues);
+
+ externalDbValues.clear();
+ externalDbValues.put(_SPECIAL_FORMAT, specialFormat);
+ final String externalDbSelection = MediaColumns._ID + "=?";
+ final String[] externalDbSelectionArgs = new String[]{String.valueOf(id)};
+ boolean externalDbWriteSuccess =
+ externalDb.update("files", externalDbValues, externalDbSelection,
+ externalDbSelectionArgs)
+ == 1;
+
+ if (pickerDbWriteSuccess && externalDbWriteSuccess) {
count++;
- } else {
- Log.e(TAG, "Unable to update _SPECIAL_FORMAT for id = " + id);
}
}
- Log.d(TAG, "Updated _SPECIAL_FORMAT for " + count + " items");
+ Log.d(TAG, "Updated standard_mime_type_extension for " + count + " items");
}
private int getSpecialFormatValue(String path) {
@@ -1975,6 +2002,8 @@
}
}
+ /** TODO(b/242153950) :Add negative tests for permission check of file lookup of synthetic
+ * paths. */
private FileLookupResult handlePickerFileLookup(int userId, int uid, @NonNull String path) {
final File file = new File(path);
final List<String> syntheticRelativePathSegments =
@@ -2019,7 +2048,8 @@
// .../picker/<user-id>/<authority>/media/<media-id.extension>
final String fileUserId = syntheticRelativePathSegments.get(1);
final String authority = syntheticRelativePathSegments.get(2);
- result = preparePickerMediaIdPathSegment(file, authority, lastSegment, fileUserId);
+ result = preparePickerMediaIdPathSegment(file, authority, lastSegment, fileUserId,
+ uid);
break;
}
@@ -2092,14 +2122,13 @@
}
private boolean preparePickerMediaIdPathSegment(File file, String authority, String fileName,
- String userId) {
+ String userId, int uid) {
final String mediaId = extractFileName(fileName);
final String[] projection = new String[] { MediaStore.PickerMediaColumns.SIZE };
final Uri uri = Uri.parse("content://media/picker/" + userId + "/" + authority + "/media/"
+ mediaId);
- try (Cursor cursor = mPickerUriResolver.query(uri, projection, /* callingUid */0,
- android.os.Process.myUid())) {
+ try (Cursor cursor = mPickerUriResolver.query(uri, projection, /* callingPid */0, uid)) {
if (cursor != null && cursor.moveToFirst()) {
final int sizeBytesIdx = cursor.getColumnIndex(MediaStore.PickerMediaColumns.SIZE);
@@ -2647,9 +2676,18 @@
boolean allowHidden = isCallingPackageAllowedHidden();
final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE,
matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null);
+
+ // uriOldPath may use Files uri which doesn't allow modifying AudioColumns. Include
+ // AudioColumns projection map if we are modifying any audio columns while renaming
+ // database rows.
+ if (values.containsKey(AudioColumns.IS_RINGTONE)) {
+ qbForUpdate.setProjectionMap(getProjectionMap(AudioColumns.class, FileColumns.class));
+ }
+
if (values.containsKey(FileColumns._MODIFIER)) {
qbForUpdate.allowColumn(FileColumns._MODIFIER);
}
+
final String selection = MediaColumns.DATA + " =? ";
int count = 0;
boolean retryUpdateWithReplace = false;
@@ -2722,12 +2760,12 @@
values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE);
}
- final boolean allowHidden = isCallingPackageAllowedHidden();
- if (!newMimeType.equalsIgnoreCase("null") &&
- matchUri(getContentUriForFile(path, newMimeType), allowHidden) == AUDIO_MEDIA) {
+ if (MimeUtils.isAudioMimeType(newMimeType) && !values.containsKey(FileColumns._MODIFIER)) {
computeAudioLocalizedValues(values);
computeAudioKeyValues(values);
+ FileUtils.computeAudioTypeValuesFromData(path, values::put);
}
+
FileUtils.computeValuesFromData(values, isFuseThread());
return values;
}
@@ -3128,27 +3166,31 @@
return OsConstants.EPERM;
}
- // TODO(b/177049768): We shouldn't use getExternalStorageDirectory for these checks.
- final File directoryAndroid = new File(Environment.getExternalStorageDirectory(),
- DIRECTORY_ANDROID_LOWER_CASE);
+ final File directoryAndroid = new File(
+ extractVolumePath(oldPath).toLowerCase(Locale.ROOT),
+ DIRECTORY_ANDROID_LOWER_CASE
+ );
final File directoryAndroidMedia = new File(directoryAndroid, DIRECTORY_MEDIA);
+ String newPathLowerCase = newPath.toLowerCase(Locale.ROOT);
if (directoryAndroidMedia.getAbsolutePath().equalsIgnoreCase(oldPath)) {
// Don't allow renaming 'Android/media' directory.
// Android/[data|obb] are bind mounted and these paths don't go through FUSE.
Log.e(TAG, errorMessage + oldPath + " is a default folder in app external "
+ "directory. Renaming a default folder is not allowed.");
return OsConstants.EPERM;
- } else if (FileUtils.contains(directoryAndroid, new File(newPath))) {
- if (newRelativePath.length == 1) {
- // New path is Android/*. Path is directly under Android. Don't allow moving
- // files and directories to Android/.
+ } else if (FileUtils.contains(directoryAndroid, new File(newPathLowerCase))) {
+ if (newRelativePath.length <= 2) {
+ // Path is directly under Android, Android/media, Android/data, Android/obb or
+ // some other directory under Android. Don't allow moving files and directories
+ // in these paths. Files and directories are only allowed to move to path
+ // Android/media/<app_specific_directory>/*
Log.e(TAG, errorMessage + newPath + " is in app external directory. "
+ "Renaming a file/directory to app external directory is not "
+ "allowed.");
return OsConstants.EPERM;
- } else if(!FileUtils.contains(directoryAndroidMedia, new File(newPath))) {
- // New path is Android/*/*. Don't allow moving of files or directories
- // to app external directory other than media directory.
+ } else if (!FileUtils.contains(directoryAndroidMedia, new File(newPathLowerCase))) {
+ // New path is not in Android/media/*. Don't allow moving of files or
+ // directories to app external directory other than media directory.
Log.e(TAG, errorMessage + newPath + " is not in external media directory."
+ "File/directory can only be renamed to a path in external media "
+ "directory. Renaming file/directory to path in other external "
@@ -3971,8 +4013,8 @@
// gallery is not allowed to create non-default top level directory.
final boolean createNonDefaultTopLevelDir = primary != null &&
!FileUtils.buildPath(volumePath, primary).exists();
- validPath = !createNonDefaultTopLevelDir && canAccessMediaFile(
- res.getAbsolutePath(), /*excludeNonSystemGallery*/ true);
+ validPath = !createNonDefaultTopLevelDir && canSystemGalleryAccessTheFile(
+ res.getAbsolutePath());
}
// Nothing left to check; caller can't use this path
@@ -6676,6 +6718,7 @@
final File[] files = thumbDir.listFiles();
for (File thumbFile : (files != null) ? files : new File[0]) {
if (Objects.equals(thumbFile.getName(), FILE_DATABASE_UUID)) continue;
+ if (Objects.equals(thumbFile.getName(), MEDIA_IGNORE_FILENAME)) continue;
final String name = FileUtils.extractFileName(thumbFile.getName());
try {
final long id = Long.parseLong(name);
@@ -7362,14 +7405,14 @@
helper.postBackground(() -> {
scanFileAsMediaProvider(file, REASON_DEMAND);
if (notifyTranscodeHelper) {
- notifyTranscodeHelperOnUriPublished(updatedUri);
+ notifyTranscodeHelperOnUriPublished(updatedUri, file);
}
});
} else {
helper.postBlocking(() -> {
scanFileAsMediaProvider(file, REASON_DEMAND);
if (notifyTranscodeHelper) {
- notifyTranscodeHelperOnUriPublished(updatedUri);
+ notifyTranscodeHelperOnUriPublished(updatedUri, file);
}
});
}
@@ -7397,9 +7440,8 @@
}
// 2. Check if the calling package is a special app which has global access
- if (isCallingPackageManager() ||
- (canAccessMediaFile(srcPath, /* excludeNonSystemGallery */ true) &&
- (canAccessMediaFile(destPath, /* excludeNonSystemGallery */ true)))) {
+ if (isCallingPackageManager() || (canSystemGalleryAccessTheFile(srcPath) &&
+ (canSystemGalleryAccessTheFile(destPath)))) {
return true;
}
@@ -7424,7 +7466,11 @@
return isSrcUpdateAllowed && isDestUpdateAllowed;
}
- private void notifyTranscodeHelperOnUriPublished(Uri uri) {
+ private void notifyTranscodeHelperOnUriPublished(Uri uri, File file) {
+ if (!mTranscodeHelper.supportsTranscode(file.getPath())) {
+ return;
+ }
+
BackgroundThread.getExecutor().execute(() -> {
final LocalCallingIdentity token = clearLocalCallingIdentity();
try {
@@ -7437,6 +7483,10 @@
private void notifyTranscodeHelperOnFileOpen(String path, String ioPath, int uid,
int transformsReason) {
+ if (!mTranscodeHelper.supportsTranscode(path)) {
+ return;
+ }
+
BackgroundThread.getExecutor().execute(() -> {
final LocalCallingIdentity token = clearLocalCallingIdentity();
try {
@@ -7818,8 +7868,6 @@
}
private boolean isPickerUri(Uri uri) {
- // TODO(b/188394433): move this method to PickerResolver in the spirit of not
- // adding picker logic to MediaProvider
final int match = matchUri(uri, /* allowHidden */ isCallingPackageAllowedHidden());
return match == PICKER_ID;
}
@@ -8299,10 +8347,9 @@
requireOwnershipForItem(ownerPackageName, uri);
}
- final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName);
// Figure out if we need to redact contents
- final boolean redactionNeeded =
- (redactedUri != null) || (!callerIsOwner && isRedactionNeeded(uri));
+ final boolean redactionNeeded = isRedactionNeededForOpenViaContentResolver(redactedUri,
+ ownerPackageName, file);
final RedactionInfo redactionInfo;
try {
redactionInfo = redactionNeeded ? getRedactionRanges(file)
@@ -8420,6 +8467,27 @@
}
}
+ private boolean isRedactionNeededForOpenViaContentResolver(Uri redactedUri,
+ String ownerPackageName, File file) {
+ // Redacted Uris should always redact information
+ if (redactedUri != null) {
+ return true;
+ }
+
+ final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName);
+ if (callerIsOwner) {
+ return false;
+ }
+
+ // To be consistent with FUSE redaction checks we allow similar access for File Manager
+ // and System Gallery apps.
+ if (isCallingPackageManager() || canSystemGalleryAccessTheFile(file.getPath())) {
+ return false;
+ }
+
+ return isRedactionNeeded();
+ }
+
private void deleteAndInvalidate(@NonNull Path path) {
deleteAndInvalidate(path.toFile());
}
@@ -8431,7 +8499,7 @@
private void deleteIfAllowed(Uri uri, Bundle extras, String path) {
try {
- final File file = new File(path);
+ final File file = new File(path).getCanonicalFile();
checkAccess(uri, extras, file, true);
deleteAndInvalidate(file);
} catch (Exception e) {
@@ -8478,9 +8546,6 @@
// We bypass db operations for legacy system galleries with W_E_S (see b/167307393).
// Tracking a longer term solution in b/168784136.
return true;
- } else if (isCallingPackageRequestingLegacy()) {
- // If requesting legacy, app should have W_E_S along with SystemGallery appops.
- return false;
} else if (!SdkLevel.isAtLeastS()) {
// We don't parse manifest flags for SdkLevel<=R yet. Hence, we don't bypass
// database updates for SystemGallery targeting R or above on R OS.
@@ -8497,25 +8562,23 @@
return MimeUtils.resolveMediaType(mimeType);
}
- private boolean canAccessMediaFile(String filePath, boolean excludeNonSystemGallery) {
- if (excludeNonSystemGallery && !isCallingPackageSystemGallery()) {
+ private boolean canSystemGalleryAccessTheFile(String filePath) {
+
+ if (!isCallingPackageSystemGallery()) {
return false;
}
- switch (getFileMediaType(filePath)) {
- case FileColumns.MEDIA_TYPE_IMAGE:
- return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES);
- case FileColumns.MEDIA_TYPE_VIDEO:
- return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO);
- default:
- return false;
- }
+
+ final int mediaType = getFileMediaType(filePath);
+
+ return mediaType == FileColumns.MEDIA_TYPE_IMAGE ||
+ mediaType == FileColumns.MEDIA_TYPE_VIDEO;
}
/**
* Returns true if:
* <ul>
* <li>the calling identity is an app targeting Q or older versions AND is requesting legacy
- * storage
+ * storage and has the corresponding legacy access (read/write) permissions
* <li>the calling identity holds {@code MANAGE_EXTERNAL_STORAGE}
* <li>the calling identity owns or has access to the filePath (eg /Android/data/com.foo)
* <li>the calling identity has permission to write images and the given file is an image file
@@ -8540,7 +8603,7 @@
// Apps with write access to images and/or videos can bypass our restrictions if all of the
// the files they're accessing are of the compatible media type.
- if (canAccessMediaFile(filePath, /*excludeNonSystemGallery*/ false)) {
+ if (canSystemGalleryAccessTheFile(filePath)) {
return true;
}
@@ -10616,7 +10679,13 @@
}
private boolean isCallingPackageSystemGallery() {
- return mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM_GALLERY);
+ if (mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM_GALLERY)) {
+ if (isCallingPackageRequestingLegacy()) {
+ return isCallingPackageLegacyWrite();
+ }
+ return true;
+ }
+ return false;
}
private int getCallingUidOrSelf() {
@@ -10686,6 +10755,37 @@
mTranscodeHelper.dump(writer);
writer.println();
+ dumpNoMedia(writer);
+ writer.println();
+
Logging.dumpPersistent(writer);
}
+
+ private void dumpNoMedia(PrintWriter writer) {
+ final DatabaseHelper helper;
+ try {
+ helper = getDatabaseForUri(MediaStore.Files.EXTERNAL_CONTENT_URI);
+ } catch (VolumeNotFoundException e) {
+ Log.w(TAG, "Volume not found", e);
+ return;
+ }
+
+ writer.println(MediaStore.VOLUME_EXTERNAL + " nomedia files:");
+ final int noMediaDumpFrequency = 100;
+
+ try (Cursor cursor = helper.runWithoutTransaction(
+ db -> db.query("files", new String[]{FileColumns.DATA},
+ FileColumns.DATA + " LIKE '%.nomedia'", null, null, null, null))) {
+ final int dataColumnIndex = cursor.getColumnIndex(FileColumns.DATA);
+ final StringBuilder nomediaPaths = new StringBuilder();
+ while (cursor.moveToNext()) {
+ nomediaPaths.append(cursor.getString(dataColumnIndex)).append("\n");
+ if (cursor.getPosition() % noMediaDumpFrequency == 0) {
+ writer.print(nomediaPaths);
+ nomediaPaths.setLength(0);
+ }
+ }
+ writer.println(nomediaPaths);
+ }
+ }
}
diff --git a/src/com/android/providers/media/TranscodeHelperImpl.java b/src/com/android/providers/media/TranscodeHelperImpl.java
index 09e132f..8b7ae40 100644
--- a/src/com/android/providers/media/TranscodeHelperImpl.java
+++ b/src/com/android/providers/media/TranscodeHelperImpl.java
@@ -57,9 +57,9 @@
import android.media.MediaFeature;
import android.media.MediaFormat;
import android.media.MediaTranscodingManager;
-import android.media.MediaTranscodingManager.VideoTranscodingRequest;
import android.media.MediaTranscodingManager.TranscodingRequest.VideoFormatResolver;
import android.media.MediaTranscodingManager.TranscodingSession;
+import android.media.MediaTranscodingManager.VideoTranscodingRequest;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -101,7 +101,6 @@
import java.io.BufferedReader;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -116,8 +115,8 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.Set;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -1074,7 +1073,7 @@
}
public void onFileOpen(String path, String ioPath, int uid, int transformsReason) {
- if (!isTranscodeEnabled()) {
+ if (!isTranscodeEnabled() || !supportsTranscode(path)) {
return;
}
@@ -1090,43 +1089,41 @@
};
try (Cursor c = queryFileForTranscode(path, resolverInfoProjection)) {
- if (c != null && c.moveToNext()) {
- if (supportsTranscode(path)
- && isModernFormat(c.getString(0), c.getInt(6), c.getInt(7))) {
- if (transformsReason == 0) {
- MediaProviderStatsLog.write(
- TRANSCODING_DATA,
- getMetricsSafeNameForUid(uid) /* owner_package_name */,
- MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_TYPE__READ_DIRECT,
- c.getLong(1) /* file size */,
- TRANSCODING_DATA__TRANSCODE_RESULT__UNDEFINED,
- -1 /* transcoding_duration */,
- c.getLong(2) /* video_duration */,
- c.getLong(3) /* capture_framerate */,
- -1 /* transcode_reason */,
- c.getLong(4) /* width */,
- c.getLong(5) /* height */,
- false /*hit_anr*/,
- TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN,
- TranscodingSession.ERROR_NONE);
- } else if (isTranscodeFileCached(path, ioPath)) {
- MediaProviderStatsLog.write(
- TRANSCODING_DATA,
- getMetricsSafeNameForUid(uid) /* owner_package_name */,
- MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_TYPE__READ_CACHE,
- c.getLong(1) /* file size */,
- TRANSCODING_DATA__TRANSCODE_RESULT__UNDEFINED,
- -1 /* transcoding_duration */,
- c.getLong(2) /* video_duration */,
- c.getLong(3) /* capture_framerate */,
- transformsReason /* transcode_reason */,
- c.getLong(4) /* width */,
- c.getLong(5) /* height */,
- false /*hit_anr*/,
- TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN,
- TranscodingSession.ERROR_NONE);
- } // else if file is not in cache, we'll log at read(2) when we transcode
- }
+ if (c != null && c.moveToNext()
+ && isModernFormat(c.getString(0), c.getInt(6), c.getInt(7))) {
+ if (transformsReason == 0) {
+ MediaProviderStatsLog.write(
+ TRANSCODING_DATA,
+ getMetricsSafeNameForUid(uid) /* owner_package_name */,
+ MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_TYPE__READ_DIRECT,
+ c.getLong(1) /* file size */,
+ TRANSCODING_DATA__TRANSCODE_RESULT__UNDEFINED,
+ -1 /* transcoding_duration */,
+ c.getLong(2) /* video_duration */,
+ c.getLong(3) /* capture_framerate */,
+ -1 /* transcode_reason */,
+ c.getLong(4) /* width */,
+ c.getLong(5) /* height */,
+ false /*hit_anr*/,
+ TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN,
+ TranscodingSession.ERROR_NONE);
+ } else if (isTranscodeFileCached(path, ioPath)) {
+ MediaProviderStatsLog.write(
+ TRANSCODING_DATA,
+ getMetricsSafeNameForUid(uid) /* owner_package_name */,
+ MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_TYPE__READ_CACHE,
+ c.getLong(1) /* file size */,
+ TRANSCODING_DATA__TRANSCODE_RESULT__UNDEFINED,
+ -1 /* transcoding_duration */,
+ c.getLong(2) /* video_duration */,
+ c.getLong(3) /* capture_framerate */,
+ transformsReason /* transcode_reason */,
+ c.getLong(4) /* width */,
+ c.getLong(5) /* height */,
+ false /*hit_anr*/,
+ TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN,
+ TranscodingSession.ERROR_NONE);
+ } // else if file is not in cache, we'll log at read(2) when we transcode
}
} catch (IllegalStateException e) {
Log.w(TAG, "Unable to log metrics on file open", e);
@@ -1657,7 +1654,8 @@
errorCode = mErrorCode;
}
- return String.format("<%s. Src: %s. Dst: %s. BlockedUids: %s. DurationMs: %sms"
+ return String.format(Locale.ROOT,
+ "<%s. Src: %s. Dst: %s. BlockedUids: %s. DurationMs: %sms"
+ ". Start: %s. Finish: %sms. HasAnr: %b. FailureReason: %d. ErrorCode: %d>",
session.toString(), mSrcPath, mDstPath, session.getClientUids(), durationMs,
startTime, finishTime, hasAnr, failureReason, errorCode);
@@ -1669,10 +1667,8 @@
private static final int ALERT_DISMISS_DELAY_MS = 1000;
private static final int SHOW_PROGRESS_THRESHOLD_TIME_MS = 1000;
private static final String TRANSCODE_ALERT_CHANNEL_ID = "native_transcode_alert_channel";
- private static final String TRANSCODE_ALERT_CHANNEL_NAME = "Native Transcode Alerts";
private static final String TRANSCODE_PROGRESS_CHANNEL_ID =
"native_transcode_progress_channel";
- private static final String TRANSCODE_PROGRESS_CHANNEL_NAME = "Native Transcode Progress";
// Related to notification settings
private static final String TRANSCODE_NOTIFICATION_SYS_PROP_KEY =
@@ -1794,7 +1790,8 @@
private static void createAlertNotificationChannel(Context context) {
NotificationChannel channel = new NotificationChannel(TRANSCODE_ALERT_CHANNEL_ID,
- TRANSCODE_ALERT_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
+ getString(context, R.string.transcode_alert_channel),
+ NotificationManager.IMPORTANCE_HIGH);
NotificationManager notificationManager = context.getSystemService(
NotificationManager.class);
notificationManager.createNotificationChannel(channel);
@@ -1802,7 +1799,8 @@
private static void createProgressNotificationChannel(Context context) {
NotificationChannel channel = new NotificationChannel(TRANSCODE_PROGRESS_CHANNEL_ID,
- TRANSCODE_PROGRESS_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
+ getString(context, R.string.transcode_progress_channel),
+ NotificationManager.IMPORTANCE_LOW);
NotificationManager notificationManager = context.getSystemService(
NotificationManager.class);
notificationManager.createNotificationChannel(channel);
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
index a4e121c..724e474 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
@@ -35,6 +35,7 @@
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.UserHandle;
@@ -103,6 +104,7 @@
private int mToolbarHeight = 0;
private boolean mIsAccessibilityEnabled;
+ private boolean mShouldLogCancelledResult = true;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -139,8 +141,8 @@
try {
mPickerViewModel.parseValuesFromIntent(intent);
} catch (IllegalArgumentException e) {
- Log.e(TAG, "Finished activity due to an exception while parsing extras", e);
- setCancelledResultAndFinishSelf();
+ Log.e(TAG, "Finish activity due to an exception while parsing extras", e);
+ finishWithoutLoggingCancelledResult();
}
mDragBar = findViewById(R.id.drag_bar);
@@ -254,8 +256,8 @@
// GET_CONTENT for all (media and non-media) files opens DocumentsUi, but it still shows
// "Photo Picker app option. When the user clicks on "Photo Picker", the same intent which
// includes filters to show non-media files as well is forwarded to PhotoPicker.
- // Make sure Photo Picker is opened when the intent is explicitly forwarded.
- if (isIntentForwarded(intent)) {
+ // Make sure Photo Picker is opened when the intent is explicitly forwarded by documentsUi
+ if (isIntentReferredByDocumentsUi(getReferrer())) {
Log.i(TAG, "Open PhotoPicker when a forwarded ACTION_GET_CONTENT intent is received");
return;
}
@@ -265,8 +267,11 @@
}
}
- private static boolean isIntentForwarded(Intent intent) {
- return (intent.getFlags() & Intent.FLAG_ACTIVITY_FORWARD_RESULT) > 0;
+ private boolean isIntentReferredByDocumentsUi(Uri referrerAppUri) {
+ ComponentName documentsUiComponentName = getDocumentsUiComponentName(this);
+ String documentsUiPackageName = documentsUiComponentName != null
+ ? documentsUiComponentName.getPackageName() : null;
+ return referrerAppUri != null && referrerAppUri.getHost().equals(documentsUiPackageName);
}
private void launchDocumentsUiAndFinishPicker() {
@@ -274,7 +279,12 @@
startActivityAsUser(getDocumentsUiForwardingIntent(this, getIntent()),
UserId.CURRENT_USER.getUserHandle());
- finish();
+ // RESULT_CANCELLED is not returned to the calling app as the DocumentsUi result will be
+ // returned. We don't have to log as this flow can be called in 2 cases:
+ // 1. GET_CONTENT had non-media filters, so the user or the app should be unaffected as they
+ // see that DocumentsUi was opened directly.
+ // 2. User clicked on "Browse.." button, in that case we already log that event separately.
+ finishWithoutLoggingCancelledResult();
}
@VisibleForTesting
@@ -422,12 +432,37 @@
public void setResultAndFinishSelf() {
setResult(Activity.RESULT_OK, getPickerResponseIntent(mSelection.canSelectMultiple(),
mSelection.getSelectedItems()));
+
+ logPickerSelectionConfirmed(mSelection.getSelectedItems().size());
+ finishWithoutLoggingCancelledResult();
+ }
+
+ /**
+ * This should be called if:
+ * * We are finishing Picker explicitly before the user has seen PhotoPicker UI due to known
+ * checks/workflow.
+ * * We are not returning {@link Activity#RESULT_CANCELED}
+ */
+ private void finishWithoutLoggingCancelledResult() {
+ mShouldLogCancelledResult = false;
finish();
}
- private void setCancelledResultAndFinishSelf() {
- setResult(Activity.RESULT_CANCELED);
- finish();
+ @Override
+ public void finish() {
+ if (mShouldLogCancelledResult) {
+ logPickerCancelled();
+ }
+ super.finish();
+ }
+
+ private void logPickerSelectionConfirmed(int countOfItemsConfirmed) {
+ mPickerViewModel.logPickerConfirm(Binder.getCallingUid(), getCallingPackage(),
+ countOfItemsConfirmed);
+ }
+
+ private void logPickerCancelled() {
+ mPickerViewModel.logPickerCancel(Binder.getCallingUid(), getCallingPackage());
}
/**
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
index 28a21f5..79f7326 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
@@ -72,7 +72,7 @@
CloudProviderQueryExtras.fromCloudMediaBundle(extras);
return mDbFacade.queryMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
- queryExtras.getMimeType());
+ queryExtras.getMimeTypes());
}
@Override
@@ -88,7 +88,7 @@
final CloudProviderQueryExtras queryExtras =
CloudProviderQueryExtras.fromCloudMediaBundle(extras);
- return mDbFacade.queryAlbums(queryExtras.getMimeType());
+ return mDbFacade.queryAlbums(queryExtras.getMimeTypes());
}
@Override
diff --git a/src/com/android/providers/media/photopicker/PickerDataLayer.java b/src/com/android/providers/media/photopicker/PickerDataLayer.java
index 671d5c1..217a799 100644
--- a/src/com/android/providers/media/photopicker/PickerDataLayer.java
+++ b/src/com/android/providers/media/photopicker/PickerDataLayer.java
@@ -109,6 +109,7 @@
final List<Cursor> cursors = new ArrayList<>();
final Bundle cursorExtra = new Bundle();
cursorExtra.putString(MediaStore.EXTRA_CLOUD_PROVIDER, cloudProvider);
+ cursorExtra.putString(MediaStore.EXTRA_LOCAL_PROVIDER, mLocalProvider);
// Favorites and Videos are merged albums.
final Cursor mergedAlbums = mDbFacade.getMergedAlbums(queryExtras.toQueryFilter());
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index d0fb856..e14cdc9 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -55,6 +55,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.R;
import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
import com.android.providers.media.util.ForegroundThread;
import com.android.providers.media.util.StringUtils;
@@ -77,8 +78,10 @@
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_PENDING_NOTIFICATTION =
+ private static final String PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION =
"cloud_provider_pending_notification";
+ private static final String PREFS_KEY_IS_USER_CLOUD_MEDIA_AWARE =
+ "user_aware_about_cloud_media_app_settings";
private static final String PREFS_KEY_CLOUD_PREFIX = "cloud_provider:";
private static final String PREFS_KEY_LOCAL_PREFIX = "local_provider:";
@@ -111,6 +114,8 @@
private final Runnable mSyncAllMediaCallback;
private final Set<String> mAllowedCloudProviders;
+ private final PhotoPickerUiEventLogger mLogger;
+
@GuardedBy("mLock")
private CloudProviderInfo mCloudProviderInfo;
@@ -125,13 +130,15 @@
mLocalProvider = localProvider;
mSyncDelayMs = syncDelayMs;
mSyncAllMediaCallback = this::syncAllMedia;
+ mLogger = new PhotoPickerUiEventLogger();
final String cachedAuthority = mUserPrefs.getString(
PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, null);
mAllowedCloudProviders = parseAllowedCloudProviders(allowedCloudProviders);
- final CloudProviderInfo defaultInfo = getDefaultCloudProviderInfo(cachedAuthority);
+ final CloudProviderInfo defaultInfo = getDefaultCloudProviderInfo(cachedAuthority,
+ isUserAwareAboutCloudMediaAppSettings());
if (Objects.equals(defaultInfo.authority, cachedAuthority)) {
// Just set it without persisting since it's not changing and persisting would
@@ -268,6 +275,9 @@
// reset on the facade
mDbFacade.setCloudProvider(null);
+ // TODO(b/242897322): Log from PickerViewModel using its InstanceId when relevant
+ mLogger.logPickerCloudProviderChanged(newProviderInfo.uid,
+ newProviderInfo.packageName);
Log.i(TAG, "Cloud provider changed successfully. Old: "
+ oldAuthority + ". New: " + newProviderInfo.authority);
}
@@ -386,7 +396,7 @@
}
final boolean hasPendingNotification = mUserPrefs.getBoolean(
- PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false);
+ PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION, /* defaultValue */ false);
if (!hasPendingNotification || (packageName == null)) {
Log.d(TAG, "No pending UI notification");
@@ -413,8 +423,30 @@
});
// Clear the notification
+ updateBooleanUserPref(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION, false);
+ }
+
+ /**
+ * Notifies about cloud media app banner displayed in picker UI
+ */
+ @VisibleForTesting
+ void notifyUserCloudMediaAware() {
+ updateBooleanUserPref(PREFS_KEY_IS_USER_CLOUD_MEDIA_AWARE, true);
+ }
+
+ private void updateBooleanUserPref(String key, boolean value) {
final SharedPreferences.Editor editor = mUserPrefs.edit();
- editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false);
+ editor.putBoolean(key, value);
+ editor.apply();
+ }
+
+ /**
+ * Clears the flag - user aware about cloud media app settings
+ */
+ @VisibleForTesting
+ void clearUserAwareAboutCloudMediaAppSettingsFlag() {
+ final SharedPreferences.Editor editor = mUserPrefs.edit();
+ editor.remove(PREFS_KEY_IS_USER_CLOUD_MEDIA_AWARE);
editor.apply();
}
@@ -563,10 +595,10 @@
if (info.isEmpty()) {
editor.remove(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY);
- editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false);
+ editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION, false);
} else {
editor.putString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, authority);
- editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, true);
+ editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION, true);
}
editor.apply();
@@ -724,7 +756,18 @@
+ totalRowcount + ". Cursor count: " + cursorCount);
}
- private CloudProviderInfo getDefaultCloudProviderInfo(String cachedProvider) {
+ /**
+ * Get the default {@link CloudProviderInfo} at {@link PickerSyncController} construction
+ */
+ @VisibleForTesting
+ CloudProviderInfo getDefaultCloudProviderInfo(String cachedProvider,
+ boolean isUserAwareAboutCloudMediaAppSettings) {
+ if (cachedProvider == null && isUserAwareAboutCloudMediaAppSettings) {
+ Log.i(TAG, "Skipping default cloud provider selection since the user has made an "
+ + "explicit empty choice");
+ return CloudProviderInfo.EMPTY;
+ }
+
final List<CloudProviderInfo> infos =
getSupportedCloudProviders(/* ignoreAllowList */ false);
@@ -740,7 +783,7 @@
if (cachedProvider != null) {
for (CloudProviderInfo info : infos) {
- if (info.authority.equals(defaultCloudProviderAuthority)) {
+ if (info.authority.equals(cachedProvider)) {
return info;
}
}
@@ -759,6 +802,17 @@
return CloudProviderInfo.EMPTY;
}
+ /**
+ * @return the value of the user pref
+ * {@link PREFS_KEY_IS_USER_CLOUD_MEDIA_AWARE} with the default value as
+ * {@code false}
+ */
+ @VisibleForTesting
+ boolean isUserAwareAboutCloudMediaAppSettings() {
+ return mUserPrefs.getBoolean(PREFS_KEY_IS_USER_CLOUD_MEDIA_AWARE,
+ /* defaultValue */ false);
+ }
+
private Set<String> parseAllowedCloudProviders(String config) {
Set<String> allowedProviders = new ArraySet<>();
final String[] allowedProvidersConfig = config.split(",");
diff --git a/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java b/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java
index 0100c05..4a962bb 100644
--- a/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java
+++ b/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java
@@ -18,6 +18,7 @@
import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.BOOLEAN_DEFAULT;
import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LIMIT_DEFAULT;
import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_ARRAY_DEFAULT;
import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
import android.os.Bundle;
@@ -31,7 +32,7 @@
public class CloudProviderQueryExtras {
private final String mAlbumId;
private final String mAlbumAuthority;
- private final String mMimeType;
+ private final String[] mMimeTypes;
private final long mSizeBytes;
private final long mGeneration;
private final int mLimit;
@@ -41,7 +42,7 @@
private CloudProviderQueryExtras() {
mAlbumId = STRING_DEFAULT;
mAlbumAuthority = STRING_DEFAULT;
- mMimeType = STRING_DEFAULT;
+ mMimeTypes = STRING_ARRAY_DEFAULT;
mSizeBytes = LONG_DEFAULT;
mGeneration = LONG_DEFAULT;
mLimit = LIMIT_DEFAULT;
@@ -49,11 +50,11 @@
mIsVideo = BOOLEAN_DEFAULT;
}
- private CloudProviderQueryExtras (String albumId, String albumAuthority, String mimeType,
+ private CloudProviderQueryExtras(String albumId, String albumAuthority, String[] mimeTypes,
long sizeBytes, long generation, int limit, boolean isFavorite, boolean isVideo) {
mAlbumId = albumId;
mAlbumAuthority = albumAuthority;
- mMimeType = mimeType;
+ mMimeTypes = mimeTypes;
mSizeBytes = sizeBytes;
mGeneration = generation;
mLimit = limit;
@@ -70,18 +71,16 @@
final String albumId = bundle.getString(MediaStore.QUERY_ARG_ALBUM_ID, STRING_DEFAULT);
final String albumAuthority = bundle.getString(MediaStore.QUERY_ARG_ALBUM_AUTHORITY,
STRING_DEFAULT);
- final String mimeType = bundle.getString(MediaStore.QUERY_ARG_MIME_TYPE, STRING_DEFAULT);
+ final String[] mimeTypes = bundle.getStringArray(MediaStore.QUERY_ARG_MIME_TYPE);
final long sizeBytes = bundle.getLong(MediaStore.QUERY_ARG_SIZE_BYTES, LONG_DEFAULT);
final long generation = LONG_DEFAULT;
final int limit = bundle.getInt(MediaStore.QUERY_ARG_LIMIT, LIMIT_DEFAULT);
- final boolean isFavorite = localProvider.equals(albumAuthority)
- && AlbumColumns.ALBUM_ID_FAVORITES.equals(albumId);
- final boolean isVideo = localProvider.equals(albumAuthority)
- && AlbumColumns.ALBUM_ID_VIDEOS.equals(albumId);
+ final boolean isFavorite = AlbumColumns.ALBUM_ID_FAVORITES.equals(albumId);
+ final boolean isVideo = AlbumColumns.ALBUM_ID_VIDEOS.equals(albumId);
- return new CloudProviderQueryExtras(albumId, albumAuthority, mimeType, sizeBytes,
+ return new CloudProviderQueryExtras(albumId, albumAuthority, mimeTypes, sizeBytes,
generation, limit, isFavorite, isVideo);
}
@@ -93,8 +92,8 @@
final String albumId = bundle.getString(CloudMediaProviderContract.EXTRA_ALBUM_ID,
STRING_DEFAULT);
final String albumAuthority = STRING_DEFAULT;
- final String mimeType = bundle.getString(CloudMediaProviderContract.EXTRA_MIME_TYPE,
- STRING_DEFAULT);
+ final String[] mimeTypes = bundle.getStringArray(
+ CloudMediaProviderContract.EXTRA_MIME_TYPE);
final long sizeBytes = bundle.getLong(CloudMediaProviderContract.EXTRA_SIZE_LIMIT_BYTES,
LONG_DEFAULT);
final long generation = bundle.getLong(CloudMediaProviderContract.EXTRA_SYNC_GENERATION,
@@ -104,14 +103,14 @@
final boolean isFavorite = BOOLEAN_DEFAULT;
final boolean isVideo = BOOLEAN_DEFAULT;
- return new CloudProviderQueryExtras(albumId, albumAuthority, mimeType, sizeBytes,
+ return new CloudProviderQueryExtras(albumId, albumAuthority, mimeTypes, sizeBytes,
generation, limit, isFavorite, isVideo);
}
public PickerDbFacade.QueryFilter toQueryFilter() {
PickerDbFacade.QueryFilterBuilder qfb = new PickerDbFacade.QueryFilterBuilder(mLimit);
qfb.setSizeBytes(mSizeBytes);
- qfb.setMimeType(mMimeType);
+ qfb.setMimeTypes(mMimeTypes);
qfb.setIsFavorite(mIsFavorite);
qfb.setIsVideo(mIsVideo);
qfb.setAlbumId(mAlbumId);
@@ -121,7 +120,7 @@
public Bundle toCloudMediaBundle() {
final Bundle extras = new Bundle();
extras.putString(CloudMediaProviderContract.EXTRA_ALBUM_ID, mAlbumId);
- extras.putString(CloudMediaProviderContract.EXTRA_MIME_TYPE, mMimeType);
+ extras.putStringArray(CloudMediaProviderContract.EXTRA_MIME_TYPE, mMimeTypes);
extras.putLong(CloudMediaProviderContract.EXTRA_SIZE_LIMIT_BYTES, mSizeBytes);
return extras;
@@ -135,8 +134,8 @@
return mAlbumAuthority;
}
- public String getMimeType() {
- return mMimeType;
+ public String[] getMimeTypes() {
+ return mMimeTypes;
}
public long getSizeBytes() {
diff --git a/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java b/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java
index d849c1f..98f0693 100644
--- a/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java
@@ -27,10 +27,10 @@
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.addMimeTypesToQueryBuilderAndSelectionArgs;
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorLong;
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
import static com.android.providers.media.util.DatabaseUtils.bindList;
-import static com.android.providers.media.util.DatabaseUtils.replaceMatchAnyChar;
import android.content.ContentValues;
import android.content.Context;
@@ -272,7 +272,7 @@
* Returns all items from the files table where {@link MediaColumns#GENERATION_MODIFIED}
* is greater than {@code generation}.
*/
- public Cursor queryMedia(long generation, String albumId, String mimeType) {
+ public Cursor queryMedia(long generation, String albumId, String[] mimeTypes) {
final List<String> selectionArgs = new ArrayList<>();
final String orderBy = CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS + " DESC";
@@ -281,7 +281,7 @@
qb.appendWhereStandalone(WHERE_GREATER_GENERATION);
selectionArgs.add(String.valueOf(generation));
- selectionArgs.addAll(appendWhere(qb, albumId, mimeType));
+ selectionArgs.addAll(appendWhere(qb, albumId, mimeTypes));
return qb.query(db, PROJECTION_MEDIA_COLUMNS, /* select */ null,
selectionArgs.toArray(new String[selectionArgs.size()]), /* groupBy */ null,
@@ -374,14 +374,14 @@
* Categories are determined with the {@link #LOCAL_ALBUM_IDS}.
* If there are no media items under an albumId, the album is skipped from the results.
*/
- public Cursor queryAlbums(String mimeType) {
+ public Cursor queryAlbums(String[] mimeTypes) {
final MatrixCursor c = new MatrixCursor(AlbumColumns.ALL_PROJECTION);
for (String albumId: LOCAL_ALBUM_IDS) {
Cursor cursor = mDatabaseHelper.runWithTransaction(db -> {
final SQLiteQueryBuilder qb = createMediaQueryBuilder();
final List<String> selectionArgs = new ArrayList<>();
- selectionArgs.addAll(appendWhere(qb, albumId, mimeType));
+ selectionArgs.addAll(appendWhere(qb, albumId, mimeTypes));
return qb.query(db, PROJECTION_ALBUM_DB, /* selection */ null,
selectionArgs.toArray(new String[selectionArgs.size()]), /* groupBy */ null,
@@ -419,13 +419,10 @@
}
private static List<String> appendWhere(SQLiteQueryBuilder qb, String albumId,
- String mimeType) {
+ String[] mimeTypes) {
final List<String> selectionArgs = new ArrayList<>();
- if (mimeType != null) {
- qb.appendWhereStandalone(WHERE_MIME_TYPE);
- selectionArgs.add(replaceMatchAnyChar(mimeType));
- }
+ addMimeTypesToQueryBuilderAndSelectionArgs(qb, selectionArgs, mimeTypes);
if (albumId == null) {
return selectionArgs;
diff --git a/src/com/android/providers/media/photopicker/data/ItemsProvider.java b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
index fb32872..07fed26 100644
--- a/src/com/android/providers/media/photopicker/data/ItemsProvider.java
+++ b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
@@ -16,8 +16,6 @@
package com.android.providers.media.photopicker.data;
-import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentProvider;
@@ -30,15 +28,12 @@
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
-import android.provider.CloudMediaProviderContract;
-import android.provider.CloudMediaProviderContract.AlbumColumns;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.PickerUriResolver;
-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.UserId;
@@ -57,7 +52,7 @@
/**
* Returns a {@link Cursor} to all images/videos based on the param passed for
- * {@code categoryType}, {@code offset}, {@code limit}, {@code mimeType} and {@code userId}.
+ * {@code categoryType}, {@code offset}, {@code limit}, {@code mimeTypes} and {@code userId}.
*
* <p>
* By default the returned {@link Cursor} sorts by latest date taken.
@@ -77,13 +72,13 @@
*/
@Nullable
public Cursor getItems(Category category, int offset,
- int limit, @Nullable String mimeType, @Nullable UserId userId) throws
+ int limit, @Nullable String[] mimeTypes, @Nullable UserId userId) throws
IllegalArgumentException {
if (userId == null) {
userId = UserId.CURRENT_USER;
}
- return queryMedia(limit, mimeType, category, userId);
+ return queryMedia(limit, mimeTypes, category, userId);
}
/**
@@ -107,15 +102,15 @@
* in the category,
*/
@Nullable
- public Cursor getCategories(@Nullable String mimeType, @Nullable UserId userId) {
+ public Cursor getCategories(@Nullable String[] mimeTypes, @Nullable UserId userId) {
if (userId == null) {
userId = UserId.CURRENT_USER;
}
- return queryAlbums(mimeType, userId);
+ return queryAlbums(mimeTypes, userId);
}
- private Cursor queryMedia(int limit, @Nullable String mimeType,
+ private Cursor queryMedia(int limit, String[] mimeTypes,
@NonNull Category category, @NonNull UserId userId)
throws IllegalStateException {
final Bundle extras = new Bundle();
@@ -127,7 +122,9 @@
return null;
}
extras.putInt(MediaStore.QUERY_ARG_LIMIT, limit);
- extras.putString(MediaStore.QUERY_ARG_MIME_TYPE, mimeType);
+ if (mimeTypes != null) {
+ extras.putStringArray(MediaStore.QUERY_ARG_MIME_TYPE, mimeTypes);
+ }
extras.putString(MediaStore.QUERY_ARG_ALBUM_ID, category.getId());
extras.putString(MediaStore.QUERY_ARG_ALBUM_AUTHORITY, category.getAuthority());
@@ -144,7 +141,7 @@
}
@Nullable
- private Cursor queryAlbums(@Nullable String mimeType, @NonNull UserId userId) {
+ private Cursor queryAlbums(@Nullable String[] mimeTypes, @NonNull UserId userId) {
final Bundle extras = new Bundle();
try (ContentProviderClient client = userId.getContentResolver(mContext)
.acquireUnstableContentProviderClient(MediaStore.AUTHORITY)) {
@@ -153,7 +150,9 @@
+ MediaStore.AUTHORITY);
return null;
}
- extras.putString(MediaStore.QUERY_ARG_MIME_TYPE, mimeType);
+ if (mimeTypes != null) {
+ extras.putStringArray(MediaStore.QUERY_ARG_MIME_TYPE, mimeTypes);
+ }
final Uri uri = PickerUriResolver.PICKER_INTERNAL_URI.buildUpon()
.appendPath(PickerUriResolver.ALBUM_PATH).build();
diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
index 3585a94..de1d6b6 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
@@ -37,8 +37,6 @@
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
-import android.os.SystemProperties;
-import android.provider.DeviceConfig;
import android.provider.CloudMediaProviderContract;
import android.provider.MediaStore;
import android.text.TextUtils;
@@ -115,7 +113,6 @@
public static final String KEY_DURATION_MS = "duration_ms";
@VisibleForTesting
public static final String KEY_MIME_TYPE = "mime_type";
- @VisibleForTesting
public static final String KEY_STANDARD_MIME_TYPE_EXTENSION = "standard_mime_type_extension";
@VisibleForTesting
public static final String KEY_IS_FAVORITE = "is_favorite";
@@ -145,14 +142,6 @@
KEY_DATE_TAKEN_MS, KEY_DATE_TAKEN_MS, KEY_ID);
private static final String WHERE_ALBUM_ID = KEY_ALBUM_ID + " = ?";
- private static final String[] PROJECTION_ALBUM_DB = new String[] {
- "COUNT(" + KEY_ID + ") AS " + CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT,
- "MAX(" + KEY_DATE_TAKEN_MS + ") AS "
- + CloudMediaProviderContract.AlbumColumns.DATE_TAKEN_MILLIS,
- String.format("IFNULL(%s, %s) AS %s", KEY_CLOUD_ID,
- KEY_LOCAL_ID, CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID)
- };
-
// Matches all media including cloud+local, cloud-only and local-only
private static final SQLiteQueryBuilder QB_MATCH_ALL = createMediaQueryBuilder();
// Matches media with id
@@ -240,6 +229,10 @@
* Returns {@link DbWriteOperation} to clear album media for a given albumId from the picker
* db.
*
+ * <p>The {@link DbWriteOperation} clears local or cloud album based on {@code authority} and
+ * {@code albumId}. If {@code albumId} is null, it clears all local or cloud albums based on
+ * {@code authority}.
+ *
* @param authority to determine whether local or cloud media should be cleared
*/
public DbWriteOperation beginResetAlbumMediaOperation(String authority, String albumId) {
@@ -247,37 +240,38 @@
}
/**
+ * Returns {@link UpdateMediaOperation} to update media belonging to {@code authority} in the
+ * picker db.
+ *
+ * @param authority to determine whether local or cloud media should be updated
+ */
+ public UpdateMediaOperation beginUpdateMediaOperation(String authority) {
+ return new UpdateMediaOperation(mDatabase, isLocal(authority));
+ }
+
+ /**
* Represents an atomic write operation to the picker database.
*
* <p>This class is not thread-safe and is meant to be used within a single thread only.
*/
- public static abstract class DbWriteOperation implements AutoCloseable {
+ public abstract static class DbWriteOperation implements AutoCloseable {
private final SQLiteDatabase mDatabase;
private final boolean mIsLocal;
- private final String mAlbumId;
private boolean mIsSuccess = false;
- // Needed for Album Media Write operations.
private DbWriteOperation(SQLiteDatabase database, boolean isLocal) {
- this(database, isLocal, "");
- }
-
- // Needed for Album Media Write operations.
- private DbWriteOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
mDatabase = database;
mIsLocal = isLocal;
- mAlbumId = albumId;
mDatabase.beginTransaction();
}
- /*
- * Execute the write operation.
+ /**
+ * Execute a write operation.
*
* @param cursor containing items to add/remove
- * @return {@link WriteResult} indicating success/failure and the number of {@code cursor}
- * items that were inserted/updated/deleted in the picker db
+ * @return number of {@code cursor} items that were inserted/updated/deleted in the db
* @throws {@link IllegalStateException} if no DB transaction is active
*/
public int execute(@Nullable Cursor cursor) {
@@ -315,21 +309,17 @@
return mIsLocal;
}
- String albumId() {
- return mAlbumId;
- }
-
int updateMedia(SQLiteQueryBuilder qb, ContentValues values,
String[] selectionArgs) {
try {
if (qb.update(mDatabase, values, /* selection */ null, selectionArgs) > 0) {
return SUCCESS;
} else {
- Log.d(TAG, "Failed to update picker db media. ContentValues: " + values);
+ Log.v(TAG, "Failed to update picker db media. ContentValues: " + values);
return FAIL;
}
} catch (SQLiteConstraintException e) {
- Log.d(TAG, "Failed to update picker db media. ContentValues: " + values, e);
+ Log.v(TAG, "Failed to update picker db media. ContentValues: " + values, e);
return RETRY;
}
}
@@ -348,6 +338,41 @@
}
}
+ /**
+ * Represents an atomic media update operation to the picker database.
+ *
+ * <p>This class is not thread-safe and is meant to be used within a single thread only.
+ */
+ public static final class UpdateMediaOperation extends DbWriteOperation {
+
+ private UpdateMediaOperation(SQLiteDatabase database, boolean isLocal) {
+ super(database, isLocal);
+ }
+
+ /**
+ * Execute a media update operation.
+ *
+ * @param id id of the media to be updated
+ * @param contentValues key-value pairs indicating fields to be updated for the media
+ * @return boolean indicating success/failure of the update
+ * @throws {@link IllegalStateException} if no DB transaction is active
+ */
+ public boolean execute(String id, ContentValues contentValues) {
+ final SQLiteDatabase database = getDatabase();
+ if (!database.inTransaction()) {
+ throw new IllegalStateException("No ongoing DB transaction.");
+ }
+
+ final SQLiteQueryBuilder qb = isLocal() ? QB_MATCH_LOCAL_ONLY : QB_MATCH_CLOUD;
+ return qb.update(database, contentValues, /* selection */ null, new String[] {id}) > 0;
+ }
+
+ @Override
+ int executeInternal(@Nullable Cursor cursor) {
+ throw new UnsupportedOperationException("Cursor updates are not supported.");
+ }
+ }
+
private static final class AddMediaOperation extends DbWriteOperation {
private AddMediaOperation(SQLiteDatabase database, boolean isLocal) {
@@ -394,11 +419,11 @@
if (QB_MATCH_ALL.insert(getDatabase(), values) > 0) {
return SUCCESS;
} else {
- Log.d(TAG, "Failed to insert picker db media. ContentValues: " + values);
+ Log.v(TAG, "Failed to insert picker db media. ContentValues: " + values);
return FAIL;
}
} catch (SQLiteConstraintException e) {
- Log.d(TAG, "Failed to insert picker db media. ContentValues: " + values, e);
+ Log.v(TAG, "Failed to insert picker db media. ContentValues: " + values, e);
return RETRY;
}
}
@@ -408,7 +433,7 @@
int res = insertMedia(values);
if (res == RETRY) {
// Attempt equivalent of CONFLICT_REPLACE resolution
- Log.d(TAG, "Retrying failed insert as update. ContentValues: " + values);
+ Log.v(TAG, "Retrying failed insert as update. ContentValues: " + values);
res = updateMedia(qb, values, selectionArgs);
}
@@ -545,12 +570,12 @@
private final long mId;
private final String mAlbumId;
private final long mSizeBytes;
- private final String mMimeType;
+ private final String[] mMimeTypes;
private final boolean mIsFavorite;
private final boolean mIsVideo;
private QueryFilter(int limit, long dateTakenBeforeMs, long dateTakenAfterMs, long id,
- String albumId, long sizeBytes, String mimeType, boolean isFavorite,
+ String albumId, long sizeBytes, String[] mimeTypes, boolean isFavorite,
boolean isVideo) {
this.mLimit = limit;
this.mDateTakenBeforeMs = dateTakenBeforeMs;
@@ -558,7 +583,7 @@
this.mId = id;
this.mAlbumId = albumId;
this.mSizeBytes = sizeBytes;
- this.mMimeType = mimeType;
+ this.mMimeTypes = mimeTypes;
this.mIsFavorite = isFavorite;
this.mIsVideo = isVideo;
}
@@ -568,6 +593,7 @@
public static class QueryFilterBuilder {
public static final long LONG_DEFAULT = -1;
public static final String STRING_DEFAULT = null;
+ public static final String[] STRING_ARRAY_DEFAULT = null;
public static final boolean BOOLEAN_DEFAULT = false;
public static final int LIMIT_DEFAULT = 1000;
@@ -578,7 +604,7 @@
private long id = LONG_DEFAULT;
private String albumId = STRING_DEFAULT;
private long sizeBytes = LONG_DEFAULT;
- private String mimeType = STRING_DEFAULT;
+ private String[] mimeTypes = STRING_ARRAY_DEFAULT;
private boolean isFavorite = BOOLEAN_DEFAULT;
private boolean mIsVideo = BOOLEAN_DEFAULT;
@@ -622,8 +648,8 @@
return this;
}
- public QueryFilterBuilder setMimeType(String mimeType) {
- this.mimeType = mimeType;
+ public QueryFilterBuilder setMimeTypes(String[] mimeTypes) {
+ this.mimeTypes = mimeTypes;
return this;
}
@@ -649,7 +675,7 @@
public QueryFilter build() {
return new QueryFilter(limit, dateTakenBeforeMs, dateTakenAfterMs, id, albumId,
- sizeBytes, mimeType, isFavorite, mIsVideo);
+ sizeBytes, mimeTypes, isFavorite, mIsVideo);
}
}
@@ -737,12 +763,9 @@
qb.appendWhereStandalone(WHERE_MIME_TYPE);
selectionArgs.add("video/%");
}
- if (query.mMimeType != null) {
- qb.appendWhereStandalone(WHERE_MIME_TYPE);
- selectionArgs.add(query.mMimeType.replace('*', '%'));
- }
+ addMimeTypesToQueryBuilderAndSelectionArgs(qb, selectionArgs, query.mMimeTypes);
- Cursor cursor = qb.query(mDatabase, PROJECTION_ALBUM_DB, /* selection */ null,
+ Cursor cursor = qb.query(mDatabase, getAlbumProjection(), /* selection */ null,
selectionArgs.toArray(new String[0]), /* groupBy */ null, /* having */ null,
/* orderBy */ null, /* limit */ null);
@@ -761,13 +784,27 @@
/* displayName */ albumId,
getCursorString(cursor, AlbumColumns.MEDIA_COVER_ID),
String.valueOf(count),
- mLocalProvider,
+ getCursorString(cursor, AlbumColumns.AUTHORITY),
};
c.addRow(projectionValue);
}
return c;
}
+ private String[] getAlbumProjection() {
+ return new String[] {
+ "COUNT(" + KEY_ID + ") AS " + CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT,
+ "MAX(" + KEY_DATE_TAKEN_MS + ") AS "
+ + CloudMediaProviderContract.AlbumColumns.DATE_TAKEN_MILLIS,
+ String.format("IFNULL(%s, %s) AS %s", KEY_CLOUD_ID,
+ KEY_LOCAL_ID, CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID),
+ // Note that we prefer local provider over cloud provider if a media item is present
+ // locally and on cloud.
+ String.format("CASE WHEN %s IS NULL THEN '%s' ELSE '%s' END AS %s",
+ KEY_LOCAL_ID, mCloudProvider, mLocalProvider, AlbumColumns.AUTHORITY)
+ };
+ }
+
private boolean isLocal(String authority) {
return mLocalProvider.equals(authority);
}
@@ -908,7 +945,7 @@
private static ContentValues cursorToContentValue(Cursor cursor, boolean isLocal,
String albumId) {
final ContentValues values = new ContentValues();
- if(TextUtils.isEmpty(albumId)) {
+ if (TextUtils.isEmpty(albumId)) {
values.put(KEY_IS_VISIBLE, 1);
}
else {
@@ -957,7 +994,7 @@
values.put(KEY_DURATION_MS, cursor.getLong(index));
break;
case CloudMediaProviderContract.MediaColumns.IS_FAVORITE:
- if(TextUtils.isEmpty(albumId)) {
+ if (TextUtils.isEmpty(albumId)) {
values.put(KEY_IS_FAVORITE, cursor.getInt(index));
}
break;
@@ -1004,10 +1041,8 @@
selectArgs.add(String.valueOf(query.mSizeBytes));
}
- if (query.mMimeType != null) {
- qb.appendWhereStandalone(WHERE_MIME_TYPE);
- selectArgs.add(replaceMatchAnyChar(query.mMimeType));
- }
+ addMimeTypesToQueryBuilderAndSelectionArgs(qb, selectArgs, query.mMimeTypes);
+
if (query.mIsVideo) {
qb.appendWhereStandalone(WHERE_MIME_TYPE);
selectArgs.add(VIDEO_MIME_TYPES);
@@ -1025,6 +1060,27 @@
return selectArgs.toArray(new String[selectArgs.size()]);
}
+ static void addMimeTypesToQueryBuilderAndSelectionArgs(SQLiteQueryBuilder qb,
+ List<String> selectionArgs, String[] mimeTypes) {
+ if (mimeTypes == null) {
+ return;
+ }
+
+ mimeTypes = replaceMatchAnyChar(mimeTypes);
+ ArrayList<String> whereMimeTypes = new ArrayList<>();
+ for (String mimeType : mimeTypes) {
+ if (!TextUtils.isEmpty(mimeType)) {
+ whereMimeTypes.add(WHERE_MIME_TYPE);
+ selectionArgs.add(mimeType);
+ }
+ }
+
+ if (whereMimeTypes.isEmpty()) {
+ return;
+ }
+ qb.appendWhereStandalone(TextUtils.join(" OR ", whereMimeTypes));
+ }
+
private static SQLiteQueryBuilder createMediaQueryBuilder() {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(TABLE_MEDIA);
@@ -1087,25 +1143,35 @@
return qb;
}
- private static final class ResetAlbumOperation extends DbWriteOperation {
- /**
- * Resets the given cloud or local album_media identified by {@code isLocal} and
- * {@code albumId}. If {@code albumId} is null, resets all the respective cloud or
- * local albums.
- */
+ private abstract static class AlbumWriteOperation extends DbWriteOperation {
+
+ private final String mAlbumId;
+
+ private AlbumWriteOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
+ super(database, isLocal);
+ mAlbumId = albumId;
+ }
+
+ String getAlbumId() {
+ return mAlbumId;
+ }
+ }
+
+ private static final class ResetAlbumOperation extends AlbumWriteOperation {
+
private ResetAlbumOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
super(database, isLocal, albumId);
}
@Override
int executeInternal(@Nullable Cursor unused) {
- final String albumId = albumId();
+ final String albumId = getAlbumId();
final boolean isLocal = isLocal();
final SQLiteQueryBuilder qb = createAlbumMediaQueryBuilder(isLocal);
String[] selectionArgs = null;
- if(!TextUtils.isEmpty(albumId)) {
+ if (!TextUtils.isEmpty(albumId)) {
qb.appendWhereStandalone(WHERE_ALBUM_ID);
selectionArgs = new String[]{albumId};
}
@@ -1115,10 +1181,12 @@
}
}
- private static final class AddAlbumMediaOperation extends DbWriteOperation {
+ private static final class AddAlbumMediaOperation extends AlbumWriteOperation {
+
private AddAlbumMediaOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
super(database, isLocal, albumId);
- if(TextUtils.isEmpty(albumId)) {
+
+ if (TextUtils.isEmpty(albumId)) {
throw new IllegalArgumentException("Missing albumId.");
}
}
@@ -1126,7 +1194,7 @@
@Override
int executeInternal(@Nullable Cursor cursor) {
final boolean isLocal = isLocal();
- final String albumId = albumId();
+ final String albumId = getAlbumId();
final SQLiteQueryBuilder qb = createAlbumMediaQueryBuilder(isLocal);
int counter = 0;
@@ -1136,10 +1204,10 @@
if (qb.insert(getDatabase(), values) > 0) {
counter++;
} else {
- Log.d(TAG, "Failed to insert album_media. ContentValues: " + values);
+ Log.v(TAG, "Failed to insert album_media. ContentValues: " + values);
}
} catch (SQLiteConstraintException e) {
- Log.d(TAG, "Failed to insert album_media. ContentValues: " + values, e);
+ Log.v(TAG, "Failed to insert album_media. ContentValues: " + values, e);
}
}
diff --git a/src/com/android/providers/media/photopicker/data/model/Category.java b/src/com/android/providers/media/photopicker/data/model/Category.java
index 6166d4a..90a2143 100644
--- a/src/com/android/providers/media/photopicker/data/model/Category.java
+++ b/src/com/android/providers/media/photopicker/data/model/Category.java
@@ -17,44 +17,36 @@
package com.android.providers.media.photopicker.data.model;
import static android.provider.CloudMediaProviderContract.AlbumColumns;
-import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS;
-import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA;
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS;
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 com.android.providers.media.photopicker.util.CursorUtils.getCursorInt;
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
-import android.annotation.StringDef;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Environment;
-import android.provider.CloudMediaProviderContract;
import android.provider.MediaStore;
-import android.provider.MediaStore.Files.FileColumns;
import android.text.TextUtils;
-import android.util.ArrayMap;
+import android.util.Log;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.providers.media.R;
import com.android.providers.media.photopicker.data.ItemsProvider;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
import java.util.Locale;
/**
* Defines each category (which is group of items) for the photo picker.
*/
public class Category {
+ public static final String TAG = "PhotoPicker";
public static final Category DEFAULT = new Category();
private final String mId;
@@ -147,14 +139,20 @@
* Create a {@link Category} from the {@code cursor}.
*/
public static Category fromCursor(@NonNull Cursor cursor, @NonNull UserId userId) {
- final boolean isLocal;
String authority = getCursorString(cursor, AlbumColumns.AUTHORITY);
- if (authority != null) {
- isLocal = true;
- } else {
- isLocal = false;
- authority = cursor.getExtras().getString(MediaStore.EXTRA_CLOUD_PROVIDER);
+ if (authority == null) {
+ // Authority will be null for cloud albums in cursor.
+ String cloudProvider = cursor.getExtras().getString(MediaStore.EXTRA_CLOUD_PROVIDER);
+ if (cloudProvider == null) {
+ // If cloud provider is null, cloud albums will not show up properly.
+ Log.e(TAG, "Cloud provider is set by the user but not passed in album media cursor"
+ + " extras.");
+ } else {
+ authority = cloudProvider;
+ }
}
+ final boolean isLocal = authority != null
+ && authority.equals(cursor.getExtras().getString(MediaStore.EXTRA_LOCAL_PROVIDER));
final Uri coverUri = ItemsProvider.getItemsUri(
getCursorString(cursor, AlbumColumns.MEDIA_COVER_ID), authority, userId);
diff --git a/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java b/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java
index 076ad31..30c2fbe 100644
--- a/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java
+++ b/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java
@@ -31,7 +31,17 @@
@UiEvent(doc = "Photo picker opened via GET_CONTENT intent")
PHOTO_PICKER_OPEN_GET_CONTENT(1080),
@UiEvent(doc = "DocumentsUi opened by clicking on Browse in Photo picker")
- PHOTO_PICKER_BROWSE_DOCUMENTSUI(1085);
+ PHOTO_PICKER_BROWSE_DOCUMENTSUI(1085),
+ @UiEvent(doc = "Photo picker cancelled in work profile")
+ PHOTO_PICKER_CANCEL_WORK_PROFILE(1125),
+ @UiEvent(doc = "Photo picker cancelled in personal profile")
+ PHOTO_PICKER_CANCEL_PERSONAL_PROFILE(1126),
+ @UiEvent(doc = "Confirmed selection in Photo picker in work profile")
+ PHOTO_PICKER_CONFIRM_WORK_PROFILE(1127),
+ @UiEvent(doc = "Confirmed selection in Photo picker in personal profile")
+ PHOTO_PICKER_CONFIRM_PERSONAL_PROFILE(1128),
+ @UiEvent(doc = "User changed the active Photo picker cloud provider")
+ PHOTO_PICKER_CLOUD_PROVIDER_CHANGED(1135);
private final int mId;
@@ -90,4 +100,66 @@
callingPackage,
instanceId);
}
+
+ /**
+ * Log metrics to notify that user has confirmed selection in personal profile
+ */
+ public void logPickerConfirmPersonal(InstanceId instanceId, int callingUid,
+ String callingPackage, int countOfItemsConfirmed) {
+ logger.logWithInstanceIdAndPosition(
+ PhotoPickerEvent.PHOTO_PICKER_CONFIRM_PERSONAL_PROFILE,
+ callingUid,
+ callingPackage,
+ instanceId,
+ countOfItemsConfirmed);
+ }
+
+ /**
+ * Log metrics to notify that user has confirmed selection in work profile
+ */
+ public void logPickerConfirmWork(InstanceId instanceId, int callingUid,
+ String callingPackage, int countOfItemsConfirmed) {
+ logger.logWithInstanceIdAndPosition(
+ PhotoPickerEvent.PHOTO_PICKER_CONFIRM_WORK_PROFILE,
+ callingUid,
+ callingPackage,
+ instanceId,
+ countOfItemsConfirmed);
+ }
+
+ /**
+ * Log metrics to notify that user has cancelled picker (without any selection) in personal
+ * profile
+ */
+ public void logPickerCancelPersonal(InstanceId instanceId, int callingUid,
+ String callingPackage) {
+ logger.logWithInstanceId(
+ PhotoPickerEvent.PHOTO_PICKER_CANCEL_PERSONAL_PROFILE,
+ callingUid,
+ callingPackage,
+ instanceId);
+ }
+
+ /**
+ * Log metrics to notify that user has cancelled picker (without any selection) in work
+ * profile
+ */
+ public void logPickerCancelWork(InstanceId instanceId, int callingUid,
+ String callingPackage) {
+ logger.logWithInstanceId(
+ PhotoPickerEvent.PHOTO_PICKER_CANCEL_WORK_PROFILE,
+ callingUid,
+ callingPackage,
+ instanceId);
+ }
+
+ /**
+ * Log metrics to notify that the user has changed the active cloud provider
+ * @param cloudProviderUid new active cloud provider uid
+ * @param cloudProviderPackage new active cloud provider package name
+ */
+ public void logPickerCloudProviderChanged(int cloudProviderUid, String cloudProviderPackage) {
+ logger.log(PhotoPickerEvent.PHOTO_PICKER_CLOUD_PROVIDER_CHANGED, cloudProviderUid,
+ cloudProviderPackage);
+ }
}
diff --git a/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java b/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java
index 3f946ca..5060486 100644
--- a/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java
@@ -47,7 +47,7 @@
setEmptyMessage(R.string.picker_albums_empty_message);
final AlbumsTabAdapter adapter = new AlbumsTabAdapter(mImageLoader, this::onItemClick,
- mPickerViewModel.hasMimeTypeFilter());
+ mPickerViewModel.hasMimeTypeFilters());
mPickerViewModel.getCategories().observe(this, categoryList -> {
adapter.updateCategoryList(categoryList);
// Handle emptyView's visibility
diff --git a/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java b/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
deleted file mode 100644
index 44ceea2..0000000
--- a/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
+++ /dev/null
@@ -1,321 +0,0 @@
-/*
- * 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.photopicker.ui;
-
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.media.AudioFocusRequest;
-import android.media.AudioManager;
-import android.net.Uri;
-import android.view.View;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-
-import com.android.providers.media.R;
-import com.android.providers.media.photopicker.data.MuteStatus;
-
-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.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.ui.StyledPlayerView;
-import com.google.android.exoplayer2.upstream.ContentDataSource;
-import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
-import com.google.android.exoplayer2.util.Clock;
-
-/**
- * A helper class that assists in initialize/prepare/play/release of ExoPlayer. The class assumes
- * that all its public methods are called from main thread only.
- */
-class ExoPlayerWrapper {
- private static final String TAG = "ExoPlayerWrapper";
- // 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 static final long PLAYER_CONTROL_ON_PLAY_TIMEOUT_MS = 1000;
- private static final float VOLUME_LEVEL_MUTE = 0.0f;
- private static final AudioAttributes sAudioAttributes = new AudioAttributes.Builder()
- .setContentType(AudioAttributes.USAGE_MEDIA)
- .setUsage(AudioAttributes.CONTENT_TYPE_MOVIE)
- .build();
-
- private final Context mContext;
- private final MuteStatus mMuteStatus;
- private ExoPlayer mExoPlayer;
- private boolean mIsPlayerReleased = true;
- private boolean mShouldShowControlsForNext = true;
- private boolean mIsAccessibilityEnabled = false;
- private AudioFocusRequest mAudioFocusRequest = null;
-
- public ExoPlayerWrapper(Context context, MuteStatus muteStatus) {
- mContext = context;
- mMuteStatus = muteStatus;
- }
-
- /**
- * Prepares the {@link ExoPlayer} and attaches it to given {@code styledPlayerView} and starts
- * playing.
- * Note: The method tries to release the {@link ExoPlayer} before preparing the new one. As we
- * don't have previous page's {@link StyledPlayerView}, we can't switch the player from previous
- * {@link StyledPlayerView} to new one. Hence, we try to create a new {@link ExoPlayer} instead.
- */
- public void prepareAndPlay(StyledPlayerView styledPlayerView, ImageView imageView, Uri uri) {
- // TODO(b/197083539): Explore options for not re-creating ExoPlayer everytime.
- initializeExoPlayer(uri);
-
- setupPlayerLayout(styledPlayerView, imageView);
-
- // Prepare the player and play the video
- mExoPlayer.prepare();
- mExoPlayer.setPlayWhenReady(true);
- mIsPlayerReleased = false;
- }
-
- public void resetPlayerIfNecessary() {
- // Clear state of the previous player controls visibility state. Controls visibility state
- // will only be tracked and used for contiguous videos in the preview.
- mShouldShowControlsForNext = true;
- // Release the player if necessary.
- releaseIfNecessary();
- }
-
- private void initializeExoPlayer(Uri uri) {
- // Try releasing the ExoPlayer first.
- releaseIfNecessary();
-
- mExoPlayer = createExoPlayer();
- // We always start from the beginning of the video, and we always repeat the video in a loop
- mExoPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
- // We only play one video in the player, hence we should always use setMediaItem instead of
- // ExoPlayer#addMediaItem
- mExoPlayer.setMediaItem(MediaItem.fromUri(uri));
- }
-
- 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))
- .setHandleAudioBecomingNoisy(true)
- .build();
- }
-
- private void setupPlayerLayout(StyledPlayerView styledPlayerView, ImageView imageView) {
- // Step1: Set-up Player layout
- // TODO(b/197083539): Remove this if it drains battery.
- styledPlayerView.setKeepScreenOn(true);
- styledPlayerView.setPlayer(mExoPlayer);
- styledPlayerView.setVisibility(View.VISIBLE);
-
- // Hide ImageView when the player is ready.
- mExoPlayer.addListener(new Player.Listener() {
- @Override
- public void onPlaybackStateChanged(int playbackState) {
- if (playbackState == Player.STATE_READY ) {
- imageView.setVisibility(View.GONE);
- }
- }
- });
-
- // Step2: Set-up player control view
- // Set-up video controls for accessibility mode
- // Set Accessibility listeners and update the video controller visibility accordingly
- AccessibilityManager accessibilityManager =
- mContext.getSystemService(AccessibilityManager.class);
- accessibilityManager.addAccessibilityStateChangeListener(
- enabled -> updateControllerForAccessibilty(enabled, styledPlayerView));
- updateControllerForAccessibilty(accessibilityManager.isEnabled(), styledPlayerView);
-
- // Set-up video controls for non-accessibility mode
- // Track if the controller layout should be visible for the next video.
- styledPlayerView.setControllerVisibilityListener(
- visibility -> mShouldShowControlsForNext = (visibility == View.VISIBLE));
- // Video controls will be visible if
- // 1. this is the first video preview page or
- // 2. the previous video had controls visible when the page was swiped or
- // 3. the previous page was not a video preview
- // or if we are in accessibility mode.
- if (mShouldShowControlsForNext) {
- styledPlayerView.showController();
- }
-
- // Player controls needs to be auto-hidden if they are shown
- // 1. when the video starts previewing or
- // 2. when the video starts playing from paused state.
- // To achieve this, we hide the controller whenever player state changes to 'play'
- mExoPlayer.addListener(new Player.Listener() {
- @Override
- public void onIsPlayingChanged(boolean isPlaying) {
- if (mIsAccessibilityEnabled) {
- // Player controls are always visible in accessibility mode.
- return;
- }
-
- // We don't have to hide controls if the state changed to PAUSED or controller
- // isn't visible.
- if (!isPlaying || !mShouldShowControlsForNext) return;
-
- // Set controller visibility of the next video to false so that we don't show the
- // controls on the next video.
- mShouldShowControlsForNext = false;
- // Auto hide controller after 1s of player state changing to "Play".
- styledPlayerView.postDelayed(() -> styledPlayerView.hideController(),
- PLAYER_CONTROL_ON_PLAY_TIMEOUT_MS);
- }
- });
-
- // Step3: Set-up mute button
- final ImageButton muteButton = styledPlayerView.findViewById(R.id.preview_mute);
- handleAudioFocusAndInitVolumeState(muteButton);
-
- // Add click listeners for mute button
- muteButton.setOnClickListener(v -> {
- mMuteStatus.setVolumeMuted(!mMuteStatus.isVolumeMuted());
- handleAudioFocusAndInitVolumeState(muteButton);
- });
-
- // Request or abandon audio focus on player state change.
- mExoPlayer.addListener(new Player.Listener() {
- @Override
- public void onIsPlayingChanged(boolean isPlaying) {
- if (isPlaying) {
- handleAudioFocusAndInitVolumeState(muteButton);
- } else {
- abandonAudioFocusIfAny();
- }
- }
- });
- }
-
- /**
- * Requests AudioFocus if current state of the volume state is volume on. Sets the volume of
- * the playback if the AudioFocus request is granted.
- * Also, updates the mute button based on the state of the muteStatus.
- */
- private void handleAudioFocusAndInitVolumeState(ImageButton muteButton) {
- if (mMuteStatus.isVolumeMuted()) {
- mExoPlayer.setVolume(VOLUME_LEVEL_MUTE);
- abandonAudioFocusIfAny();
- } else if (requestAudioFocus() == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- mExoPlayer.setVolume(getAudioManager().getStreamVolume(AudioManager.STREAM_MUSIC));
- }
-
- updateMuteButtonState(muteButton, mMuteStatus.isVolumeMuted());
- }
-
- /**
- * Abandons the AudioFocus request so that the previous focus owner can resume their playback
- */
- private void abandonAudioFocusIfAny() {
- if (mAudioFocusRequest == null) return;
-
- getAudioManager().abandonAudioFocusRequest(mAudioFocusRequest);
- mAudioFocusRequest = null;
- }
-
- private void updateControllerForAccessibilty(boolean isEnabled,
- StyledPlayerView styledPlayerView) {
- mIsAccessibilityEnabled = isEnabled;
- if (isEnabled) {
- styledPlayerView.showController();
- styledPlayerView.setControllerHideOnTouch(false);
- } else {
- styledPlayerView.setControllerHideOnTouch(true);
- }
- }
-
- private void updateMuteButtonState(ImageButton muteButton, boolean isVolumeMuted) {
- updateMuteButtonContentDescription(muteButton, isVolumeMuted);
- updateMuteButtonIcon(muteButton, isVolumeMuted);
- }
-
- private void updateMuteButtonContentDescription(ImageButton muteButton, boolean isVolumeMuted) {
- muteButton.setContentDescription(
- mContext.getString(
- isVolumeMuted ? R.string.picker_unmute_video : R.string.picker_mute_video));
- }
-
- private void updateMuteButtonIcon(ImageButton muteButton, boolean isVolumeMuted) {
- muteButton.setImageResource(
- isVolumeMuted ? R.drawable.ic_volume_off : R.drawable.ic_volume_up);
- }
-
- private AudioManager getAudioManager() {
- return mContext.getSystemService(AudioManager.class);
- }
-
- private int requestAudioFocus() {
- // Always request new AudioFocus
- abandonAudioFocusIfAny();
-
- mAudioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
- .setAudioAttributes(sAudioAttributes)
- .setWillPauseWhenDucked(true)
- .setAcceptsDelayedFocusGain(true)
- .setOnAudioFocusChangeListener(focusChange -> {
- if (focusChange == AudioManager.AUDIOFOCUS_LOSS
- || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
- || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
- mExoPlayer.setPlayWhenReady(false);
- }
- }).build();
-
- // We don't need to reset mAudioFocusRequest to null on failure of requestAudioFocus. This
- // is because we always reset the AudioFocus before requesting, reset mechanism will also
- // try to abandon AudioFocus if there is any.
- return getAudioManager().requestAudioFocus(mAudioFocusRequest);
- }
-
- private void releaseIfNecessary() {
- // Release the player only when it's not already released. ExoPlayer doesn't crash if we try
- // to release already released player, but ExoPlayer#release() may not be a no-op, hence we
- // call release() only when it's not already released.
- if (!mIsPlayerReleased) {
- mExoPlayer.release();
- abandonAudioFocusIfAny();
- mIsPlayerReleased = true;
- }
- }
-}
diff --git a/src/com/android/providers/media/photopicker/ui/ImageLoader.java b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
index d629471..69f5e3d 100644
--- a/src/com/android/providers/media/photopicker/ui/ImageLoader.java
+++ b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
@@ -17,6 +17,7 @@
package com.android.providers.media.photopicker.ui;
import android.content.Context;
+import android.graphics.Bitmap;
import android.graphics.ImageDecoder;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@@ -26,12 +27,15 @@
import android.widget.ImageView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
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.RequestBuilder;
import com.bumptech.glide.load.Option;
+import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.signature.ObjectKey;
@@ -44,6 +48,8 @@
public static final Option<Boolean> THUMBNAIL_REQUEST =
Option.memory(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB, false);
private static final String TAG = "ImageLoader";
+ private static final RequestOptions THUMBNAIL_OPTION =
+ RequestOptions.option(THUMBNAIL_REQUEST, /* enableThumbnail */ true);
private final Context mContext;
public ImageLoader(Context context) {
@@ -60,11 +66,8 @@
// Always show all thumbnails as bitmap images instead of drawables
// This is to ensure that we do not animate any thumbnail (for eg GIF)
// TODO(b/194285082): Use drawable instead of bitmap, as it saves memory.
- Glide.with(mContext)
- .asBitmap()
- .load(category.getCoverUri())
- .apply(RequestOptions.option(THUMBNAIL_REQUEST, true))
- .into(imageView);
+ loadWithGlide(getBitmapRequestBuilder(category.getCoverUri()), THUMBNAIL_OPTION,
+ /* signature */ null, imageView);
}
/**
@@ -74,16 +77,11 @@
* @param imageView the imageView shows the thumbnail
*/
public void loadPhotoThumbnail(@NonNull Item item, @NonNull ImageView imageView) {
- Uri uri = item.getContentUri();
// Always show all thumbnails as bitmap images instead of drawables
// This is to ensure that we do not animate any thumbnail (for eg GIF)
// TODO(b/194285082): Use drawable instead of bitmap, as it saves memory.
- Glide.with(mContext)
- .asBitmap()
- .load(uri)
- .signature(getGlideSignature(item, /* prefix */ ""))
- .apply(RequestOptions.option(THUMBNAIL_REQUEST, true))
- .into(imageView);
+ loadWithGlide(getBitmapRequestBuilder(item.getContentUri()), THUMBNAIL_OPTION,
+ getGlideSignature(item, /* prefix */ ""), imageView);
}
/**
@@ -94,11 +92,8 @@
*/
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);
+ loadWithGlide(getGifRequestBuilder(item.getContentUri()), /* requestOptions */ null,
+ getGlideSignature(item, /* prefix */ ""), imageView);
return;
}
@@ -108,11 +103,8 @@
}
// Preview as bitmap image for all other image types
- Glide.with(mContext)
- .asBitmap()
- .load(item.getContentUri())
- .signature(getGlideSignature(item, /* prefix */ ""))
- .into(imageView);
+ loadWithGlide(getBitmapRequestBuilder(item.getContentUri()), /* requestOptions */ null,
+ getGlideSignature(item, /* prefix */ ""), imageView);
}
private void loadAnimatedWebpPreview(@NonNull Item item, @NonNull ImageView imageView) {
@@ -129,22 +121,16 @@
// If we failed to decode drawable for a source using ImageDecoder, then try using uri
// directly. Glide will show static image for an animated webp. That is okay as we tried our
// best to load animated webp but couldn't, and we anyway show the GIF badge in preview.
- Glide.with(mContext)
- .load(drawable == null ? uri : drawable)
- .signature(getGlideSignature(item, /* prefix */ ""))
- .into(imageView);
+ loadWithGlide(getDrawableRequestBuilder(drawable == null ? uri : drawable),
+ /* requestOptions */ null, getGlideSignature(item, /* prefix */ ""), imageView);
}
/**
* Loads the image from first frame of the given video item
*/
public void loadImageFromVideoForPreview(@NonNull Item item, @NonNull ImageView imageView) {
- Glide.with(mContext)
- .asBitmap()
- .load(item.getContentUri())
- .apply(new RequestOptions().frame(1000))
- .signature(getGlideSignature(item, "Preview"))
- .into(imageView);
+ loadWithGlide(getBitmapRequestBuilder(item.getContentUri()),
+ new RequestOptions().frame(1000), getGlideSignature(item, "Preview"), imageView);
}
private ObjectKey getGlideSignature(Item item, String prefix) {
@@ -153,4 +139,37 @@
MediaStore.getVersion(mContext) + prefix + item.getContentUri().toString() +
item.getGenerationModified());
}
+
+ private RequestBuilder<Bitmap> getBitmapRequestBuilder(Uri uri) {
+ return Glide.with(mContext)
+ .asBitmap()
+ .load(uri);
+ }
+
+ private RequestBuilder<GifDrawable> getGifRequestBuilder(Uri uri) {
+ return Glide.with(mContext)
+ .asGif()
+ .load(uri);
+ }
+
+ private RequestBuilder<Drawable> getDrawableRequestBuilder(Object model) {
+ return Glide.with(mContext)
+ .load(model);
+ }
+
+ private <T> void loadWithGlide(RequestBuilder<T> requestBuilder,
+ @Nullable RequestOptions requestOptions, @Nullable ObjectKey signature,
+ ImageView imageView) {
+ RequestBuilder<T> newRequestBuilder = requestBuilder.clone();
+
+ if (requestOptions != null) {
+ newRequestBuilder = newRequestBuilder.apply(requestOptions);
+ }
+
+ if (signature != null) {
+ newRequestBuilder = newRequestBuilder.signature(signature);
+ }
+
+ newRequestBuilder.into(imageView);
+ }
}
diff --git a/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java b/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java
deleted file mode 100644
index 7d31af9..0000000
--- a/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * 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.photopicker.ui;
-
-import android.content.Context;
-import android.net.Uri;
-import android.os.Looper;
-import android.util.Log;
-import android.view.View;
-import android.widget.ImageView;
-
-import com.android.providers.media.R;
-import com.android.providers.media.photopicker.data.MuteStatus;
-import com.android.providers.media.photopicker.data.model.Item;
-
-import com.google.android.exoplayer2.ui.StyledPlayerView;
-
-/**
- * A class to handle page selected state to initiate video playback or release video player
- * resources. All the public methods of this class must be called from main thread as ExoPlayer
- * should be prepared/released from main thread.
- */
-class PlaybackHandler {
- private static final String TAG = "PlaybackHandler";
- // Only main thread can call the methods in this class, hence we don't need to guard mVideoUri
- // with lock while reading or writing to it.
- private Uri mVideoUri = null;
- private final ExoPlayerWrapper mExoPlayerWrapper;
-
- PlaybackHandler(Context context, MuteStatus muteStatus) {
- mExoPlayerWrapper = new ExoPlayerWrapper(context, muteStatus);
- }
-
- /**
- * Handles video playback for the {@link ViewPager2} page when it's selected i.e., completely
- * visible.
- * <ul>
- * <li> If the selected page is a video page, prepare and play the video associated with
- * selected page
- * <li> If the selected page is a video page and the same video is already playing, then no
- * action will be taken.
- * <li> If the selected page is non-video page, try releasing the ExoPlayer associated with
- * previous page that was selected.
- * </ul>
- * @param view {@link RecyclerView.ViewHolder#itemView} of the selected page.
- */
- public void handleVideoPlayback(View view) {
- assertMainThread();
-
- final Object tag = view.getTag();
- if (!(tag instanceof Item)) {
- throw new IllegalStateException("Expected Item tag to be set to " + view);
- }
-
- final Item item = (Item) tag;
- if (!item.isVideo()) {
- // We only need to handle video playback. For everything else, try releasing ExoPlayer
- // if there is a prepared ExoPlayer of the previous page, also reset any player states
- // when necessary.
- mExoPlayerWrapper.resetPlayerIfNecessary();
- mVideoUri = null;
- return;
- }
-
- final Uri videoUri = item.getContentUri();
- if (mVideoUri != null && mVideoUri.equals(videoUri)) {
- // Selected video is already handled. This must be a slight drag and drop, and we don't
- // have to change state of the player.
- Log.d(TAG, "Ignoring handlePageSelected of already selected page, with uri "
- + videoUri);
- return;
- }
-
- final StyledPlayerView styledPlayerView = view.findViewById(R.id.preview_player_view);
- if (styledPlayerView == null) {
- throw new IllegalStateException("Expected to find StyledPlayerView in " + view);
- }
- final ImageView imageView = view.findViewById(R.id.preview_video_image);
-
- mVideoUri = videoUri;
- mExoPlayerWrapper.prepareAndPlay(styledPlayerView, imageView, mVideoUri);
- }
-
- public void onViewAttachedToWindow(View itemView) {
- final ImageView imageView = itemView.findViewById(R.id.preview_video_image);
- imageView.setVisibility(View.VISIBLE);
- final StyledPlayerView styledPlayerView = itemView.findViewById(R.id.preview_player_view);
- styledPlayerView.setVisibility(View.GONE);
- styledPlayerView.setControllerVisibilityListener(null);
- styledPlayerView.hideController();
- }
-
- /**
- * Releases ExoPlayer if there is any. Also resets the saved video uri.
- */
- public void releaseResources() {
- assertMainThread();
-
- mVideoUri = null;
- mExoPlayerWrapper.resetPlayerIfNecessary();
- }
-
- private void assertMainThread() {
- if (Looper.getMainLooper().isCurrentThread()) return;
-
- throw new IllegalStateException("PlaybackHandler methods are expected to be called from"
- + " main thread. Current thread " + Looper.myLooper().getThread()
- + ", Main thread" + Looper.getMainLooper().getThread());
- }
-}
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
index 8ce7311..1df4877 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
@@ -41,13 +41,10 @@
private List<Item> mItemList = new ArrayList<>();
private final ImageLoader mImageLoader;
private final RemotePreviewHandler mRemotePreviewHandler;
- private final PlaybackHandler mPlaybackHandler;
- private final boolean mIsRemotePreviewEnabled = RemotePreviewHandler.isRemotePreviewEnabled();
PreviewAdapter(Context context, MuteStatus muteStatus) {
mImageLoader = new ImageLoader(context);
mRemotePreviewHandler = new RemotePreviewHandler(context, muteStatus);
- mPlaybackHandler = new PlaybackHandler(context, muteStatus);
}
@NonNull
@@ -55,10 +52,8 @@
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
if (viewType == ITEM_TYPE_IMAGE) {
return new PreviewImageHolder(viewGroup.getContext(), viewGroup, mImageLoader);
- } else {
- return new PreviewVideoHolder(viewGroup.getContext(), viewGroup, mImageLoader,
- mIsRemotePreviewEnabled);
}
+ return new PreviewVideoHolder(viewGroup.getContext(), viewGroup, mImageLoader);
}
@Override
@@ -76,17 +71,8 @@
final Item item = (Item) holder.itemView.getTag();
if (item.isVideo()) {
- // TODO(b/222506900): Refactor thumbnail show / hide logic to be handled from a single
- // place. Currently, we show the thumbnail here and hide it when playback starts in
- // PlaybackHandler/RemotePreviewHandler.
PreviewVideoHolder videoHolder = (PreviewVideoHolder) holder;
-
- if (mIsRemotePreviewEnabled) {
- mRemotePreviewHandler.onViewAttachedToWindow(videoHolder, item);
- return;
- }
-
- mPlaybackHandler.onViewAttachedToWindow(holder.itemView);
+ mRemotePreviewHandler.onViewAttachedToWindow(videoHolder, item);
}
}
@@ -106,28 +92,16 @@
}
void onHandlePageSelected(View itemView) {
- if (mIsRemotePreviewEnabled) {
- final Item item = (Item) itemView.getTag();
- mRemotePreviewHandler.onHandlePageSelected(item);
- return;
- }
-
- mPlaybackHandler.handleVideoPlayback(itemView);
+ final Item item = (Item) itemView.getTag();
+ mRemotePreviewHandler.onHandlePageSelected(item);
}
void onStop() {
- if (mIsRemotePreviewEnabled) {
- mRemotePreviewHandler.onStop();
- return;
- }
-
- mPlaybackHandler.releaseResources();
+ mRemotePreviewHandler.onStop();
}
void onDestroy() {
- if (mIsRemotePreviewEnabled) {
- mRemotePreviewHandler.onDestroy();
- }
+ mRemotePreviewHandler.onDestroy();
}
Item getItem(int position) {
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java b/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java
index dcb1696..e828767 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java
@@ -45,25 +45,17 @@
private final ImageButton mPlayPauseButton;
private final ImageButton mMuteButton;
- PreviewVideoHolder(Context context, ViewGroup parent, ImageLoader imageLoader,
- boolean enabledCloudMediaPreview) {
- super(context, parent, enabledCloudMediaPreview ? R.layout.item_cloud_video_preview
- : R.layout.item_video_preview);
+ PreviewVideoHolder(Context context, ViewGroup parent, ImageLoader imageLoader) {
+ super(context, parent, R.layout.item_video_preview);
mImageLoader = imageLoader;
mImageView = itemView.findViewById(R.id.preview_video_image);
- mSurfaceView = enabledCloudMediaPreview ? itemView.findViewById(R.id.preview_player_view)
- : null;
- mPlayerFrame = enabledCloudMediaPreview ?
- itemView.findViewById(R.id.preview_player_frame) : null;
- mPlayerContainer = enabledCloudMediaPreview ?
- itemView.findViewById(R.id.preview_player_container) : null;
- mPlayerControlsRoot = enabledCloudMediaPreview ? itemView.findViewById(
- R.id.preview_player_controls) : null;
- mPlayPauseButton = enabledCloudMediaPreview ? itemView.findViewById(
- R.id.exo_play_pause) : null;
- mMuteButton = enabledCloudMediaPreview ? itemView.findViewById(
- R.id.preview_mute) : null;
+ mSurfaceView = itemView.findViewById(R.id.preview_player_view);
+ mPlayerFrame = itemView.findViewById(R.id.preview_player_frame);
+ mPlayerContainer = itemView.findViewById(R.id.preview_player_container);
+ mPlayerControlsRoot = itemView.findViewById(R.id.preview_player_controls);
+ mPlayPauseButton = itemView.findViewById(R.id.exo_play_pause);
+ mMuteButton = itemView.findViewById(R.id.preview_mute);
}
@Override
diff --git a/src/com/android/providers/media/photopicker/ui/SafetyProtectionSectionView.java b/src/com/android/providers/media/photopicker/ui/SafetyProtectionSectionView.java
new file mode 100644
index 0000000..21dd503
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/SafetyProtectionSectionView.java
@@ -0,0 +1,78 @@
+/*
+ * 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.annotation.Nullable;
+import android.content.Context;
+import android.text.Html;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.providers.media.R;
+import com.android.providers.media.photopicker.util.SafetyProtectionUtils;
+
+/**
+ * A custom view class for Safety Protection widget.
+ */
+public class SafetyProtectionSectionView extends LinearLayout {
+ public SafetyProtectionSectionView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public SafetyProtectionSectionView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public SafetyProtectionSectionView(Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ public SafetyProtectionSectionView(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context);
+ }
+
+ private void init(Context context) {
+ setGravity(Gravity.CENTER);
+ setOrientation(HORIZONTAL);
+ int visibility = SafetyProtectionUtils.shouldShowSafetyProtectionResources(context)
+ ? View.VISIBLE : View.GONE;
+ setVisibility(visibility);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ Context context = getContext();
+ if (SafetyProtectionUtils.shouldShowSafetyProtectionResources(context)) {
+ LayoutInflater.from(context).inflate(R.layout.safety_protection_section, this);
+ TextView safetyProtectionDisplayTextView =
+ requireViewById(R.id.safety_protection_display_text);
+ safetyProtectionDisplayTextView.setText(Html.fromHtml(
+ context.getString(android.R.string.safety_protection_display_text), 0));
+ }
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/util/MimeFilterUtils.java b/src/com/android/providers/media/photopicker/util/MimeFilterUtils.java
index b028a00..7872343 100644
--- a/src/com/android/providers/media/photopicker/util/MimeFilterUtils.java
+++ b/src/com/android/providers/media/photopicker/util/MimeFilterUtils.java
@@ -57,24 +57,31 @@
/**
* Extracts relevant mime type filter for the given intent
*/
- public static String getMimeTypeFilter(Intent intent) {
+ public static String[] getMimeTypeFilters(Intent intent) throws IllegalArgumentException {
// EXTRA_MIME_TYPES has higher priority over getType() filter.
if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
final String[] extraMimeTypes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
- if (extraMimeTypes.length == 1) {
- // We only support 1 mime type filter
- // TODO(b/224756380): Add support for multiple mime type filters
- return extraMimeTypes[0];
+ if (requiresUnsupportedFilters(extraMimeTypes)) {
+ if (Intent.ACTION_GET_CONTENT.equals(intent.getAction())) {
+ // This is a special case in which PhotoPicker is explicitly opened from
+ // DocumentsUI as it is seen as one of the options. In this show all images
+ // and videos.
+ // If this was not a special case, then the picker would close itself and
+ // redirect the request to DocumentsUI before hitting this point.
+ return null;
+ }
+
+ throw new IllegalArgumentException("Invalid EXTRA_MIME_TYPES value, only media "
+ + "mime type filters are accepted");
}
- // Show all images/videos for multiple or empty mime type filters
- return null;
+ return extraMimeTypes;
}
final String mimeType = intent.getType();
if (MimeFilterUtils.isMimeTypeMedia(mimeType)) {
- return mimeType;
+ return new String[] { mimeType };
}
return null;
@@ -86,13 +93,8 @@
return true;
}
- // TODO(b/224756380): Add support for multiple mime type filters
- if (mimeTypeFilters.length > 1) {
- return true;
- }
-
for (String mimeTypeFilter : mimeTypeFilters) {
- if (!MimeFilterUtils.isMimeTypeMedia(mimeTypeFilter)) {
+ if (!isMimeTypeMedia(mimeTypeFilter)) {
return true;
}
}
diff --git a/src/com/android/providers/media/photopicker/util/SafetyProtectionUtils.java b/src/com/android/providers/media/photopicker/util/SafetyProtectionUtils.java
new file mode 100644
index 0000000..e3f459a
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/util/SafetyProtectionUtils.java
@@ -0,0 +1,55 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Build;
+import android.provider.DeviceConfig;
+
+import androidx.annotation.ChecksSdkIntAtLeast;
+
+import com.android.modules.utils.build.SdkLevel;
+
+/**
+ * Util class for whether we should show the safety protection resources.
+ */
+public class SafetyProtectionUtils {
+ private static final String SAFETY_PROTECTION_RESOURCES_ENABLED = "safety_protection_enabled";
+
+ /**
+ * Determines whether we should show the safety protection resources.
+ * We show the resources only if
+ * (1) the build version is T or after and
+ * (2) the feature flag safety_protection_enabled is enabled and
+ * (3) the config value config_safetyProtectionEnabled is enabled/true and
+ * (4) the resources exist (currently the resources only exist on GMS devices)
+ */
+ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
+ public static boolean shouldShowSafetyProtectionResources(Context context) {
+ return SdkLevel.isAtLeastT()
+ && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
+ SAFETY_PROTECTION_RESOURCES_ENABLED, false)
+ && context.getResources().getBoolean(
+ Resources.getSystem()
+ .getIdentifier("config_safetyProtectionEnabled",
+ "bool", "android"))
+ && context.getDrawable(android.R.drawable.ic_safety_protection) != null
+ && context.getString(android.R.string.safety_protection_display_text) != null
+ && !context.getString(android.R.string.safety_protection_display_text).isEmpty();
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
index 1de5839..a285cbf 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
@@ -76,7 +76,7 @@
private InstanceId mInstanceId;
private PhotoPickerUiEventLogger mLogger;
- private String mMimeTypeFilter = null;
+ private String[] mMimeTypeFilters = null;
private int mBottomSheetState;
private Category mCurrentCategory;
@@ -151,7 +151,7 @@
final List<Item> items = new ArrayList<>();
try (Cursor cursor = mItemsProvider.getItems(category, /* offset */ 0,
- /* limit */ -1, mMimeTypeFilter, userId)) {
+ /* limit */ -1, mMimeTypeFilters, userId)) {
if (cursor == null || cursor.getCount() == 0) {
Log.d(TAG, "Didn't receive any items for " + category
+ ", either cursor is null or cursor count is zero");
@@ -268,7 +268,7 @@
private List<Category> loadCategories(UserId userId) {
final List<Category> categoryList = new ArrayList<>();
- try (final Cursor cursor = mItemsProvider.getCategories(mMimeTypeFilter, userId)) {
+ try (final Cursor cursor = mItemsProvider.getCategories(mMimeTypeFilters, userId)) {
if (cursor == null || cursor.getCount() == 0) {
Log.d(TAG, "Didn't receive any categories, either cursor is null or"
+ " cursor count is zero");
@@ -304,10 +304,10 @@
}
/**
- * Return whether the {@link #mMimeTypeFilter} is {@code null} or not
+ * Return whether the {@link #mMimeTypeFilters} is {@code null} or not
*/
- public boolean hasMimeTypeFilter() {
- return !TextUtils.isEmpty(mMimeTypeFilter);
+ public boolean hasMimeTypeFilters() {
+ return mMimeTypeFilters != null && mMimeTypeFilters.length > 0;
}
/**
@@ -316,7 +316,7 @@
public void parseValuesFromIntent(Intent intent) throws IllegalArgumentException {
mUserIdManager.setIntentAndCheckRestrictions(intent);
- mMimeTypeFilter = MimeFilterUtils.getMimeTypeFilter(intent);
+ mMimeTypeFilters = MimeFilterUtils.getMimeTypeFilters(intent);
mSelection.parseSelectionValuesFromIntent(intent);
}
@@ -357,6 +357,30 @@
mLogger.logBrowseToDocumentsUi(mInstanceId, callingUid, callingPackage);
}
+ /**
+ * Log metrics to notify that the user has confirmed selection
+ */
+ public void logPickerConfirm(int callingUid, String callingPackage, int countOfItemsConfirmed) {
+ if (getUserIdManager().isManagedUserSelected()) {
+ mLogger.logPickerConfirmWork(mInstanceId, callingUid, callingPackage,
+ countOfItemsConfirmed);
+ } else {
+ mLogger.logPickerConfirmPersonal(mInstanceId, callingUid, callingPackage,
+ countOfItemsConfirmed);
+ }
+ }
+
+ /**
+ * Log metrics to notify that the user has exited Picker without any selection
+ */
+ public void logPickerCancel(int callingUid, String callingPackage) {
+ if (getUserIdManager().isManagedUserSelected()) {
+ mLogger.logPickerCancelWork(mInstanceId, callingUid, callingPackage);
+ } else {
+ mLogger.logPickerCancelPersonal(mInstanceId, callingUid, callingPackage);
+ }
+ }
+
public InstanceId getInstanceId() {
return mInstanceId;
}
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 41f53d5..5afe15e 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -92,7 +92,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.MediaVolume;
import com.android.providers.media.util.DatabaseUtils;
import com.android.providers.media.util.ExifUtils;
@@ -119,7 +118,6 @@
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -1210,22 +1208,6 @@
return op;
}
- private static ArrayMap<String, String> sAudioTypes = new ArrayMap<>();
-
- static {
- sAudioTypes.put(Environment.DIRECTORY_RINGTONES, AudioColumns.IS_RINGTONE);
- sAudioTypes.put(Environment.DIRECTORY_NOTIFICATIONS, AudioColumns.IS_NOTIFICATION);
- sAudioTypes.put(Environment.DIRECTORY_ALARMS, AudioColumns.IS_ALARM);
- sAudioTypes.put(Environment.DIRECTORY_PODCASTS, AudioColumns.IS_PODCAST);
- sAudioTypes.put(Environment.DIRECTORY_AUDIOBOOKS, AudioColumns.IS_AUDIOBOOK);
- sAudioTypes.put(Environment.DIRECTORY_MUSIC, AudioColumns.IS_MUSIC);
- if (SdkLevel.isAtLeastS()) {
- sAudioTypes.put(Environment.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
- } else {
- sAudioTypes.put(FileUtils.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
- }
- }
-
private static @NonNull ContentProviderOperation.Builder scanItemAudio(long existingId,
File file, BasicFileAttributes attrs, String mimeType, int mediaType,
String volumeName) {
@@ -1236,17 +1218,7 @@
op.withValue(MediaColumns.ALBUM, file.getParentFile().getName());
op.withValue(AudioColumns.TRACK, null);
- final String lowPath = file.getAbsolutePath().toLowerCase(Locale.ROOT);
- boolean anyMatch = false;
- for (int i = 0; i < sAudioTypes.size(); i++) {
- final boolean match = lowPath
- .contains('/' + sAudioTypes.keyAt(i).toLowerCase(Locale.ROOT) + '/');
- op.withValue(sAudioTypes.valueAt(i), match ? 1 : 0);
- anyMatch |= match;
- }
- if (!anyMatch) {
- op.withValue(AudioColumns.IS_MUSIC, 1);
- }
+ FileUtils.computeAudioTypeValuesFromData(file.getAbsolutePath(), op::withValue);
try (FileInputStream is = new FileInputStream(file)) {
try (MediaMetadataRetriever mmr = new MediaMetadataRetriever()) {
@@ -1510,9 +1482,13 @@
@VisibleForTesting
static @NonNull Optional<Integer> parseOptionalOrientation(int orientation) {
switch (orientation) {
+ case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
case ExifInterface.ORIENTATION_NORMAL: return Optional.of(0);
+ case ExifInterface.ORIENTATION_TRANSPOSE:
case ExifInterface.ORIENTATION_ROTATE_90: return Optional.of(90);
+ case ExifInterface.ORIENTATION_FLIP_VERTICAL:
case ExifInterface.ORIENTATION_ROTATE_180: return Optional.of(180);
+ case ExifInterface.ORIENTATION_TRANSVERSE:
case ExifInterface.ORIENTATION_ROTATE_270: return Optional.of(270);
default: return Optional.empty();
}
diff --git a/src/com/android/providers/media/util/DatabaseUtils.java b/src/com/android/providers/media/util/DatabaseUtils.java
index 55efafc..cec6f27 100644
--- a/src/com/android/providers/media/util/DatabaseUtils.java
+++ b/src/com/android/providers/media/util/DatabaseUtils.java
@@ -535,8 +535,14 @@
return sb.toString();
}
- public static String replaceMatchAnyChar(@NonNull String arg) {
- return arg.replace('*', '%');
+ public static String[] replaceMatchAnyChar(@NonNull String[] arg) {
+ String[] result = arg.clone();
+ for (int i = 0; i < arg.length; i++) {
+ if (result[i] != null) {
+ result[i] = result[i].replace('*', '%');
+ }
+ }
+ return result;
}
public static boolean parseBoolean(@Nullable Object value, boolean def) {
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index ee421c9..d4cdc7e 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -49,17 +49,19 @@
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
-import android.os.UserHandle;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.MediaColumns;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.text.TextUtils;
import android.text.format.DateUtils;
+import android.util.ArrayMap;
import android.util.Log;
import android.webkit.MimeTypeMap;
@@ -91,6 +93,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
+import java.util.function.ObjIntConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -417,8 +420,8 @@
// When file size exceeds MAX_READ_STRING_SIZE, file is either
// corrupted or doesn't the contain expected data. Hence we return
// Optional.empty() which will be interpreted as empty file.
- Logging.logPersistent(String.format("Ignored reading %s, file size exceeds %d", file,
- MAX_READ_STRING_SIZE));
+ Logging.logPersistent(String.format(Locale.ROOT,
+ "Ignored reading %s, file size exceeds %d", file, MAX_READ_STRING_SIZE));
} catch (NoSuchFileException ignored) {
}
return Optional.empty();
@@ -973,12 +976,13 @@
"(?i)^Android/(?:data|media|obb)/([^/]+)(/?.*)?");
/**
- * Regex that matches Android/obb or Android/data path.
+ * Regex that matches exactly Android/obb or Android/data or Android/obb/ or Android/data/
+ * suffix absolute file path.
*/
private static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
"(?i)^/storage/[^/]+/(?:[0-9]+/)?"
+ PROP_CROSS_USER_ROOT_PATTERN
- + "Android/(?:data|obb)(?:/.*)?$");
+ + "Android/(?:data|obb)/?$");
/**
* Regex that matches Android/obb or Android/data relative path (as defined in
@@ -1399,9 +1403,53 @@
resolvedDisplayName = displayName;
}
- final File filePath = buildPath(volumePath,
- values.getAsString(MediaColumns.RELATIVE_PATH), resolvedDisplayName);
- values.put(MediaColumns.DATA, filePath.getAbsolutePath());
+ String relativePath = values.getAsString(MediaColumns.RELATIVE_PATH);
+ if (relativePath == null) {
+ relativePath = "";
+ }
+ try {
+ final File filePath = buildPath(volumePath, relativePath, resolvedDisplayName);
+ values.put(MediaColumns.DATA, filePath.getCanonicalPath());
+ } catch (IOException e) {
+ throw new IllegalArgumentException(
+ String.format("Failure in conversion to canonical file path. Failure path: %s.",
+ relativePath.concat(resolvedDisplayName)), e);
+ }
+ }
+
+ @VisibleForTesting
+ static ArrayMap<String, String> sAudioTypes = new ArrayMap<>();
+
+ static {
+ sAudioTypes.put(Environment.DIRECTORY_RINGTONES, AudioColumns.IS_RINGTONE);
+ sAudioTypes.put(Environment.DIRECTORY_NOTIFICATIONS, AudioColumns.IS_NOTIFICATION);
+ sAudioTypes.put(Environment.DIRECTORY_ALARMS, AudioColumns.IS_ALARM);
+ sAudioTypes.put(Environment.DIRECTORY_PODCASTS, AudioColumns.IS_PODCAST);
+ sAudioTypes.put(Environment.DIRECTORY_AUDIOBOOKS, AudioColumns.IS_AUDIOBOOK);
+ sAudioTypes.put(Environment.DIRECTORY_MUSIC, AudioColumns.IS_MUSIC);
+ if (SdkLevel.isAtLeastS()) {
+ sAudioTypes.put(Environment.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
+ } else {
+ sAudioTypes.put(FileUtils.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
+ }
+ }
+
+ /**
+ * Compute values for columns in {@code sAudioTypes} based on the given {@code filePath}.
+ */
+ public static void computeAudioTypeValuesFromData(@NonNull String filePath,
+ @NonNull ObjIntConsumer<String> consumer) {
+ final String lowPath = filePath.toLowerCase(Locale.ROOT);
+ boolean anyMatch = false;
+ for (int i = 0; i < sAudioTypes.size(); i++) {
+ final boolean match = lowPath
+ .contains('/' + sAudioTypes.keyAt(i).toLowerCase(Locale.ROOT) + '/');
+ consumer.accept(sAudioTypes.valueAt(i), match ? 1 : 0);
+ anyMatch |= match;
+ }
+ if (!anyMatch) {
+ consumer.accept(AudioColumns.IS_MUSIC, 1);
+ }
}
public static void sanitizeValues(@NonNull ContentValues values,
@@ -1559,7 +1607,7 @@
}
// .nomedia is present which makes this directory as hidden directory
- Logging.logPersistent("Observed non-standard " + nomedia);
+ Log.d(TAG, "Observed non-standard " + nomedia);
return true;
}
@@ -1625,18 +1673,28 @@
Log.i(TAG, "Clearing cache for all apps");
final File rootDataDir = buildPath(Environment.getExternalStorageDirectory(),
"Android", "data");
- for (File appDataDir : rootDataDir.listFiles()) {
- try {
- final File appCacheDir = new File(appDataDir, "cache");
- if (appCacheDir.isDirectory()) {
- FileUtils.deleteContents(appCacheDir);
+ File[] appDataDirs = rootDataDir.listFiles();
+ if (appDataDirs == null) {
+ // Couldn't delete any app cache dirs because the call to list files in root data dir
+ // failed (b/234521806). It is not clear why this call would fail because root data
+ // dir path should be well-formed.
+ Log.e(TAG, String.format("Couldn't delete any app cache dirs in root data dir %s !",
+ rootDataDir.getAbsolutePath()));
+ status = OsConstants.EIO;
+ } else {
+ for (File appDataDir : appDataDirs) {
+ try {
+ final File appCacheDir = new File(appDataDir, "cache");
+ if (appCacheDir.isDirectory()) {
+ FileUtils.deleteContents(appCacheDir);
+ }
+ } catch (Exception e) {
+ // We want to avoid crashing MediaProvider at all costs, so we handle all
+ // "generic" exceptions here, and just report to the caller that an IO exception
+ // has occurred. We still try to clear the rest of the directories.
+ Log.e(TAG, "Couldn't delete all app cache dirs!", e);
+ status = OsConstants.EIO;
}
- } catch (Exception e) {
- // We want to avoid crashing MediaProvider at all costs, so we handle all "generic"
- // exceptions here, and just report to the caller that an IO exception has occurred.
- // We still try to clear the rest of the directories.
- Log.e(TAG, "Couldn't delete all app cache dirs!", e);
- status = OsConstants.EIO;
}
}
return status;
diff --git a/src/com/android/providers/media/util/IsoInterface.java b/src/com/android/providers/media/util/IsoInterface.java
index 03b46c9..8da64b9 100644
--- a/src/com/android/providers/media/util/IsoInterface.java
+++ b/src/com/android/providers/media/util/IsoInterface.java
@@ -48,6 +48,7 @@
private static final String TAG = "IsoInterface";
private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
+ public static final int BOX_ILST = 0x696c7374;
public static final int BOX_FTYP = 0x66747970;
public static final int BOX_HDLR = 0x68646c72;
public static final int BOX_UUID = 0x75756964;
@@ -96,7 +97,7 @@
private static class Box {
public final int type;
- public final long[] range;
+ public long[] range;
public UUID uuid;
public byte[] data;
public List<Box> children;
@@ -132,7 +133,7 @@
return new UUID(high, low);
}
- private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end,
+ private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end, int parentType,
@NonNull String prefix) throws ErrnoException, IOException {
final long pos = Os.lseek(fd, 0, OsConstants.SEEK_CUR);
@@ -210,6 +211,9 @@
box.headerSize += 4;
}
Os.lseek(fd, pos + box.headerSize, OsConstants.SEEK_SET);
+ } else if (type == BOX_XYZ && parentType == BOX_ILST) {
+ box.range = new long[] {box.range[0], headerSize,
+ box.range[0] + headerSize, box.range[1] - headerSize};
}
if (LOGV) {
@@ -222,7 +226,7 @@
box.children = new ArrayList<>();
Box child;
- while ((child = parseNextBox(fd, pos + len, prefix + " ")) != null) {
+ while ((child = parseNextBox(fd, pos + len, type, prefix + " ")) != null) {
box.children.add(child);
}
}
@@ -252,7 +256,7 @@
final long end = Os.lseek(fd, 0, OsConstants.SEEK_END);
Os.lseek(fd, 0, OsConstants.SEEK_SET);
Box box;
- while ((box = parseNextBox(fd, end, "")) != null) {
+ while ((box = parseNextBox(fd, end, -1, "")) != null) {
mRoots.add(box);
}
} catch (ErrnoException e) {
@@ -291,9 +295,11 @@
public @NonNull long[] getBoxRanges(int type) {
LongArray res = new LongArray();
for (Box box : mFlattened) {
- if (box.type == type) {
- res.add(box.range[0] + box.headerSize);
- res.add(box.range[0] + box.range[1]);
+ for (int i = 0; i < box.range.length; i += 2) {
+ if (box.type == type) {
+ res.add(box.range[i] + box.headerSize);
+ res.add(box.range[i] + box.range[i + 1]);
+ }
}
}
return res.toArray();
@@ -302,9 +308,11 @@
public @NonNull long[] getBoxRanges(@NonNull UUID uuid) {
LongArray res = new LongArray();
for (Box box : mFlattened) {
- if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
- res.add(box.range[0] + box.headerSize);
- res.add(box.range[0] + box.range[1]);
+ for (int i = 0; i < box.range.length; i += 2) {
+ if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
+ res.add(box.range[i] + box.headerSize);
+ res.add(box.range[i] + box.range[i + 1]);
+ }
}
}
return res.toArray();
diff --git a/src/com/android/providers/media/util/Logging.java b/src/com/android/providers/media/util/Logging.java
index ff69e5c..1afd18b 100644
--- a/src/com/android/providers/media/util/Logging.java
+++ b/src/com/android/providers/media/util/Logging.java
@@ -34,9 +34,8 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
-import java.util.Comparator;
import java.util.Date;
-import java.util.Optional;
+import java.util.Locale;
import java.util.stream.Stream;
public class Logging {
@@ -53,7 +52,7 @@
private static final int PERSISTENT_COUNT = 4;
private static final long PERSISTENT_AGE = DateUtils.WEEK_IN_MILLIS;
private static final SimpleDateFormat DATE_FORMAT =
- new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.ROOT);
private static final Object LOCK = new Object();
@GuardedBy("LOCK")
@@ -77,7 +76,9 @@
/**
* Write the given message to persistent logs.
*/
- public static void logPersistent(@NonNull String msg) {
+ public static void logPersistent(@NonNull String format, @Nullable Object ... args) {
+ final String msg = String.format(Locale.ROOT, format, args);
+
Log.i(TAG, msg);
synchronized (LOCK) {
diff --git a/src/com/android/providers/media/util/Metrics.java b/src/com/android/providers/media/util/Metrics.java
index 024151d..b97df56 100644
--- a/src/com/android/providers/media/util/Metrics.java
+++ b/src/com/android/providers/media/util/Metrics.java
@@ -45,10 +45,10 @@
public class Metrics {
public static void logScan(@NonNull String volumeName, int reason, long itemCount,
long durationMillis, int insertCount, int updateCount, int deleteCount) {
- Logging.logPersistent(String.format(
+ Logging.logPersistent(
"Scanned %s due to %s, found %d items in %dms, %d inserts %d updates %d deletes",
volumeName, translateReason(reason), itemCount, durationMillis, insertCount,
- updateCount, deleteCount));
+ updateCount, deleteCount);
final float normalizedDurationMillis = ((float) durationMillis) / itemCount;
final float normalizedInsertCount = ((float) insertCount) / itemCount;
@@ -90,9 +90,9 @@
public static void logPermissionGranted(@NonNull String volumeName, int uid, String packageName,
int itemCount) {
- Logging.logPersistent(String.format(
+ Logging.logPersistent(
"Granted permission to %3$d items on %1$s to %2$s",
- volumeName, packageName, itemCount));
+ volumeName, packageName, itemCount);
MediaProviderStatsLog.write(MEDIA_PROVIDER_PERMISSION_REQUESTED,
translateVolumeName(volumeName), uid, itemCount,
@@ -101,9 +101,9 @@
public static void logPermissionDenied(@NonNull String volumeName, int uid, String packageName,
int itemCount) {
- Logging.logPersistent(String.format(
+ Logging.logPersistent(
"Denied permission to %3$d items on %1$s to %2$s",
- volumeName, packageName, itemCount));
+ volumeName, packageName, itemCount);
MediaProviderStatsLog.write(MEDIA_PROVIDER_PERMISSION_REQUESTED,
translateVolumeName(volumeName), uid, itemCount,
@@ -112,9 +112,9 @@
public static void logSchemaChange(@NonNull String volumeName, int versionFrom, int versionTo,
long itemCount, long durationMillis, @NonNull String databaseUuid) {
- Logging.logPersistent(String.format(
+ Logging.logPersistent(
"Changed schema version on %s from %d to %d, %d items taking %dms UUID %s",
- volumeName, versionFrom, versionTo, itemCount, durationMillis, databaseUuid));
+ volumeName, versionFrom, versionTo, itemCount, durationMillis, databaseUuid);
final float normalizedDurationMillis = ((float) durationMillis) / itemCount;
@@ -125,9 +125,9 @@
public static void logIdleMaintenance(@NonNull String volumeName, long itemCount,
long durationMillis, int staleThumbnails, int expiredMedia) {
- Logging.logPersistent(String.format(
+ Logging.logPersistent(
"Idle maintenance on %s, %d items taking %dms, %d stale, %d expired",
- volumeName, itemCount, durationMillis, staleThumbnails, expiredMedia));
+ volumeName, itemCount, durationMillis, staleThumbnails, expiredMedia);
final float normalizedDurationMillis = ((float) durationMillis) / itemCount;
final float normalizedStaleThumbnails = ((float) staleThumbnails) / itemCount;
diff --git a/src/com/android/providers/media/util/Preconditions.java b/src/com/android/providers/media/util/Preconditions.java
index fb87130..ffb29c6 100644
--- a/src/com/android/providers/media/util/Preconditions.java
+++ b/src/com/android/providers/media/util/Preconditions.java
@@ -16,6 +16,8 @@
package com.android.providers.media.util;
+import java.util.Locale;
+
public final class Preconditions {
/**
@@ -49,11 +51,11 @@
String valueName) {
if (value < lower) {
throw new IllegalArgumentException(
- String.format(
+ String.format(Locale.ROOT,
"%s is out of range of [%d, %d] (too low)", valueName, lower, upper));
} else if (value > upper) {
throw new IllegalArgumentException(
- String.format(
+ String.format(Locale.ROOT,
"%s is out of range of [%d, %d] (too high)", valueName, lower, upper));
}
diff --git a/tests/Android.bp b/tests/Android.bp
index 05aa556..17f90db 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -158,7 +158,7 @@
"androidx.core_core",
"androidx.test.rules",
"guava",
- "mockito-target-extended-minus-junit4",
+ "mockito-target",
"modules-utils-build",
"truth-prebuilt",
"com.google.android.material_material",
@@ -177,12 +177,6 @@
"exoplayer-mediaprovider-ui",
],
- // these are needed for Extended Mockito
- jni_libs: [
- "libdexmakerjvmtiagent",
- "libstaticjvmtiagent",
- ],
-
certificate: "media",
aaptflags: ["--custom-package com.android.providers.media"],
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index fd94863..a3c81af 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -9,10 +9,8 @@
<uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
<uses-permission android:name="android.permission.MANAGE_USERS" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
- <!--android:debuggable and android:largeHeap attributes are needed for Extended Mockito-->
- <application android:label="MediaProvider Tests"
- android:debuggable="true"
- android:largeHeap="true">
+ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+ <application android:label="MediaProvider Tests">
<uses-library android:name="android.test.runner" />
<activity android:name="com.android.providers.media.GetResultActivity" />
diff --git a/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java b/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java
index 9aaaae2..e9b9cbf 100644
--- a/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java
+++ b/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java
@@ -396,7 +396,7 @@
public void onChange(boolean selfChange, Uri uri, int flags) {
Log.v(TAG, String.format("onChange(%b, %s, %d)", selfChange, uri.toString(), flags));
- if (flags == this.flags) {
+ if ((flags & this.flags) == this.flags) {
latch.countDown();
}
}
diff --git a/tests/client/src/com/android/providers/media/client/PerformanceTest.java b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
index 0088514..b792ee2 100644
--- a/tests/client/src/com/android/providers/media/client/PerformanceTest.java
+++ b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
@@ -18,7 +18,6 @@
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.ContentProviderOperation;
@@ -27,9 +26,7 @@
import android.content.ContentValues;
import android.database.ContentObserver;
import android.net.Uri;
-import android.os.Bundle;
import android.os.Environment;
-import android.os.SystemClock;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import android.util.Log;
@@ -383,10 +380,10 @@
selfChange, asSet(uris).toString(), flags));
if (this.uriCount == 1) {
- if (asSet(uris).size() == 1 && flags == this.flags) {
+ if (asSet(uris).size() == 1 && (flags & this.flags) == this.flags) {
latch.countDown();
}
- } else if (flags == this.flags) {
+ } else if ((flags & this.flags) == this.flags) {
// NotifyChange for bulk operations will be sent in batches.
final int receivedCount = asSet(uris).size();
diff --git a/tests/res/raw/test_video_gps_ilst_tag.mp4 b/tests/res/raw/test_video_gps_ilst_tag.mp4
new file mode 100644
index 0000000..d65ff15
--- /dev/null
+++ b/tests/res/raw/test_video_gps_ilst_tag.mp4
Binary files differ
diff --git a/tests/src/com/android/providers/media/IdleServiceTest.java b/tests/src/com/android/providers/media/IdleServiceTest.java
index 80d2261..3d81b25 100644
--- a/tests/src/com/android/providers/media/IdleServiceTest.java
+++ b/tests/src/com/android/providers/media/IdleServiceTest.java
@@ -121,6 +121,7 @@
final File b = touch(buildPath(dir, DIRECTORY_MOVIES, ".thumbnails", "7654321.jpg"));
final File c = touch(buildPath(dir, DIRECTORY_PICTURES, ".thumbnails", id + ".jpg"));
final File d = touch(buildPath(dir, DIRECTORY_PICTURES, ".thumbnails", "random.bin"));
+ final File e = touch(buildPath(dir, DIRECTORY_PICTURES, ".thumbnails", ".nomedia"));
// Idle maintenance pass should clean up unknown files
MediaStore.runIdleMaintenance(resolver);
@@ -128,6 +129,7 @@
assertFalse(exists(b));
assertTrue(exists(c));
assertFalse(exists(d));
+ assertTrue(exists(e));
// And change the UUID, which emulates ejecting and mounting a different
// storage device; all thumbnails should then be invalidated
@@ -136,12 +138,13 @@
delete(uuidFile);
touch(uuidFile);
- // Idle maintenance pass should clean up all files
+ // Idle maintenance pass should clean up all files except .nomedia file
MediaStore.runIdleMaintenance(resolver);
assertFalse(exists(a));
assertFalse(exists(b));
assertFalse(exists(c));
assertFalse(exists(d));
+ assertTrue(exists(e));
}
/**
@@ -246,9 +249,8 @@
assertThat(cr.getCount()).isEqualTo(1);
assertThat(cr.moveToFirst()).isNotNull();
assertThat(cr.getInt(0)).isEqualTo(_SPECIAL_FORMAT_NONE);
- // Make sure updating special format column updates GENERATION_MODIFIED;
- // This is essential for picker db to know which rows were modified.
- assertThat(cr.getInt(1)).isGreaterThan(initialGenerationModified);
+ // Make sure that updating special format column doesn't update GENERATION_MODIFIED
+ assertThat(cr.getInt(1)).isEqualTo(initialGenerationModified);
}
} finally {
file.delete();
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 01578fa..ff56ee8 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -78,6 +78,7 @@
import com.android.providers.media.util.SQLiteQueryBuilder;
import org.junit.AfterClass;
+import org.junit.Assert;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Ignore;
@@ -375,6 +376,42 @@
}
}
+ @Test
+ public void testInsertionWithInvalidFilePath_throwsIllegalArgumentException() {
+ final ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.RELATIVE_PATH, "Android/media/com.example");
+ values.put(MediaStore.Images.Media.DISPLAY_NAME,
+ "./../../../../../../../../../../../data/media/test.txt");
+
+ IllegalArgumentException illegalArgumentException = Assert.assertThrows(
+ IllegalArgumentException.class, () -> sIsolatedResolver.insert(
+ MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
+ values));
+
+ assertThat(illegalArgumentException).hasMessageThat().contains(
+ "Primary directory Android not allowed for content://media/external_primary/file;"
+ + " allowed directories are [Download, Documents]");
+ }
+
+ @Test
+ public void testUpdationWithInvalidFilePath_throwsIllegalArgumentException() {
+ final ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.RELATIVE_PATH, "Download");
+ values.put(MediaStore.Images.Media.DISPLAY_NAME, "test.txt");
+ Uri uri = sIsolatedResolver.insert(
+ MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
+ values);
+
+ final ContentValues newValues = new ContentValues();
+ newValues.put(MediaStore.MediaColumns.DATA, "/storage/emulated/0/../../../data/media/");
+ IllegalArgumentException illegalArgumentException = Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> sIsolatedResolver.update(uri, newValues, null));
+
+ assertThat(illegalArgumentException).hasMessageThat().contains(
+ "Requested path /data/media doesn't appear under [/storage/emulated/0]");
+ }
+
/**
* We already have solid coverage of this logic in
* {@code CtsProviderTestCases}, but the coverage system currently doesn't
diff --git a/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java b/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
index 2f90d2f..adeaa3b 100644
--- a/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
+++ b/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
@@ -23,6 +23,7 @@
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
import static android.provider.CloudMediaProviderContract.MediaColumns;
import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_ARRAY_DEFAULT;
import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
import android.content.ContentResolver;
@@ -34,7 +35,6 @@
import android.provider.CloudMediaProvider;
import android.provider.CloudMediaProviderContract;
import android.text.TextUtils;
-import android.util.Log;
import com.android.providers.media.photopicker.LocalProvider;
@@ -96,8 +96,9 @@
private Bundle mCursorExtra;
// TODO(b/214592293): Add pagination support for testing purposes.
- public Cursor getMedia(long generation, String albumId, String mimeType, long sizeBytes) {
- final Cursor cursor = getCursor(mMedia, generation, albumId, mimeType, sizeBytes,
+ public Cursor getMedia(long generation, String albumId, String[] mimeTypes,
+ long sizeBytes) {
+ final Cursor cursor = getCursor(mMedia, generation, albumId, mimeTypes, sizeBytes,
/* isDeleted */ false);
if (mCursorExtra != null) {
@@ -112,8 +113,8 @@
return cursor;
}
- public Cursor getAlbums(String mimeType, long sizeBytes, boolean isLocal) {
- final Cursor cursor = getCursor(mAlbums, mimeType, sizeBytes, isLocal);
+ public Cursor getAlbums(String[] mimeTypes, long sizeBytes, boolean isLocal) {
+ final Cursor cursor = getCursor(mAlbums, mimeTypes, sizeBytes, isLocal);
if (mCursorExtra != null) {
cursor.setExtras(mCursorExtra);
@@ -130,7 +131,7 @@
// TODO(b/214592293): Add pagination support for testing purposes.
public Cursor getDeletedMedia(long generation) {
final Cursor cursor = getCursor(mDeletedMedia, generation, /* albumId */ STRING_DEFAULT,
- /* mimeType */ STRING_DEFAULT, /* sizeBytes */ LONG_DEFAULT,
+ STRING_ARRAY_DEFAULT, /* sizeBytes */ LONG_DEFAULT,
/* isDeleted */ true);
if (mCursorExtra != null) {
@@ -260,7 +261,7 @@
}
private static Cursor getCursor(List<TestMedia> mediaList, long generation,
- String albumId, String mimeType, long sizeBytes, boolean isDeleted) {
+ String albumId, String[] mimeTypes, long sizeBytes, boolean isDeleted) {
final MatrixCursor matrix;
if (isDeleted) {
matrix = new MatrixCursor(DELETED_MEDIA_PROJECTION);
@@ -272,22 +273,22 @@
for (TestMedia media : mediaList) {
if (!TextUtils.isEmpty(albumId) && matchesFilter(media,
- albumId, mimeType, sizeBytes)) {
+ albumId, mimeTypes, sizeBytes)) {
matrix.addRow(media.toAlbumMediaArray());
} else if (media.generation > generation
- && matchesFilter(media, albumId, mimeType, sizeBytes)) {
+ && matchesFilter(media, albumId, mimeTypes, sizeBytes)) {
matrix.addRow(media.toArray(isDeleted));
}
}
return matrix;
}
- private static Cursor getCursor(List<TestAlbum> albumList, String mimeType, long sizeBytes,
- boolean isLocal) {
+ private static Cursor getCursor(List<TestAlbum> albumList, String[] mimeTypes,
+ long sizeBytes, boolean isLocal) {
final MatrixCursor matrix = new MatrixCursor(ALBUM_PROJECTION);
for (TestAlbum album : albumList) {
- final String[] res = album.toArray(mimeType, sizeBytes, isLocal);
+ final String[] res = album.toArray(mimeTypes, sizeBytes, isLocal);
if (res != null) {
matrix.addRow(res);
}
@@ -398,13 +399,13 @@
this.media = media;
}
- public String[] toArray(String mimeType, long sizeBytes, boolean isLocal) {
+ public String[] toArray(String[] mimeTypes, long sizeBytes, boolean isLocal) {
long mediaCount = 0;
String mediaCoverId = null;
long dateTakenMs = 0;
for (TestMedia m : media) {
- if (matchesFilter(m, id, mimeType, sizeBytes)) {
+ if (matchesFilter(m, id, mimeTypes, sizeBytes)) {
if (mediaCount++ == 0) {
mediaCoverId = m.getId();
dateTakenMs = m.dateTakenMs;
@@ -442,14 +443,26 @@
}
}
- private static boolean matchesFilter(TestMedia media, String albumId, String mimeType,
+ private static boolean matchesFilter(TestMedia media, String albumId, String[] mimeTypes,
long sizeBytes) {
if (!Objects.equals(albumId, STRING_DEFAULT) && !Objects.equals(albumId, media.albumId)) {
return false;
}
- if (!Objects.equals(mimeType, STRING_DEFAULT) && !media.mimeType.startsWith(mimeType)) {
- return false;
+
+ if (mimeTypes != null) {
+ boolean matchesMimeType = false;
+ for (String m : mimeTypes) {
+ if (m != null && media.mimeType.startsWith(m)) {
+ matchesMimeType = true;
+ break;
+ }
+ }
+
+ if (!matchesMimeType) {
+ return false;
+ }
}
+
if (sizeBytes != LONG_DEFAULT && media.sizeBytes > sizeBytes) {
return false;
}
diff --git a/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java b/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java
index a007139..d56328e 100644
--- a/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java
+++ b/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java
@@ -54,7 +54,7 @@
CloudProviderQueryExtras.fromCloudMediaBundle(extras);
return mMediaGenerator.getMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
- queryExtras.getMimeType(), queryExtras.getSizeBytes());
+ queryExtras.getMimeTypes(), queryExtras.getSizeBytes());
}
@Override
@@ -70,7 +70,7 @@
final CloudProviderQueryExtras queryExtras =
CloudProviderQueryExtras.fromCloudMediaBundle(extras);
- return mMediaGenerator.getAlbums(queryExtras.getMimeType(), queryExtras.getSizeBytes(),
+ return mMediaGenerator.getAlbums(queryExtras.getMimeTypes(), queryExtras.getSizeBytes(),
/* isLocal */ false);
}
diff --git a/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java b/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java
index abcb92f..a00cbaf 100644
--- a/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java
+++ b/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java
@@ -16,7 +16,6 @@
package com.android.providers.media.cloudproviders;
-import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
import android.content.res.AssetFileDescriptor;
@@ -54,7 +53,7 @@
CloudProviderQueryExtras.fromCloudMediaBundle(extras);
return mMediaGenerator.getMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
- queryExtras.getMimeType(), queryExtras.getSizeBytes());
+ queryExtras.getMimeTypes(), queryExtras.getSizeBytes());
}
@Override
@@ -70,7 +69,7 @@
final CloudProviderQueryExtras queryExtras =
CloudProviderQueryExtras.fromCloudMediaBundle(extras);
- return mMediaGenerator.getAlbums(queryExtras.getMimeType(), queryExtras.getSizeBytes(),
+ return mMediaGenerator.getAlbums(queryExtras.getMimeTypes(), queryExtras.getSizeBytes(),
/* isLocal */ false);
}
diff --git a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
index 7608c92..dbddc3e 100644
--- a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
@@ -482,7 +482,7 @@
File videoFile = assertCreateNewVideo();
try {
final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
- /* limit */ -1, /* mimeType */ "image/*", /* userId */ null);
+ /* limit */ -1, /* mimeType */ new String[]{ "image/*"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(1);
@@ -504,7 +504,7 @@
File imageFile = assertCreateNewImage();
try {
final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
- /* limit */ -1, /* mimeType */ "image/png", /* userId */ null);
+ /* limit */ -1, /* mimeType */ new String[]{"image/png"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(0);
} finally {
@@ -526,7 +526,7 @@
File videoFileHidden = assertCreateNewVideo(hiddenDir);
try {
final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
- /* limit */ -1, /* mimeType */ "image/*", /* userId */ null);
+ /* limit */ -1, /* mimeType */ new String[]{"image/*"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(0);
} finally {
@@ -549,7 +549,7 @@
File videoFile = assertCreateNewVideo();
try {
final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
- /* limit */ -1, /* mimeType */ "video/*", /* userId */ null);
+ /* limit */ -1, /* mimeType */ new String[]{"video/*"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(1);
@@ -571,7 +571,7 @@
File videoFile = assertCreateNewVideo();
try {
final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
- /* limit */ -1, /* mimeType */ "video/mp4", /* userId */ null);
+ /* limit */ -1, /* mimeType */ new String[]{"video/mp4"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(1);
} finally {
@@ -592,7 +592,7 @@
File videoFileHidden = assertCreateNewVideo(hiddenDir);
try {
final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
- /* limit */ -1, /* mimeType */ "video/*", /* userId */ null);
+ /* limit */ -1, /* mimeType */ new String[]{"video/*"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(0);
} finally {
diff --git a/tests/src/com/android/providers/media/photopicker/LocalProvider.java b/tests/src/com/android/providers/media/photopicker/LocalProvider.java
index 3916c9d..b1c1281 100644
--- a/tests/src/com/android/providers/media/photopicker/LocalProvider.java
+++ b/tests/src/com/android/providers/media/photopicker/LocalProvider.java
@@ -16,7 +16,6 @@
package com.android.providers.media.photopicker;
-import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
import android.content.res.AssetFileDescriptor;
@@ -53,7 +52,7 @@
CloudProviderQueryExtras.fromCloudMediaBundle(extras);
return mMediaGenerator.getMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
- queryExtras.getMimeType(), queryExtras.getSizeBytes());
+ queryExtras.getMimeTypes(), queryExtras.getSizeBytes());
}
@Override
@@ -69,7 +68,7 @@
final CloudProviderQueryExtras queryExtras =
CloudProviderQueryExtras.fromCloudMediaBundle(extras);
- return mMediaGenerator.getAlbums(queryExtras.getMimeType(), queryExtras.getSizeBytes(),
+ return mMediaGenerator.getAlbums(queryExtras.getMimeTypes(), queryExtras.getSizeBytes(),
/* isLocal */ true);
}
diff --git a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
index 6931cd6..59c5582 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
@@ -343,7 +343,12 @@
try (Cursor cr = mDataLayer.fetchAlbums(defaultQueryArgs)) {
assertThat(cr.getCount()).isEqualTo(4);
- assertAlbumCursor(cr, ALBUM_ID_FAVORITES, LOCAL_PROVIDER_AUTHORITY);
+ // Most recent media item marked as favorite will be the cover of the Favorites album.
+ // In this scenario, Favorites album cover was generated with cloud authority, so the
+ // Favorites album authority should be cloud provider authority. Similarly, the most
+ // recent video was generated with local authority, so the Videos album authority should
+ // be local provider authority.
+ assertAlbumCursor(cr, ALBUM_ID_FAVORITES, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertAlbumCursor(cr, ALBUM_ID_VIDEOS, LOCAL_PROVIDER_AUTHORITY);
assertAlbumCursor(cr, ALBUM_ID_1, LOCAL_PROVIDER_AUTHORITY);
assertAlbumCursor(cr, ALBUM_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -504,7 +509,10 @@
try (Cursor cr = mDataLayer.fetchAlbums(mimeTypeAndSizeQueryArgs)) {
assertWithMessage("Merged and Local album count").that(cr.getCount()).isEqualTo(3);
- assertAlbumCursor(cr, ALBUM_ID_VIDEOS, LOCAL_PROVIDER_AUTHORITY);
+ // Most recent video will be the cover of the Videos album. In this scenario, Videos
+ // album cover was generated with cloud authority, so the Videos album authority should
+ // be cloud provider authority.
+ assertAlbumCursor(cr, ALBUM_ID_VIDEOS, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertAlbumCursor(cr, ALBUM_ID_1, LOCAL_PROVIDER_AUTHORITY);
assertAlbumCursor(cr, ALBUM_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -559,7 +567,9 @@
private static Bundle buildQueryArgs(String mimeType, long sizeBytes) {
final Bundle queryArgs = new Bundle();
- queryArgs.putString(MediaStore.QUERY_ARG_MIME_TYPE, mimeType);
+ if (mimeType != null) {
+ queryArgs.putStringArray(MediaStore.QUERY_ARG_MIME_TYPE, new String[]{mimeType});
+ }
queryArgs.putLong(MediaStore.QUERY_ARG_SIZE_BYTES, sizeBytes);
return queryArgs;
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index 53d6837..5f3ddcc 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -658,13 +658,33 @@
}
@Test
- public void testSelectDefaultCloudProivder_DefaultAuthoritySet() {
+ public void testSelectDefaultCloudProvider_defaultAuthoritySet() {
PickerSyncController controller = createControllerWithDefaultProvider(
CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@Test
+ public void testSelectDefaultCloudProvider_userAwareAboutCloudMediaAppSettings() {
+ PickerSyncController controller = createControllerWithDefaultProvider(
+ CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+ assertThat(controller.getDefaultCloudProviderInfo(null, false).isEmpty()).isFalse();
+ assertThat(controller.getDefaultCloudProviderInfo(null, true).isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testNotifyUserCloudMediaAware() {
+ assertThat(mController.isUserAwareAboutCloudMediaAppSettings()).isFalse();
+
+ mController.notifyUserCloudMediaAware();
+
+ assertThat(mController.isUserAwareAboutCloudMediaAppSettings()).isTrue();
+
+ mController.clearUserAwareAboutCloudMediaAppSettingsFlag();
+ }
+
+ @Test
public void testIsProviderAuthorityEnabled() {
assertThat(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY)).isTrue();
assertThat(mController.isProviderEnabled(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isFalse();
@@ -1052,8 +1072,10 @@
when(mockContext.getResources()).thenReturn(mockResources);
when(mockContext.getPackageManager()).thenReturn(mContext.getPackageManager());
- when(mockContext.getSystemService(StorageManager.class))
- .thenReturn(mContext.getSystemService(StorageManager.class));
+ when(mockContext.getSystemServiceName(StorageManager.class)).thenReturn(
+ mContext.getSystemServiceName(StorageManager.class));
+ 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));
});
diff --git a/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java b/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java
new file mode 100644
index 0000000..1c15d97
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.DeviceConfig;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.providers.media.photopicker.util.SafetyProtectionUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SafetyProtectionUtilsTest {
+ private final Context mContext = InstrumentationRegistry.getTargetContext();
+ private static final String SAFETY_PROTECTION_RESOURCES_ENABLED = "safety_protection_enabled";
+ private static final String TRUE_STRING = "true";
+ private static final String FALSE_STRING = "false";
+ private String mOriginalSafetyProtectionResourcesFlagStatus = "false";
+
+ @Before
+ public void setUp() {
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ mOriginalSafetyProtectionResourcesFlagStatus = DeviceConfig.getProperty(
+ DeviceConfig.NAMESPACE_PRIVACY, SAFETY_PROTECTION_RESOURCES_ENABLED);
+ });
+ }
+
+ @After
+ public void tearDown() {
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
+ SAFETY_PROTECTION_RESOURCES_ENABLED,
+ mOriginalSafetyProtectionResourcesFlagStatus, false);
+ });
+ }
+
+ @Test
+ public void testShouldNotUseSafetyProtectionResourcesWhenSOrBelow() {
+ assumeFalse(SdkLevel.isAtLeastT());
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
+ SAFETY_PROTECTION_RESOURCES_ENABLED, TRUE_STRING, false);
+ assertThat(SafetyProtectionUtils.shouldShowSafetyProtectionResources(mContext))
+ .isFalse();
+ });
+ }
+
+ @Test
+ public void testWhetherShouldUseSafetyProtectionResourcesWhenTOrAboveAndFeatureFlagOn() {
+ assumeTrue(SdkLevel.isAtLeastT());
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
+ SAFETY_PROTECTION_RESOURCES_ENABLED, TRUE_STRING, false);
+ assertThat(SafetyProtectionUtils.shouldShowSafetyProtectionResources(mContext))
+ .isEqualTo(isSafetyProtectionConfigEnabled());
+ });
+ }
+
+ @Test
+ public void testWhetherShouldUseSafetyProtectionResourcesWhenTOrAboveAndFeatureFlagOff() {
+ assumeTrue(SdkLevel.isAtLeastT());
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
+ SAFETY_PROTECTION_RESOURCES_ENABLED, FALSE_STRING, false);
+ assertThat(SafetyProtectionUtils.shouldShowSafetyProtectionResources(mContext))
+ .isFalse();
+ });
+ }
+
+ protected boolean isSafetyProtectionConfigEnabled() {
+ try {
+ return mContext.getResources().getBoolean(
+ Resources.getSystem()
+ .getIdentifier("config_safetyProtectionEnabled", "bool",
+ "android"));
+ } catch (Resources.NotFoundException e) {
+ return false;
+ }
+ }
+}
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 3a37efa..3e07a2e 100644
--- a/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java
@@ -81,7 +81,9 @@
private static final long GENERATION_MODIFIED5 = 5;
private static final long SIZE = 8000;
private static final String IMAGE_MIME_TYPE = "image/jpeg";
+ private static final String[] IMAGE_MIME_TYPES_QUERY = new String[]{"image/jpeg"};
private static final String VIDEO_MIME_TYPE = "video/mp4";
+ private static final String[] VIDEO_MIME_TYPES_QUERY = new String[]{"video/mp4"};
private static final long DURATION_MS = 5;
private static final int IS_FAVORITE = 0;
@@ -542,12 +544,12 @@
}
try (Cursor cursor = facade.queryMedia(/* generation */ 0,
- /* albumId */ null, VIDEO_MIME_TYPE)) {
+ /* albumId */ null, VIDEO_MIME_TYPES_QUERY)) {
assertThat(cursor.getCount()).isEqualTo(0);
}
try (Cursor cursor = facade.queryMedia(/* generation */ 0,
- /* albumId */ null, IMAGE_MIME_TYPE)) {
+ /* albumId */ null, IMAGE_MIME_TYPES_QUERY)) {
assertThat(cursor.getCount()).isEqualTo(1);
cursor.moveToFirst();
@@ -616,17 +618,17 @@
}
try (Cursor cursor = facade.queryMedia(/* generation */ 0,
- ALBUM_ID_SCREENSHOTS, IMAGE_MIME_TYPE)) {
+ ALBUM_ID_SCREENSHOTS, IMAGE_MIME_TYPES_QUERY)) {
assertThat(cursor.getCount()).isEqualTo(0);
}
try (Cursor cursor = facade.queryMedia(/* generation */ 0,
- ALBUM_ID_CAMERA, VIDEO_MIME_TYPE)) {
+ ALBUM_ID_CAMERA, VIDEO_MIME_TYPES_QUERY)) {
assertThat(cursor.getCount()).isEqualTo(0);
}
try (Cursor cursor = facade.queryMedia(/* generation */ 0,
- ALBUM_ID_CAMERA, IMAGE_MIME_TYPE)) {
+ ALBUM_ID_CAMERA, IMAGE_MIME_TYPES_QUERY)) {
assertThat(cursor.getCount()).isEqualTo(1);
cursor.moveToFirst();
@@ -762,7 +764,7 @@
cv1.put(MediaColumns.RELATIVE_PATH, ExternalDbFacade.RELATIVE_PATH_CAMERA);
helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv1));
- // Insert video in camera ablum
+ // Insert video in camera album
ContentValues cv2 = getContentValues(DATE_TAKEN_MS5, GENERATION_MODIFIED5);
cv2.put(FileColumns.MIME_TYPE, VIDEO_MIME_TYPE);
cv2.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_VIDEO);
@@ -772,7 +774,7 @@
assertThat(cursor.getCount()).isEqualTo(2);
}
- try (Cursor cursor = facade.queryAlbums(IMAGE_MIME_TYPE)) {
+ try (Cursor cursor = facade.queryAlbums(IMAGE_MIME_TYPES_QUERY)) {
assertThat(cursor.getCount()).isEqualTo(1);
// We verify the order of the albums only the image in camera is shown
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
index 264e9bd..0305bbb 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.assertThrows;
+import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
@@ -49,7 +50,9 @@
private static final String CLOUD_ID = "asdfghjkl;";
private static final String ALBUM_ID = "testAlbum";
private static final String VIDEO_MIME_TYPE = "video/mp4";
+ private static final String[] VIDEO_MIME_TYPES_QUERY = new String[]{"video/mp4"};
private static final String IMAGE_MIME_TYPE = "image/jpeg";
+ private static final String[] IMAGE_MIME_TYPES_QUERY = new String[]{"image/jpeg"};
private static final int STANDARD_MIME_TYPE_EXTENSION =
MediaColumns.STANDARD_MIME_TYPE_EXTENSION_GIF;
@@ -609,7 +612,7 @@
}
@Test
- public void testQueryWithMimeTypeFilter() throws Exception {
+ public void testQueryWithMimeTypesFilter() throws Exception {
Cursor cursor1 = getMediaCursor(LOCAL_ID, DATE_TAKEN_MS, GENERATION_MODIFIED,
/* mediaStoreUri */ null, SIZE_BYTES, "video/webm",
STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
@@ -622,12 +625,12 @@
// Verify all
PickerDbFacade.QueryFilterBuilder qfbAll = new PickerDbFacade.QueryFilterBuilder(1000);
- qfbAll.setMimeType("*/*");
+ qfbAll.setMimeTypes(new String[]{"*/*"});
try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
assertThat(cr.getCount()).isEqualTo(2);
}
- qfbAll.setMimeType("video/mp4");
+ qfbAll.setMimeTypes(new String[]{"video/mp4"});
try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
assertThat(cr.getCount()).isEqualTo(1);
@@ -639,14 +642,14 @@
PickerDbFacade.QueryFilterBuilder qfbAfter = new PickerDbFacade.QueryFilterBuilder(1000);
qfbAfter.setDateTakenAfterMs(DATE_TAKEN_MS - 1);
qfbAfter.setId(0);
- qfbAfter.setMimeType("video/*");
+ qfbAfter.setMimeTypes(new String[]{"video/*"});
try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
assertThat(cr.getCount()).isEqualTo(2);
}
qfbAfter.setDateTakenAfterMs(DATE_TAKEN_MS - 1);
qfbAfter.setId(0);
- qfbAfter.setMimeType("video/webm");
+ qfbAfter.setMimeTypes(new String[]{"video/webm"});
try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
assertThat(cr.getCount()).isEqualTo(1);
@@ -658,14 +661,14 @@
PickerDbFacade.QueryFilterBuilder qfbBefore = new PickerDbFacade.QueryFilterBuilder(1000);
qfbBefore.setDateTakenBeforeMs(DATE_TAKEN_MS + 1);
qfbBefore.setId(0);
- qfbBefore.setMimeType("video/*");
+ qfbBefore.setMimeTypes(new String[]{"video/*"});
try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
assertThat(cr.getCount()).isEqualTo(2);
}
qfbBefore.setDateTakenBeforeMs(DATE_TAKEN_MS + 1);
qfbBefore.setId(0);
- qfbBefore.setMimeType("video/mp4");
+ qfbBefore.setMimeTypes(new String[]{"video/mp4"});
try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
assertThat(cr.getCount()).isEqualTo(1);
@@ -675,7 +678,7 @@
}
@Test
- public void testQueryWithSizeAndMimeTypeFilter() throws Exception {
+ public void testQueryWithSizeAndMimeTypesFilter() throws Exception {
Cursor cursor1 = getMediaCursor(LOCAL_ID, DATE_TAKEN_MS, GENERATION_MODIFIED,
/* mediaStoreUri */ null, /* sizeBytes */ 2, "video/webm",
STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
@@ -688,14 +691,14 @@
// mime_type and size filter matches all
PickerDbFacade.QueryFilterBuilder qfbAll = new PickerDbFacade.QueryFilterBuilder(1000);
- qfbAll.setMimeType("*/*");
+ qfbAll.setMimeTypes(new String[]{"*/*"});
qfbAll.setSizeBytes(10);
try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
assertThat(cr.getCount()).isEqualTo(2);
}
// mime_type and size filter matches none
- qfbAll.setMimeType("video/webm");
+ qfbAll.setMimeTypes(new String[]{"video/webm"});
qfbAll.setSizeBytes(1);
try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
assertThat(cr.getCount()).isEqualTo(0);
@@ -882,7 +885,7 @@
}
@Test
- public void testGetFavoritesAlbumWithMimeTypeFilter() throws Exception {
+ public void testGetFavoritesAlbumWithMimeTypesFilter() throws Exception {
Cursor localCursor1 = getMediaCursor(LOCAL_ID + "1", DATE_TAKEN_MS, GENERATION_MODIFIED,
/* mediaStoreUri */ null, SIZE_BYTES, VIDEO_MIME_TYPE,
STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ true);
@@ -933,7 +936,7 @@
/* count */ 2);
}
- qfb.setMimeType(IMAGE_MIME_TYPE);
+ qfb.setMimeTypes(IMAGE_MIME_TYPES_QUERY);
try (Cursor cr = mFacade.getMergedAlbums(qfb.build())) {
assertThat(cr.getCount()).isEqualTo(1);
cr.moveToFirst();
@@ -945,7 +948,7 @@
/* count */ 1);
}
- qfb.setMimeType(VIDEO_MIME_TYPE);
+ qfb.setMimeTypes(VIDEO_MIME_TYPES_QUERY);
try (Cursor cr = mFacade.getMergedAlbums(qfb.build())) {
assertThat(cr.getCount()).isEqualTo(2);
cr.moveToFirst();
@@ -964,7 +967,7 @@
/* count */ 2);
}
- qfb.setMimeType("foo");
+ qfb.setMimeTypes(new String[]{"foo"});
try (Cursor cr = mFacade.getMergedAlbums(qfb.build())) {
assertThat(cr.getCount()).isEqualTo(0);
}
@@ -1012,6 +1015,68 @@
}
}
+ @Test
+ public void testUpdateMediaSuccess() throws Exception {
+ Cursor localCursor = getMediaCursor(LOCAL_ID, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, SIZE_BYTES, VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ true);
+ try (PickerDbFacade.DbWriteOperation operation =
+ mFacade.beginAddMediaOperation(LOCAL_PROVIDER)) {
+ operation.execute(localCursor);
+ operation.setSuccess();
+ }
+
+ try (PickerDbFacade.UpdateMediaOperation operation =
+ mFacade.beginUpdateMediaOperation(LOCAL_PROVIDER)) {
+ ContentValues values = new ContentValues();
+ values.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION,
+ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP);
+ assertThat(operation.execute(LOCAL_ID, values)).isTrue();
+ operation.setSuccess();
+ }
+
+ try (Cursor cursor = queryMediaAll()) {
+ assertThat(cursor.getCount()).isEqualTo(1);
+
+ // Assert that STANDARD_MIME_TYPE_EXTENSION has been updated
+ cursor.moveToFirst();
+ assertThat(cursor.getInt(cursor.getColumnIndex(
+ MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
+ .isEqualTo(MediaColumns.STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP);
+ }
+ }
+
+ @Test
+ public void testUpdateMediaFailure() throws Exception {
+ Cursor localCursor = getMediaCursor(LOCAL_ID, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, SIZE_BYTES, VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ true);
+ try (PickerDbFacade.DbWriteOperation operation =
+ mFacade.beginAddMediaOperation(LOCAL_PROVIDER)) {
+ operation.execute(localCursor);
+ operation.setSuccess();
+ }
+
+ try (PickerDbFacade.UpdateMediaOperation operation =
+ mFacade.beginUpdateMediaOperation(LOCAL_PROVIDER)) {
+ ContentValues values = new ContentValues();
+ values.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION,
+ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP);
+ assertThat(operation.execute(CLOUD_ID, values)).isFalse();
+ operation.setSuccess();
+ }
+
+ try (Cursor cursor = queryMediaAll()) {
+ assertThat(cursor.getCount()).isEqualTo(1);
+
+ // Assert that STANDARD_MIME_TYPE_EXTENSION is same as before
+ cursor.moveToFirst();
+ assertThat(cursor.getInt(cursor.getColumnIndex(
+ MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
+ .isEqualTo(STANDARD_MIME_TYPE_EXTENSION);
+ }
+ }
+
private Cursor queryMediaAll() {
return mFacade.queryMediaForUi(
new PickerDbFacade.QueryFilterBuilder(1000).build());
@@ -1113,7 +1178,8 @@
}
private static Cursor getAlbumMediaCursor(String id, long dateTakenMs, long generationModified,
- String mediaStoreUri, long sizeBytes, String mimeType, int standardMimeTypeExtension) {
+ String mediaStoreUri, long sizeBytes, String mimeType,
+ int standardMimeTypeExtension) {
String[] projectionKey = new String[] {
MediaColumns.ID,
MediaColumns.MEDIA_STORE_URI,
diff --git a/tests/src/com/android/providers/media/photopicker/data/model/CategoryTest.java b/tests/src/com/android/providers/media/photopicker/data/model/CategoryTest.java
index 808f70b..930a5b7 100644
--- a/tests/src/com/android/providers/media/photopicker/data/model/CategoryTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/model/CategoryTest.java
@@ -23,8 +23,8 @@
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
-import android.net.Uri;
-import android.provider.CloudMediaProviderContract;
+import android.os.Bundle;
+import android.provider.MediaStore;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -34,11 +34,11 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.List;
-
@RunWith(AndroidJUnit4.class)
public class CategoryTest {
+ private static final String LOCAL_PROVIDER_AUTHORITY =
+ "com.android.providers.media.photopicker";
@Test
public void testConstructor() {
final Context context = InstrumentationRegistry.getTargetContext();
@@ -54,10 +54,11 @@
final Category category = Category.fromCursor(cursor, UserId.CURRENT_USER);
assertThat(category.getDisplayName(context)).isEqualTo(categoryName);
+ assertThat(category.getAuthority()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
assertThat(category.isLocal()).isEqualTo(categoryIsLocal);
assertThat(category.getItemCount()).isEqualTo(itemCount);
assertThat(category.getCoverUri()).isEqualTo(ItemsProvider.getItemsUri(coverId,
- /* authority */ "foo", UserId.CURRENT_USER));
+ /* authority */ LOCAL_PROVIDER_AUTHORITY, UserId.CURRENT_USER));
assertThat(category.getId()).isEqualTo(categoryId);
}
@@ -65,7 +66,10 @@
String coverId, int itemCount, boolean isLocal) {
final MatrixCursor cursor = new MatrixCursor(AlbumColumns.ALL_PROJECTION);
cursor.addRow(new Object[] {categoryId, 1, categoryName, coverId, itemCount,
- "foo"});
+ LOCAL_PROVIDER_AUTHORITY});
+ Bundle extras = new Bundle();
+ extras.putString(MediaStore.EXTRA_LOCAL_PROVIDER, LOCAL_PROVIDER_AUTHORITY);
+ cursor.setExtras(extras);
return cursor;
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
index f2c80e7..91c309b 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
@@ -155,7 +155,8 @@
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
- Manifest.permission.INTERACT_ACROSS_USERS);
+ Manifest.permission.INTERACT_ACROSS_USERS,
+ Manifest.permission.READ_DEVICE_CONFIG);
sIsolatedContext = new IsolatedContext(getTargetContext(), "modern",
/* asFuseThread */ false);
diff --git a/tests/src/com/android/providers/media/photopicker/util/MimeFilterUtilsTest.java b/tests/src/com/android/providers/media/photopicker/util/MimeFilterUtilsTest.java
index 2c98c3f..966f0ed 100644
--- a/tests/src/com/android/providers/media/photopicker/util/MimeFilterUtilsTest.java
+++ b/tests/src/com/android/providers/media/photopicker/util/MimeFilterUtilsTest.java
@@ -18,10 +18,15 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
import android.content.Intent;
+import android.provider.MediaStore;
import org.junit.Test;
+import java.util.Arrays;
+
public class MimeFilterUtilsTest {
private static final String[] MEDIA_MIME_TYPES = new String[] {"image/*", "video/*"};
@@ -59,7 +64,7 @@
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_MIME_TYPES, MEDIA_MIME_TYPES);
- assertThat(MimeFilterUtils.requiresUnsupportedFilters(intent)).isTrue();
+ assertThat(MimeFilterUtils.requiresUnsupportedFilters(intent)).isFalse();
}
@Test
@@ -80,7 +85,7 @@
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"video/mp4", "image/gif"});
- assertThat(MimeFilterUtils.requiresUnsupportedFilters(intent)).isTrue();
+ assertThat(MimeFilterUtils.requiresUnsupportedFilters(intent)).isFalse();
}
@Test
@@ -102,25 +107,47 @@
}
@Test
- public void testGetMimeTypeFilter() {
+ public void testGetMimeTypeFilter_setType() {
Intent intent = new Intent();
String mimeType = "image/*";
intent.setType(mimeType);
- assertThat(MimeFilterUtils.getMimeTypeFilter(intent).equals(mimeType)).isTrue();
+ assertThat(Arrays.equals(MimeFilterUtils.getMimeTypeFilters(intent),
+ new String[]{mimeType})).isTrue();
mimeType = "video/mp4";
intent.setType(mimeType);
- assertThat(MimeFilterUtils.getMimeTypeFilter(intent).equals(mimeType)).isTrue();
mimeType = "*/*";
intent.setType(mimeType);
- assertThat(MimeFilterUtils.getMimeTypeFilter(intent)).isNull();
+ assertThat(MimeFilterUtils.getMimeTypeFilters(intent)).isNull();
+ // Test EXTRA_MIME_TYPE has higher priority than setType
mimeType = "image/*";
intent.setType(mimeType);
- String[] extraMimeTypes = new String[] {"video/*"};
+ String[] extraMimeTypes = new String[] {"video/mp4", "video/dng"};
intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes);
- assertThat(MimeFilterUtils.getMimeTypeFilter(intent).equals("video/*")).isTrue();
+ assertThat(Arrays.equals(MimeFilterUtils.getMimeTypeFilters(intent),
+ extraMimeTypes)).isTrue();
+ }
+
+ @Test
+ public void testGetMimeTypeFilter_extraMimeType_pickImages() {
+ Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ String[] extraMimeTypes = new String[] {"audio/mp4", "video/dng"};
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes);
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ MimeFilterUtils.getMimeTypeFilters(intent);
+ });
+ }
+
+ @Test
+ public void testGetMimeTypeFilter_extraMimeType_getContent() {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ String[] extraMimeTypes = new String[] {"audio/mp4", "video/dng"};
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes);
+
+ assertThat(MimeFilterUtils.getMimeTypeFilters(intent)).isNull();
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
index ec4afa1..ab50696 100644
--- a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
@@ -21,6 +21,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -30,7 +31,7 @@
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
-import android.provider.CloudMediaProviderContract;
+import android.provider.MediaStore;
import android.text.format.DateUtils;
import androidx.annotation.NonNull;
@@ -325,7 +326,7 @@
@Override
public Cursor getItems(Category category, int offset,
- int limit, @Nullable String mimeType, @Nullable UserId userId) throws
+ int limit, @Nullable String[] mimeType, @Nullable UserId userId) throws
IllegalArgumentException, IllegalStateException {
final MatrixCursor c = new MatrixCursor(MediaColumns.ALL_PROJECTION);
@@ -349,7 +350,7 @@
}
@Nullable
- public Cursor getCategories(@Nullable String mimeType, @Nullable UserId userId) {
+ public Cursor getCategories(@Nullable String[] mimeType, @Nullable UserId userId) {
if (mCategoriesCursor != null) {
return mCategoriesCursor;
}
@@ -368,31 +369,75 @@
}
@Test
- public void testParseValuesFromIntent_noMimeType_defaultFalse() {
- final Intent intent = new Intent();
+ public void testParseValuesFromPickImagesIntent_noMimeType_defaultFalse() {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
mPickerViewModel.parseValuesFromIntent(intent);
- assertThat(mPickerViewModel.hasMimeTypeFilter()).isFalse();
+ assertThat(mPickerViewModel.hasMimeTypeFilters()).isFalse();
+ }
+
+ @Test
+ public void testParseValuesFromGetContentIntent_noMimeType_defaultFalse() {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.setType("*/*");
+
+ mPickerViewModel.parseValuesFromIntent(intent);
+
+ assertThat(mPickerViewModel.hasMimeTypeFilters()).isFalse();
}
@Test
public void testParseValuesFromIntent_validMimeType() {
- final Intent intent = new Intent();
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
intent.setType("image/png");
mPickerViewModel.parseValuesFromIntent(intent);
- assertThat(mPickerViewModel.hasMimeTypeFilter()).isTrue();
+ assertThat(mPickerViewModel.hasMimeTypeFilters()).isTrue();
}
@Test
- public void testParseValuesFromIntent_ignoreInvalidMimeType() {
- final Intent intent = new Intent();
- intent.setType("audio/*");
+ public void testParseValuesFromPickImagesIntent_validExtraMimeType() {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/gif", "video/*"});
mPickerViewModel.parseValuesFromIntent(intent);
- assertThat(mPickerViewModel.hasMimeTypeFilter()).isFalse();
+ assertThat(mPickerViewModel.hasMimeTypeFilters()).isTrue();
+ }
+
+ @Test
+ public void testParseValuesFromPickImagesIntent_invalidExtraMimeType() {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"audio/*", "video/*"});
+
+ try {
+ mPickerViewModel.parseValuesFromIntent(intent);
+ fail("Photo Picker does not support non-media mime type filters");
+ } catch (IllegalArgumentException expected) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testParseValuesFromGetContentIntent_validExtraMimeType() {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/gif", "video/*"});
+
+ mPickerViewModel.parseValuesFromIntent(intent);
+
+ assertThat(mPickerViewModel.hasMimeTypeFilters()).isTrue();
+ }
+
+ @Test
+ public void testParseValuesFromGetContentIntent_invalidExtraMimeType() {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"audio/*", "video/*"});
+
+ mPickerViewModel.parseValuesFromIntent(intent);
+
+ // non-media filters for GET_CONTENT show all images and videos
+ assertThat(mPickerViewModel.hasMimeTypeFilters()).isFalse();
}
}
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index 0450c1c..e3c56f1 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -198,8 +198,14 @@
assertEquals(270,
(int) parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_270).get());
- // We can't represent this as an orientation
- assertFalse(parseOptionalOrientation(ExifInterface.ORIENTATION_TRANSPOSE).isPresent());
+ assertEquals(0,
+ (int) parseOptionalOrientation(ExifInterface.ORIENTATION_FLIP_HORIZONTAL).get());
+ assertEquals(90,
+ (int) parseOptionalOrientation(ExifInterface.ORIENTATION_TRANSPOSE).get());
+ assertEquals(180,
+ (int) parseOptionalOrientation(ExifInterface.ORIENTATION_FLIP_VERTICAL).get());
+ assertEquals(270,
+ (int) parseOptionalOrientation(ExifInterface.ORIENTATION_TRANSVERSE).get());
}
@Test
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 266c144..8c3d5d4 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -59,10 +59,12 @@
import static com.android.providers.media.util.FileUtils.translateModeStringToPosix;
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 static org.junit.Assert.fail;
@@ -70,6 +72,7 @@
import android.os.Environment;
import android.os.SystemProperties;
import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.MediaColumns;
import android.text.TextUtils;
@@ -89,7 +92,9 @@
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
import java.util.Optional;
@@ -1021,11 +1026,16 @@
assertThat(isDataOrObbPath("/storage/emulated/0/Android/obb")).isTrue();
assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/data")).isTrue();
assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obb")).isTrue();
- assertThat(isDataOrObbPath("/storage/emulated/0/Android/data/foo")).isTrue();
- assertThat(isDataOrObbPath("/storage/emulated/0/Android/obb/foo")).isTrue();
- assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/data/foo")).isTrue();
- assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obb/foo")).isTrue();
+ assertThat(isDataOrObbPath("/storage/emulated/0/Android/data/foo")).isFalse();
+ assertThat(isDataOrObbPath("/storage/emulated/0/Android/obb/foo")).isFalse();
+ assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/data/foo")).isFalse();
+ assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obb/foo")).isFalse();
+ assertThat(isDataOrObbPath("/storage/emulated/10/Android/obb/foo")).isFalse();
+ assertThat(isDataOrObbPath("/storage/emulated//Android/obb/foo")).isFalse();
+ assertThat(isDataOrObbPath("/storage/emulated//Android/obb")).isFalse();
+ assertThat(isDataOrObbPath("/storage/emulated/0//Android/obb")).isFalse();
+ assertThat(isDataOrObbPath("/storage/emulated/0//Android/obb/foo")).isFalse();
assertThat(isDataOrObbPath("/storage/emulated/0/Android/")).isFalse();
assertThat(isDataOrObbPath("/storage/emulated/0/Android/media/")).isFalse();
assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/media/")).isFalse();
@@ -1200,4 +1210,64 @@
assertTrue(values.containsKey(MediaColumns.BUCKET_DISPLAY_NAME));
assertNull(values.get(MediaColumns.BUCKET_DISPLAY_NAME));
}
+
+ @Test
+ public void testComputeAudioTypeValuesFromData() {
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Ringtones/a.mp3",
+ AudioColumns.IS_RINGTONE);
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Notifications/a.mp3",
+ AudioColumns.IS_NOTIFICATION);
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Alarms/a.mp3",
+ AudioColumns.IS_ALARM);
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Podcasts/a.mp3",
+ AudioColumns.IS_PODCAST);
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Audiobooks/a.mp3",
+ AudioColumns.IS_AUDIOBOOK);
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Recordings/a.mp3",
+ AudioColumns.IS_RECORDING);
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Music/a.mp3",
+ AudioColumns.IS_MUSIC);
+
+ // Categorized as music if it doesn't match any other category
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/a/a.mp3", AudioColumns.IS_MUSIC);
+
+ // All matches are case-insensitive
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/ringtones/a.mp3",
+ AudioColumns.IS_RINGTONE);
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/a/ringtones/a.mp3",
+ AudioColumns.IS_RINGTONE);
+ }
+
+ @Test
+ public void testComputeAudioTypeValuesFromData_multiple() {
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Ringtones/Recordings/a.mp3",
+ Arrays.asList(AudioColumns.IS_RINGTONE, AudioColumns.IS_RECORDING));
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Alarms/Notifications/a.mp3",
+ Arrays.asList(AudioColumns.IS_ALARM, AudioColumns.IS_NOTIFICATION));
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Audiobooks/Podcasts/a.mp3",
+ Arrays.asList(AudioColumns.IS_AUDIOBOOK, AudioColumns.IS_PODCAST));
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Audiobooks/Ringtones/a.mp3",
+ Arrays.asList(AudioColumns.IS_AUDIOBOOK, AudioColumns.IS_RINGTONE));
+ testComputeAudioTypeValuesFromData("/storage/emulated/0/Music/Ringtones/a.mp3",
+ Arrays.asList(AudioColumns.IS_MUSIC, AudioColumns.IS_RINGTONE));
+ }
+
+ private void testComputeAudioTypeValuesFromData(String path, String expectedColumn) {
+ testComputeAudioTypeValuesFromData(path, Collections.singletonList(expectedColumn));
+ }
+
+ private void testComputeAudioTypeValuesFromData(String path, List<String> expectedColumns) {
+ final ContentValues values = new ContentValues();
+ FileUtils.computeAudioTypeValuesFromData(path, values::put);
+
+ for (String column : FileUtils.sAudioTypes.values()) {
+ if (expectedColumns.contains(column)) {
+ assertWithMessage("Expected " + column + " to be set for " + path)
+ .that(values.get(column)).isEqualTo(1);
+ } else {
+ assertWithMessage("Expected " + column + " to be unset for " + path)
+ .that(values.get(column)).isEqualTo(0);
+ }
+ }
+ }
}
diff --git a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
index 7783957..37dd48f 100644
--- a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
+++ b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
@@ -73,6 +73,19 @@
}
@Test
+ public void testGpsInIlst() throws Exception {
+ final File file = stageFile(R.raw.test_video_gps_ilst_tag);
+ final IsoInterface mp4 = IsoInterface.fromFile(file);
+
+ final long[] ranges = mp4.getBoxRanges(0xa978797a); // ?xyz
+ assertEquals(4, ranges.length);
+ assertEquals(2267, ranges[0]);
+ assertEquals(2267, ranges[1]);
+ assertEquals(2275, ranges[2]);
+ assertEquals(2309, ranges[3]);
+ }
+
+ @Test
public void testXmp() throws Exception {
final File file = stageFile(R.raw.test_video_xmp);
final IsoInterface mp4 = IsoInterface.fromFile(file);
diff --git a/tests/src/com/android/providers/media/util/LoggingTest.java b/tests/src/com/android/providers/media/util/LoggingTest.java
index bb2905e..b5301d9 100644
--- a/tests/src/com/android/providers/media/util/LoggingTest.java
+++ b/tests/src/com/android/providers/media/util/LoggingTest.java
@@ -55,6 +55,18 @@
final String nonce = String.valueOf(System.nanoTime());
Logging.logPersistent(nonce);
+ assertLoggedMessageContains(nonce);
+ }
+
+ @Test
+ public void testFormat() throws Exception {
+ final long nonce = System.nanoTime();
+ Logging.logPersistent("%d", nonce);
+
+ assertLoggedMessageContains(String.valueOf(nonce));
+ }
+
+ private void assertLoggedMessageContains(String nonce) {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
final PrintWriter pw = new PrintWriter(os, true);
Logging.dumpPersistent(pw);